Merge "MicrodroidHostTests: Fix typo in @CddTest" into main
diff --git a/OWNERS b/OWNERS
index 40c709f..717a4db 100644
--- a/OWNERS
+++ b/OWNERS
@@ -28,3 +28,10 @@
 tabba@google.com
 vdonnefort@google.com
 victorhsieh@google.com
+
+# Ferrochrome
+per-file android/FerrochromeApp/**=jiyong@google.com,jeongik@google.com
+per-file android/LinuxInstaller/**=jiyong@google.com,jeongik@google.com
+per-file android/TerminalApp/**=jiyong@google.com,jeongik@google.com
+per-file android/VmLauncherApp/**=jiyong@google.com,jeongik@google.com
+per-file libs/vm_launcher_lib/**=jiyong@google.com,jeongik@google.com
diff --git a/android/LinuxInstaller/linux_image_builder/setup_x86_64.sh b/android/LinuxInstaller/linux_image_builder/setup_x86_64.sh
index 9748ce2..c543b2a 100755
--- a/android/LinuxInstaller/linux_image_builder/setup_x86_64.sh
+++ b/android/LinuxInstaller/linux_image_builder/setup_x86_64.sh
@@ -4,7 +4,7 @@
 tempdir=$(mktemp -d)
 echo Get Debian image and dependencies...
 wget https://cloud.debian.org/images/cloud/bookworm/latest/debian-12-nocloud-amd64.raw -O ${tempdir}/debian.img
-wget https://github.com/tsl0922/ttyd/releases/download/1.7.7/ttyd.tyd.x86_64 -O ${tempdir}/ttyd
+wget https://github.com/tsl0922/ttyd/releases/download/1.7.7/ttyd.x86_64 -O ${tempdir}/ttyd
 
 echo Customize the image...
 virt-customize --commands-from-file <(sed "s|/tmp|$tempdir|g" commands) -a ${tempdir}/debian.img
diff --git a/android/LinuxInstaller/linux_image_builder/ttyd.service b/android/LinuxInstaller/linux_image_builder/ttyd.service
index 3a8f181..f71557d 100644
--- a/android/LinuxInstaller/linux_image_builder/ttyd.service
+++ b/android/LinuxInstaller/linux_image_builder/ttyd.service
@@ -3,7 +3,7 @@
 After=syslog.target
 After=network.target
 [Service]
-ExecStart=/usr/local/bin/ttyd -W login
+ExecStart=/usr/local/bin/ttyd -W screen -aAxR -S main login
 Type=simple
 Restart=always
 User=root
diff --git a/android/TerminalApp/Android.bp b/android/TerminalApp/Android.bp
index 3ae014e..1a7c581 100644
--- a/android/TerminalApp/Android.bp
+++ b/android/TerminalApp/Android.bp
@@ -9,8 +9,10 @@
     static_libs: [
         "vm_launcher_lib",
     ],
-    sdk_version: "system_current",
+    platform_apis: true,
+    privileged: true,
     optimize: {
+        proguard_flags_files: ["proguard.flags"],
         shrink_resources: true,
     },
     apex_available: [
diff --git a/android/TerminalApp/AndroidManifest.xml b/android/TerminalApp/AndroidManifest.xml
index c92da67..e338c49 100644
--- a/android/TerminalApp/AndroidManifest.xml
+++ b/android/TerminalApp/AndroidManifest.xml
@@ -2,9 +2,13 @@
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
     package="com.android.virtualization.terminal" >
 
+    <uses-permission android:name="android.permission.MANAGE_VIRTUAL_MACHINE" />
+    <uses-permission android:name="android.permission.USE_CUSTOM_VIRTUAL_MACHINE" />
     <uses-permission android:name="android.permission.INTERNET" />
-    <uses-permission android:name="com.android.virtualization.vmlauncher.permission.USE_VM_LAUNCHER"/>
+    <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
+    <uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE"/>
 
+    <uses-feature android:name="android.software.virtualization_framework" android:required="true" />
     <application
 	android:label="@string/app_name"
         android:icon="@mipmap/ic_launcher"
@@ -27,6 +31,20 @@
                 <category android:name="android.intent.category.LAUNCHER" />
             </intent-filter>
         </activity-alias>
+
+        <service
+            android:name="com.android.virtualization.vmlauncher.VmLauncherService"
+            android:enabled="true"
+            android:exported="false"
+            android:foregroundServiceType="specialUse">
+            <property
+                android:name="android.app.PROPERTY_SPECIAL_USE_FGS_SUBTYPE"
+                android:value="Run VM instances" />
+            <intent-filter>
+                <action android:name="android.virtualization.START_VM_LAUNCHER_SERVICE" />
+                <category android:name="android.intent.category.DEFAULT" />
+            </intent-filter>
+        </service>
     </application>
 
 </manifest>
diff --git a/android/TerminalApp/proguard.flags b/android/TerminalApp/proguard.flags
new file mode 100644
index 0000000..13ec24e
--- /dev/null
+++ b/android/TerminalApp/proguard.flags
@@ -0,0 +1,7 @@
+# Keep the no-args constructor of the deserialized class
+-keepclassmembers class com.android.virtualization.vmlauncher.ConfigJson {
+  <init>();
+}
+-keepclassmembers class com.android.virtualization.vmlauncher.ConfigJson$* {
+  <init>();
+}
diff --git a/android/VmLauncherApp/Android.bp b/android/VmLauncherApp/Android.bp
index 7dd2473..2e8cc93 100644
--- a/android/VmLauncherApp/Android.bp
+++ b/android/VmLauncherApp/Android.bp
@@ -11,7 +11,7 @@
         "android.system.virtualizationservice_internal-java",
         // TODO(b/331708504): will be removed when AVF framework handles surface
         "libcrosvm_android_display_service-java",
-        "gson",
+        "vm_launcher_lib",
     ],
     libs: [
         "framework-virtualization.impl",
diff --git a/android/VmLauncherApp/AndroidManifest.xml b/android/VmLauncherApp/AndroidManifest.xml
index 583fce7..4fb4b5c 100644
--- a/android/VmLauncherApp/AndroidManifest.xml
+++ b/android/VmLauncherApp/AndroidManifest.xml
@@ -6,8 +6,6 @@
     <uses-permission android:name="android.permission.USE_CUSTOM_VIRTUAL_MACHINE" />
     <uses-permission android:name="android.permission.INTERNET" />
     <uses-permission android:name="android.permission.RECORD_AUDIO" />
-    <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
-    <uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE"/>
     <uses-feature android:name="android.software.virtualization_framework" android:required="true" />
 
     <permission android:name="com.android.virtualization.vmlauncher.permission.USE_VM_LAUNCHER"
@@ -28,20 +26,6 @@
                 <category android:name="android.intent.category.DEFAULT" />
             </intent-filter>
         </activity>
-        <service
-            android:name=".VmLauncherService"
-            android:enabled="true"
-            android:exported="true"
-            android:permission="com.android.virtualization.vmlauncher.permission.USE_VM_LAUNCHER"
-            android:foregroundServiceType="specialUse">
-            <property
-                android:name="android.app.PROPERTY_SPECIAL_USE_FGS_SUBTYPE"
-                android:value="Run VM instances" />
-            <intent-filter>
-                <action android:name="android.virtualization.START_VM_LAUNCHER_SERVICE" />
-                <category android:name="android.intent.category.DEFAULT" />
-            </intent-filter>
-        </service>
 
     </application>
 
diff --git a/android/fd_server/Android.bp b/android/fd_server/Android.bp
index 32a8fec..748c660 100644
--- a/android/fd_server/Android.bp
+++ b/android/fd_server/Android.bp
@@ -18,7 +18,7 @@
         "liblog_rust",
         "libnix",
         "librpcbinder_rs",
-        "libsafe_ownedfd",
+        "librustutils",
     ],
     prefer_rlib: true,
     apex_available: ["com.android.virt"],
@@ -40,7 +40,7 @@
         "liblog_rust",
         "libnix",
         "librpcbinder_rs",
-        "libsafe_ownedfd",
+        "librustutils",
     ],
     prefer_rlib: true,
     test_suites: ["general-tests"],
diff --git a/android/fd_server/src/aidl.rs b/android/fd_server/src/aidl.rs
index 2f3697c..5f91987 100644
--- a/android/fd_server/src/aidl.rs
+++ b/android/fd_server/src/aidl.rs
@@ -14,21 +14,20 @@
  * limitations under the License.
  */
 
-use anyhow::{Context, Result};
+use anyhow::Result;
 use log::error;
 use nix::{
     errno::Errno, fcntl::openat, fcntl::OFlag, sys::stat::fchmod, sys::stat::mkdirat,
     sys::stat::mode_t, sys::stat::Mode, sys::statvfs::statvfs, sys::statvfs::Statvfs,
     unistd::unlinkat, unistd::UnlinkatFlags,
 };
-use safe_ownedfd::take_fd_ownership;
 use std::cmp::min;
 use std::collections::{btree_map, BTreeMap};
 use std::convert::TryInto;
 use std::fs::File;
 use std::io;
 use std::os::unix::fs::FileExt;
-use std::os::unix::io::{AsFd, AsRawFd, BorrowedFd, OwnedFd};
+use std::os::unix::io::{AsFd, AsRawFd, BorrowedFd, FromRawFd, OwnedFd};
 use std::path::{Component, Path, PathBuf, MAIN_SEPARATOR};
 use std::sync::{Arc, RwLock};
 
@@ -39,8 +38,7 @@
     get_fsverity_metadata_path, parse_fsverity_metadata, FSVerityMetadata,
 };
 use binder::{
-    BinderFeatures, ExceptionCode, Interface, IntoBinderResult, Result as BinderResult, Status,
-    StatusCode, Strong,
+    BinderFeatures, ExceptionCode, Interface, Result as BinderResult, Status, StatusCode, Strong,
 };
 
 /// Bitflags of forbidden file mode, e.g. setuid, setgid and sticky bit.
@@ -301,11 +299,9 @@
                     mode,
                 )
                 .map_err(new_errno_error)?;
-                let new_fd = take_fd_ownership(new_fd)
-                    .context("Failed to take ownership of fd for file")
-                    .or_service_specific_exception(-1)?;
-                let new_file = File::from(new_fd);
-                Ok((new_file.as_raw_fd(), FdConfig::ReadWrite(new_file)))
+                // SAFETY: new_fd is just created and not an error.
+                let new_file = unsafe { File::from_raw_fd(new_fd) };
+                Ok((new_fd, FdConfig::ReadWrite(new_file)))
             }
             _ => Err(new_errno_error(Errno::ENOTDIR)),
         })
@@ -331,10 +327,9 @@
                     Mode::empty(),
                 )
                 .map_err(new_errno_error)?;
-                let fd_owner = take_fd_ownership(new_dir_fd)
-                    .context("Failed to take ownership of the fd for directory")
-                    .or_service_specific_exception(-1)?;
-                Ok((fd_owner.as_raw_fd(), FdConfig::OutputDir(fd_owner)))
+                // SAFETY: new_dir_fd is just created and not an error.
+                let fd_owner = unsafe { OwnedFd::from_raw_fd(new_dir_fd) };
+                Ok((new_dir_fd, FdConfig::OutputDir(fd_owner)))
             }
             _ => Err(new_errno_error(Errno::ENOTDIR)),
         })
@@ -413,11 +408,9 @@
 
 fn open_readonly_at(dir_fd: BorrowedFd, path: &Path) -> nix::Result<File> {
     let new_fd = openat(Some(dir_fd.as_raw_fd()), path, OFlag::O_RDONLY, Mode::empty())?;
-    let new_fd = take_fd_ownership(new_fd).map_err(|e| match e {
-        safe_ownedfd::Error::Errno(e) => e,
-        _ => Errno::UnknownErrno,
-    })?;
-    Ok(File::from(new_fd))
+    // SAFETY: new_fd is just created successfully and not owned.
+    let new_file = unsafe { File::from_raw_fd(new_fd) };
+    Ok(new_file)
 }
 
 fn validate_and_cast_offset(offset: i64) -> Result<u64, Status> {
diff --git a/android/fd_server/src/main.rs b/android/fd_server/src/main.rs
index 26315bf..07f0896 100644
--- a/android/fd_server/src/main.rs
+++ b/android/fd_server/src/main.rs
@@ -29,9 +29,10 @@
 use log::debug;
 use nix::sys::stat::{umask, Mode};
 use rpcbinder::RpcServer;
+use rustutils::inherited_fd::take_fd_ownership;
 use std::collections::BTreeMap;
 use std::fs::File;
-use std::os::unix::io::{FromRawFd, OwnedFd};
+use std::os::unix::io::OwnedFd;
 
 use aidl::{FdConfig, FdService};
 use authfs_fsverity_metadata::parse_fsverity_metadata;
@@ -39,20 +40,6 @@
 // TODO(b/259920193): support dynamic port for multiple fd_server instances
 const RPC_SERVICE_PORT: u32 = 3264;
 
-fn is_fd_valid(fd: i32) -> bool {
-    // SAFETY: a query-only syscall
-    let retval = unsafe { libc::fcntl(fd, libc::F_GETFD) };
-    retval >= 0
-}
-
-fn fd_to_owned<T: FromRawFd>(fd: i32) -> Result<T> {
-    if !is_fd_valid(fd) {
-        bail!("Bad FD: {}", fd);
-    }
-    // SAFETY: The caller is supposed to provide valid FDs to this process.
-    Ok(unsafe { T::from_raw_fd(fd) })
-}
-
 fn parse_arg_ro_fds(arg: &str) -> Result<(i32, FdConfig)> {
     let result: Result<Vec<i32>, _> = arg.split(':').map(|x| x.parse::<i32>()).collect();
     let fds = result?;
@@ -62,13 +49,13 @@
     Ok((
         fds[0],
         FdConfig::Readonly {
-            file: fd_to_owned(fds[0])?,
+            file: take_fd_ownership(fds[0])?.into(),
             // Alternative metadata source, if provided
             alt_metadata: fds
                 .get(1)
-                .map(|fd| fd_to_owned(*fd))
+                .map(|fd| take_fd_ownership(*fd))
                 .transpose()?
-                .and_then(|f| parse_fsverity_metadata(f).ok()),
+                .and_then(|f| parse_fsverity_metadata(f.into()).ok()),
         },
     ))
 }
@@ -105,23 +92,26 @@
         fd_pool.insert(fd, config);
     }
     for fd in args.rw_fds {
-        let file = fd_to_owned::<File>(fd)?;
+        let file: File = take_fd_ownership(fd)?.into();
         if file.metadata()?.len() > 0 {
             bail!("File is expected to be empty");
         }
         fd_pool.insert(fd, FdConfig::ReadWrite(file));
     }
     for fd in args.ro_dirs {
-        fd_pool.insert(fd, FdConfig::InputDir(fd_to_owned(fd)?));
+        fd_pool.insert(fd, FdConfig::InputDir(take_fd_ownership(fd)?));
     }
     for fd in args.rw_dirs {
-        fd_pool.insert(fd, FdConfig::OutputDir(fd_to_owned(fd)?));
+        fd_pool.insert(fd, FdConfig::OutputDir(take_fd_ownership(fd)?));
     }
-    let ready_fd = args.ready_fd.map(fd_to_owned).transpose()?;
+    let ready_fd = args.ready_fd.map(take_fd_ownership).transpose()?;
     Ok((fd_pool, ready_fd))
 }
 
 fn main() -> Result<()> {
+    // SAFETY: nobody has taken ownership of the inherited FDs yet.
+    unsafe { rustutils::inherited_fd::init_once()? };
+
     android_logger::init_once(
         android_logger::Config::default()
             .with_tag("fd_server")
diff --git a/android/virtmgr/Android.bp b/android/virtmgr/Android.bp
index 62ff8d8..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",
@@ -54,7 +53,6 @@
         "libregex",
         "librpcbinder_rs",
         "librustutils",
-        "libsafe_ownedfd",
         "libsemver",
         "libselinux_bindgen",
         "libserde",
@@ -71,6 +69,7 @@
         "liblibfdt",
         "libfsfdt",
         "libhypervisor_props",
+        "libzerocopy",
         "libuuid",
         // TODO(b/202115393) stabilize the interface
         "packagemanager_aidl-rust",
@@ -95,6 +94,13 @@
     apex_available: ["com.android.virt"],
 }
 
+xsd_config {
+    name: "early_vms",
+    srcs: ["early_vms.xsd"],
+    api_dir: "schema",
+    package_name: "android.system.virtualizationservice",
+}
+
 rust_test {
     name: "virtualizationmanager_device_test",
     srcs: ["src/main.rs"],
diff --git a/android/virtmgr/early_vms.xsd b/android/virtmgr/early_vms.xsd
new file mode 100644
index 0000000..14dbf7b
--- /dev/null
+++ b/android/virtmgr/early_vms.xsd
@@ -0,0 +1,38 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright (C) 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.
+-->
+<!-- KEEP IN SYNC WITH aidl.rs -->
+<xs:schema version="2.0"
+           xmlns:xs="http://www.w3.org/2001/XMLSchema">
+    <xs:element name="early_vms">
+        <xs:complexType>
+            <xs:sequence>
+                <xs:element name="early_vm" type="early_vm" minOccurs="0" maxOccurs="unbounded"/>
+            </xs:sequence>
+        </xs:complexType>
+    </xs:element>
+    <xs:complexType name="early_vm">
+        <!-- Name of the VM, which will be passed to VirtualMachineConfig. -->
+        <xs:attribute name="name" type="xs:string"/>
+        <!-- CID of the VM. Available ranges:
+             * system: 100 ~ 199
+             * system_ext / product: 200 ~ 299
+             * vendor / odm: 300 ~ 399
+        -->
+        <xs:attribute name="cid" type="xs:int"/>
+        <!-- Absolute file path of the client executable running the VM. -->
+        <xs:attribute name="path" type="xs:string"/>
+    </xs:complexType>
+</xs:schema>
diff --git a/android/virtmgr/schema/current.txt b/android/virtmgr/schema/current.txt
new file mode 100644
index 0000000..b21c909
--- /dev/null
+++ b/android/virtmgr/schema/current.txt
@@ -0,0 +1,27 @@
+// Signature format: 2.0
+package android.system.virtualizationservice {
+
+  public class EarlyVm {
+    ctor public EarlyVm();
+    method public int getCid();
+    method public String getName();
+    method public String getPath();
+    method public void setCid(int);
+    method public void setName(String);
+    method public void setPath(String);
+  }
+
+  public class EarlyVms {
+    ctor public EarlyVms();
+    method public java.util.List<android.system.virtualizationservice.EarlyVm> getEarly_vm();
+  }
+
+  public class XmlParser {
+    ctor public XmlParser();
+    method public static android.system.virtualizationservice.EarlyVms read(java.io.InputStream) throws javax.xml.datatype.DatatypeConfigurationException, java.io.IOException, org.xmlpull.v1.XmlPullParserException;
+    method public static String readText(org.xmlpull.v1.XmlPullParser) throws java.io.IOException, org.xmlpull.v1.XmlPullParserException;
+    method public static void skip(org.xmlpull.v1.XmlPullParser) throws java.io.IOException, org.xmlpull.v1.XmlPullParserException;
+  }
+
+}
+
diff --git a/android/virtmgr/schema/last_current.txt b/android/virtmgr/schema/last_current.txt
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/android/virtmgr/schema/last_current.txt
diff --git a/android/virtmgr/schema/last_removed.txt b/android/virtmgr/schema/last_removed.txt
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/android/virtmgr/schema/last_removed.txt
diff --git a/android/virtmgr/schema/removed.txt b/android/virtmgr/schema/removed.txt
new file mode 100644
index 0000000..d802177
--- /dev/null
+++ b/android/virtmgr/schema/removed.txt
@@ -0,0 +1 @@
+// Signature format: 2.0
diff --git a/android/virtmgr/src/aidl.rs b/android/virtmgr/src/aidl.rs
index fb3d353..87fb611 100644
--- a/android/virtmgr/src/aidl.rs
+++ b/android/virtmgr/src/aidl.rs
@@ -17,7 +17,7 @@
 use crate::{get_calling_pid, get_calling_uid, get_this_pid};
 use crate::atom::{write_vm_booted_stats, write_vm_creation_stats};
 use crate::composite::make_composite_image;
-use crate::crosvm::{AudioConfig, CrosvmConfig, DiskFile, DisplayConfig, GpuConfig, InputDeviceOption, PayloadState, VmContext, VmInstance, VmState};
+use crate::crosvm::{AudioConfig, CrosvmConfig, DiskFile, DisplayConfig, GpuConfig, InputDeviceOption, PayloadState, UsbConfig, VmContext, VmInstance, VmState};
 use crate::debug_config::DebugConfig;
 use crate::dt_overlay::{create_device_tree_overlay, VM_DT_OVERLAY_MAX_SIZE, VM_DT_OVERLAY_PATH};
 use crate::payload::{add_microdroid_payload_images, add_microdroid_system_images, add_microdroid_vendor_image};
@@ -45,6 +45,7 @@
     VirtualMachineRawConfig::VirtualMachineRawConfig,
     VirtualMachineState::VirtualMachineState,
 };
+use android_system_virtualizationservice_internal::aidl::android::system::virtualizationservice_internal::IGlobalVmContext::IGlobalVmContext;
 use android_system_virtualizationservice_internal::aidl::android::system::virtualizationservice_internal::IVirtualizationServiceInternal::IVirtualizationServiceInternal;
 use android_system_virtualmachineservice::aidl::android::system::virtualmachineservice::IVirtualMachineService::{
         BnVirtualMachineService, IVirtualMachineService,
@@ -72,17 +73,18 @@
 use nix::unistd::pipe;
 use rpcbinder::RpcServer;
 use rustutils::system_properties;
-use safe_ownedfd::take_fd_ownership;
 use semver::VersionReq;
+use serde::Deserialize;
 use std::collections::HashSet;
 use std::convert::TryInto;
 use std::fs;
 use std::ffi::CStr;
-use std::fs::{canonicalize, read_dir, remove_file, File, OpenOptions};
+use std::fs::{canonicalize, create_dir_all, read_dir, remove_dir_all, remove_file, File, OpenOptions};
 use std::io::{BufRead, BufReader, Error, ErrorKind, Seek, SeekFrom, Write};
 use std::iter;
 use std::num::{NonZeroU16, NonZeroU32};
-use std::os::unix::io::{AsRawFd, IntoRawFd};
+use std::ops::Range;
+use std::os::unix::io::{AsRawFd, FromRawFd, IntoRawFd};
 use std::os::unix::raw::pid_t;
 use std::path::{Path, PathBuf};
 use std::sync::{Arc, Mutex, Weak, LazyLock};
@@ -120,8 +122,12 @@
 
 pub static GLOBAL_SERVICE: LazyLock<Strong<dyn IVirtualizationServiceInternal>> =
     LazyLock::new(|| {
-        wait_for_interface(BINDER_SERVICE_IDENTIFIER)
-            .expect("Could not connect to VirtualizationServiceInternal")
+        if cfg!(early) {
+            panic!("Early virtmgr must not connect to VirtualizatinoServiceInternal")
+        } else {
+            wait_for_interface(BINDER_SERVICE_IDENTIFIER)
+                .expect("Could not connect to VirtualizationServiceInternal")
+        }
     });
 static SUPPORTED_OS_NAMES: LazyLock<HashSet<String>> =
     LazyLock::new(|| get_supported_os_names().expect("Failed to get list of supported os names"));
@@ -340,11 +346,110 @@
     }
 }
 
+/// Implementation of the AIDL `IGlobalVmContext` interface for early VMs.
+#[derive(Debug, Default)]
+struct EarlyVmContext {
+    /// The unique CID assigned to the VM for vsock communication.
+    cid: Cid,
+    /// Temporary directory for this VM instance.
+    temp_dir: PathBuf,
+}
+
+impl EarlyVmContext {
+    fn new(cid: Cid, temp_dir: PathBuf) -> Result<Self> {
+        // Remove the entire directory before creating a VM. Early VMs use predefined CIDs and AVF
+        // should trust clients, e.g. they won't run two VMs at the same time
+        let _ = remove_dir_all(&temp_dir);
+        create_dir_all(&temp_dir).context(format!("can't create '{}'", temp_dir.display()))?;
+
+        Ok(Self { cid, temp_dir })
+    }
+}
+
+impl Interface for EarlyVmContext {}
+
+impl Drop for EarlyVmContext {
+    fn drop(&mut self) {
+        if let Err(e) = remove_dir_all(&self.temp_dir) {
+            error!("Cannot remove {} upon dropping: {e}", self.temp_dir.display());
+        }
+    }
+}
+
+impl IGlobalVmContext for EarlyVmContext {
+    fn getCid(&self) -> binder::Result<i32> {
+        Ok(self.cid as i32)
+    }
+
+    fn getTemporaryDirectory(&self) -> binder::Result<String> {
+        Ok(self.temp_dir.to_string_lossy().to_string())
+    }
+
+    fn setHostConsoleName(&self, _pathname: &str) -> binder::Result<()> {
+        Err(Status::new_exception_str(
+            ExceptionCode::UNSUPPORTED_OPERATION,
+            Some("Early VM doesn't support setting host console name"),
+        ))
+    }
+}
+
+fn find_partition(path: &Path) -> binder::Result<String> {
+    match path.components().nth(1) {
+        Some(std::path::Component::Normal(partition)) => {
+            Ok(partition.to_string_lossy().into_owned())
+        }
+        _ => Err(anyhow!("Can't find partition in '{}'", path.display()))
+            .or_service_specific_exception(-1),
+    }
+}
+
 impl VirtualizationService {
     pub fn init() -> VirtualizationService {
         VirtualizationService::default()
     }
 
+    fn create_early_vm_context(
+        &self,
+        config: &VirtualMachineConfig,
+    ) -> binder::Result<(VmContext, Cid, PathBuf)> {
+        let calling_exe_path = format!("/proc/{}/exe", get_calling_pid());
+        let link = fs::read_link(&calling_exe_path)
+            .context(format!("can't read_link '{calling_exe_path}'"))
+            .or_service_specific_exception(-1)?;
+        let partition = find_partition(&link)?;
+
+        let name = match config {
+            VirtualMachineConfig::RawConfig(config) => &config.name,
+            VirtualMachineConfig::AppConfig(config) => &config.name,
+        };
+        let early_vm =
+            find_early_vm_for_partition(&partition, name).or_service_specific_exception(-1)?;
+        if Path::new(&early_vm.path) != link {
+            return Err(anyhow!(
+                "VM '{name}' in partition '{partition}' must be created with '{}', not '{}'",
+                &early_vm.path,
+                link.display()
+            ))
+            .or_service_specific_exception(-1);
+        }
+
+        let cid = early_vm.cid as Cid;
+        let temp_dir = PathBuf::from(format!("/mnt/vm/early/{cid}"));
+
+        let context = EarlyVmContext::new(cid, temp_dir.clone())
+            .context(format!("Can't create early vm contexts for {cid}"))
+            .or_service_specific_exception(-1)?;
+        let service = VirtualMachineService::new_binder(self.state.clone(), cid).as_binder();
+
+        // Start VM service listening for connections from the new CID on port=CID.
+        let port = cid;
+        let vm_server = RpcServer::new_vsock(service, cid, port)
+            .context(format!("Could not start RpcServer on port {port}"))
+            .or_service_specific_exception(-1)?;
+        vm_server.start();
+        Ok((VmContext::new(Strong::new(Box::new(context)), vm_server), cid, temp_dir))
+    }
+
     fn create_vm_context(
         &self,
         requester_debug_pid: pid_t,
@@ -386,8 +491,16 @@
 
         check_config_features(config)?;
 
+        if cfg!(early) {
+            check_config_allowed_for_early_vms(config)?;
+        }
+
         // Allocating VM context checks the MANAGE_VIRTUAL_MACHINE permission.
-        let (vm_context, cid, temporary_directory) = self.create_vm_context(requester_debug_pid)?;
+        let (vm_context, cid, temporary_directory) = if cfg!(early) {
+            self.create_early_vm_context(config)?
+        } else {
+            self.create_vm_context(requester_debug_pid)?
+        };
 
         if is_custom_config(config) {
             check_use_custom_virtual_machine()?;
@@ -584,6 +697,13 @@
             None
         };
 
+        let usb_config = config
+            .usbConfig
+            .as_ref()
+            .map(UsbConfig::new)
+            .unwrap_or(Ok(UsbConfig { controller: false }))
+            .or_binder_exception(ExceptionCode::BAD_PARCELABLE)?;
+
         // Actually start the VM.
         let crosvm_config = CrosvmConfig {
             cid,
@@ -623,6 +743,7 @@
             gpu_config,
             audio_config,
             no_balloon: config.noBalloon,
+            usb_config,
         };
         let instance = Arc::new(
             VmInstance::new(
@@ -1110,6 +1231,10 @@
 
 /// Checks whether the caller has a specific permission
 fn check_permission(perm: &str) -> binder::Result<()> {
+    if cfg!(early) {
+        // Skip permission check for early VMs, in favor of SELinux
+        return Ok(());
+    }
     let calling_pid = get_calling_pid();
     let calling_uid = get_calling_uid();
     // Root can do anything
@@ -1279,7 +1404,7 @@
         let stream = VsockStream::connect_with_cid_port(self.instance.cid, port)
             .context("Failed to connect")
             .or_service_specific_exception(-1)?;
-        vsock_stream_to_pfd(stream)
+        Ok(vsock_stream_to_pfd(stream))
     }
 
     fn setHostConsoleName(&self, ptsname: &str) -> binder::Result<()> {
@@ -1438,12 +1563,10 @@
 }
 
 /// Converts a `VsockStream` to a `ParcelFileDescriptor`.
-fn vsock_stream_to_pfd(stream: VsockStream) -> binder::Result<ParcelFileDescriptor> {
-    let owned_fd = take_fd_ownership(stream.into_raw_fd())
-        .context("Failed to take ownership of the vsock stream")
-        .with_log()
-        .or_service_specific_exception(-1)?;
-    Ok(ParcelFileDescriptor::new(owned_fd))
+fn vsock_stream_to_pfd(stream: VsockStream) -> ParcelFileDescriptor {
+    // SAFETY: ownership is transferred from stream to f
+    let f = unsafe { File::from_raw_fd(stream.into_raw_fd()) };
+    ParcelFileDescriptor::new(f)
 }
 
 /// Parses the platform version requirement string.
@@ -1583,6 +1706,13 @@
     Ok(())
 }
 
+fn check_config_allowed_for_early_vms(config: &VirtualMachineConfig) -> binder::Result<()> {
+    check_no_vendor_modules(config)?;
+    check_no_devices(config)?;
+
+    Ok(())
+}
+
 fn clone_or_prepare_logger_fd(
     fd: Option<&ParcelFileDescriptor>,
     tag: String,
@@ -1796,6 +1926,74 @@
     }
 }
 
+// KEEP IN SYNC WITH early_vms.xsd
+#[derive(Debug, Deserialize, PartialEq)]
+struct EarlyVm {
+    #[allow(dead_code)]
+    name: String,
+    #[allow(dead_code)]
+    cid: i32,
+    #[allow(dead_code)]
+    path: String,
+}
+
+#[derive(Debug, Default, Deserialize)]
+struct EarlyVms {
+    #[allow(dead_code)]
+    early_vm: Vec<EarlyVm>,
+}
+
+fn range_for_partition(partition: &str) -> Result<Range<Cid>> {
+    match partition {
+        "system" => Ok(100..200),
+        "system_ext" | "product" => Ok(200..300),
+        _ => Err(anyhow!("Early VMs are not supported for {partition}")),
+    }
+}
+
+fn find_early_vm(xml_path: &Path, cid_range: &Range<Cid>, name: &str) -> Result<EarlyVm> {
+    if !xml_path.exists() {
+        bail!("{} doesn't exist", xml_path.display());
+    }
+
+    let xml =
+        fs::read(xml_path).with_context(|| format!("Failed to read {}", xml_path.display()))?;
+    let xml = String::from_utf8(xml)
+        .with_context(|| format!("{} is not a valid UTF-8 file", xml_path.display()))?;
+    let early_vms: EarlyVms = serde_xml_rs::from_str(&xml)
+        .with_context(|| format!("Can't parse {}", xml_path.display()))?;
+
+    let mut found_vm: Option<EarlyVm> = None;
+
+    for early_vm in early_vms.early_vm {
+        if early_vm.name != name {
+            continue;
+        }
+
+        let cid = early_vm
+            .cid
+            .try_into()
+            .with_context(|| format!("Invalid CID value {}", early_vm.cid))?;
+
+        if !cid_range.contains(&cid) {
+            bail!("VM '{}' uses CID {cid} which is out of range. Available CIDs for '{}': {cid_range:?}", xml_path.display(), early_vm.name);
+        }
+
+        if found_vm.is_some() {
+            bail!("Multiple VMs named {name} are found in {}", xml_path.display());
+        }
+
+        found_vm = Some(early_vm);
+    }
+
+    found_vm.ok_or_else(|| anyhow!("Can't find {name} in {}", xml_path.display()))
+}
+
+fn find_early_vm_for_partition(partition: &str, name: &str) -> Result<EarlyVm> {
+    let cid_range = range_for_partition(partition)?;
+    find_early_vm(Path::new(&format!("/{partition}/etc/avf/early_vms.xml")), &cid_range, name)
+}
+
 #[cfg(test)]
 mod tests {
     use super::*;
@@ -2011,4 +2209,69 @@
         }
         Ok(())
     }
+
+    #[test]
+    fn test_find_early_vms_from_xml() -> Result<()> {
+        let tmp_dir = tempfile::TempDir::new()?;
+        let tmp_dir_path = tmp_dir.path().to_owned();
+        let xml_path = tmp_dir_path.join("early_vms.xml");
+
+        std::fs::write(
+            &xml_path,
+            br#"<?xml version="1.0" encoding="utf-8"?>
+        <early_vms>
+            <early_vm>
+                <name>vm_demo_native_early</name>
+                <cid>123</cid>
+                <path>/system/bin/vm_demo_native_early</path>
+            </early_vm>
+            <early_vm>
+                <name>vm_demo_duplicated_name</name>
+                <cid>456</cid>
+                <path>/system/bin/vm_demo_duplicated_name_1</path>
+            </early_vm>
+            <early_vm>
+                <name>vm_demo_duplicated_name</name>
+                <cid>789</cid>
+                <path>/system/bin/vm_demo_duplicated_name_2</path>
+            </early_vm>
+            <early_vm>
+                <name>vm_demo_invalid_cid_1</name>
+                <cid>-1</cid>
+                <path>/system/bin/vm_demo_invalid_cid_1</path>
+            </early_vm>
+            <early_vm>
+                <name>vm_demo_invalid_cid_2</name>
+                <cid>999999</cid>
+                <path>/system/bin/vm_demo_invalid_cid_2</path>
+            </early_vm>
+        </early_vms>
+        "#,
+        )?;
+
+        let cid_range = 100..1000;
+
+        let result = find_early_vm(&xml_path, &cid_range, "vm_demo_native_early")?;
+        let expected = EarlyVm {
+            name: "vm_demo_native_early".to_owned(),
+            cid: 123,
+            path: "/system/bin/vm_demo_native_early".to_owned(),
+        };
+        assert_eq!(result, expected);
+
+        assert!(
+            find_early_vm(&xml_path, &cid_range, "vm_demo_duplicated_name").is_err(),
+            "should fail"
+        );
+        assert!(
+            find_early_vm(&xml_path, &cid_range, "vm_demo_invalid_cid_1").is_err(),
+            "should fail"
+        );
+        assert!(
+            find_early_vm(&xml_path, &cid_range, "vm_demo_invalid_cid_2").is_err(),
+            "should fail"
+        );
+
+        Ok(())
+    }
 }
diff --git a/android/virtmgr/src/atom.rs b/android/virtmgr/src/atom.rs
index 1d2d191..45b020e 100644
--- a/android/virtmgr/src/atom.rs
+++ b/android/virtmgr/src/atom.rs
@@ -99,6 +99,10 @@
     is_protected: bool,
     ret: &binder::Result<Strong<dyn IVirtualMachine>>,
 ) {
+    if cfg!(early) {
+        info!("Writing VmCreationRequested atom for early VMs is not implemented; skipping");
+        return;
+    }
     let creation_succeeded;
     let binder_exception_code;
     match ret {
@@ -165,6 +169,11 @@
     vm_identifier: &str,
     vm_start_timestamp: Option<SystemTime>,
 ) {
+    if cfg!(early) {
+        info!("Writing VmCreationRequested atom for early VMs is not implemented; skipping");
+        return;
+    }
+
     let vm_identifier = vm_identifier.to_owned();
     let duration = get_duration(vm_start_timestamp);
 
@@ -190,6 +199,10 @@
     exit_signal: Option<i32>,
     vm_metric: &VmMetric,
 ) {
+    if cfg!(early) {
+        info!("Writing VmExited atom for early VMs is not implemented; skipping");
+        return;
+    }
     let vm_identifier = vm_identifier.to_owned();
     let elapsed_time_millis = get_duration(vm_metric.start_timestamp).as_millis() as i64;
     let guest_time_millis = vm_metric.cpu_guest_time.unwrap_or_default();
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 9d688a2..b2be736 100644
--- a/android/virtmgr/src/crosvm.rs
+++ b/android/virtmgr/src/crosvm.rs
@@ -47,6 +47,7 @@
     AudioConfig::AudioConfig as AudioConfigParcelable,
     DisplayConfig::DisplayConfig as DisplayConfigParcelable,
     GpuConfig::GpuConfig as GpuConfigParcelable,
+    UsbConfig::UsbConfig as UsbConfigParcelable,
 };
 use android_system_virtualizationservice_internal::aidl::android::system::virtualizationservice_internal::IGlobalVmContext::IGlobalVmContext;
 use android_system_virtualizationservice_internal::aidl::android::system::virtualizationservice_internal::IBoundDevice::IBoundDevice;
@@ -56,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";
@@ -134,6 +134,7 @@
     pub gpu_config: Option<GpuConfig>,
     pub audio_config: Option<AudioConfig>,
     pub no_balloon: bool,
+    pub usb_config: UsbConfig,
 }
 
 #[derive(Debug)]
@@ -149,6 +150,17 @@
 }
 
 #[derive(Debug)]
+pub struct UsbConfig {
+    pub controller: bool,
+}
+
+impl UsbConfig {
+    pub fn new(raw_config: &UsbConfigParcelable) -> Result<UsbConfig> {
+        Ok(UsbConfig { controller: raw_config.controller })
+    }
+}
+
+#[derive(Debug)]
 pub struct DisplayConfig {
     pub width: NonZeroU32,
     pub height: NonZeroU32,
@@ -900,6 +912,10 @@
         command.arg("--no-balloon");
     }
 
+    if !config.usb_config.controller {
+        command.arg("--no-usb");
+    }
+
     let mut memory_mib = config.memory_mib;
 
     if config.protected {
@@ -1028,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,
         ));
@@ -1039,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 {
@@ -1254,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/virtmgr/src/main.rs b/android/virtmgr/src/main.rs
index a4e75a7..1625009 100644
--- a/android/virtmgr/src/main.rs
+++ b/android/virtmgr/src/main.rs
@@ -33,8 +33,8 @@
 use std::sync::LazyLock;
 use clap::Parser;
 use nix::unistd::{write, Pid, Uid};
+use rustutils::inherited_fd::take_fd_ownership;
 use std::os::unix::raw::{pid_t, uid_t};
-use safe_ownedfd::take_fd_ownership;
 
 const LOG_TAG: &str = "virtmgr";
 
@@ -83,6 +83,10 @@
 }
 
 fn main() {
+    // SAFETY: nobody has taken ownership of the inherited FDs yet.
+    unsafe { rustutils::inherited_fd::init_once() }
+        .expect("Failed to take ownership of inherited FDs");
+
     android_logger::init_once(
         android_logger::Config::default()
             .with_tag(LOG_TAG)
@@ -102,7 +106,17 @@
     ProcessState::start_thread_pool();
 
     if cfg!(early) {
-        panic!("Early VM not implemented");
+        // we can't access VirtualizationServiceInternal, so directly call rlimit
+        let pid = i32::from(*PID_CURRENT);
+        let lim = libc::rlimit { rlim_cur: libc::RLIM_INFINITY, rlim_max: libc::RLIM_INFINITY };
+
+        // SAFETY: borrowing the new limit struct only. prlimit doesn't use lim outside its lifetime
+        let ret = unsafe { libc::prlimit(pid, libc::RLIMIT_MEMLOCK, &lim, std::ptr::null_mut()) };
+        if ret == -1 {
+            panic!("rlimit error: {}", std::io::Error::last_os_error());
+        } else if ret != 0 {
+            panic!("Unexpected return value from prlimit(): {ret}");
+        }
     } else {
         GLOBAL_SERVICE.removeMemlockRlimit().expect("Failed to remove memlock rlimit");
     }
diff --git a/android/virtmgr/src/payload.rs b/android/virtmgr/src/payload.rs
index 82d9ba0..81e02b7 100644
--- a/android/virtmgr/src/payload.rs
+++ b/android/virtmgr/src/payload.rs
@@ -35,6 +35,7 @@
 use serde::Deserialize;
 use serde_xml_rs::from_reader;
 use std::collections::HashSet;
+use std::ffi::OsStr;
 use std::fs::{metadata, File, OpenOptions};
 use std::path::{Path, PathBuf};
 use std::process::Command;
@@ -94,11 +95,13 @@
             // For active APEXes, we run derive_classpath and parse its output to see if it
             // contributes to the classpath(s). (This allows us to handle any new classpath env
             // vars seamlessly.)
-            let classpath_vars = run_derive_classpath()?;
-            let classpath_apexes = find_apex_names_in_classpath(&classpath_vars)?;
+            if !cfg!(early) {
+                let classpath_vars = run_derive_classpath()?;
+                let classpath_apexes = find_apex_names_in_classpath(&classpath_vars)?;
 
-            for apex_info in apex_info_list.list.iter_mut() {
-                apex_info.has_classpath_jar = classpath_apexes.contains(&apex_info.name);
+                for apex_info in apex_info_list.list.iter_mut() {
+                    apex_info.has_classpath_jar = classpath_apexes.contains(&apex_info.name);
+                }
             }
 
             Ok(apex_info_list)
@@ -169,6 +172,9 @@
         let mut list = self.apex_info_list.clone();
         // When prefer_staged, we override ApexInfo by consulting "package_native"
         if prefer_staged {
+            if cfg!(early) {
+                return Err(anyhow!("Can't turn on prefer_staged on early boot VMs"));
+            }
             let pm =
                 wait_for_interface::<dyn IPackageManagerNative>(PACKAGE_MANAGER_NATIVE_SERVICE)
                     .context("Failed to get service when prefer_staged is set.")?;
@@ -293,7 +299,16 @@
     }];
 
     for (i, apex_info) in apex_infos.iter().enumerate() {
-        let apex_file = open_parcel_file(&apex_info.path, false)?;
+        let path = if cfg!(early) {
+            let path = &apex_info.preinstalled_path;
+            if path.extension().and_then(OsStr::to_str).unwrap_or("") != "apex" {
+                bail!("compressed APEX {} not supported", path.display());
+            }
+            path
+        } else {
+            &apex_info.path
+        };
+        let apex_file = open_parcel_file(path, false)?;
         partitions.push(Partition {
             label: format!("microdroid-apex-{}", i),
             image: Some(apex_file),
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/android/virtualizationservice/aidl/android/system/virtualizationservice/UsbConfig.aidl b/android/virtualizationservice/aidl/android/system/virtualizationservice/UsbConfig.aidl
new file mode 100644
index 0000000..1889d2c
--- /dev/null
+++ b/android/virtualizationservice/aidl/android/system/virtualizationservice/UsbConfig.aidl
@@ -0,0 +1,22 @@
+/*
+ * 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.
+ */
+
+package android.system.virtualizationservice;
+
+parcelable UsbConfig {
+    /** Enable the USB controller */
+    boolean controller;
+}
diff --git a/android/virtualizationservice/aidl/android/system/virtualizationservice/VirtualMachineRawConfig.aidl b/android/virtualizationservice/aidl/android/system/virtualizationservice/VirtualMachineRawConfig.aidl
index 4ac401d..f559a71 100644
--- a/android/virtualizationservice/aidl/android/system/virtualizationservice/VirtualMachineRawConfig.aidl
+++ b/android/virtualizationservice/aidl/android/system/virtualizationservice/VirtualMachineRawConfig.aidl
@@ -21,6 +21,7 @@
 import android.system.virtualizationservice.DisplayConfig;
 import android.system.virtualizationservice.GpuConfig;
 import android.system.virtualizationservice.InputDevice;
+import android.system.virtualizationservice.UsbConfig;
 
 /** Raw configuration for running a VM. */
 parcelable VirtualMachineRawConfig {
@@ -102,4 +103,7 @@
     @nullable AudioConfig audioConfig;
 
     boolean noBalloon;
+
+    /** Enable or disable USB passthrough support */
+    @nullable UsbConfig usbConfig;
 }
diff --git a/android/vm/Android.bp b/android/vm/Android.bp
index c1d9b6b..ba8b416 100644
--- a/android/vm/Android.bp
+++ b/android/vm/Android.bp
@@ -16,6 +16,7 @@
         "libbinder_rs",
         "libclap",
         "libenv_logger",
+        "libcfg_if",
         "libglob",
         "libhypervisor_props",
         "liblibc",
diff --git a/android/vm/src/main.rs b/android/vm/src/main.rs
index f2c2fa4..609bbdf 100644
--- a/android/vm/src/main.rs
+++ b/android/vm/src/main.rs
@@ -75,14 +75,14 @@
 }
 
 impl CommonConfig {
-    #[cfg(network)]
     fn network_supported(&self) -> bool {
-        self.network_supported
-    }
-
-    #[cfg(not(network))]
-    fn network_supported(&self) -> bool {
-        false
+        cfg_if::cfg_if! {
+            if #[cfg(network)] {
+                self.network_supported
+            } else {
+                false
+            }
+        }
     }
 }
 
@@ -117,14 +117,14 @@
 }
 
 impl DebugConfig {
-    #[cfg(debuggable_vms_improvements)]
     fn enable_earlycon(&self) -> bool {
-        self.enable_earlycon
-    }
-
-    #[cfg(not(debuggable_vms_improvements))]
-    fn enable_earlycon(&self) -> bool {
-        false
+        cfg_if::cfg_if! {
+            if #[cfg(debuggable_vms_improvements)] {
+                self.enable_earlycon
+            } else {
+                false
+            }
+        }
     }
 }
 
@@ -158,34 +158,34 @@
 }
 
 impl MicrodroidConfig {
-    #[cfg(vendor_modules)]
     fn vendor(&self) -> Option<&PathBuf> {
-        self.vendor.as_ref()
+        cfg_if::cfg_if! {
+            if #[cfg(vendor_modules)] {
+                self.vendor.as_ref()
+            } else {
+                None
+            }
+        }
     }
 
-    #[cfg(not(vendor_modules))]
-    fn vendor(&self) -> Option<&PathBuf> {
-        None
-    }
-
-    #[cfg(vendor_modules)]
     fn gki(&self) -> Option<&str> {
-        self.gki.as_deref()
+        cfg_if::cfg_if! {
+            if #[cfg(vendor_modules)] {
+                self.gki.as_deref()
+            } else {
+                None
+            }
+        }
     }
 
-    #[cfg(not(vendor_modules))]
-    fn gki(&self) -> Option<&str> {
-        None
-    }
-
-    #[cfg(device_assignment)]
     fn devices(&self) -> &[PathBuf] {
-        &self.devices
-    }
-
-    #[cfg(not(device_assignment))]
-    fn devices(&self) -> &[PathBuf] {
-        &[]
+        cfg_if::cfg_if! {
+            if #[cfg(device_assignment)] {
+                &self.devices
+            } else {
+                &[]
+            }
+        }
     }
 }
 
@@ -236,35 +236,36 @@
 }
 
 impl RunAppConfig {
-    #[cfg(multi_tenant)]
     fn extra_apks(&self) -> &[PathBuf] {
-        &self.extra_apks
+        cfg_if::cfg_if! {
+            if #[cfg(multi_tenant)] {
+                &self.extra_apks
+            } else {
+                &[]
+            }
+        }
     }
 
-    #[cfg(not(multi_tenant))]
-    fn extra_apks(&self) -> &[PathBuf] {
-        &[]
-    }
-
-    #[cfg(llpvm_changes)]
     fn instance_id(&self) -> Result<PathBuf, Error> {
-        Ok(self.instance_id.clone())
+        cfg_if::cfg_if! {
+            if #[cfg(llpvm_changes)] {
+                Ok(self.instance_id.clone())
+            } else {
+                Err(anyhow!("LLPVM feature is disabled, --instance_id flag not supported"))
+            }
+        }
     }
 
-    #[cfg(not(llpvm_changes))]
-    fn instance_id(&self) -> Result<PathBuf, Error> {
-        Err(anyhow!("LLPVM feature is disabled, --instance_id flag not supported"))
-    }
-
-    #[cfg(llpvm_changes)]
     fn set_instance_id(&mut self, instance_id_file: PathBuf) -> Result<(), Error> {
-        self.instance_id = instance_id_file;
-        Ok(())
-    }
-
-    #[cfg(not(llpvm_changes))]
-    fn set_instance_id(&mut self, _: PathBuf) -> Result<(), Error> {
-        Err(anyhow!("LLPVM feature is disabled, --instance_id flag not supported"))
+        cfg_if::cfg_if! {
+            if #[cfg(llpvm_changes)] {
+                self.instance_id = instance_id_file;
+                Ok(())
+            } else {
+                let _ = instance_id_file;
+                Err(anyhow!("LLPVM feature is disabled, --instance_id flag not supported"))
+            }
+        }
     }
 }
 
diff --git a/build/apex/Android.bp b/build/apex/Android.bp
index f493202..4916df7 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: [
@@ -132,13 +136,11 @@
         "vm",
     ],
     prebuilts: [
-        "features_com.android.virt.xml",
         "microdroid_initrd_debuggable",
         "microdroid_initrd_normal",
         "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/build/apex/permissions/Android.bp b/build/apex/permissions/Android.bp
index 0c925ce..678a4f2 100644
--- a/build/apex/permissions/Android.bp
+++ b/build/apex/permissions/Android.bp
@@ -21,4 +21,5 @@
     name: "features_com.android.virt.xml",
     sub_dir: "permissions",
     src: "features_com.android.virt.xml",
+    soc_specific: true,
 }
diff --git a/build/apex/product_packages.mk b/build/apex/product_packages.mk
index a024192..b2a4ca2 100644
--- a/build/apex/product_packages.mk
+++ b/build/apex/product_packages.mk
@@ -24,6 +24,7 @@
 
 PRODUCT_PACKAGES += \
     com.android.compos \
+    features_com.android.virt.xml
 
 # TODO(b/207336449): Figure out how to get these off /system
 PRODUCT_ARTIFACT_PATH_REQUIREMENT_ALLOWED_LIST := \
diff --git a/build/debian/build.sh b/build/debian/build.sh
new file mode 100755
index 0000000..8c17514
--- /dev/null
+++ b/build/debian/build.sh
@@ -0,0 +1,115 @@
+#!/bin/bash
+
+# This is a script to build a Debian image that can run in a VM created via AVF.
+# TODOs:
+# - Support x86_64 architecture
+# - Add Android-specific packages via a new class
+# - Use a stable release from debian-cloud-images
+
+show_help() {
+	echo Usage: $0 [OPTION]... [FILE]
+	echo Builds a debian image and save it to FILE.
+	echo Options:
+	echo -h         Pring usage and this help message and exit.
+}
+
+check_sudo() {
+	if [ "$EUID" -ne 0 ]; then
+		echo "Please run as root."
+		exit
+	fi
+}
+
+parse_options() {
+	while getopts ":h" option; do
+		case ${option} in
+			h)
+				show_help
+				exit;;
+		esac
+	done
+	if [ -n "$1" ]; then
+		built_image=$1
+	fi
+}
+
+install_prerequisites() {
+	apt update
+	DEBIAN_FRONTEND=noninteractive \
+	apt install --no-install-recommends --assume-yes \
+		ca-certificates \
+		debsums \
+		dosfstools \
+		fai-server \
+		fai-setup-storage \
+		fdisk \
+		make \
+		python3 \
+		python3-libcloud \
+		python3-marshmallow \
+		python3-pytest \
+		python3-yaml \
+		qemu-utils \
+		udev \
+		qemu-system-arm \
+		qemu-user-static
+
+        sed -i s/losetup\ -f/losetup\ -P\ -f/g /usr/sbin/fai-diskimage
+        sed -i 's/wget \$/wget -t 0 \$/g' /usr/share/debootstrap/functions
+
+        apt install --no-install-recommends --assume-yes curl
+        # just for testing
+        echo libseccomp: $(curl -is https://deb.debian.org/debian/pool/main/libs/libseccomp/libseccomp2_2.5.4-1+deb12u1_arm64.deb | head -n 1)
+        echo libsemanage-common: $(curl -is https://deb.debian.org/debian/pool/main/libs/libsemanage/libsemanage-common_3.4-1_all.deb | head -n 1)
+}
+
+download_debian_cloud_image() {
+	local ver=master
+	local prj=debian-cloud-images
+	local url=https://salsa.debian.org/cloud-team/${prj}/-/archive/${ver}/${prj}-${ver}.tar.gz
+	local outdir=${debian_cloud_image}
+
+	mkdir -p ${outdir}
+	wget -O - ${url} | tar xz -C ${outdir} --strip-components=1
+}
+
+copy_android_config() {
+	local src=$(dirname $0)/fai_config
+	local dst=${config_space}
+
+	cp -R ${src}/* ${dst}
+	cp $(dirname $0)/image.yaml ${resources_dir}
+
+	local ttyd_version=1.7.7
+	local url=https://github.com/tsl0922/ttyd/releases/download/${ttyd_version}/ttyd.aarch64
+	mkdir -p ${dst}/files/usr/local/bin/ttyd
+	wget ${url} -O ${dst}/files/usr/local/bin/ttyd/AVF
+	chmod 777 ${dst}/files/usr/local/bin/ttyd/AVF
+}
+
+run_fai() {
+	local out=${built_image}
+	make -C ${debian_cloud_image} image_bookworm_nocloud_arm64
+	mv ${debian_cloud_image}/image_bookworm_nocloud_arm64.raw ${out}
+}
+
+clean_up() {
+	rm -rf ${workdir}
+}
+
+set -e
+trap clean_up EXIT
+
+built_image=image.raw
+workdir=$(mktemp -d)
+debian_cloud_image=${workdir}/debian_cloud_image
+debian_version=bookworm
+config_space=${debian_cloud_image}/config_space/${debian_version}
+resources_dir=${debian_cloud_image}/src/debian_cloud_images/resources
+check_sudo
+parse_options $@
+install_prerequisites
+download_debian_cloud_image
+copy_android_config
+run_fai
+fdisk -l image.raw
diff --git a/build/debian/fai_config/class/AVF.var b/build/debian/fai_config/class/AVF.var
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/build/debian/fai_config/class/AVF.var
diff --git a/build/debian/fai_config/files/etc/systemd/system/ttyd.service/AVF b/build/debian/fai_config/files/etc/systemd/system/ttyd.service/AVF
new file mode 100644
index 0000000..f71557d
--- /dev/null
+++ b/build/debian/fai_config/files/etc/systemd/system/ttyd.service/AVF
@@ -0,0 +1,12 @@
+[Unit]
+Description=TTYD
+After=syslog.target
+After=network.target
+[Service]
+ExecStart=/usr/local/bin/ttyd -W screen -aAxR -S main login
+Type=simple
+Restart=always
+User=root
+Group=root
+[Install]
+WantedBy=multi-user.target
diff --git a/build/debian/fai_config/files/etc/systemd/system/vsockip.service/AVF b/build/debian/fai_config/files/etc/systemd/system/vsockip.service/AVF
new file mode 100644
index 0000000..a29020b
--- /dev/null
+++ b/build/debian/fai_config/files/etc/systemd/system/vsockip.service/AVF
@@ -0,0 +1,12 @@
+[Unit]
+Description=vsock ip service
+After=syslog.target
+After=network.target
+[Service]
+ExecStart=/usr/bin/python3 /usr/local/bin/vsock.py
+Type=simple
+Restart=always
+User=root
+Group=root
+[Install]
+WantedBy=multi-user.target
diff --git a/build/debian/fai_config/files/usr/local/bin/vsock.py/AVF b/build/debian/fai_config/files/usr/local/bin/vsock.py/AVF
new file mode 100755
index 0000000..292d953
--- /dev/null
+++ b/build/debian/fai_config/files/usr/local/bin/vsock.py/AVF
@@ -0,0 +1,57 @@
+#!/usr/bin/env python3
+
+import socket
+
+# Constants for vsock (from linux/vm_sockets.h)
+AF_VSOCK = 40
+SOCK_STREAM = 1
+VMADDR_CID_ANY = -1
+
+def get_local_ip():
+    """Retrieves the first IPv4 address found on the system.
+
+    Returns:
+        str: The local IPv4 address, or '127.0.0.1' if no IPv4 address is found.
+    """
+
+    s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
+    try:
+        s.connect(('8.8.8.8', 80))
+        ip = s.getsockname()[0]
+    except Exception:
+        ip = '127.0.0.1'
+    finally:
+        s.close()
+    return ip
+
+def main():
+    PORT = 1024
+
+    # Create a vsock socket
+    server_socket = socket.socket(AF_VSOCK, SOCK_STREAM)
+
+    # Bind the socket to the server address
+    server_address = (VMADDR_CID_ANY, PORT)
+    server_socket.bind(server_address)
+
+    # Listen for incoming connections
+    server_socket.listen(1)
+    print(f"VSOCK server listening on port {PORT}...")
+
+    while True:
+        # Accept a connection
+        connection, client_address = server_socket.accept()
+        print(f"Connection from: {client_address}")
+
+        try:
+            # Get the local IP address
+            local_ip = get_local_ip()
+
+            # Send the IP address to the client
+            connection.sendall(local_ip.encode())
+        finally:
+            # Close the connection
+            connection.close()
+
+if __name__ == "__main__":
+    main()
diff --git a/build/debian/fai_config/hooks/partition.ARM64 b/build/debian/fai_config/hooks/partition.ARM64
new file mode 100755
index 0000000..b3b603b
--- /dev/null
+++ b/build/debian/fai_config/hooks/partition.ARM64
@@ -0,0 +1,53 @@
+#!/bin/sh
+set -eu
+touch $LOGDIR/skip.partition
+
+set -- $disklist
+device=/dev/$1
+
+wait_for_device() {
+  for s in $(seq 10); do
+    if [ -e "$1" ]; then
+      break
+    fi
+    sleep 1
+  done
+}
+
+sfdisk "$device" << EOF
+label: gpt
+unit: sectors
+
+# EFI system
+p15 : start=2048, size=260096, type="EFI System", uuid=${PARTUUID_ESP}
+# Linux
+p1 : start=262144, type="Linux root (ARM-64)", uuid=${PARTUUID_ROOT}
+EOF
+
+file=$(losetup -O BACK-FILE ${device} | tail -1)
+
+root_offset=$(parted -m ${device} unit B print | awk -F '[B:]' '/1:/{ print $2 }')
+root_size=$(  parted -m ${device} unit B print | awk -F '[B:]' '/1:/{ print $6 }')
+efi_offset=$( parted -m ${device} unit B print | awk -F '[B:]' '/15:/{ print $2 }')
+efi_size=$(   parted -m ${device} unit B print | awk -F '[B:]' '/15:/{ print $6 }')
+device_root=$(losetup -o ${root_offset} --sizelimit ${root_size} --show -f ${file})
+device_efi=$(losetup -o ${efi_offset} --sizelimit ${efi_size} --show -f ${file})
+rm -f ${device}p1
+rm -f ${device}p15
+ln -sf ${device_root} ${device}p1
+ln -sf ${device_efi} ${device}p15
+
+ls -al /dev/loop*
+losetup -a -l
+parted ${device} unit B print
+
+partprobe "$device"
+
+wait_for_device "$device_root"
+mkfs.ext4 -U "$FSUUID_ROOT" "$device_root"
+tune2fs -c 0 -i 0 "$device_root"
+
+wait_for_device "$device_efi"
+mkfs.vfat "$device_efi"
+
+parted ${device} unit B print
diff --git a/build/debian/fai_config/package_config/AVF b/build/debian/fai_config/package_config/AVF
new file mode 100644
index 0000000..7d86d41
--- /dev/null
+++ b/build/debian/fai_config/package_config/AVF
@@ -0,0 +1,4 @@
+PACKAGES install
+
+# Just for testing
+tree
diff --git a/build/debian/fai_config/scripts/AVF/10-systemd b/build/debian/fai_config/scripts/AVF/10-systemd
new file mode 100755
index 0000000..e04a562
--- /dev/null
+++ b/build/debian/fai_config/scripts/AVF/10-systemd
@@ -0,0 +1,6 @@
+#!/bin/bash
+
+chmod +x $target/usr/local/bin/ttyd
+chmod +x $target/usr/local/bin/vsock.py
+ln -s /etc/systemd/system/ttyd.service $target/etc/systemd/system/multi-user.target.wants/ttyd.service
+ln -s /etc/systemd/system/vsockip.service $target/etc/systemd/system/multi-user.target.wants/vsockip.service
\ No newline at end of file
diff --git a/build/debian/image.yaml b/build/debian/image.yaml
new file mode 100644
index 0000000..eb42a07
--- /dev/null
+++ b/build/debian/image.yaml
@@ -0,0 +1,60 @@
+# After modifications, please call:
+# "python3 -m debian_cloud_images.cli.generate_ci .gitlab/ci/generated.yml"
+---
+apiVersion: cloud.debian.org/v1alpha1
+kind: ImageConfig
+
+archs:
+- name: amd64
+  azureName: X64
+  ociArch: amd64
+  faiClasses: [AMD64]
+- name: arm64
+  azureName: Arm64
+  ociArch: arm64
+  faiClasses: [ARM64]
+- name: ppc64el
+  faiClasses: [PPC64EL]
+  ociArch: ppc64le
+- name: riscv64
+  faiClasses: [RISCV64]
+  ociArch: riscv64
+
+releases:
+- name: bookworm
+  basename: bookworm
+  id: '12'
+  baseid: '12'
+  faiClasses: [BOOKWORM, LINUX_VERSION_BASE, EXTRAS]
+  matches:
+  - matchArches: [amd64, arm64, ppc64el]
+- name: bookworm-backports
+  basename: bookworm-backports
+  id: 12-backports
+  baseid: '12'
+  faiClasses: [BOOKWORM, LINUX_VERSION_BACKPORTS, EXTRAS]
+  matches:
+  - matchArches: [amd64, arm64, ppc64el]
+- name: trixie
+  basename: trixie
+  id: '13'
+  baseid: '13'
+  faiClasses: [TRIXIE, LINUX_VERSION_BASE, EXTRAS]
+  matches:
+  - matchArches: [amd64, arm64, ppc64el]
+
+vendors:
+- name: nocloud
+  faiClasses: [SYSTEM_BOOT, NOCLOUD, LINUX_VARIANT_BASE, TIME_SYSTEMD, AVF]
+  size: 2
+
+types:
+- name: dev
+  faiClasses: [TYPE_DEV]
+  outputName: 'debian-{release}-{vendor}-{arch}-{build_type}-{build_id}-{version}'
+  outputVersion: '{version}'
+  outputVersionAzure: '0.0.{version!s}'
+- name: official
+  outputName: 'debian-{release}-{vendor}-{arch}-{build_type}-{version}'
+  outputVersion: '{date}-{version}'
+  outputVersionAzure: '0.{date!s}.{version!s}'
diff --git a/build/debian/kokoro/build.sh b/build/debian/kokoro/build.sh
new file mode 100644
index 0000000..fb2a1a3
--- /dev/null
+++ b/build/debian/kokoro/build.sh
@@ -0,0 +1,7 @@
+#!/bin/bash
+
+set -e
+
+cd "${KOKORO_ARTIFACTS_DIR}/git/avf/build/debian/"
+sudo losetup -D
+sudo ./build.sh
diff --git a/build/debian/kokoro/continuous.cfg b/build/debian/kokoro/continuous.cfg
new file mode 100644
index 0000000..018812c
--- /dev/null
+++ b/build/debian/kokoro/continuous.cfg
@@ -0,0 +1,12 @@
+# -*- protobuffer -*-
+# proto-file: google3/devtools/kokoro/config/proto/build.proto
+# proto-message: BuildConfig
+
+# Location of the bash script. Should have value <git_on_borg_scm.name>/<path_from_repository_root>.
+# git_on_borg_scm.name is specified in the job configuration (next section).
+build_file: "avf/build/debian/kokoro/build.sh"
+container_properties {
+  docker_image: "us-central1-docker.pkg.dev/kokoro-container-bakery/kokoro/ubuntu/ubuntu2204/full:next"
+  docker_privileged: true
+  docker_user: "root"
+}
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/early_vm.md b/docs/early_vm.md
new file mode 100644
index 0000000..44b71ff
--- /dev/null
+++ b/docs/early_vm.md
@@ -0,0 +1,52 @@
+# Early VM
+
+Early VMs are specialized virtual machines that can run even before the `/data`
+partition is mounted, unlike regular VMs. `early_virtmgr` is a binary that
+serves as the interface for early VMs, functioning similarly to `virtmgr`,
+which provides the [`IVirtualizationService`](../android/virtualizationservice/aidl/android/system/virtualizationservice/IVirtualizationService.aidl)
+aidl interface.
+
+To run an early VM, clients must follow these steps.
+
+1) Early VMs need to be defined in `{partition}/etc/avf/early_vms.xml`. The
+schema for this file is defined in [`early_vms.xsd`](../android/virtmgr/early_vms.xsd).
+
+```early_vms.xml
+<early_vms>
+    <early_vm>
+        <name>vm_demo_native_early</name>
+        <cid>123</cid>
+        <path>/system/bin/vm_demo_native_early</path>
+    </early_vm>
+</early_vms>
+```
+
+In this example, the binary `/system/bin/vm_demo_native_early` can establish a
+connection with `early_virtmgr` and create a VM named `vm_demo_native_early`,
+which will be assigned the static CID 123.
+
+2) The client must have the following three or four capabilities.
+
+* `IPC_LOCK`
+* `NET_BIND_SERVICE`
+* `SYS_NICE` (required if `RELEASE_AVF_ENABLE_VIRT_CPUFREQ` is enabled)
+* `SYS_RESOURCES`
+
+Typically, the client is defined as a service in an init script, where
+capabilities can also be specified.
+
+```vm_demo_native_early.rc
+service vm_demo_native_early /system/bin/vm_demo_native_early
+    user system
+    group system virtualmachine
+    capabilities IPC_LOCK NET_BIND_SERVICE SYS_RESOURCE SYS_NICE
+    oneshot
+    stdio_to_kmsg
+    class early_hal
+```
+
+3) The client forks `early_virtmgr` instead of `virtmgr`.
+
+The remaining steps are identical to those for regular VMs: connect to
+`early_virtmgr`, obtain the `IVirtualizationService` interface, then create and
+run the VM.
diff --git a/docs/img/pvm-dice.png b/docs/img/pvm-dice.png
new file mode 100644
index 0000000..5b26038
--- /dev/null
+++ b/docs/img/pvm-dice.png
Binary files differ
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/pvm_dice_chain.md b/docs/pvm_dice_chain.md
new file mode 100644
index 0000000..67d1f28
--- /dev/null
+++ b/docs/pvm_dice_chain.md
@@ -0,0 +1,54 @@
+# pVM DICE Chain
+
+Unlike KeyMint, which only needs a vendor DICE chain, the pVM DICE
+chain combines the vendor's DICE chain with additional pVM DICE nodes
+describing the protected VM's environment.
+
+![][pvm-dice-chain-img]
+
+The full [RKP VM DICE chain][rkpvm-dice-chain], starting from `UDS_Pub`
+rooted in ROM, is sent to the RKP server during
+[pVM remote attestation][vm-attestation].
+
+[vm-attestation]: vm_remote_attestation.md
+[pvm-dice-chain-img]: img/pvm-dice.png
+[rkpvm-dice-chain]: vm_remote_attestation.md#rkp-vm-marker
+
+## Key derivation
+
+Key derivation is a critical step in the DICE handover process within
+[pvmfw][pvmfw]. Vendors need to ensure that both pvmfw and their final DICE
+node use the same method to derive a key pair from `CDI_Attest` in order to
+maintain a valid certificate chain. Pvmfw use [open-dice][open-dice] with the
+following formula:
+
+```
+CDI_Attest_pub, CDI_Attest_priv = KDF_ASYM(KDF(CDI_Attest))
+```
+
+Where KDF = HKDF-SHA-512 (RFC 5869).
+
+Currently, KDF_ASYM = Ed25519, but EC p-384 and p-256 (RFC 6979) support is
+coming soon.
+
+Vendors must use a supported algorithm for the last DICE node to ensure
+compatibility and chain integrity.
+
+[pvmfw]: ../guest/pvmfw
+[open-dice]: https://cs.android.com/android/platform/superproject/main/+/main:external/open-dice/
+
+## Testing
+
+To verify that the DICE handover is successful in pvmfw and eventually the pVM
+has a valid DICE chain, you can run the VSR test
+`MicrodroidTests#protectedVmHasValidDiceChain`. The test retrieves the DICE
+chain from within a Microdroid VM in protected mode and checks the following
+properties using the [hwtrust][hwtrust] library:
+
+1. All the fields in the DICE chain conform to
+   [Android Profile for DICE][android-open-dice].
+2. The DICE chain is a valid certificate chain, where the subject public key in
+   each certificate can be used to verify the signature of the next certificate.
+
+[hwtrust]: https://cs.android.com/android/platform/superproject/main/+/main:tools/security/remote_provisioning/hwtrust/
+[android-open-dice]: https://android.googlesource.com/platform/external/open-dice/+/refs/heads/main/docs/android.md
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/authfs_service/Android.bp b/guest/authfs_service/Android.bp
index e508c17..2101a36 100644
--- a/guest/authfs_service/Android.bp
+++ b/guest/authfs_service/Android.bp
@@ -18,7 +18,6 @@
         "libnix",
         "librpcbinder_rs",
         "librustutils",
-        "libsafe_ownedfd",
         "libshared_child",
     ],
     prefer_rlib: true,
diff --git a/guest/authfs_service/src/authfs.rs b/guest/authfs_service/src/authfs.rs
index cfd5766..f2638c2 100644
--- a/guest/authfs_service/src/authfs.rs
+++ b/guest/authfs_service/src/authfs.rs
@@ -89,12 +89,9 @@
             &config.outputDirFdAnnotations,
             debuggable,
         )?;
-        wait_until_authfs_ready(&child, &mountpoint).map_err(|e| {
-            match child.wait() {
-                Ok(status) => debug!("Wait for authfs: {}", status),
-                Err(e) => warn!("Failed to wait for child: {}", e),
-            }
-            e
+        wait_until_authfs_ready(&child, &mountpoint).inspect_err(|_| match child.wait() {
+            Ok(status) => debug!("Wait for authfs: {}", status),
+            Err(e) => warn!("Failed to wait for child: {}", e),
         })?;
 
         let authfs = AuthFs { mountpoint, process: child };
diff --git a/guest/authfs_service/src/main.rs b/guest/authfs_service/src/main.rs
index ff2f770..be0f1b2 100644
--- a/guest/authfs_service/src/main.rs
+++ b/guest/authfs_service/src/main.rs
@@ -26,10 +26,8 @@
 use log::*;
 use rpcbinder::RpcServer;
 use rustutils::sockets::android_get_control_socket;
-use safe_ownedfd::take_fd_ownership;
 use std::ffi::OsString;
 use std::fs::{create_dir, read_dir, remove_dir_all, remove_file};
-use std::os::unix::io::OwnedFd;
 use std::sync::atomic::{AtomicUsize, Ordering};
 
 use authfs_aidl_interface::aidl::com::android::virt::fs::AuthFsConfig::AuthFsConfig;
@@ -109,14 +107,11 @@
     Ok(())
 }
 
-/// Prepares a socket file descriptor for the authfs service.
-fn prepare_authfs_service_socket() -> Result<OwnedFd> {
-    let raw_fd = android_get_control_socket(AUTHFS_SERVICE_SOCKET_NAME)?;
-    Ok(take_fd_ownership(raw_fd)?)
-}
-
 #[allow(clippy::eq_op)]
 fn try_main() -> Result<()> {
+    // SAFETY: nobody has taken ownership of the inherited FDs yet.
+    unsafe { rustutils::inherited_fd::init_once()? };
+
     let debuggable = env!("TARGET_BUILD_VARIANT") != "user";
     let log_level = if debuggable { log::LevelFilter::Trace } else { log::LevelFilter::Info };
     android_logger::init_once(
@@ -125,7 +120,7 @@
 
     clean_up_working_directory()?;
 
-    let socket_fd = prepare_authfs_service_socket()?;
+    let socket_fd = android_get_control_socket(AUTHFS_SERVICE_SOCKET_NAME)?;
     let service = AuthFsService::new_binder(debuggable).as_binder();
     debug!("{} is starting as a rpc service.", AUTHFS_SERVICE_SOCKET_NAME);
     let server = RpcServer::new_bound_socket(service, socket_fd)?;
diff --git a/guest/microdroid_manager/Android.bp b/guest/microdroid_manager/Android.bp
index 82e26b7..9c9a3d0 100644
--- a/guest/microdroid_manager/Android.bp
+++ b/guest/microdroid_manager/Android.bp
@@ -48,7 +48,6 @@
         "libprotobuf",
         "librpcbinder_rs",
         "librustutils",
-        "libsafe_ownedfd",
         "libsecretkeeper_client",
         "libsecretkeeper_comm_nostd",
         "libscopeguard",
@@ -60,7 +59,6 @@
         "libvsock",
         "librand",
         "libzeroize",
-        "libsafe_ownedfd",
     ],
     init_rc: ["microdroid_manager.rc"],
     multilib: {
diff --git a/guest/microdroid_manager/src/main.rs b/guest/microdroid_manager/src/main.rs
index 8b676b8..fa089fa 100644
--- a/guest/microdroid_manager/src/main.rs
+++ b/guest/microdroid_manager/src/main.rs
@@ -50,7 +50,6 @@
 use rustutils::sockets::android_get_control_socket;
 use rustutils::system_properties;
 use rustutils::system_properties::PropertyWatcher;
-use safe_ownedfd::take_fd_ownership;
 use secretkeeper_comm::data_types::ID_SIZE;
 use std::borrow::Cow::{Borrowed, Owned};
 use std::env;
@@ -171,6 +170,9 @@
 }
 
 fn main() -> Result<()> {
+    // SAFETY: nobody has taken ownership of the inherited FDs yet.
+    unsafe { rustutils::inherited_fd::init_once()? };
+
     // If debuggable, print full backtrace to console log with stdio_to_kmsg
     if is_debuggable()? {
         env::set_var("RUST_BACKTRACE", "full");
@@ -200,7 +202,7 @@
     );
     info!("started.");
 
-    let vm_payload_service_fd = prepare_vm_payload_service_socket()?;
+    let vm_payload_service_fd = android_get_control_socket(VM_PAYLOAD_SERVICE_SOCKET_NAME)?;
 
     load_crashkernel_if_supported().context("Failed to load crashkernel")?;
 
@@ -265,7 +267,7 @@
     // Verify the payload before using it.
     let extracted_data = verify_payload(metadata, saved_data.as_ref())
         .context("Payload verification failed")
-        .map_err(|e| MicrodroidError::PayloadVerificationFailed(e.to_string()))?;
+        .map_err(|e| MicrodroidError::PayloadVerificationFailed(format!("{:?}", e)))?;
 
     // In case identity is ignored (by debug policy), we should reuse existing payload data, even
     // when the payload is changed. This is to keep the derived secret same as before.
@@ -481,12 +483,6 @@
         .context("Could not connect to IVirtualMachineService")
 }
 
-/// Prepares a socket file descriptor for the vm payload service.
-fn prepare_vm_payload_service_socket() -> Result<OwnedFd> {
-    let raw_fd = android_get_control_socket(VM_PAYLOAD_SERVICE_SOCKET_NAME)?;
-    Ok(take_fd_ownership(raw_fd)?)
-}
-
 fn is_strict_boot() -> bool {
     Path::new(AVF_STRICT_BOOT).exists()
 }
@@ -636,7 +632,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/guest/microdroid_manager/src/payload.rs b/guest/microdroid_manager/src/payload.rs
index 98fe24b..8cb0f4e 100644
--- a/guest/microdroid_manager/src/payload.rs
+++ b/guest/microdroid_manager/src/payload.rs
@@ -16,7 +16,7 @@
 
 use crate::instance::ApexData;
 use crate::ioutil::wait_for_file;
-use anyhow::Result;
+use anyhow::{Context, Result};
 use log::{info, warn};
 use microdroid_metadata::{read_metadata, ApexPayload, Metadata};
 use std::time::Duration;
@@ -38,7 +38,8 @@
         .iter()
         .map(|apex| {
             let apex_path = format!("/dev/block/by-name/{}", apex.partition_name);
-            let extracted = apexutil::verify(&apex_path)?;
+            let extracted =
+                apexutil::verify(&apex_path).context(format!("Failed to parse {}", &apex_path))?;
             if let Some(manifest_name) = &extracted.name {
                 if &apex.name != manifest_name {
                     warn!("Apex named {} is named {} in its manifest", apex.name, manifest_name);
diff --git a/guest/pvmfw/Android.bp b/guest/pvmfw/Android.bp
index 144e81e..b502af6 100644
--- a/guest/pvmfw/Android.bp
+++ b/guest/pvmfw/Android.bp
@@ -13,7 +13,6 @@
     rustlibs: [
         "libaarch64_paging",
         "libbssl_avf_nostd",
-        "libbssl_sys_nostd",
         "libcbor_util_nostd",
         "libciborium_nostd",
         "libciborium_io_nostd",
diff --git a/guest/pvmfw/README.md b/guest/pvmfw/README.md
index 4712d77..58ba10c 100644
--- a/guest/pvmfw/README.md
+++ b/guest/pvmfw/README.md
@@ -251,10 +251,13 @@
 }
 ```
 
-and contains the _Compound Device Identifiers_ ("CDIs"), used to derive the
-next-stage secret, and a certificate chain, intended for pVM attestation. Note
-that it differs from the `AndroidDiceHandover` defined by the specification in
-that its `DiceCertChain` field is mandatory (while optional in the original).
+It contains the _Compound Device Identifiers_ (CDIs), used for deriving the
+next-stage secret, and a certificate chain, necessary for building the full
+[pVM DICE chain][pvm-dice-chain] required by features like
+[pVM remote attestation][vm-attestation].
+
+Note that it differs from the `AndroidDiceHandover` defined by the specification
+in that its `DiceCertChain` field is mandatory (while optional in the original).
 
 Devices that fully implement DICE should provide a certificate rooted at the
 Unique Device Secret (UDS) in a boot stage preceding the pvmfw loader (typically
@@ -262,16 +265,6 @@
 can be passed to [`DiceAndroidHandoverMainFlow`][DiceAndroidHandoverMainFlow] along with
 the inputs described below.
 
-Otherwise, as an intermediate step towards supporting DICE throughout the
-software stack of the device, incomplete implementations may root the DICE chain
-at the pvmfw loader, using an arbitrary constant as initial CDI. The pvmfw
-loader can easily do so by:
-
-1. Building an "empty" `AndroidDiceHandover` using CBOR operations only
-   containing constant CDIs ([example][Trusty-BCC])
-1. Passing the resulting `AndroidDiceHandover` to `DiceAndroidHandoverMainFlow`
-   as described above
-
 The recommended DICE inputs at this stage are:
 
 - **Code**: hash of the pvmfw image, hypervisor (`boot.img`), and other target
@@ -291,19 +284,6 @@
 `/reserved-memory` device tree node marked as
 [`compatible=”google,open-dice”`][dice-dt].
 
-#### Testing
-
-To verify that the DICE handover is successful in pvmfw and eventually the pVM
-has a valid DICE chain, you can run the VSR test
-`MicrodroidTests#protectedVmHasValidDiceChain`. The test retrieves the DICE
-chain from within a Microdroid VM in protected mode and checks the following
-properties using the [hwtrust][hwtrust] library:
-
-1. All the fields in the DICE chain conform to
-   [Android Profile for DICE][android-open-dice].
-2. The DICE chain is a valid certificate chain, where the subject public key in
-   each certificate can be used to verify the signature of the next certificate.
-
 [AVB]: https://source.android.com/docs/security/features/verifiedboot/boot-flow
 [AndroidDiceHandover]: https://pigweed.googlesource.com/open-dice/+/42ae7760023/src/android.c#212
 [DiceAndroidHandoverMainFlow]: https://pigweed.googlesource.com/open-dice/+/42ae7760023/src/android.c#221
@@ -311,9 +291,8 @@
 [dice-mode]: https://pigweed.googlesource.com/open-dice/+/refs/heads/main/docs/specification.md#Mode-Value-Details
 [dice-dt]: https://www.kernel.org/doc/Documentation/devicetree/bindings/reserved-memory/google%2Copen-dice.yaml
 [Layering]: https://pigweed.googlesource.com/open-dice/+/refs/heads/main/docs/specification.md#layering-details
-[Trusty-BCC]: https://android.googlesource.com/trusty/lib/+/1696be0a8f3a7103/lib/hwbcc/common/swbcc.c#554
-[hwtrust]: https://cs.android.com/android/platform/superproject/main/+/main:tools/security/remote_provisioning/hwtrust/
-[android-open-dice]: https://android.googlesource.com/platform/external/open-dice/+/refs/heads/main/docs/android.md
+[pvm-dice-chain]: ../../docs/pvm_dice_chain.md
+[vm-attestation]: ../../docs/vm_remote_attestation.md
 
 ### Platform Requirements
 
diff --git a/guest/pvmfw/src/entry.rs b/guest/pvmfw/src/entry.rs
index 8f9340b..945ad6a 100644
--- a/guest/pvmfw/src/entry.rs
+++ b/guest/pvmfw/src/entry.rs
@@ -17,7 +17,6 @@
 use crate::config;
 use crate::fdt;
 use crate::memory;
-use bssl_sys::CRYPTO_library_init;
 use core::arch::asm;
 use core::mem::{drop, size_of};
 use core::num::NonZeroUsize;
@@ -216,12 +215,6 @@
     // - only access non-pvmfw memory once (and while) it has been mapped
 
     log::set_max_level(LevelFilter::Info);
-    // TODO(https://crbug.com/boringssl/35): Remove this init when BoringSSL can handle this
-    // internally.
-    // SAFETY: Configures the internal state of the library - may be called multiple times.
-    unsafe {
-        CRYPTO_library_init();
-    }
 
     let page_table = memory::init_page_table().map_err(|e| {
         error!("Failed to set up the dynamic page tables: {e}");
diff --git a/guest/rialto/Android.bp b/guest/rialto/Android.bp
index b26a1c4..4c18bf9 100644
--- a/guest/rialto/Android.bp
+++ b/guest/rialto/Android.bp
@@ -10,7 +10,6 @@
     rustlibs: [
         "libaarch64_paging",
         "libbssl_avf_nostd",
-        "libbssl_sys_nostd",
         "libciborium_io_nostd",
         "libciborium_nostd",
         "libcstr",
diff --git a/guest/rialto/src/main.rs b/guest/rialto/src/main.rs
index a98ec25..9265775 100644
--- a/guest/rialto/src/main.rs
+++ b/guest/rialto/src/main.rs
@@ -28,7 +28,6 @@
 use crate::error::{Error, Result};
 use crate::fdt::{read_dice_range_from, read_is_strict_boot, read_vendor_hashtree_root_digest};
 use alloc::boxed::Box;
-use bssl_sys::CRYPTO_library_init;
 use ciborium_io::Write;
 use core::num::NonZeroUsize;
 use core::slice;
@@ -109,40 +108,30 @@
     let fdt = libfdt::Fdt::from_slice(fdt)?;
 
     let memory_range = fdt.first_memory_range()?;
-    MEMORY.lock().as_mut().unwrap().shrink(&memory_range).map_err(|e| {
+    MEMORY.lock().as_mut().unwrap().shrink(&memory_range).inspect_err(|_| {
         error!("Failed to use memory range value from DT: {memory_range:#x?}");
-        e
     })?;
 
     if let Some(mem_sharer) = get_mem_sharer() {
         let granule = mem_sharer.granule()?;
-        MEMORY.lock().as_mut().unwrap().init_dynamic_shared_pool(granule).map_err(|e| {
+        MEMORY.lock().as_mut().unwrap().init_dynamic_shared_pool(granule).inspect_err(|_| {
             error!("Failed to initialize dynamically shared pool.");
-            e
         })?;
     } else if let Ok(swiotlb_info) = SwiotlbInfo::new_from_fdt(fdt) {
         let range = swiotlb_info.fixed_range().ok_or_else(|| {
             error!("Pre-shared pool range not specified in swiotlb node");
             Error::from(FdtError::BadValue)
         })?;
-        MEMORY.lock().as_mut().unwrap().init_static_shared_pool(range).map_err(|e| {
+        MEMORY.lock().as_mut().unwrap().init_static_shared_pool(range).inspect_err(|_| {
             error!("Failed to initialize pre-shared pool.");
-            e
         })?;
     } else {
         info!("No MEM_SHARE capability detected or swiotlb found: allocating buffers from heap.");
-        MEMORY.lock().as_mut().unwrap().init_heap_shared_pool().map_err(|e| {
+        MEMORY.lock().as_mut().unwrap().init_heap_shared_pool().inspect_err(|_| {
             error!("Failed to initialize heap-based pseudo-shared pool.");
-            e
         })?;
     }
 
-    // Initializes the crypto library before any crypto operations and after the heap is
-    // initialized.
-    // SAFETY: It is safe to call this function multiple times and concurrently.
-    unsafe {
-        CRYPTO_library_init();
-    }
     let bcc_handover: Box<dyn DiceArtifacts> = match vm_type(fdt)? {
         VmType::ProtectedVm => {
             let dice_range = read_dice_range_from(fdt)?;
@@ -153,9 +142,8 @@
             let res = unsafe {
                 MEMORY.lock().as_mut().unwrap().alloc_range_outside_main_memory(&dice_range)
             };
-            res.map_err(|e| {
+            res.inspect_err(|_| {
                 error!("Failed to use DICE range from DT: {dice_range:#x?}");
-                e
             })?;
             let dice_start = dice_range.start as *const u8;
             // SAFETY: There's no memory overlap and the region is mapped as read-only data.
@@ -233,6 +221,28 @@
     }
 }
 
+/// Flushes data caches over the provided address range.
+///
+/// # Safety
+///
+/// The provided address and size must be to an address range that is valid for read and write
+/// (typically on the stack, .bss, .data, or provided BCC) from a single allocation
+/// (e.g. stack array).
+#[no_mangle]
+unsafe extern "C" fn DiceClearMemory(
+    _ctx: *mut core::ffi::c_void,
+    size: usize,
+    addr: *mut core::ffi::c_void,
+) {
+    use core::slice;
+    use vmbase::memory::flushed_zeroize;
+
+    // SAFETY: We require our caller to provide a valid range within a single object. The open-dice
+    // always calls this on individual stack-allocated arrays which ensures that.
+    let region = unsafe { slice::from_raw_parts_mut(addr as *mut u8, size) };
+    flushed_zeroize(region)
+}
+
 generate_image_header!();
 main!(main);
 configure_heap!(SIZE_128KB * 2);
diff --git a/libs/dice/open_dice/Android.bp b/libs/dice/open_dice/Android.bp
index efe350f..d1129fb 100644
--- a/libs/dice/open_dice/Android.bp
+++ b/libs/dice/open_dice/Android.bp
@@ -22,7 +22,6 @@
         "alloc",
     ],
     whole_static_libs: [
-        "libopen_dice_cbor",
         "libcrypto_baremetal",
     ],
     visibility: [
@@ -55,6 +54,7 @@
         "//packages/modules/Virtualization:__subpackages__",
         "//system/authgraph/tests:__subpackages__",
         "//system/secretkeeper/client:__subpackages__",
+        "//system/software_defined_vehicle:__subpackages__",
     ],
     apex_available: [
         "//apex_available:platform",
diff --git a/libs/dice/sample_inputs/tests/api_test.rs b/libs/dice/sample_inputs/tests/api_test.rs
index 0823f16..d713168 100644
--- a/libs/dice/sample_inputs/tests/api_test.rs
+++ b/libs/dice/sample_inputs/tests/api_test.rs
@@ -14,6 +14,8 @@
  * limitations under the License.
  */
 
+#![cfg_attr(not(feature = "std"), no_std)]
+
 use anyhow::Result;
 use diced_open_dice::{derive_cdi_leaf_priv, sign, DiceArtifacts};
 use diced_sample_inputs::make_sample_bcc_and_cdis;
@@ -144,3 +146,21 @@
     let public_key = chain.leaf().subject_public_key();
     public_key.verify(&signature, MESSAGE)
 }
+
+/// Flushes data caches over the provided address range in open-dice.
+///
+/// # Safety
+///
+/// The provided address and size must be to an address range that is valid for read and write
+/// (typically on the stack, .bss, .data, or provided BCC) from a single allocation
+/// (e.g. stack array).
+#[cfg(not(feature = "std"))]
+#[no_mangle]
+unsafe extern "C" fn DiceClearMemory(
+    _ctx: *mut core::ffi::c_void,
+    size: usize,
+    addr: *mut core::ffi::c_void,
+) {
+    // SAFETY: The caller ensures that the address and size are valid for write.
+    unsafe { core::ptr::write_bytes(addr as *mut u8, 0, size) };
+}
diff --git a/libs/framework-virtualization/src/android/system/virtualmachine/VirtualMachineConfig.java b/libs/framework-virtualization/src/android/system/virtualmachine/VirtualMachineConfig.java
index 644a85a..de1b081 100644
--- a/libs/framework-virtualization/src/android/system/virtualmachine/VirtualMachineConfig.java
+++ b/libs/framework-virtualization/src/android/system/virtualmachine/VirtualMachineConfig.java
@@ -40,6 +40,7 @@
 import android.sysprop.HypervisorProperties;
 import android.system.virtualizationservice.DiskImage;
 import android.system.virtualizationservice.Partition;
+import android.system.virtualizationservice.UsbConfig;
 import android.system.virtualizationservice.VirtualMachineAppConfig;
 import android.system.virtualizationservice.VirtualMachinePayloadConfig;
 import android.system.virtualizationservice.VirtualMachineRawConfig;
@@ -77,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.
@@ -667,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 =
@@ -725,6 +731,15 @@
                         .map(ac -> ac.toParcelable())
                         .orElse(null);
         config.noBalloon = !customImageConfig.useAutoMemoryBalloon();
+        config.usbConfig =
+                Optional.ofNullable(customImageConfig.getUsbConfig())
+                        .map(
+                                uc -> {
+                                    UsbConfig usbConfig = new UsbConfig();
+                                    usbConfig.controller = uc.getUsbController();
+                                    return usbConfig;
+                                })
+                        .orElse(null);
         return config;
     }
 
diff --git a/libs/framework-virtualization/src/android/system/virtualmachine/VirtualMachineCustomImageConfig.java b/libs/framework-virtualization/src/android/system/virtualmachine/VirtualMachineCustomImageConfig.java
index a38ee7f..9774585 100644
--- a/libs/framework-virtualization/src/android/system/virtualmachine/VirtualMachineCustomImageConfig.java
+++ b/libs/framework-virtualization/src/android/system/virtualmachine/VirtualMachineCustomImageConfig.java
@@ -46,6 +46,7 @@
     private static final String KEY_AUDIO_CONFIG = "audio_config";
     private static final String KEY_TRACKPAD = "trackpad";
     private static final String KEY_AUTO_MEMORY_BALLOON = "auto_memory_balloon";
+    private static final String KEY_USB_CONFIG = "usb_config";
 
     @Nullable private final String name;
     @Nullable private final String kernelPath;
@@ -63,6 +64,7 @@
     @Nullable private final GpuConfig gpuConfig;
     private final boolean trackpad;
     private final boolean autoMemoryBalloon;
+    @Nullable private final UsbConfig usbConfig;
 
     @Nullable
     public Disk[] getDisks() {
@@ -139,7 +141,8 @@
             GpuConfig gpuConfig,
             AudioConfig audioConfig,
             boolean trackpad,
-            boolean autoMemoryBalloon) {
+            boolean autoMemoryBalloon,
+            UsbConfig usbConfig) {
         this.name = name;
         this.kernelPath = kernelPath;
         this.initrdPath = initrdPath;
@@ -156,6 +159,7 @@
         this.audioConfig = audioConfig;
         this.trackpad = trackpad;
         this.autoMemoryBalloon = autoMemoryBalloon;
+        this.usbConfig = usbConfig;
     }
 
     static VirtualMachineCustomImageConfig from(PersistableBundle customImageConfigBundle) {
@@ -208,6 +212,9 @@
         builder.setAudioConfig(AudioConfig.from(audioConfigPb));
         builder.useTrackpad(customImageConfigBundle.getBoolean(KEY_TRACKPAD));
         builder.useAutoMemoryBalloon(customImageConfigBundle.getBoolean(KEY_AUTO_MEMORY_BALLOON));
+        PersistableBundle usbConfigPb =
+                customImageConfigBundle.getPersistableBundle(KEY_USB_CONFIG);
+        builder.setUsbConfig(UsbConfig.from(usbConfigPb));
         return builder.build();
     }
 
@@ -266,6 +273,9 @@
                 Optional.ofNullable(audioConfig).map(ac -> ac.toPersistableBundle()).orElse(null));
         pb.putBoolean(KEY_TRACKPAD, trackpad);
         pb.putBoolean(KEY_AUTO_MEMORY_BALLOON, autoMemoryBalloon);
+        pb.putPersistableBundle(
+                KEY_USB_CONFIG,
+                Optional.ofNullable(usbConfig).map(uc -> uc.toPersistableBundle()).orElse(null));
         return pb;
     }
 
@@ -284,6 +294,11 @@
         return gpuConfig;
     }
 
+    @Nullable
+    public UsbConfig getUsbConfig() {
+        return usbConfig;
+    }
+
     /** @hide */
     public static final class Disk {
         private final boolean writable;
@@ -362,6 +377,7 @@
         private boolean trackpad;
         // TODO(b/363985291): balloon breaks Linux VM behavior
         private boolean autoMemoryBalloon = false;
+        private UsbConfig usbConfig;
 
         /** @hide */
         public Builder() {}
@@ -463,6 +479,12 @@
         }
 
         /** @hide */
+        public Builder setUsbConfig(UsbConfig usbConfig) {
+            this.usbConfig = usbConfig;
+            return this;
+        }
+
+        /** @hide */
         public VirtualMachineCustomImageConfig build() {
             return new VirtualMachineCustomImageConfig(
                     this.name,
@@ -480,7 +502,63 @@
                     gpuConfig,
                     audioConfig,
                     trackpad,
-                    autoMemoryBalloon);
+                    autoMemoryBalloon,
+                    usbConfig);
+        }
+    }
+
+    /** @hide */
+    public static final class UsbConfig {
+        private static final String KEY_USE_CONTROLLER = "use_controller";
+        public final boolean controller;
+
+        public UsbConfig(boolean controller) {
+            this.controller = controller;
+        }
+
+        public boolean getUsbController() {
+            return this.controller;
+        }
+
+        android.system.virtualizationservice.UsbConfig toParceclable() {
+            android.system.virtualizationservice.UsbConfig parcelable =
+                    new android.system.virtualizationservice.UsbConfig();
+            parcelable.controller = this.controller;
+            return parcelable;
+        }
+
+        private static UsbConfig from(PersistableBundle pb) {
+            if (pb == null) {
+                return null;
+            }
+            Builder builder = new Builder();
+            builder.setController(pb.getBoolean(KEY_USE_CONTROLLER));
+            return builder.build();
+        }
+
+        private PersistableBundle toPersistableBundle() {
+            PersistableBundle pb = new PersistableBundle();
+            pb.putBoolean(KEY_USE_CONTROLLER, this.controller);
+            return pb;
+        }
+
+        /** @hide */
+        public static class Builder {
+            private boolean useController = false;
+
+            /** @hide */
+            public Builder() {}
+
+            /** @hide */
+            public Builder setController(boolean useController) {
+                this.useController = useController;
+                return this;
+            }
+
+            /** @hide */
+            public UsbConfig build() {
+                return new UsbConfig(useController);
+            }
         }
     }
 
diff --git a/libs/libfdt/src/libfdt.rs b/libs/libfdt/src/libfdt.rs
index b2250f5..6869af6 100644
--- a/libs/libfdt/src/libfdt.rs
+++ b/libs/libfdt/src/libfdt.rs
@@ -292,7 +292,7 @@
         // SAFETY: Accesses (read-only) are constrained to the DT totalsize.
         let ret = unsafe { libfdt_bindgen::fdt_find_max_phandle(fdt, &mut phandle) };
 
-        FdtRawResult::from(ret).try_into()?;
+        () = FdtRawResult::from(ret).try_into()?;
 
         phandle.try_into()
     }
@@ -390,7 +390,7 @@
             // SAFETY: Accesses are constrained to the DT totalsize (validated by ctor).
             unsafe { libfdt_bindgen::fdt_setprop_placeholder(fdt, node, name, len, &mut data) };
 
-        FdtRawResult::from(ret).try_into()?;
+        () = FdtRawResult::from(ret).try_into()?;
 
         get_mut_slice_at_ptr(self.as_fdt_slice_mut(), data.cast(), size).ok_or(FdtError::Internal)
     }
diff --git a/libs/libsafe_ownedfd/Android.bp b/libs/libsafe_ownedfd/Android.bp
deleted file mode 100644
index 53e14dc..0000000
--- a/libs/libsafe_ownedfd/Android.bp
+++ /dev/null
@@ -1,38 +0,0 @@
-package {
-    default_applicable_licenses: ["Android-Apache-2.0"],
-}
-
-rust_defaults {
-    name: "libsafe_ownedfd.defaults",
-    crate_name: "safe_ownedfd",
-    srcs: ["src/lib.rs"],
-    edition: "2021",
-    rustlibs: [
-        "libnix",
-        "libthiserror",
-    ],
-}
-
-rust_library {
-    name: "libsafe_ownedfd",
-    defaults: ["libsafe_ownedfd.defaults"],
-    apex_available: [
-        "com.android.compos",
-        "com.android.microfuchsia",
-        "com.android.virt",
-    ],
-}
-
-rust_test {
-    name: "libsafe_ownedfd.test",
-    defaults: ["libsafe_ownedfd.defaults"],
-    rustlibs: [
-        "libanyhow",
-        "libtempfile",
-    ],
-    host_supported: true,
-    test_suites: ["general-tests"],
-    test_options: {
-        unit_test: true,
-    },
-}
diff --git a/libs/libsafe_ownedfd/src/lib.rs b/libs/libsafe_ownedfd/src/lib.rs
deleted file mode 100644
index 52ae180..0000000
--- a/libs/libsafe_ownedfd/src/lib.rs
+++ /dev/null
@@ -1,127 +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 a safer conversion from `RawFd` to `OwnedFd`
-
-use nix::fcntl::{fcntl, FdFlag, F_DUPFD, F_GETFD, F_SETFD};
-use nix::libc;
-use nix::unistd::close;
-use std::os::fd::FromRawFd;
-use std::os::fd::OwnedFd;
-use std::os::fd::RawFd;
-use std::sync::Mutex;
-use thiserror::Error;
-
-/// Errors that can occur while taking an ownership of `RawFd`
-#[derive(Debug, PartialEq, Error)]
-pub enum Error {
-    /// RawFd is not a valid file descriptor
-    #[error("{0} is not a file descriptor")]
-    Invalid(RawFd),
-
-    /// RawFd is either stdio, stdout, or stderr
-    #[error("standard IO descriptors cannot be owned")]
-    StdioNotAllowed,
-
-    /// Generic UNIX error
-    #[error("UNIX error")]
-    Errno(#[from] nix::errno::Errno),
-}
-
-static LOCK: Mutex<()> = Mutex::new(());
-
-/// Takes the ownership of `RawFd` and converts it to `OwnedFd`. It is important to know that
-/// `RawFd` is closed when this function successfully returns. The raw file descriptor of the
-/// returned `OwnedFd` is different from `RawFd`. The returned file descriptor is CLOEXEC set.
-pub fn take_fd_ownership(raw_fd: RawFd) -> Result<OwnedFd, Error> {
-    fcntl(raw_fd, F_GETFD).map_err(|_| Error::Invalid(raw_fd))?;
-
-    if [libc::STDIN_FILENO, libc::STDOUT_FILENO, libc::STDERR_FILENO].contains(&raw_fd) {
-        return Err(Error::StdioNotAllowed);
-    }
-
-    // sync is needed otherwise we can create multiple OwnedFds out of the same RawFd
-    let lock = LOCK.lock().unwrap();
-    let new_fd = fcntl(raw_fd, F_DUPFD(raw_fd))?;
-    close(raw_fd)?;
-    drop(lock);
-
-    // This is not essential, but let's follow the common practice in the Rust ecosystem
-    fcntl(new_fd, F_SETFD(FdFlag::FD_CLOEXEC)).map_err(Error::Errno)?;
-
-    // SAFETY: In this function, we have checked that RawFd is actually an open file descriptor and
-    // this is the first time to claim its ownership because we just created it by duping.
-    Ok(unsafe { OwnedFd::from_raw_fd(new_fd) })
-}
-
-#[cfg(test)]
-mod tests {
-    use super::*;
-    use anyhow::Result;
-    use nix::fcntl::{fcntl, FdFlag, F_GETFD, F_SETFD};
-    use std::os::fd::AsRawFd;
-    use std::os::fd::IntoRawFd;
-    use tempfile::tempfile;
-
-    #[test]
-    fn good_fd() -> Result<()> {
-        let raw_fd = tempfile()?.into_raw_fd();
-        assert!(take_fd_ownership(raw_fd).is_ok());
-        Ok(())
-    }
-
-    #[test]
-    fn invalid_fd() -> Result<()> {
-        let raw_fd = 12345; // randomly chosen
-        assert_eq!(take_fd_ownership(raw_fd).unwrap_err(), Error::Invalid(raw_fd));
-        Ok(())
-    }
-
-    #[test]
-    fn original_fd_closed() -> Result<()> {
-        let raw_fd = tempfile()?.into_raw_fd();
-        let owned_fd = take_fd_ownership(raw_fd)?;
-        assert_ne!(raw_fd, owned_fd.as_raw_fd());
-        assert!(fcntl(raw_fd, F_GETFD).is_err());
-        Ok(())
-    }
-
-    #[test]
-    fn cannot_use_same_rawfd_multiple_times() -> Result<()> {
-        let raw_fd = tempfile()?.into_raw_fd();
-
-        let owned_fd = take_fd_ownership(raw_fd); // once
-        let owned_fd2 = take_fd_ownership(raw_fd); // twice
-
-        assert!(owned_fd.is_ok());
-        assert!(owned_fd2.is_err());
-        Ok(())
-    }
-
-    #[test]
-    fn cloexec() -> Result<()> {
-        let raw_fd = tempfile()?.into_raw_fd();
-
-        // intentionally clear cloexec to see if it is set by take_fd_ownership
-        fcntl(raw_fd, F_SETFD(FdFlag::empty()))?;
-        let flags = fcntl(raw_fd, F_GETFD)?;
-        assert_eq!(flags, FdFlag::empty().bits());
-
-        let owned_fd = take_fd_ownership(raw_fd)?;
-        let flags = fcntl(owned_fd.as_raw_fd(), F_GETFD)?;
-        assert_eq!(flags, FdFlag::FD_CLOEXEC.bits());
-        drop(owned_fd);
-        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/libs/vm_launcher_lib/Android.bp b/libs/vm_launcher_lib/Android.bp
index 8591c8d..cb6fc9e 100644
--- a/libs/vm_launcher_lib/Android.bp
+++ b/libs/vm_launcher_lib/Android.bp
@@ -9,5 +9,12 @@
         "//apex_available:platform",
         "com.android.virt",
     ],
-    sdk_version: "system_current",
+    platform_apis: true,
+    static_libs: [
+        "gson",
+    ],
+    libs: [
+        "framework-virtualization.impl",
+        "framework-annotations-lib",
+    ],
 }
diff --git a/android/VmLauncherApp/java/com/android/virtualization/vmlauncher/ConfigJson.java b/libs/vm_launcher_lib/java/com/android/virtualization/vmlauncher/ConfigJson.java
similarity index 100%
rename from android/VmLauncherApp/java/com/android/virtualization/vmlauncher/ConfigJson.java
rename to libs/vm_launcher_lib/java/com/android/virtualization/vmlauncher/ConfigJson.java
diff --git a/android/VmLauncherApp/java/com/android/virtualization/vmlauncher/Logger.java b/libs/vm_launcher_lib/java/com/android/virtualization/vmlauncher/Logger.java
similarity index 100%
rename from android/VmLauncherApp/java/com/android/virtualization/vmlauncher/Logger.java
rename to libs/vm_launcher_lib/java/com/android/virtualization/vmlauncher/Logger.java
diff --git a/android/VmLauncherApp/java/com/android/virtualization/vmlauncher/Runner.java b/libs/vm_launcher_lib/java/com/android/virtualization/vmlauncher/Runner.java
similarity index 98%
rename from android/VmLauncherApp/java/com/android/virtualization/vmlauncher/Runner.java
rename to libs/vm_launcher_lib/java/com/android/virtualization/vmlauncher/Runner.java
index a5f58fe..9b97fee 100644
--- a/android/VmLauncherApp/java/com/android/virtualization/vmlauncher/Runner.java
+++ b/libs/vm_launcher_lib/java/com/android/virtualization/vmlauncher/Runner.java
@@ -30,7 +30,7 @@
 
 /** Utility class for creating a VM and waiting for it to finish. */
 class Runner {
-    private static final String TAG = MainActivity.TAG;
+    private static final String TAG = Runner.class.getSimpleName();
     private final VirtualMachine mVirtualMachine;
     private final Callback mCallback;
 
diff --git a/android/VmLauncherApp/java/com/android/virtualization/vmlauncher/VmLauncherService.java b/libs/vm_launcher_lib/java/com/android/virtualization/vmlauncher/VmLauncherService.java
similarity index 100%
rename from android/VmLauncherApp/java/com/android/virtualization/vmlauncher/VmLauncherService.java
rename to libs/vm_launcher_lib/java/com/android/virtualization/vmlauncher/VmLauncherService.java
diff --git a/libs/vmconfig/src/lib.rs b/libs/vmconfig/src/lib.rs
index ff115f3..ef932c2 100644
--- a/libs/vmconfig/src/lib.rs
+++ b/libs/vmconfig/src/lib.rs
@@ -18,6 +18,7 @@
     aidl::android::system::virtualizationservice::CpuTopology::CpuTopology,
     aidl::android::system::virtualizationservice::DiskImage::DiskImage as AidlDiskImage,
     aidl::android::system::virtualizationservice::Partition::Partition as AidlPartition,
+    aidl::android::system::virtualizationservice::UsbConfig::UsbConfig as AidlUsbConfig,
     aidl::android::system::virtualizationservice::VirtualMachineAppConfig::DebugLevel::DebugLevel,
     aidl::android::system::virtualizationservice::VirtualMachineConfig::VirtualMachineConfig,
     aidl::android::system::virtualizationservice::VirtualMachineRawConfig::VirtualMachineRawConfig,
@@ -68,6 +69,8 @@
     pub devices: Vec<PathBuf>,
     /// The serial device for VM console input.
     pub console_input_device: Option<String>,
+    /// The USB config of the VM.
+    pub usb_config: Option<UsbConfig>,
 }
 
 impl VmConfig {
@@ -110,6 +113,7 @@
             Some("match_host") => CpuTopology::MATCH_HOST,
             Some(cpu_topology) => bail!("Invalid cpu topology {}", cpu_topology),
         };
+        let usb_config = self.usb_config.clone().map(|x| x.to_parcelable()).transpose()?;
         Ok(VirtualMachineRawConfig {
             kernel: maybe_open_parcel_file(&self.kernel, false)?,
             initrd: maybe_open_parcel_file(&self.initrd, false)?,
@@ -128,6 +132,7 @@
                 })
                 .collect::<Result<_>>()?,
             consoleInputDevice: self.console_input_device.clone(),
+            usbConfig: usb_config,
             ..Default::default()
         })
     }
@@ -193,6 +198,19 @@
     }
 }
 
+/// USB controller and available USB devices
+#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
+pub struct UsbConfig {
+    /// Enable USB controller
+    pub controller: bool,
+}
+
+impl UsbConfig {
+    fn to_parcelable(&self) -> Result<AidlUsbConfig> {
+        Ok(AidlUsbConfig { controller: self.controller })
+    }
+}
+
 /// Try to open the given file and wrap it in a [`ParcelFileDescriptor`].
 pub fn open_parcel_file(filename: &Path, writable: bool) -> Result<ParcelFileDescriptor> {
     Ok(ParcelFileDescriptor::new(
diff --git a/microfuchsia/microfuchsiad/Android.bp b/microfuchsia/microfuchsiad/Android.bp
index ab3f865..ddf360d 100644
--- a/microfuchsia/microfuchsiad/Android.bp
+++ b/microfuchsia/microfuchsiad/Android.bp
@@ -15,9 +15,8 @@
         "libandroid_logger",
         "libanyhow",
         "libbinder_rs",
-        "liblibc",
         "liblog_rust",
-        "libsafe_ownedfd",
+        "liblibc",
         "libvmclient",
     ],
     apex_available: [
diff --git a/microfuchsia/microfuchsiad/src/instance_starter.rs b/microfuchsia/microfuchsiad/src/instance_starter.rs
index 6688447..15fcc06 100644
--- a/microfuchsia/microfuchsiad/src/instance_starter.rs
+++ b/microfuchsia/microfuchsiad/src/instance_starter.rs
@@ -23,10 +23,9 @@
 use anyhow::{ensure, Context, Result};
 use binder::{LazyServiceGuard, ParcelFileDescriptor};
 use log::info;
-use safe_ownedfd::take_fd_ownership;
 use std::ffi::CStr;
 use std::fs::File;
-use std::os::fd::AsRawFd;
+use std::os::fd::FromRawFd;
 use vmclient::VmInstance;
 
 pub struct MicrofuchsiaInstance {
@@ -134,7 +133,6 @@
             "failed to openpty"
         );
     }
-    let leader = take_fd_ownership(leader)?;
 
     // SAFETY: calling these libc functions with valid+initialized variables is safe.
     unsafe {
@@ -147,25 +145,24 @@
             c_line: 0,
             c_cc: [0u8; 19],
         };
-        ensure!(
-            libc::tcgetattr(leader.as_raw_fd(), &mut attr) == 0,
-            "failed to get termios attributes"
-        );
+        ensure!(libc::tcgetattr(leader, &mut attr) == 0, "failed to get termios attributes");
 
         // Force it to be a raw pty and re-set it.
         libc::cfmakeraw(&mut attr);
         ensure!(
-            libc::tcsetattr(leader.as_raw_fd(), libc::TCSANOW, &attr) == 0,
+            libc::tcsetattr(leader, libc::TCSANOW, &attr) == 0,
             "failed to set termios attributes"
         );
     }
 
     // Construct the return value.
+    // SAFETY: The file descriptors are valid because openpty returned without error (above).
+    let leader = unsafe { File::from_raw_fd(leader) };
     let follower_name: Vec<u8> = follower_name.iter_mut().map(|x| *x as _).collect();
     let follower_name = CStr::from_bytes_until_nul(&follower_name)
         .context("pty filename missing NUL")?
         .to_str()
         .context("pty filename invalid utf8")?
         .to_string();
-    Ok(Pty { leader: File::from(leader), follower_name })
+    Ok(Pty { leader, follower_name })
 }
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");
diff --git a/tests/vm_accessor/accessor/src/accessor.rs b/tests/vm_accessor/accessor/src/accessor.rs
index 6a9ced6..966bffb 100644
--- a/tests/vm_accessor/accessor/src/accessor.rs
+++ b/tests/vm_accessor/accessor/src/accessor.rs
@@ -31,11 +31,12 @@
     //       because 'trait Interface' requires 'static.
     vm: VmInstance,
     port: i32,
+    instance: String,
 }
 
 impl Accessor {
-    pub fn new(vm: VmInstance, port: i32) -> Self {
-        Self { vm, port }
+    pub fn new(vm: VmInstance, port: i32, instance: &str) -> Self {
+        Self { vm, port, instance: instance.into() }
     }
 }
 
@@ -43,10 +44,13 @@
 
 impl IAccessor for Accessor {
     fn addConnection(&self) -> binder::Result<ParcelFileDescriptor> {
-        self.vm.wait_until_ready(Duration::from_secs(10)).unwrap();
+        self.vm.wait_until_ready(Duration::from_secs(20)).unwrap();
 
         info!("VM is ready. Connecting to service via port {}", self.port);
 
         self.vm.vm.connectVsock(self.port)
     }
+    fn getInstanceName(&self) -> binder::Result<String> {
+        Ok(self.instance.clone())
+    }
 }
diff --git a/tests/vm_accessor/accessor/src/main.rs b/tests/vm_accessor/accessor/src/main.rs
index 27ce415..49f5794 100644
--- a/tests/vm_accessor/accessor/src/main.rs
+++ b/tests/vm_accessor/accessor/src/main.rs
@@ -42,7 +42,7 @@
     let vm = run_vm()?;
 
     // If you want to serve multiple services in a VM, then register Accessor impls multiple times.
-    let accessor = Accessor::new(vm, PORT);
+    let accessor = Accessor::new(vm, PORT, SERVICE_NAME);
     let accessor_binder = BnAccessor::new_binder(accessor, BinderFeatures::default());
     binder::register_lazy_service(SERVICE_NAME, accessor_binder.as_binder()).map_err(|e| {
         anyhow!("Failed to register lazy service, service={SERVICE_NAME}, err={e:?}",)