Merge "Add a script to build the debian guest image for AVF" into main
diff --git a/android/virtmgr/Android.bp b/android/virtmgr/Android.bp
index f0b6881..d0d7915 100644
--- a/android/virtmgr/Android.bp
+++ b/android/virtmgr/Android.bp
@@ -34,7 +34,6 @@
         "libapkverify",
         "libavf_features",
         "libavflog",
-        "libbase_rust",
         "libbinder_rs",
         "libcfg_if",
         "libclap",
@@ -70,6 +69,7 @@
         "liblibfdt",
         "libfsfdt",
         "libhypervisor_props",
+        "libzerocopy",
         "libuuid",
         // TODO(b/202115393) stabilize the interface
         "packagemanager_aidl-rust",
diff --git a/android/virtmgr/src/composite.rs b/android/virtmgr/src/composite.rs
index 681ec59..1219150 100644
--- a/android/virtmgr/src/composite.rs
+++ b/android/virtmgr/src/composite.rs
@@ -15,13 +15,16 @@
 //! Functions for creating a composite disk image.
 
 use android_system_virtualizationservice::aidl::android::system::virtualizationservice::Partition::Partition;
-use anyhow::{anyhow, Context, Error};
-use disk::{
-    create_composite_disk, create_disk_file, ImagePartitionType, PartitionInfo, MAX_NESTING_DEPTH,
-};
+use anyhow::{bail, Context, Error};
+use disk::{create_composite_disk, ImagePartitionType, PartitionInfo};
 use std::fs::{File, OpenOptions};
+use std::io::ErrorKind;
+use std::os::unix::fs::FileExt;
 use std::os::unix::io::AsRawFd;
 use std::path::{Path, PathBuf};
+use zerocopy::AsBytes;
+use zerocopy::FromBytes;
+use zerocopy::FromZeroes;
 
 use uuid::Uuid;
 
@@ -98,7 +101,7 @@
                 .context("Failed to clone partition image file descriptor")?
                 .into();
             let path = fd_path_for_file(&file);
-            let size = get_partition_size(&file, &path)?;
+            let size = get_partition_size(&file)?;
             files.push(file);
 
             Ok(PartitionInfo {
@@ -122,16 +125,74 @@
 
 /// Find the size of the partition image in the given file by parsing the header.
 ///
-/// This will work for raw, QCOW2, composite and Android sparse images.
-fn get_partition_size(partition: &File, path: &Path) -> Result<u64, Error> {
-    // TODO: Use `context` once disk::Error implements std::error::Error.
-    // TODO: Add check for is_sparse_file
-    Ok(create_disk_file(
-        partition.try_clone()?,
-        /* is_sparse_file */ false,
-        MAX_NESTING_DEPTH,
-        path,
-    )
-    .map_err(|e| anyhow!("Failed to open partition image: {}", e))?
-    .get_len()?)
+/// This will work for raw and Android sparse images. QCOW2 and composite images aren't supported.
+fn get_partition_size(file: &File) -> Result<u64, Error> {
+    match detect_image_type(file).context("failed to detect partition image type")? {
+        ImageType::Raw => Ok(file.metadata().context("failed to get metadata")?.len()),
+        ImageType::AndroidSparse => {
+            // Source: system/core/libsparse/sparse_format.h
+            #[repr(C)]
+            #[derive(Clone, Copy, Debug, AsBytes, FromZeroes, FromBytes)]
+            struct SparseHeader {
+                magic: u32,
+                major_version: u16,
+                minor_version: u16,
+                file_hdr_sz: u16,
+                chunk_hdr_size: u16,
+                blk_sz: u32,
+                total_blks: u32,
+                total_chunks: u32,
+                image_checksum: u32,
+            }
+            let mut header = SparseHeader::new_zeroed();
+            file.read_exact_at(header.as_bytes_mut(), 0)
+                .context("failed to read android sparse header")?;
+            let len = u64::from(header.total_blks)
+                .checked_mul(header.blk_sz.into())
+                .context("android sparse image len too big")?;
+            Ok(len)
+        }
+        t => bail!("unsupported partition image type: {t:?}"),
+    }
+}
+
+/// Image file types we can detect.
+#[derive(Debug, PartialEq, Eq)]
+enum ImageType {
+    Raw,
+    Qcow2,
+    CompositeDisk,
+    AndroidSparse,
+}
+
+/// Detect image type by looking for magic bytes.
+fn detect_image_type(file: &File) -> std::io::Result<ImageType> {
+    const CDISK_MAGIC: &str = "composite_disk\x1d";
+    const QCOW_MAGIC: u32 = 0x5146_49fb;
+    const SPARSE_HEADER_MAGIC: u32 = 0xed26ff3a;
+
+    let mut magic4 = [0u8; 4];
+    match file.read_exact_at(&mut magic4[..], 0) {
+        Ok(()) => {}
+        Err(e) if e.kind() == ErrorKind::UnexpectedEof => return Ok(ImageType::Raw),
+        Err(e) => return Err(e),
+    }
+    if magic4 == QCOW_MAGIC.to_be_bytes() {
+        return Ok(ImageType::Qcow2);
+    }
+    if magic4 == SPARSE_HEADER_MAGIC.to_le_bytes() {
+        return Ok(ImageType::AndroidSparse);
+    }
+
+    let mut buf = [0u8; CDISK_MAGIC.len()];
+    match file.read_exact_at(buf.as_bytes_mut(), 0) {
+        Ok(()) => {}
+        Err(e) if e.kind() == ErrorKind::UnexpectedEof => return Ok(ImageType::Raw),
+        Err(e) => return Err(e),
+    }
+    if buf == CDISK_MAGIC.as_bytes() {
+        return Ok(ImageType::CompositeDisk);
+    }
+
+    Ok(ImageType::Raw)
 }
diff --git a/android/virtmgr/src/crosvm.rs b/android/virtmgr/src/crosvm.rs
index 5886535..b2be736 100644
--- a/android/virtmgr/src/crosvm.rs
+++ b/android/virtmgr/src/crosvm.rs
@@ -57,7 +57,6 @@
 use rpcbinder::RpcServer;
 
 /// external/crosvm
-use base::UnixSeqpacketListener;
 use vm_control::{BalloonControlCommand, VmRequest, VmResponse};
 
 const CROSVM_PATH: &str = "/apex/com.android.virt/bin/crosvm";
@@ -1045,8 +1044,9 @@
     }
 
     for disk in config.disks {
+        // Disk file locking is disabled because of missing SELinux policies.
         command.arg("--block").arg(format!(
-            "path={},ro={}",
+            "path={},ro={},lock=false",
             add_preserved_fd(&mut preserved_fds, disk.image),
             !disk.writable,
         ));
@@ -1056,8 +1056,8 @@
         command.arg(add_preserved_fd(&mut preserved_fds, kernel));
     }
 
-    let control_sock = UnixSeqpacketListener::bind(crosvm_control_socket_path)
-        .context("failed to create control server")?;
+    let control_sock = create_crosvm_control_listener(crosvm_control_socket_path)
+        .context("failed to create control listener")?;
     command.arg("--socket").arg(add_preserved_fd(&mut preserved_fds, control_sock));
 
     if let Some(dt_overlay) = config.device_tree_overlay {
@@ -1271,3 +1271,22 @@
     let (read_fd, write_fd) = pipe2(OFlag::O_CLOEXEC)?;
     Ok((read_fd.into(), write_fd.into()))
 }
+
+/// Creates and binds a unix seqpacket listening socket to be passed as crosvm's `--socket`
+/// argument. See `UnixSeqpacketListener::bind` in crosvm's code for reference.
+fn create_crosvm_control_listener(crosvm_control_socket_path: &Path) -> Result<OwnedFd> {
+    use nix::sys::socket;
+    let fd = socket::socket(
+        socket::AddressFamily::Unix,
+        socket::SockType::SeqPacket,
+        socket::SockFlag::empty(),
+        None,
+    )
+    .context("socket failed")?;
+    socket::bind(fd.as_raw_fd(), &socket::UnixAddr::new(crosvm_control_socket_path)?)
+        .context("bind failed")?;
+    // The exact backlog size isn't imporant. crosvm uses 128 internally. We use 127 here
+    // because of a `nix` bug.
+    socket::listen(&fd, socket::Backlog::new(127).unwrap()).context("listen failed")?;
+    Ok(fd)
+}
diff --git a/android/virtualizationservice/aidl/Android.bp b/android/virtualizationservice/aidl/Android.bp
index c1bff5e..79a9d40 100644
--- a/android/virtualizationservice/aidl/Android.bp
+++ b/android/virtualizationservice/aidl/Android.bp
@@ -29,6 +29,7 @@
         rust: {
             enabled: true,
             apex_available: [
+                "//apex_available:platform",
                 "com.android.virt",
                 "com.android.compos",
                 "com.android.microfuchsia",
@@ -149,6 +150,7 @@
         rust: {
             enabled: true,
             apex_available: [
+                "//apex_available:platform",
                 "com.android.virt",
                 "com.android.compos",
                 "com.android.microfuchsia",
diff --git a/android/virtualizationservice/aidl/android/system/virtualizationservice/Partition.aidl b/android/virtualizationservice/aidl/android/system/virtualizationservice/Partition.aidl
index 11a2115..99dc648 100644
--- a/android/virtualizationservice/aidl/android/system/virtualizationservice/Partition.aidl
+++ b/android/virtualizationservice/aidl/android/system/virtualizationservice/Partition.aidl
@@ -20,7 +20,12 @@
     /** A label for the partition. */
     @utf8InCpp String label;
 
-    /** The backing file descriptor of the partition image. */
+    /**
+     * The backing file descriptor of the partition image.
+     *
+     * The image file must either be a raw binary file, or an android-sparse
+     * formatted file.
+     */
     ParcelFileDescriptor image;
 
     /** Whether the partition should be writable by the VM. */
diff --git a/build/apex/Android.bp b/build/apex/Android.bp
index f493202..be62d18 100644
--- a/build/apex/Android.bp
+++ b/build/apex/Android.bp
@@ -107,6 +107,7 @@
             filesystems: microdroid_filesystem_images,
             prebuilts: [
                 "rialto_bin",
+                "android_bootloader_crosvm_aarch64",
             ],
         },
         x86_64: {
@@ -125,6 +126,9 @@
                 default: [],
             }),
             filesystems: microdroid_filesystem_images,
+            prebuilts: [
+                "android_bootloader_crosvm_x86_64",
+            ],
         },
     },
     binaries: [
@@ -138,7 +142,6 @@
         "microdroid.json",
         "microdroid_kernel",
         "com.android.virt.init.rc",
-        "android_bootloader_crosvm_aarch64",
     ] + select(soong_config_variable("ANDROID", "avf_microdroid_guest_gki_version"), {
         "android15_66": [
             "microdroid_gki-android15-6.6_initrd_debuggable",
diff --git a/docs/device_assignment.md b/docs/device_assignment.md
index 4b2296c..6011d8f 100644
--- a/docs/device_assignment.md
+++ b/docs/device_assignment.md
@@ -205,6 +205,18 @@
 * `<sysfs_path>`: Sysfs path of the device in host, used to bind to the VFIO
   driver. Must be non-empty and unique in the XML.
 
+### List support assignable devices
+
+In order to query list of the devices that can be assigned to a pVM, run the
+following command:
+
+```bash
+adb shell /apex/com.android.virt/bin/vm info
+```
+
+All supported assignable devices will be located under the "Assignable devices:"
+section of the output.
+
 ## Boot with VM DTBO
 
 Bootloader should provide VM DTBO to both Android and pvmfw.
diff --git a/docs/img/rkpvm-dice-chain.png b/docs/img/rkpvm-dice-chain.png
new file mode 100644
index 0000000..6847f7f
--- /dev/null
+++ b/docs/img/rkpvm-dice-chain.png
Binary files differ
diff --git a/docs/vm_remote_attestation.md b/docs/vm_remote_attestation.md
index 79f44b9..ee20591 100644
--- a/docs/vm_remote_attestation.md
+++ b/docs/vm_remote_attestation.md
@@ -46,17 +46,17 @@
 spec.
 
 [open-dice]: https://android.googlesource.com/platform/external/open-dice/+/main/docs/android.md
-[rkpvm-marker]: https://android.googlesource.com/platform/external/open-dice/+/main/docs/android.md#Configuration-descriptor
-[rkp-hal]: https://android.googlesource.com/platform/hardware/interfaces/+/main/security/rkp/README.md
 
 ### pVM attestation
 
 Once the RKP VM is successfully attested, it acts as a trusted platform to
 attest pVMs. Leveraging its trusted status, the RKP VM validates the integrity
-of each pVM's DICE chain by comparing it against its own DICE chain. This
-validation process ensures that the pVMs are running in the expected VM
-environment and certifies the payload executed within each pVM. Currently, only
-Microdroid VMs are supported.
+of each [pVM DICE chain][pvm-dice-chain] by comparing it against its own DICE
+chain. This validation process ensures that the pVMs are running in the expected
+VM environment and certifies the payload executed within each pVM. Currently,
+only Microdroid VMs are supported.
+
+[pvm-dice-chain]: ./pvm_dice_chain.md
 
 ## API
 
@@ -113,13 +113,37 @@
 
 ## To Support It
 
-VM remote attestation is a strongly recommended feature from Android V. To support
-it, you only need to provide a valid VM DICE chain satisfying the following
-requirements:
+VM remote attestation is a strongly recommended feature from Android V. To
+support it, you only need to provide a valid VM DICE chain satisfying the
+following requirements:
 
-- The DICE chain must have a UDS-rooted public key registered at the RKP factory.
-- The DICE chain should have RKP VM markers that help identify RKP VM as required
-  by the [remote provisioning HAL][rkp-hal-markers].
+- The DICE chain must have a UDS-rooted public key registered at the RKP
+  factory.
+- The DICE chain must use [RKP VM markers][rkpvm-marker] to help identify the
+  RKP VM as required by the [remote provisioning HAL][rkp-hal].
+
+### RKP VM marker
+
+To support VM remote attestation, vendors must include an RKP VM marker in their
+DICE certificates. This marker should be present from the early boot stage
+within the TEE and continue through to the last DICE certificate before
+[pvmfw][pvmfw] takes over.
+
+![RKP VM DICE chain][rkpvm-dice-chain]
+
+Pvmfw will add an RKP VM marker when it's launching an RKP VM. The __continuous
+presence__ of this marker throughout the chain allows the RKP server to clearly
+identify legitimate RKP VM DICE chains.
+
+This mechanism also serves as a security measure. If an attacker tries to launch
+a malicious guest OS or payload, their DICE chain will be rejected by the RKP
+server because it will lack the RKP VM marker that pvmfw would have added in a
+genuine RKP VM boot process.
+
+[pvmfw]: ../guest/pvmfw/README.md
+[rkpvm-dice-chain]: img/rkpvm-dice-chain.png
+
+## To Disable It
 
 The feature is enabled by default. To disable it, you have two options:
 
@@ -133,4 +157,5 @@
 If you don't set any of these variables, VM remote attestation will be enabled
 by default.
 
-[rkp-hal-markers]: https://android.googlesource.com/platform/hardware/interfaces/+/main/security/rkp/README.md#hal
+[rkpvm-marker]: https://pigweed.googlesource.com/open-dice/+/HEAD/docs/android.md#configuration-descriptor
+[rkp-hal]: https://android.googlesource.com/platform/hardware/interfaces/+/main/security/rkp/README.md#hal
diff --git a/guest/microdroid_manager/src/main.rs b/guest/microdroid_manager/src/main.rs
index 7352a2c..8186e9d 100644
--- a/guest/microdroid_manager/src/main.rs
+++ b/guest/microdroid_manager/src/main.rs
@@ -654,7 +654,7 @@
     if requested {
         let status = Command::new("/system/bin/kexec_load").status()?;
         if !status.success() {
-            return Err(anyhow!("Failed to load crashkernel: {:?}", status));
+            return Err(anyhow!("Failed to load crashkernel: {status}"));
         }
         info!("ramdump is loaded: debuggable={debuggable}, ramdump={ramdump}");
     }
diff --git a/libs/framework-virtualization/src/android/system/virtualmachine/VirtualMachineConfig.java b/libs/framework-virtualization/src/android/system/virtualmachine/VirtualMachineConfig.java
index cb21ccf..de1b081 100644
--- a/libs/framework-virtualization/src/android/system/virtualmachine/VirtualMachineConfig.java
+++ b/libs/framework-virtualization/src/android/system/virtualmachine/VirtualMachineConfig.java
@@ -78,7 +78,8 @@
     private static final String TAG = "VirtualMachineConfig";
 
     private static String[] EMPTY_STRING_ARRAY = {};
-    private static final String U_BOOT_PREBUILT_PATH = "/apex/com.android.virt/etc/u-boot.bin";
+    private static final String U_BOOT_PREBUILT_PATH_ARM = "/apex/com.android.virt/etc/u-boot.bin";
+    private static final String U_BOOT_PREBUILT_PATH_X86 = "/apex/com.android.virt/etc/u-boot.rom";
 
     // These define the schema of the config file persisted on disk.
     // Please bump up the version number when adding a new key.
@@ -668,7 +669,11 @@
                         .orElse(null);
 
         if (config.kernel == null && config.bootloader == null) {
-            config.bootloader = openOrNull(new File(U_BOOT_PREBUILT_PATH), MODE_READ_ONLY);
+          if (Arrays.stream(Build.SUPPORTED_ABIS).anyMatch("x86_64"::equals)) {
+            config.bootloader = openOrNull(new File(U_BOOT_PREBUILT_PATH_X86), MODE_READ_ONLY);
+          } else {
+            config.bootloader = openOrNull(new File(U_BOOT_PREBUILT_PATH_ARM), MODE_READ_ONLY);
+          }
         }
 
         config.params =
diff --git a/libs/libinherited_fd/Android.bp b/libs/libinherited_fd/Android.bp
deleted file mode 100644
index 28ec2e5..0000000
--- a/libs/libinherited_fd/Android.bp
+++ /dev/null
@@ -1,44 +0,0 @@
-package {
-    default_applicable_licenses: ["Android-Apache-2.0"],
-}
-
-rust_defaults {
-    name: "libinherited_fd.defaults",
-    crate_name: "inherited_fd",
-    srcs: ["src/lib.rs"],
-    edition: "2021",
-    rustlibs: [
-        "libnix",
-        "libonce_cell",
-        "libthiserror",
-    ],
-}
-
-rust_library {
-    name: "libinherited_fd",
-    defaults: ["libinherited_fd.defaults"],
-    apex_available: [
-        "com.android.compos",
-        "com.android.virt",
-    ],
-}
-
-rust_test {
-    name: "libinherited_fd.test",
-    defaults: ["libinherited_fd.defaults"],
-    rustlibs: [
-        "libanyhow",
-        "libtempfile",
-    ],
-    host_supported: true,
-    test_suites: ["general-tests"],
-    test_options: {
-        unit_test: true,
-    },
-    // this is to run each test function in a separate process.
-    // note that they still run in parallel.
-    flags: [
-        "-C panic=abort",
-        "-Z panic_abort_tests",
-    ],
-}
diff --git a/libs/libinherited_fd/src/lib.rs b/libs/libinherited_fd/src/lib.rs
deleted file mode 100644
index f5e2d6b..0000000
--- a/libs/libinherited_fd/src/lib.rs
+++ /dev/null
@@ -1,270 +0,0 @@
-// Copyright 2024, The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//     http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-//! Library for safely obtaining `OwnedFd` for inherited file descriptors.
-
-use nix::fcntl::{fcntl, FdFlag, F_SETFD};
-use nix::libc;
-use std::collections::HashMap;
-use std::fs::canonicalize;
-use std::fs::read_dir;
-use std::os::fd::FromRawFd;
-use std::os::fd::OwnedFd;
-use std::os::fd::RawFd;
-use std::sync::Mutex;
-use std::sync::OnceLock;
-use thiserror::Error;
-
-/// Errors that can occur while taking an ownership of `RawFd`
-#[derive(Debug, PartialEq, Error)]
-pub enum Error {
-    /// init_once() not called
-    #[error("init_once() not called")]
-    NotInitialized,
-
-    /// Ownership already taken
-    #[error("Ownership of FD {0} is already taken")]
-    OwnershipTaken(RawFd),
-
-    /// Not an inherited file descriptor
-    #[error("FD {0} is either invalid file descriptor or not an inherited one")]
-    FileDescriptorNotInherited(RawFd),
-
-    /// Failed to set CLOEXEC
-    #[error("Failed to set CLOEXEC on FD {0}")]
-    FailCloseOnExec(RawFd),
-}
-
-static INHERITED_FDS: OnceLock<Mutex<HashMap<RawFd, Option<OwnedFd>>>> = OnceLock::new();
-
-/// Take ownership of all open file descriptors in this process, which later can be obtained by
-/// calling `take_fd_ownership`.
-///
-/// # Safety
-/// This function has to be called very early in the program before the ownership of any file
-/// descriptors (except stdin/out/err) is taken.
-pub unsafe fn init_once() -> Result<(), std::io::Error> {
-    let mut fds = HashMap::new();
-
-    let fd_path = canonicalize("/proc/self/fd")?;
-
-    for entry in read_dir(&fd_path)? {
-        let entry = entry?;
-
-        // Files in /prod/self/fd are guaranteed to be numbers. So parsing is always successful.
-        let file_name = entry.file_name();
-        let raw_fd = file_name.to_str().unwrap().parse::<RawFd>().unwrap();
-
-        // We don't take ownership of the stdio FDs as the Rust runtime owns them.
-        if [libc::STDIN_FILENO, libc::STDOUT_FILENO, libc::STDERR_FILENO].contains(&raw_fd) {
-            continue;
-        }
-
-        // Exceptional case: /proc/self/fd/* may be a dir fd created by read_dir just above. Since
-        // the file descriptor is owned by read_dir (and thus closed by it), we shouldn't take
-        // ownership to it.
-        if entry.path().read_link()? == fd_path {
-            continue;
-        }
-
-        // SAFETY: /proc/self/fd/* are file descriptors that are open. If `init_once()` was called
-        // at the very beginning of the program execution (as requested by the safety requirement
-        // of this function), this is the first time to claim the ownership of these file
-        // descriptors.
-        let owned_fd = unsafe { OwnedFd::from_raw_fd(raw_fd) };
-        fds.insert(raw_fd, Some(owned_fd));
-    }
-
-    INHERITED_FDS
-        .set(Mutex::new(fds))
-        .or(Err(std::io::Error::other("Inherited fds were already initialized")))
-}
-
-/// Take the ownership of the given `RawFd` and returns `OwnedFd` for it. The returned FD is set
-/// CLOEXEC. `Error` is returned when the ownership was already taken (by a prior call to this
-/// function with the same `RawFd`) or `RawFd` is not an inherited file descriptor.
-pub fn take_fd_ownership(raw_fd: RawFd) -> Result<OwnedFd, Error> {
-    let mut fds = INHERITED_FDS.get().ok_or(Error::NotInitialized)?.lock().unwrap();
-
-    if let Some(value) = fds.get_mut(&raw_fd) {
-        if let Some(owned_fd) = value.take() {
-            fcntl(raw_fd, F_SETFD(FdFlag::FD_CLOEXEC)).or(Err(Error::FailCloseOnExec(raw_fd)))?;
-            Ok(owned_fd)
-        } else {
-            Err(Error::OwnershipTaken(raw_fd))
-        }
-    } else {
-        Err(Error::FileDescriptorNotInherited(raw_fd))
-    }
-}
-
-#[cfg(test)]
-mod test {
-    use super::*;
-    use anyhow::Result;
-    use nix::fcntl::{fcntl, FdFlag, F_GETFD, F_SETFD};
-    use nix::unistd::close;
-    use std::os::fd::{AsRawFd, IntoRawFd};
-    use tempfile::tempfile;
-
-    struct Fixture {
-        fds: Vec<RawFd>,
-    }
-
-    impl Fixture {
-        fn setup(num_fds: usize) -> Result<Self> {
-            let mut fds = Vec::new();
-            for _ in 0..num_fds {
-                fds.push(tempfile()?.into_raw_fd());
-            }
-            Ok(Fixture { fds })
-        }
-
-        fn open_new_file(&mut self) -> Result<RawFd> {
-            let raw_fd = tempfile()?.into_raw_fd();
-            self.fds.push(raw_fd);
-            Ok(raw_fd)
-        }
-    }
-
-    impl Drop for Fixture {
-        fn drop(&mut self) {
-            self.fds.iter().for_each(|fd| {
-                let _ = close(*fd);
-            });
-        }
-    }
-
-    fn is_fd_opened(raw_fd: RawFd) -> bool {
-        fcntl(raw_fd, F_GETFD).is_ok()
-    }
-
-    #[test]
-    fn happy_case() -> Result<()> {
-        let fixture = Fixture::setup(2)?;
-        let f0 = fixture.fds[0];
-        let f1 = fixture.fds[1];
-
-        // SAFETY: assume files opened by Fixture are inherited ones
-        unsafe {
-            init_once()?;
-        }
-
-        let f0_owned = take_fd_ownership(f0)?;
-        let f1_owned = take_fd_ownership(f1)?;
-        assert_eq!(f0, f0_owned.as_raw_fd());
-        assert_eq!(f1, f1_owned.as_raw_fd());
-
-        drop(f0_owned);
-        drop(f1_owned);
-        assert!(!is_fd_opened(f0));
-        assert!(!is_fd_opened(f1));
-        Ok(())
-    }
-
-    #[test]
-    fn access_non_inherited_fd() -> Result<()> {
-        let mut fixture = Fixture::setup(2)?;
-
-        // SAFETY: assume files opened by Fixture are inherited ones
-        unsafe {
-            init_once()?;
-        }
-
-        let f = fixture.open_new_file()?;
-        assert_eq!(Some(Error::FileDescriptorNotInherited(f)), take_fd_ownership(f).err());
-        Ok(())
-    }
-
-    #[test]
-    fn call_init_once_multiple_times() -> Result<()> {
-        let _ = Fixture::setup(2)?;
-
-        // SAFETY: assume files opened by Fixture are inherited ones
-        unsafe {
-            init_once()?;
-        }
-
-        // SAFETY: for testing
-        let res = unsafe { init_once() };
-        assert!(res.is_err());
-        Ok(())
-    }
-
-    #[test]
-    fn access_without_init_once() -> Result<()> {
-        let fixture = Fixture::setup(2)?;
-
-        let f = fixture.fds[0];
-        assert_eq!(Some(Error::NotInitialized), take_fd_ownership(f).err());
-        Ok(())
-    }
-
-    #[test]
-    fn double_ownership() -> Result<()> {
-        let fixture = Fixture::setup(2)?;
-        let f = fixture.fds[0];
-
-        // SAFETY: assume files opened by Fixture are inherited ones
-        unsafe {
-            init_once()?;
-        }
-
-        let f_owned = take_fd_ownership(f)?;
-        let f_double_owned = take_fd_ownership(f);
-        assert_eq!(Some(Error::OwnershipTaken(f)), f_double_owned.err());
-
-        // just to highlight that f_owned is kept alive when the second call to take_fd_ownership
-        // is made.
-        drop(f_owned);
-        Ok(())
-    }
-
-    #[test]
-    fn take_drop_retake() -> Result<()> {
-        let fixture = Fixture::setup(2)?;
-        let f = fixture.fds[0];
-
-        // SAFETY: assume files opened by Fixture are inherited ones
-        unsafe {
-            init_once()?;
-        }
-
-        let f_owned = take_fd_ownership(f)?;
-        drop(f_owned);
-
-        let f_double_owned = take_fd_ownership(f);
-        assert_eq!(Some(Error::OwnershipTaken(f)), f_double_owned.err());
-        Ok(())
-    }
-
-    #[test]
-    fn cloexec() -> Result<()> {
-        let fixture = Fixture::setup(2)?;
-        let f = fixture.fds[0];
-
-        // SAFETY: assume files opened by Fixture are inherited ones
-        unsafe {
-            init_once()?;
-        }
-
-        // Intentionally cleaar cloexec to see if it is set by take_fd_ownership
-        fcntl(f, F_SETFD(FdFlag::empty()))?;
-
-        let f_owned = take_fd_ownership(f)?;
-        let flags = fcntl(f_owned.as_raw_fd(), F_GETFD)?;
-        assert_eq!(flags, FdFlag::FD_CLOEXEC.bits());
-        Ok(())
-    }
-}
diff --git a/libs/libvmclient/Android.bp b/libs/libvmclient/Android.bp
index 5bd59da..d318d0e 100644
--- a/libs/libvmclient/Android.bp
+++ b/libs/libvmclient/Android.bp
@@ -23,6 +23,7 @@
         "com.android.compos",
         "com.android.microfuchsia",
         "com.android.virt",
+        "//apex_available:platform",
     ],
 }
 
diff --git a/libs/libvmclient/src/lib.rs b/libs/libvmclient/src/lib.rs
index bc9d683..ce7d5a5 100644
--- a/libs/libvmclient/src/lib.rs
+++ b/libs/libvmclient/src/lib.rs
@@ -55,6 +55,7 @@
     time::Duration,
 };
 
+const EARLY_VIRTMGR_PATH: &str = "/apex/com.android.virt/bin/early_virtmgr";
 const VIRTMGR_PATH: &str = "/apex/com.android.virt/bin/virtmgr";
 const VIRTMGR_THREADS: usize = 2;
 
@@ -122,10 +123,20 @@
     /// Spawns a new instance of virtmgr, a child process that will host
     /// the VirtualizationService AIDL service.
     pub fn new() -> Result<VirtualizationService, io::Error> {
+        Self::new_with_path(VIRTMGR_PATH)
+    }
+
+    /// Spawns a new instance of early_virtmgr, a child process that will host
+    /// the VirtualizationService AIDL service for early VMs.
+    pub fn new_early() -> Result<VirtualizationService, io::Error> {
+        Self::new_with_path(EARLY_VIRTMGR_PATH)
+    }
+
+    fn new_with_path(virtmgr_path: &str) -> Result<VirtualizationService, io::Error> {
         let (wait_fd, ready_fd) = posix_pipe()?;
         let (client_fd, server_fd) = posix_socketpair()?;
 
-        let mut command = Command::new(VIRTMGR_PATH);
+        let mut command = Command::new(virtmgr_path);
         // Can't use BorrowedFd as it doesn't implement Display
         command.arg("--rpc-server-fd").arg(format!("{}", server_fd.as_raw_fd()));
         command.arg("--ready-fd").arg(format!("{}", ready_fd.as_raw_fd()));
diff --git a/tests/hostside/helper/java/com/android/microdroid/test/host/KvmHypTracer.java b/tests/hostside/helper/java/com/android/microdroid/test/host/KvmHypTracer.java
index 3814cdd..8604553 100644
--- a/tests/hostside/helper/java/com/android/microdroid/test/host/KvmHypTracer.java
+++ b/tests/hostside/helper/java/com/android/microdroid/test/host/KvmHypTracer.java
@@ -78,9 +78,9 @@
 /** This class provides utilities to interact with the hyp tracing subsystem */
 public final class KvmHypTracer {
 
-    private static final String HYP_TRACING_ROOT = "/sys/kernel/tracing/hyp/";
     private static final int DEFAULT_BUF_SIZE_KB = 4 * 1024;
 
+    private final String mHypTracingRoot;
     private final CommandRunner mRunner;
     private final ITestDevice mDevice;
     private final int mNrCpus;
@@ -88,17 +88,41 @@
 
     private final ArrayList<File> mTraces;
 
-    private void setNode(String node, int val) throws Exception {
-        mRunner.run("echo " + val + " > " + HYP_TRACING_ROOT + node);
+    private static String getHypTracingRoot(ITestDevice device) throws Exception {
+        String legacy = "/sys/kernel/tracing/hyp/";
+        String path = "/sys/kernel/tracing/hypervisor/";
+
+        if (device.doesFileExist(path)) {
+            return path;
+        }
+
+        if (device.doesFileExist(legacy)) {
+            return legacy;
+        }
+
+        throw new Exception("Hypervisor tracing not found");
     }
 
-    private static String eventDir(String event) {
-        return "events/hyp/" + event + "/";
+    private static String getHypEventsDir(String root) {
+        if (root.endsWith("/hypervisor/"))
+            return "events/hypervisor/";
+
+        return "events/hyp/";
     }
 
     public static boolean isSupported(ITestDevice device, String[] events) throws Exception {
-        for (String event : events) {
-            if (!device.doesFileExist(HYP_TRACING_ROOT + eventDir(event) + "/enable")) return false;
+        String dir;
+
+        try {
+            dir = getHypTracingRoot(device);
+            dir += getHypEventsDir(dir);
+        } catch (Exception e) {
+            return false;
+        }
+
+        for (String event: events) {
+            if (!device.doesFileExist(dir + event + "/enable"))
+                return false;
         }
         return true;
     }
@@ -108,6 +132,7 @@
                 .that(isSupported(device, events))
                 .isTrue();
 
+        mHypTracingRoot = getHypTracingRoot(device);
         mDevice = device;
         mRunner = new CommandRunner(mDevice);
         mTraces = new ArrayList<File>();
@@ -115,17 +140,25 @@
         mHypEvents = events;
     }
 
+    private void setNode(String node, int val) throws Exception {
+        mRunner.run("echo " + val + " > " + mHypTracingRoot + node);
+    }
+
     public String run(String payload_cmd) throws Exception {
         mTraces.clear();
 
         setNode("tracing_on", 0);
-        mRunner.run("echo 0 | tee " + HYP_TRACING_ROOT + "events/*/*/enable");
+        mRunner.run("echo 0 | tee " + mHypTracingRoot + "events/*/*/enable");
         setNode("buffer_size_kb", DEFAULT_BUF_SIZE_KB);
-        for (String event : mHypEvents) setNode(eventDir(event) + "/enable", 1);
+
+        for (String event: mHypEvents) {
+            setNode(getHypEventsDir(mHypTracingRoot) + event + "/enable", 1);
+        }
+
         setNode("trace", 0);
 
         /* Cat each per-cpu trace_pipe in its own tmp file in the background */
-        String cmd = "cd " + HYP_TRACING_ROOT + ";";
+        String cmd = "cd " + mHypTracingRoot + ";";
         String trace_pipes[] = new String[mNrCpus];
         for (int i = 0; i < mNrCpus; i++) {
             trace_pipes[i] = mRunner.run("mktemp -t trace_pipe.cpu" + i + ".XXXXXXXXXX");