Merge "Remove lib_open_dice_cbor dependency from nostd open-dice library" into main
diff --git a/android/FerrochromeApp/Android.bp b/android/FerrochromeApp/Android.bp
index 9f0c735..3e4ad14 100644
--- a/android/FerrochromeApp/Android.bp
+++ b/android/FerrochromeApp/Android.bp
@@ -2,17 +2,22 @@
     default_applicable_licenses: ["Android-Apache-2.0"],
 }
 
+java_defaults {
+    name: "VmPayloadInstaller",
+    init_rc: [":custom_vm_setup.rc"],
+    required: ["custom_vm_setup"],
+    // TODO(b/348113995): move this app to product partition
+    system_ext_specific: true,
+    platform_apis: true,
+    privileged: true,
+}
+
 android_app {
     name: "FerrochromeApp",
     srcs: ["java/**/*.java"],
     resource_dirs: ["res"],
-    platform_apis: true,
-    // TODO(b/348113995): move this app to product partition
-    system_ext_specific: true,
-    privileged: true,
-    init_rc: ["custom_vm_setup.rc"],
+    defaults: ["VmPayloadInstaller"],
     required: [
-        "custom_vm_setup",
         "privapp-permissions-ferrochrome.xml",
     ],
 }
@@ -24,6 +29,11 @@
     system_ext_specific: true,
 }
 
+filegroup {
+    name: "custom_vm_setup.rc",
+    srcs: ["custom_vm_setup.rc"],
+}
+
 sh_binary {
     name: "custom_vm_setup",
     src: "custom_vm_setup.sh",
diff --git a/android/FerrochromeApp/custom_vm_setup.sh b/android/FerrochromeApp/custom_vm_setup.sh
index 4dce0c7..df1a3a6 100644
--- a/android/FerrochromeApp/custom_vm_setup.sh
+++ b/android/FerrochromeApp/custom_vm_setup.sh
@@ -15,12 +15,13 @@
   cp -u ${src_dir}/vm_config.json ${dst_dir}
   chmod 666 ${dst_dir}/*
 
-  # increase the size of state.img to the multiple of 4096
-  num_blocks=$(du -b -K ${dst_dir}state.img | cut -f 1)
-  required_num_blocks=$(round_up ${num_blocks} 4)
-  additional_blocks=$((( ${required_num_blocks} - ${num_blocks} )))
-  dd if=/dev/zero bs=512 count=${additional_blocks} >> ${dst_dir}state.img
-
+  if [ -f ${dst_dir}state.img ]; then
+    # increase the size of state.img to the multiple of 4096
+    num_blocks=$(du -b -K ${dst_dir}state.img | cut -f 1)
+    required_num_blocks=$(round_up ${num_blocks} 4)
+    additional_blocks=$((( ${required_num_blocks} - ${num_blocks} )))
+    dd if=/dev/zero bs=512 count=${additional_blocks} >> ${dst_dir}state.img
+  fi
   rm ${src_dir}/images.tar.gz*
   rm ${src_dir}/vm_config.json
 }
diff --git a/android/LinuxInstaller/.gitignore b/android/LinuxInstaller/.gitignore
new file mode 100644
index 0000000..e81da29
--- /dev/null
+++ b/android/LinuxInstaller/.gitignore
@@ -0,0 +1,2 @@
+assets/*
+!assets/.gitkeep
diff --git a/android/LinuxInstaller/Android.bp b/android/LinuxInstaller/Android.bp
new file mode 100644
index 0000000..f70452d
--- /dev/null
+++ b/android/LinuxInstaller/Android.bp
@@ -0,0 +1,41 @@
+package {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+android_app {
+    name: "LinuxInstallerApp",
+    srcs: ["java/**/*.java"],
+    resource_dirs: ["res"],
+    asset_dirs: ["assets"],
+    manifest: "AndroidManifest.xml",
+    defaults: ["VmPayloadInstaller"],
+    overrides: ["LinuxInstallerAppStub"],
+    required: [
+        "privapp-permissions-linuxinstaller.xml",
+    ],
+    certificate: ":com.android.virtualization.linuxinstaller_certificate",
+}
+
+android_app {
+    name: "LinuxInstallerAppStub",
+    srcs: ["java/**/*.java"],
+    resource_dirs: ["res"],
+    manifest: "AndroidManifest_stub.xml",
+    defaults: ["VmPayloadInstaller"],
+    required: [
+        "privapp-permissions-linuxinstaller.xml",
+    ],
+    certificate: ":com.android.virtualization.linuxinstaller_certificate",
+}
+
+prebuilt_etc {
+    name: "privapp-permissions-linuxinstaller.xml",
+    src: "privapp-permissions-linuxinstaller.xml",
+    sub_dir: "permissions",
+    system_ext_specific: true,
+}
+
+android_app_certificate {
+    name: "com.android.virtualization.linuxinstaller_certificate",
+    certificate: "com_android_virtualization_linuxinstaller",
+}
diff --git a/android/LinuxInstaller/AndroidManifest.xml b/android/LinuxInstaller/AndroidManifest.xml
new file mode 100644
index 0000000..e5653f6
--- /dev/null
+++ b/android/LinuxInstaller/AndroidManifest.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="com.android.virtualization.linuxinstaller"
+    android:versionCode="2100000000" >
+    <uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" />
+    <uses-permission android:name="android.permission.CHANGE_COMPONENT_ENABLED_STATE" />
+    <queries>
+        <intent>
+            <action android:name="android.virtualization.VM_TERMINAL" />
+        </intent>
+    </queries>
+    <application
+        android:label="LinuxInstaller">
+        <activity android:name=".MainActivity"
+                  android:exported="true">
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN" />
+                <category android:name="android.intent.category.LAUNCHER" />
+            </intent-filter>
+        </activity>
+    </application>
+
+</manifest>
diff --git a/android/LinuxInstaller/AndroidManifest_stub.xml b/android/LinuxInstaller/AndroidManifest_stub.xml
new file mode 100644
index 0000000..49365ea
--- /dev/null
+++ b/android/LinuxInstaller/AndroidManifest_stub.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="com.android.virtualization.linuxinstaller" >
+    <uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" />
+    <uses-permission android:name="android.permission.CHANGE_COMPONENT_ENABLED_STATE" />
+    <queries>
+        <intent>
+            <action android:name="android.virtualization.VM_TERMINAL" />
+        </intent>
+    </queries>
+    <application
+        android:label="LinuxInstaller">
+        <activity android:name=".MainActivity"
+                  android:exported="true">
+        </activity>
+    </application>
+
+</manifest>
diff --git a/android/LinuxInstaller/assets/.gitkeep b/android/LinuxInstaller/assets/.gitkeep
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/android/LinuxInstaller/assets/.gitkeep
diff --git a/android/LinuxInstaller/com_android_virtualization_linuxinstaller.pk8 b/android/LinuxInstaller/com_android_virtualization_linuxinstaller.pk8
new file mode 100644
index 0000000..3f74303
--- /dev/null
+++ b/android/LinuxInstaller/com_android_virtualization_linuxinstaller.pk8
Binary files differ
diff --git a/android/LinuxInstaller/com_android_virtualization_linuxinstaller.x509.pem b/android/LinuxInstaller/com_android_virtualization_linuxinstaller.x509.pem
new file mode 100644
index 0000000..3ca64b7
--- /dev/null
+++ b/android/LinuxInstaller/com_android_virtualization_linuxinstaller.x509.pem
@@ -0,0 +1,24 @@
+-----BEGIN CERTIFICATE-----
+MIIEETCCAvmgAwIBAgIUfBxyELS+ri3QErq8DXHu+47xx4EwDQYJKoZIhvcNAQEL
+BQAwgZYxCzAJBgNVBAYTAlVTMRMwEQYDVQQIDApDYWxpZm9ybmlhMRYwFAYDVQQH
+DA1Nb3VudGFpbiBWaWV3MRQwEgYDVQQKDAtHb29nbGUgSW5jLjEQMA4GA1UECwwH
+QW5kcm9pZDEyMDAGA1UEAwwpY29tX2FuZHJvaWRfdmlydHVhbGl6YXRpb25fbGlu
+dXhpbnN0YWxsZXIwIBcNMjQwODMwMTIyNjU2WhgPMjA1MjAxMTYxMjI2NTZaMIGW
+MQswCQYDVQQGEwJVUzETMBEGA1UECAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwNTW91
+bnRhaW4gVmlldzEUMBIGA1UECgwLR29vZ2xlIEluYy4xEDAOBgNVBAsMB0FuZHJv
+aWQxMjAwBgNVBAMMKWNvbV9hbmRyb2lkX3ZpcnR1YWxpemF0aW9uX2xpbnV4aW5z
+dGFsbGVyMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA8V/rH9ju6Wce
+1BdWuxfWaLmZJHGShXeDO6MB86Wrm10m26j9PFzd8/8FRKsZaujZphwNZsqBsdlt
+pWeNKts9T9luZn19Ci4E8A2EtgSxmfI8Fjwj/OJHHO0hG5+JcwIlUnmFQPcGtu/r
+EL3i7SfcF2ok+IC6aKYohnSbo+YkjyCSwb39i6POe6v6cPIZJtmOnecThS+fYCYR
+2yoMSSr3Bf8ayySrG0pJp7xZ1I5NixK6hUFZhQRLusyiv/KYTpAElMd+n1YJEYbf
+pW30DYAu+31S0hx8JXncFmI0uG3Zxx+LgNQwY8OPV6NPFfVwMPluZR6ep0tZ6q7e
+KIV2w5uC7QIDAQABo1MwUTAdBgNVHQ4EFgQU6FBYv7mW+9DR9q0c9uS4NNdX4Acw
+HwYDVR0jBBgwFoAU6FBYv7mW+9DR9q0c9uS4NNdX4AcwDwYDVR0TAQH/BAUwAwEB
+/zANBgkqhkiG9w0BAQsFAAOCAQEAj3bvUpwKjvpCggXzjMNkn7fAaQ0s1BubnkFe
+ge4zwz4tObP3OGRcxt5V9R5EZ7UY6bPcybA/rfg9FCzjcUQOBjmuepcQpbNHFW2I
+lasFa42UHkHSUFzeg2n9UC5iO3B+sclOr4EPaEE4HbG4B2vj++BYMW3C7PDyHc7R
+fq5ZsEEWcYUa8qZCO46I8AbMZ8iv1HpR4mZeQMkSxhD3uVHDQW+VqDTpzne/YBkJ
+yNfjpgFVZ/Y1E6BvvjzWZpBfj668fo7P3DekWHbvPPr/DiZ7OA6PCmAH1FBsi2c+
+xPgb9clDc2Zjb2Cd9lAoZdeB14zDOh6ZCF1c/i+qYt5tA9t+GA==
+-----END CERTIFICATE-----
diff --git a/android/LinuxInstaller/java/com/android/virtualization/linuxinstaller/MainActivity.java b/android/LinuxInstaller/java/com/android/virtualization/linuxinstaller/MainActivity.java
new file mode 100644
index 0000000..1d875cb
--- /dev/null
+++ b/android/LinuxInstaller/java/com/android/virtualization/linuxinstaller/MainActivity.java
@@ -0,0 +1,207 @@
+/*
+ * 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.
+ */
+
+package com.android.virtualization.linuxinstaller;
+
+import android.annotation.WorkerThread;
+import android.app.Activity;
+import android.content.ComponentName;
+import android.content.Intent;
+import android.content.pm.ActivityInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.os.Bundle;
+import android.os.Environment;
+import android.os.SystemProperties;
+import android.text.TextUtils;
+import android.util.Log;
+import android.widget.TextView;
+
+import libcore.io.Streams;
+
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.StandardCopyOption;
+import java.util.List;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+
+public class MainActivity extends Activity {
+    private static final String TAG = "LinuxInstaller";
+    private static final String ACTION_VM_TERMINAL = "android.virtualization.VM_TERMINAL";
+
+    private static final Path DEST_DIR =
+            Path.of(Environment.getExternalStorageDirectory().getPath(), "linux");
+
+    private static final String ASSET_DIR = "linux";
+    private static final String HASH_FILE_NAME = "hash";
+    private static final Path HASH_FILE = Path.of(DEST_DIR.toString(), HASH_FILE_NAME);
+
+    ExecutorService executorService = Executors.newSingleThreadExecutor();
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        setContentView(R.layout.activity_main);
+
+        executorService.execute(this::installLinuxImage);
+    }
+
+    private void installLinuxImage() {
+        ComponentName vmTerminalComponent = resolve(getPackageManager(), ACTION_VM_TERMINAL);
+        if (vmTerminalComponent == null) {
+            updateStatus("Failed to resolve VM terminal");
+            return;
+        }
+
+        if (!hasLocalAssets()) {
+            updateStatus("No local assets");
+            return;
+        }
+        try {
+            updateImageIfNeeded();
+        } catch (IOException e) {
+            Log.e(TAG, "failed to update image", e);
+            return;
+        }
+        updateStatus("Enabling terminal app...");
+        getPackageManager()
+                .setComponentEnabledSetting(
+                        vmTerminalComponent,
+                        PackageManager.COMPONENT_ENABLED_STATE_ENABLED,
+                        PackageManager.DONT_KILL_APP);
+        updateStatus("Done.");
+    }
+
+    @WorkerThread
+    private boolean hasLocalAssets() {
+        try {
+            String[] files = getAssets().list(ASSET_DIR);
+            return files != null && files.length > 0;
+        } catch (IOException e) {
+            Log.e(TAG, "there is an error during listing up assets", e);
+            return false;
+        }
+    }
+
+    @WorkerThread
+    private void updateImageIfNeeded() throws IOException {
+        if (!isUpdateNeeded()) {
+            Log.d(TAG, "No update needed.");
+            return;
+        }
+
+        try {
+            if (Files.notExists(DEST_DIR)) {
+                Files.createDirectory(DEST_DIR);
+            }
+
+            updateStatus("Copying images...");
+            String[] files = getAssets().list(ASSET_DIR);
+            for (String file : files) {
+                updateStatus(file);
+                Path dst = Path.of(DEST_DIR.toString(), file);
+                updateFile(getAssets().open(ASSET_DIR + "/" + file), dst);
+            }
+        } catch (IOException e) {
+            Log.e(TAG, "Error while updating image: " + e);
+            updateStatus("Failed to update image.");
+            throw e;
+        }
+        extractImages(DEST_DIR.toAbsolutePath().toString());
+    }
+
+    @WorkerThread
+    private void extractImages(String destDir) throws IOException {
+        updateStatus("Extracting images...");
+
+        if (TextUtils.isEmpty(destDir)) {
+            throw new RuntimeException("Internal error: destDir shouldn't be null");
+        }
+
+        SystemProperties.set("debug.custom_vm_setup.path", destDir);
+        SystemProperties.set("debug.custom_vm_setup.done", "false");
+        SystemProperties.set("debug.custom_vm_setup.start", "true");
+        while (!SystemProperties.getBoolean("debug.custom_vm_setup.done", false)) {
+            try {
+                Thread.sleep(1000);
+            } catch (InterruptedException e) {
+                Log.e(TAG, "Error while extracting image: " + e);
+                updateStatus("Failed to extract image.");
+                throw new IOException("extracting image is interrupted", e);
+            }
+        }
+    }
+
+    @WorkerThread
+    private boolean isUpdateNeeded() {
+        Path[] pathsToCheck = {DEST_DIR, HASH_FILE};
+        for (Path p : pathsToCheck) {
+            if (Files.notExists(p)) {
+                Log.d(TAG, p.toString() + " does not exist.");
+                return true;
+            }
+        }
+
+        try {
+            String installedHash = readAll(new FileInputStream(HASH_FILE.toFile()));
+            String updatedHash = readAll(getAssets().open(ASSET_DIR + "/" + HASH_FILE_NAME));
+            if (installedHash.equals(updatedHash)) {
+                return false;
+            }
+            Log.d(TAG, "Hash mismatch. Installed: " + installedHash + "  Updated: " + updatedHash);
+        } catch (IOException e) {
+            Log.e(TAG, "Error while checking hash: " + e);
+        }
+        return true;
+    }
+
+    private static String readAll(InputStream input) throws IOException {
+        return Streams.readFully(new InputStreamReader(input)).strip();
+    }
+
+    private static void updateFile(InputStream input, Path path) throws IOException {
+        try (input) {
+            Files.copy(input, path, StandardCopyOption.REPLACE_EXISTING);
+        }
+    }
+
+    private void updateStatus(String line) {
+        runOnUiThread(
+                () -> {
+                    TextView statusView = findViewById(R.id.status_txt_view);
+                    statusView.append(line + "\n");
+                });
+    }
+
+    private ComponentName resolve(PackageManager pm, String action) {
+        Intent intent = new Intent(action);
+        List<ResolveInfo> resolveInfos = pm.queryIntentActivities(intent, PackageManager.MATCH_ALL);
+        if (resolveInfos.size() != 1) {
+            Log.w(
+                    TAG,
+                    "Failed to resolve activity, action=" + action + ", resolved=" + resolveInfos);
+            return null;
+        }
+        ActivityInfo activityInfo = resolveInfos.getFirst().activityInfo;
+        // MainActivityAlias shows in Launcher
+        return new ComponentName(activityInfo.packageName, activityInfo.name + "Alias");
+    }
+}
diff --git a/android/LinuxInstaller/linux_image_builder/commands b/android/LinuxInstaller/linux_image_builder/commands
new file mode 100644
index 0000000..4d27475
--- /dev/null
+++ b/android/LinuxInstaller/linux_image_builder/commands
@@ -0,0 +1,11 @@
+upload init.sh:/root
+upload vsock.py:/usr/local/bin
+upload /tmp/ttyd:/usr/local/bin
+upload ttyd.service:/etc/systemd/system
+upload vsockip.service:/etc/systemd/system
+chmod 0777:/root/init.sh
+firstboot-command "/root/init.sh"
+chmod 0644:/etc/systemd/system/vsockip.service
+chmod 0644:/etc/systemd/system/ttyd.service
+chmod 0777:/usr/local/bin/vsock.py
+chmod 0777:/usr/local/bin/ttyd
diff --git a/android/LinuxInstaller/linux_image_builder/init.sh b/android/LinuxInstaller/linux_image_builder/init.sh
new file mode 100644
index 0000000..bec5ac5
--- /dev/null
+++ b/android/LinuxInstaller/linux_image_builder/init.sh
@@ -0,0 +1,4 @@
+#!/bin/bash
+systemctl daemon-reload
+systemctl start ttyd && sudo systemctl enable ttyd
+systemctl start vsockip && sudo systemctl enable vsockip
diff --git a/android/LinuxInstaller/linux_image_builder/setup.sh b/android/LinuxInstaller/linux_image_builder/setup.sh
new file mode 100755
index 0000000..2883e61
--- /dev/null
+++ b/android/LinuxInstaller/linux_image_builder/setup.sh
@@ -0,0 +1,29 @@
+#!/bin/bash
+
+pushd $(dirname $0) > /dev/null
+tempdir=$(mktemp -d)
+echo Get Debian image and dependencies...
+wget https://cloud.debian.org/images/cloud/bookworm/latest/debian-12-nocloud-arm64.raw -O ${tempdir}/debian.img
+wget https://github.com/tsl0922/ttyd/releases/download/1.7.7/ttyd.aarch64 -O ${tempdir}/ttyd
+
+echo Customize the image...
+virt-customize --commands-from-file <(sed "s|/tmp|$tempdir|g" commands) -a ${tempdir}/debian.img
+
+asset_dir=../assets/linux
+mkdir -p ${asset_dir}
+
+echo Copy files...
+
+pushd ${tempdir} > /dev/null
+tar czvS -f images.tar.gz debian.img
+popd > /dev/null
+mv ${tempdir}/images.tar.gz ${asset_dir}/images.tar.gz
+cp vm_config.json ${asset_dir}
+
+echo Calculating hash...
+hash=$(cat ${asset_dir}/images.tar.gz ${asset_dir}/vm_config.json | sha1sum | cut -d' ' -f 1)
+echo ${hash} > ${asset_dir}/hash
+
+popd > /dev/null
+echo Cleaning up...
+rm -rf ${tempdir}
\ No newline at end of file
diff --git a/android/LinuxInstaller/linux_image_builder/setup_x86_64.sh b/android/LinuxInstaller/linux_image_builder/setup_x86_64.sh
new file mode 100755
index 0000000..c543b2a
--- /dev/null
+++ b/android/LinuxInstaller/linux_image_builder/setup_x86_64.sh
@@ -0,0 +1,29 @@
+#!/bin/bash
+
+pushd $(dirname $0) > /dev/null
+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.x86_64 -O ${tempdir}/ttyd
+
+echo Customize the image...
+virt-customize --commands-from-file <(sed "s|/tmp|$tempdir|g" commands) -a ${tempdir}/debian.img
+
+asset_dir=../assets/linux
+mkdir -p ${asset_dir}
+
+echo Copy files...
+
+pushd ${tempdir} > /dev/null
+tar czvS -f images.tar.gz debian.img
+popd > /dev/null
+mv ${tempdir}/images.tar.gz ${asset_dir}/images.tar.gz
+cp vm_config.json ${asset_dir}
+
+echo Calculating hash...
+hash=$(cat ${asset_dir}/images.tar.gz ${asset_dir}/vm_config.json | sha1sum | cut -d' ' -f 1)
+echo ${hash} > ${asset_dir}/hash
+
+popd > /dev/null
+echo Cleaning up...
+rm -rf ${tempdir}
\ No newline at end of file
diff --git a/android/LinuxInstaller/linux_image_builder/ttyd.service b/android/LinuxInstaller/linux_image_builder/ttyd.service
new file mode 100644
index 0000000..3a8f181
--- /dev/null
+++ b/android/LinuxInstaller/linux_image_builder/ttyd.service
@@ -0,0 +1,12 @@
+[Unit]
+Description=TTYD
+After=syslog.target
+After=network.target
+[Service]
+ExecStart=/usr/local/bin/ttyd -W login
+Type=simple
+Restart=always
+User=root
+Group=root
+[Install]
+WantedBy=multi-user.target
diff --git a/android/LinuxInstaller/linux_image_builder/vm_config.json b/android/LinuxInstaller/linux_image_builder/vm_config.json
new file mode 100644
index 0000000..21462b8
--- /dev/null
+++ b/android/LinuxInstaller/linux_image_builder/vm_config.json
@@ -0,0 +1,19 @@
+{
+    "name": "debian",
+    "disks": [
+        {
+            "image": "/data/local/tmp/debian.img",
+            "partitions": [],
+            "writable": true
+        }
+    ],
+    "protected": false,
+    "cpu_topology": "match_host",
+    "platform_version": "~1.0",
+    "memory_mib": 4096,
+    "debuggable": true,
+    "console_out": true,
+    "connect_console": true,
+    "console_input_device": "ttyS0",
+    "network": true
+}
diff --git a/android/LinuxInstaller/linux_image_builder/vsock.py b/android/LinuxInstaller/linux_image_builder/vsock.py
new file mode 100644
index 0000000..292d953
--- /dev/null
+++ b/android/LinuxInstaller/linux_image_builder/vsock.py
@@ -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/android/LinuxInstaller/linux_image_builder/vsockip.service b/android/LinuxInstaller/linux_image_builder/vsockip.service
new file mode 100644
index 0000000..a29020b
--- /dev/null
+++ b/android/LinuxInstaller/linux_image_builder/vsockip.service
@@ -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/android/LinuxInstaller/privapp-permissions-linuxinstaller.xml b/android/LinuxInstaller/privapp-permissions-linuxinstaller.xml
new file mode 100644
index 0000000..e46ec97
--- /dev/null
+++ b/android/LinuxInstaller/privapp-permissions-linuxinstaller.xml
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="utf-8"?>
+<permissions>
+    <privapp-permissions package="com.android.virtualization.linuxinstaller">
+        <permission name="android.permission.CHANGE_COMPONENT_ENABLED_STATE"/>
+    </privapp-permissions>
+</permissions>
\ No newline at end of file
diff --git a/android/LinuxInstaller/res/layout/activity_main.xml b/android/LinuxInstaller/res/layout/activity_main.xml
new file mode 100644
index 0000000..3967167
--- /dev/null
+++ b/android/LinuxInstaller/res/layout/activity_main.xml
@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="utf-8"?>
+<RelativeLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:fitsSystemWindows="true"
+    android:paddingLeft="16dp"
+    android:paddingRight="16dp">
+  <TextView
+      android:layout_width="match_parent"
+      android:layout_height="wrap_content"
+      android:textSize="28sp"
+      android:id="@+id/status_txt_view"/>
+
+</RelativeLayout>
diff --git a/android/TerminalApp/AndroidManifest.xml b/android/TerminalApp/AndroidManifest.xml
index 27b2b46..c92da67 100644
--- a/android/TerminalApp/AndroidManifest.xml
+++ b/android/TerminalApp/AndroidManifest.xml
@@ -6,18 +6,27 @@
     <uses-permission android:name="com.android.virtualization.vmlauncher.permission.USE_VM_LAUNCHER"/>
 
     <application
-        android:label="VmTerminalApp"
+	android:label="@string/app_name"
+        android:icon="@mipmap/ic_launcher"
         android:usesCleartextTraffic="true">
         <activity android:name=".MainActivity"
-                  android:screenOrientation="landscape"
-                  android:configChanges="orientation|screenSize|keyboard|keyboardHidden|navigation|uiMode"
-                  android:exported="true"
-                  android:enabled="false">
+                  android:configChanges="orientation|screenSize|keyboard|keyboardHidden|navigation|uiMode|screenLayout|smallestScreenSize"
+                  android:exported="true">
+            <intent-filter>
+                <action android:name="android.virtualization.VM_TERMINAL" />
+                <category android:name="android.intent.category.DEFAULT" />
+            </intent-filter>
+        </activity>
+        <activity-alias
+            android:name=".MainActivityAlias"
+            android:targetActivity="com.android.virtualization.terminal.MainActivity"
+            android:exported="true"
+            android:enabled="false" >
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
                 <category android:name="android.intent.category.LAUNCHER" />
             </intent-filter>
-        </activity>
+        </activity-alias>
     </application>
 
 </manifest>
diff --git a/android/TerminalApp/java/com/android/virtualization/terminal/MainActivity.java b/android/TerminalApp/java/com/android/virtualization/terminal/MainActivity.java
index e6e56d9..a6723fb 100644
--- a/android/TerminalApp/java/com/android/virtualization/terminal/MainActivity.java
+++ b/android/TerminalApp/java/com/android/virtualization/terminal/MainActivity.java
@@ -16,14 +16,19 @@
 package com.android.virtualization.terminal;
 
 import android.app.Activity;
+import android.content.ClipData;
+import android.content.ClipboardManager;
 import android.os.Bundle;
 import android.os.Handler;
 import android.os.Looper;
 import android.util.Log;
+import android.view.Menu;
+import android.view.MenuItem;
 import android.webkit.WebChromeClient;
 import android.webkit.WebView;
 import android.webkit.WebViewClient;
 import android.widget.TextView;
+import android.widget.Toast;
 
 import com.android.virtualization.vmlauncher.VmLauncherServices;
 
@@ -35,6 +40,7 @@
     @Override
     protected void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
+        Toast.makeText(this, R.string.vm_creation_message, Toast.LENGTH_SHORT).show();
         VmLauncherServices.startVmLauncherService(this, this);
 
         setContentView(R.layout.activity_headless);
@@ -53,6 +59,12 @@
                 });
     }
 
+    @Override
+    protected void onDestroy() {
+        VmLauncherServices.stopVmLauncherService(this);
+        super.onDestroy();
+    }
+
     private void gotoURL(String url) {
         runOnUiThread(() -> mWebView.loadUrl(url));
     }
@@ -62,11 +74,13 @@
     }
 
     public void onVmStop() {
+        Toast.makeText(this, R.string.vm_stop_message, Toast.LENGTH_SHORT).show();
         Log.i(TAG, "onVmStop()");
         finish();
     }
 
     public void onVmError() {
+        Toast.makeText(this, R.string.vm_error_message, Toast.LENGTH_SHORT).show();
         Log.i(TAG, "onVmError()");
         finish();
     }
@@ -79,4 +93,25 @@
         new Handler(Looper.getMainLooper())
                 .postDelayed(() -> gotoURL("http://" + mVmIpAddr + ":7681"), 2000);
     }
+
+    @Override
+    public boolean onCreateOptionsMenu(Menu menu) {
+        getMenuInflater().inflate(R.menu.main_menu, menu);
+        return true;
+    }
+
+    @Override
+    public boolean onMenuItemSelected(int featureId, MenuItem item) {
+        int id = item.getItemId();
+        if (id == R.id.copy_ip_addr) {
+            // TODO(b/340126051): remove this menu item when port forwarding is supported.
+            getSystemService(ClipboardManager.class)
+                    .setPrimaryClip(ClipData.newPlainText("A VM's IP address", mVmIpAddr));
+            return true;
+        } else if (id == R.id.stop_vm) {
+            VmLauncherServices.stopVmLauncherService(this);
+            return true;
+        }
+        return super.onMenuItemSelected(featureId, item);
+    }
 }
diff --git a/android/TerminalApp/res/drawable/ic_launcher_background.xml b/android/TerminalApp/res/drawable/ic_launcher_background.xml
new file mode 100644
index 0000000..07d5da9
--- /dev/null
+++ b/android/TerminalApp/res/drawable/ic_launcher_background.xml
@@ -0,0 +1,170 @@
+<?xml version="1.0" encoding="utf-8"?>
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="108dp"
+    android:height="108dp"
+    android:viewportWidth="108"
+    android:viewportHeight="108">
+    <path
+        android:fillColor="#3DDC84"
+        android:pathData="M0,0h108v108h-108z" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M9,0L9,108"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M19,0L19,108"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M29,0L29,108"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M39,0L39,108"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M49,0L49,108"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M59,0L59,108"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M69,0L69,108"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M79,0L79,108"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M89,0L89,108"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M99,0L99,108"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M0,9L108,9"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M0,19L108,19"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M0,29L108,29"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M0,39L108,39"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M0,49L108,49"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M0,59L108,59"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M0,69L108,69"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M0,79L108,79"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M0,89L108,89"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M0,99L108,99"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M19,29L89,29"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M19,39L89,39"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M19,49L89,49"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M19,59L89,59"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M19,69L89,69"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M19,79L89,79"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M29,19L29,89"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M39,19L39,89"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M49,19L49,89"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M59,19L59,89"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M69,19L69,89"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M79,19L79,89"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+</vector>
diff --git a/android/TerminalApp/res/drawable/ic_launcher_foreground.xml b/android/TerminalApp/res/drawable/ic_launcher_foreground.xml
new file mode 100644
index 0000000..8b28c8e
--- /dev/null
+++ b/android/TerminalApp/res/drawable/ic_launcher_foreground.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="108dp"
+    android:height="108dp"
+    android:viewportWidth="142"
+    android:viewportHeight="168.75">
+  <group android:scaleX="0.37325713"
+      android:scaleY="0.44357142"
+      android:translateX="43.332314"
+      android:translateY="39.324776">
+    <group android:translateY="133.59375">
+      <path android:pathData="M9.078125,-77.484375L69.75,-51.40625L69.75,-37.765625L9.078125,-11.609375L9.078125,-28.40625L52.53125,-44.71875L9.078125,-60.75L9.078125,-77.484375Z"
+          android:fillColor="#3BBA46"/>
+      <path android:pathData="M139.76562,0L139.76562,13.5L75.21875,13.5L75.21875,0L139.76562,0Z"
+          android:fillColor="#3BBA46"/>
+    </group>
+  </group>
+</vector>
\ No newline at end of file
diff --git a/android/TerminalApp/res/layout/activity_headless.xml b/android/TerminalApp/res/layout/activity_headless.xml
index 2a640f3..3fe5271 100644
--- a/android/TerminalApp/res/layout/activity_headless.xml
+++ b/android/TerminalApp/res/layout/activity_headless.xml
@@ -5,6 +5,7 @@
     android:layout_width="match_parent"
     android:layout_height="match_parent"
     android:orientation="vertical"
+    android:fitsSystemWindows="true"
     tools:context=".MainActivity">
     <TextView
         android:id="@+id/ip_addr_textview"
diff --git a/android/TerminalApp/res/menu/main_menu.xml b/android/TerminalApp/res/menu/main_menu.xml
new file mode 100644
index 0000000..cc34cda
--- /dev/null
+++ b/android/TerminalApp/res/menu/main_menu.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<menu xmlns:android="http://schemas.android.com/apk/res/android">
+    <item android:id="@+id/copy_ip_addr"
+        android:title="Copy the IP address"/>
+    <item android:id="@+id/stop_vm"
+        android:title="Stop the existing VM instance"/>
+</menu>
\ No newline at end of file
diff --git a/android/TerminalApp/res/mipmap-anydpi-v26/ic_launcher.xml b/android/TerminalApp/res/mipmap-anydpi-v26/ic_launcher.xml
new file mode 100644
index 0000000..7353dbd
--- /dev/null
+++ b/android/TerminalApp/res/mipmap-anydpi-v26/ic_launcher.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
+    <background android:drawable="@color/ic_launcher_background"/>
+    <foreground android:drawable="@drawable/ic_launcher_foreground"/>
+</adaptive-icon>
\ No newline at end of file
diff --git a/android/TerminalApp/res/mipmap-anydpi-v26/ic_launcher_round.xml b/android/TerminalApp/res/mipmap-anydpi-v26/ic_launcher_round.xml
new file mode 100644
index 0000000..7353dbd
--- /dev/null
+++ b/android/TerminalApp/res/mipmap-anydpi-v26/ic_launcher_round.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
+    <background android:drawable="@color/ic_launcher_background"/>
+    <foreground android:drawable="@drawable/ic_launcher_foreground"/>
+</adaptive-icon>
\ No newline at end of file
diff --git a/android/TerminalApp/res/mipmap-hdpi/ic_launcher_round.webp b/android/TerminalApp/res/mipmap-hdpi/ic_launcher_round.webp
new file mode 100644
index 0000000..9be8219
--- /dev/null
+++ b/android/TerminalApp/res/mipmap-hdpi/ic_launcher_round.webp
Binary files differ
diff --git a/android/TerminalApp/res/mipmap-mdpi/ic_launcher_round.webp b/android/TerminalApp/res/mipmap-mdpi/ic_launcher_round.webp
new file mode 100644
index 0000000..662c81e
--- /dev/null
+++ b/android/TerminalApp/res/mipmap-mdpi/ic_launcher_round.webp
Binary files differ
diff --git a/android/TerminalApp/res/mipmap-xhdpi/ic_launcher_round.webp b/android/TerminalApp/res/mipmap-xhdpi/ic_launcher_round.webp
new file mode 100644
index 0000000..2d7990d
--- /dev/null
+++ b/android/TerminalApp/res/mipmap-xhdpi/ic_launcher_round.webp
Binary files differ
diff --git a/android/TerminalApp/res/mipmap-xxhdpi/ic_launcher_round.webp b/android/TerminalApp/res/mipmap-xxhdpi/ic_launcher_round.webp
new file mode 100644
index 0000000..7941000
--- /dev/null
+++ b/android/TerminalApp/res/mipmap-xxhdpi/ic_launcher_round.webp
Binary files differ
diff --git a/android/TerminalApp/res/mipmap-xxxhdpi/ic_launcher_round.webp b/android/TerminalApp/res/mipmap-xxxhdpi/ic_launcher_round.webp
new file mode 100644
index 0000000..55f8020
--- /dev/null
+++ b/android/TerminalApp/res/mipmap-xxxhdpi/ic_launcher_round.webp
Binary files differ
diff --git a/android/TerminalApp/res/values/ic_launcher_background.xml b/android/TerminalApp/res/values/ic_launcher_background.xml
new file mode 100644
index 0000000..337764a
--- /dev/null
+++ b/android/TerminalApp/res/values/ic_launcher_background.xml
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+    <color name="ic_launcher_background">#070E1E</color>
+</resources>
\ No newline at end of file
diff --git a/android/TerminalApp/res/values/strings.xml b/android/TerminalApp/res/values/strings.xml
new file mode 100644
index 0000000..79da7cd
--- /dev/null
+++ b/android/TerminalApp/res/values/strings.xml
@@ -0,0 +1,23 @@
+<?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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="app_name">Terminal</string>
+    <string name="vm_creation_message">Virtual machine is booting. Please wait.</string>
+    <string name="vm_stop_message">Virtual machine is stopped. Exiting.</string>
+    <string name="vm_error_message">Virtual machine crashed. Exiting.</string>
+</resources>
diff --git a/android/VmLauncherApp/java/com/android/virtualization/vmlauncher/VmLauncherService.java b/android/VmLauncherApp/java/com/android/virtualization/vmlauncher/VmLauncherService.java
index ec98f4c..5e78f99 100644
--- a/android/VmLauncherApp/java/com/android/virtualization/vmlauncher/VmLauncherService.java
+++ b/android/VmLauncherApp/java/com/android/virtualization/vmlauncher/VmLauncherService.java
@@ -76,6 +76,10 @@
 
     @Override
     public int onStartCommand(Intent intent, int flags, int startId) {
+        if (isVmRunning()) {
+            Log.d(TAG, "there is already the running VM instance");
+            return START_NOT_STICKY;
+        }
         mExecutorService = Executors.newCachedThreadPool();
 
         ConfigJson json = ConfigJson.from(VM_CONFIG_PATH);
@@ -85,7 +89,9 @@
         try {
             runner = Runner.create(this, config);
         } catch (VirtualMachineException e) {
-            throw new RuntimeException(e);
+            Log.e(TAG, "cannot create runner", e);
+            stopSelf();
+            return START_NOT_STICKY;
         }
         mVirtualMachine = runner.getVm();
         mResultReceiver =
@@ -117,13 +123,32 @@
     @Override
     public void onDestroy() {
         super.onDestroy();
-        mExecutorService.shutdownNow();
+        if (isVmRunning()) {
+            try {
+                mVirtualMachine.stop();
+                stopForeground(STOP_FOREGROUND_REMOVE);
+            } catch (VirtualMachineException e) {
+                Log.e(TAG, "failed to stop a VM instance", e);
+            }
+            mExecutorService.shutdownNow();
+            mExecutorService = null;
+            mVirtualMachine = null;
+        }
+    }
+
+    private boolean isVmRunning() {
+        return mVirtualMachine != null
+                && mVirtualMachine.getStatus() == VirtualMachine.STATUS_RUNNING;
     }
 
     // TODO(b/359523803): Use AVF API to get ip addr when it exists
     private void gatherIpAddrFromVm(Handler handler) {
         handler.postDelayed(
                 () -> {
+                    if (!isVmRunning()) {
+                        Log.d(TAG, "A virtual machine instance isn't running");
+                        return;
+                    }
                     int INTERNAL_VSOCK_SERVER_PORT = 1024;
                     try (ParcelFileDescriptor pfd =
                             mVirtualMachine.connectVsock(INTERNAL_VSOCK_SERVER_PORT)) {
diff --git a/android/virtmgr/Android.bp b/android/virtmgr/Android.bp
index 4828057..f0b6881 100644
--- a/android/virtmgr/Android.bp
+++ b/android/virtmgr/Android.bp
@@ -44,7 +44,6 @@
         "libglob",
         "libhex",
         "libhypervisor_props",
-        "liblazy_static",
         "liblibc",
         "liblog_rust",
         "libmicrodroid_metadata",
@@ -55,7 +54,6 @@
         "libregex",
         "librpcbinder_rs",
         "librustutils",
-        "libsafe_ownedfd",
         "libsemver",
         "libselinux_bindgen",
         "libserde",
@@ -96,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 7a357f3..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,
@@ -67,26 +68,26 @@
 };
 use cstr::cstr;
 use glob::glob;
-use lazy_static::lazy_static;
 use log::{debug, error, info, warn};
 use microdroid_payload_config::{ApkConfig, Task, TaskType, VmPayloadConfig};
 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};
+use std::sync::{Arc, Mutex, Weak, LazyLock};
 use vbmeta::VbMetaImage;
 use vmconfig::{VmConfig, get_debug_level};
 use vsock::VsockStream;
@@ -119,13 +120,17 @@
 
 const VM_REFERENCE_DT_ON_HOST_PATH: &str = "/proc/device-tree/avf/reference";
 
-lazy_static! {
-    pub static ref GLOBAL_SERVICE: Strong<dyn IVirtualizationServiceInternal> =
-        wait_for_interface(BINDER_SERVICE_IDENTIFIER)
-            .expect("Could not connect to VirtualizationServiceInternal");
-    static ref SUPPORTED_OS_NAMES: HashSet<String> =
-        get_supported_os_names().expect("Failed to get list of supported os names");
-}
+pub static GLOBAL_SERVICE: LazyLock<Strong<dyn IVirtualizationServiceInternal>> =
+    LazyLock::new(|| {
+        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"));
 
 fn create_or_update_idsig_file(
     input_fd: &ParcelFileDescriptor,
@@ -341,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,
@@ -387,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()?;
@@ -585,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 +742,8 @@
             boost_uclamp: config.boostUclamp,
             gpu_config,
             audio_config,
+            no_balloon: config.noBalloon,
+            usb_config,
         };
         let instance = Arc::new(
             VmInstance::new(
@@ -989,6 +1110,10 @@
 
         vm_config.devices.clone_from(&custom_config.devices);
         vm_config.networkSupported = custom_config.networkSupported;
+
+        for param in custom_config.extraKernelCmdlineParams.iter() {
+            append_kernel_param(param, &mut vm_config);
+        }
     }
 
     if config.memoryMib > 0 {
@@ -1106,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
@@ -1275,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<()> {
@@ -1434,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.
@@ -1541,6 +1668,17 @@
     Ok(())
 }
 
+fn check_no_extra_kernel_cmdline_params(config: &VirtualMachineConfig) -> binder::Result<()> {
+    let VirtualMachineConfig::AppConfig(config) = config else { return Ok(()) };
+    if let Some(custom_config) = &config.customConfig {
+        if !custom_config.extraKernelCmdlineParams.is_empty() {
+            return Err(anyhow!("debuggable_vms_improvements feature is disabled"))
+                .or_binder_exception(ExceptionCode::UNSUPPORTED_OPERATION);
+        }
+    }
+    Ok(())
+}
+
 fn check_protected_vm_is_supported() -> binder::Result<()> {
     let is_pvm_supported =
         hypervisor_props::is_protected_vm_supported().or_service_specific_exception(-1)?;
@@ -1562,6 +1700,16 @@
     if !cfg!(multi_tenant) {
         check_no_extra_apks(config)?;
     }
+    if !cfg!(debuggable_vms_improvements) {
+        check_no_extra_kernel_cmdline_params(config)?;
+    }
+    Ok(())
+}
+
+fn check_config_allowed_for_early_vms(config: &VirtualMachineConfig) -> binder::Result<()> {
+    check_no_vendor_modules(config)?;
+    check_no_devices(config)?;
+
     Ok(())
 }
 
@@ -1778,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::*;
@@ -1993,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/crosvm.rs b/android/virtmgr/src/crosvm.rs
index 08a9e47..5886535 100644
--- a/android/virtmgr/src/crosvm.rs
+++ b/android/virtmgr/src/crosvm.rs
@@ -20,7 +20,6 @@
 use anyhow::{anyhow, bail, Context, Error, Result};
 use binder::ParcelFileDescriptor;
 use command_fds::CommandFdExt;
-use lazy_static::lazy_static;
 use libc::{sysconf, _SC_CLK_TCK};
 use log::{debug, error, info};
 use semver::{Version, VersionReq};
@@ -39,7 +38,7 @@
 use std::os::unix::process::ExitStatusExt;
 use std::path::{Path, PathBuf};
 use std::process::{Command, ExitStatus};
-use std::sync::{Arc, Condvar, Mutex};
+use std::sync::{Arc, Condvar, Mutex, LazyLock};
 use std::time::{Duration, SystemTime};
 use std::thread::{self, JoinHandle};
 use android_system_virtualizationcommon::aidl::android::system::virtualizationcommon::DeathReason::DeathReason;
@@ -48,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;
@@ -89,16 +89,16 @@
 /// Serial (emulated uart)
 const CONSOLE_TTYS0: &str = "ttyS0";
 
-lazy_static! {
-    /// If the VM doesn't move to the Started state within this amount time, a hang-up error is
-    /// triggered.
-    static ref BOOT_HANGUP_TIMEOUT: Duration = if nested_virt::is_nested_virtualization().unwrap() {
+/// If the VM doesn't move to the Started state within this amount time, a hang-up error is
+/// triggered.
+static BOOT_HANGUP_TIMEOUT: LazyLock<Duration> = LazyLock::new(|| {
+    if nested_virt::is_nested_virtualization().unwrap() {
         // Nested virtualization is slow, so we need a longer timeout.
         Duration::from_secs(300)
     } else {
         Duration::from_secs(30)
-    };
-}
+    }
+});
 
 /// Configuration for a VM to run with crosvm.
 #[derive(Debug)]
@@ -134,6 +134,8 @@
     pub boost_uclamp: bool,
     pub gpu_config: Option<GpuConfig>,
     pub audio_config: Option<AudioConfig>,
+    pub no_balloon: bool,
+    pub usb_config: UsbConfig,
 }
 
 #[derive(Debug)]
@@ -149,6 +151,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,
@@ -892,12 +905,18 @@
         .arg("--cid")
         .arg(config.cid.to_string());
 
-    if system_properties::read_bool("hypervisor.memory_reclaim.supported", false)? {
+    if system_properties::read_bool("hypervisor.memory_reclaim.supported", false)?
+        && !config.no_balloon
+    {
         command.arg("--balloon-page-reporting");
     } else {
         command.arg("--no-balloon");
     }
 
+    if !config.usb_config.controller {
+        command.arg("--no-usb");
+    }
+
     let mut memory_mib = config.memory_mib;
 
     if config.protected {
diff --git a/android/virtmgr/src/debug_config.rs b/android/virtmgr/src/debug_config.rs
index 52ac964..74559de 100644
--- a/android/virtmgr/src/debug_config.rs
+++ b/android/virtmgr/src/debug_config.rs
@@ -18,7 +18,6 @@
     VirtualMachineAppConfig::DebugLevel::DebugLevel, VirtualMachineConfig::VirtualMachineConfig,
 };
 use anyhow::{anyhow, Context, Error, Result};
-use lazy_static::lazy_static;
 use libfdt::{Fdt, FdtError};
 use log::{info, warn};
 use rustutils::system_properties;
@@ -26,6 +25,7 @@
 use std::fs;
 use std::io::ErrorKind;
 use std::path::{Path, PathBuf};
+use std::sync::LazyLock;
 use vmconfig::get_debug_level;
 
 const CUSTOM_DEBUG_POLICY_OVERLAY_SYSPROP: &str =
@@ -56,11 +56,12 @@
     }
 }
 
-lazy_static! {
-    static ref DP_LOG_PATH: DPPath = DPPath::new("/avf/guest/common", "log").unwrap();
-    static ref DP_RAMDUMP_PATH: DPPath = DPPath::new("/avf/guest/common", "ramdump").unwrap();
-    static ref DP_ADB_PATH: DPPath = DPPath::new("/avf/guest/microdroid", "adb").unwrap();
-}
+static DP_LOG_PATH: LazyLock<DPPath> =
+    LazyLock::new(|| DPPath::new("/avf/guest/common", "log").unwrap());
+static DP_RAMDUMP_PATH: LazyLock<DPPath> =
+    LazyLock::new(|| DPPath::new("/avf/guest/common", "ramdump").unwrap());
+static DP_ADB_PATH: LazyLock<DPPath> =
+    LazyLock::new(|| DPPath::new("/avf/guest/microdroid", "adb").unwrap());
 
 /// Get debug policy value in bool. It's true iff the value is explicitly set to <1>.
 fn get_debug_policy_bool(path: &Path) -> Result<bool> {
diff --git a/android/virtmgr/src/main.rs b/android/virtmgr/src/main.rs
index 7d29685..67e7282 100644
--- a/android/virtmgr/src/main.rs
+++ b/android/virtmgr/src/main.rs
@@ -25,24 +25,22 @@
 
 use crate::aidl::{GLOBAL_SERVICE, VirtualizationService};
 use android_system_virtualizationservice::aidl::android::system::virtualizationservice::IVirtualizationService::BnVirtualizationService;
-use anyhow::{bail, Result};
+use anyhow::{bail, Context, Result};
 use binder::{BinderFeatures, ProcessState};
-use lazy_static::lazy_static;
 use log::{info, LevelFilter};
 use rpcbinder::{FileDescriptorTransportMode, RpcServer};
-use std::os::unix::io::{AsFd, RawFd};
+use std::os::unix::io::{AsFd, FromRawFd, OwnedFd, RawFd};
+use std::sync::LazyLock;
 use clap::Parser;
+use nix::fcntl::{fcntl, F_GETFD, F_SETFD, FdFlag};
 use nix::unistd::{write, Pid, Uid};
 use std::os::unix::raw::{pid_t, uid_t};
-use safe_ownedfd::take_fd_ownership;
 
 const LOG_TAG: &str = "virtmgr";
 
-lazy_static! {
-    static ref PID_CURRENT: Pid = Pid::this();
-    static ref PID_PARENT: Pid = Pid::parent();
-    static ref UID_CURRENT: Uid = Uid::current();
-}
+static PID_CURRENT: LazyLock<Pid> = LazyLock::new(Pid::this);
+static PID_PARENT: LazyLock<Pid> = LazyLock::new(Pid::parent);
+static UID_CURRENT: LazyLock<Uid> = LazyLock::new(Uid::current);
 
 fn get_this_pid() -> pid_t {
     // Return the process ID of this process.
@@ -73,6 +71,32 @@
     ready_fd: RawFd,
 }
 
+fn take_fd_ownership(raw_fd: RawFd, owned_fds: &mut Vec<RawFd>) -> Result<OwnedFd, anyhow::Error> {
+    // Basic check that the integer value does correspond to a file descriptor.
+    fcntl(raw_fd, F_GETFD).with_context(|| format!("Invalid file descriptor {raw_fd}"))?;
+
+    // The file descriptor had CLOEXEC disabled to be inherited from the parent.
+    // Re-enable it to make sure it is not accidentally inherited further.
+    fcntl(raw_fd, F_SETFD(FdFlag::FD_CLOEXEC))
+        .with_context(|| format!("Could not set CLOEXEC on file descriptor {raw_fd}"))?;
+
+    // Creating OwnedFd for stdio FDs is not safe.
+    if [libc::STDIN_FILENO, libc::STDOUT_FILENO, libc::STDERR_FILENO].contains(&raw_fd) {
+        bail!("File descriptor {raw_fd} is standard I/O descriptor");
+    }
+
+    // Reject RawFds that already have a corresponding OwnedFd.
+    if owned_fds.contains(&raw_fd) {
+        bail!("File descriptor {raw_fd} already owned");
+    }
+    owned_fds.push(raw_fd);
+
+    // SAFETY: Initializing OwnedFd for a RawFd provided in cmdline arguments.
+    // We checked that the integer value corresponds to a valid FD and that this
+    // is the first argument to claim its ownership.
+    Ok(unsafe { OwnedFd::from_raw_fd(raw_fd) })
+}
+
 fn check_vm_support() -> Result<()> {
     if hypervisor_props::is_any_vm_supported()? {
         Ok(())
@@ -96,15 +120,27 @@
 
     let args = Args::parse();
 
-    let rpc_server_fd =
-        take_fd_ownership(args.rpc_server_fd).expect("Failed to take ownership of rpc_server_fd");
-    let ready_fd = take_fd_ownership(args.ready_fd).expect("Failed to take ownership of ready_fd");
+    let mut owned_fds = vec![];
+    let rpc_server_fd = take_fd_ownership(args.rpc_server_fd, &mut owned_fds)
+        .expect("Failed to take ownership of rpc_server_fd");
+    let ready_fd = take_fd_ownership(args.ready_fd, &mut owned_fds)
+        .expect("Failed to take ownership of ready_fd");
 
     // Start thread pool for kernel Binder connection to VirtualizationServiceInternal.
     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/Android.bp b/android/virtualizationservice/Android.bp
index f9034af..fb6e39a 100644
--- a/android/virtualizationservice/Android.bp
+++ b/android/virtualizationservice/Android.bp
@@ -38,7 +38,6 @@
         "libbinder_rs",
         "libhex",
         "libhypervisor_props",
-        "liblazy_static",
         "liblibc",
         "liblibsqlite3_sys",
         "liblog_rust",
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/VirtualMachineAppConfig.aidl b/android/virtualizationservice/aidl/android/system/virtualizationservice/VirtualMachineAppConfig.aidl
index ee39d75..9123742 100644
--- a/android/virtualizationservice/aidl/android/system/virtualizationservice/VirtualMachineAppConfig.aidl
+++ b/android/virtualizationservice/aidl/android/system/virtualizationservice/VirtualMachineAppConfig.aidl
@@ -127,6 +127,9 @@
 
         /** Whether the VM should have network feature. */
         boolean networkSupported;
+
+        /** Additional parameters to pass to the VM's kernel cmdline. */
+        String[] extraKernelCmdlineParams;
     }
 
     /** Configuration parameters guarded by android.permission.USE_CUSTOM_VIRTUAL_MACHINE */
diff --git a/android/virtualizationservice/aidl/android/system/virtualizationservice/VirtualMachineRawConfig.aidl b/android/virtualizationservice/aidl/android/system/virtualizationservice/VirtualMachineRawConfig.aidl
index 07d52db..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 {
@@ -100,4 +101,9 @@
     @nullable GpuConfig gpuConfig;
 
     @nullable AudioConfig audioConfig;
+
+    boolean noBalloon;
+
+    /** Enable or disable USB passthrough support */
+    @nullable UsbConfig usbConfig;
 }
diff --git a/android/virtualizationservice/src/aidl.rs b/android/virtualizationservice/src/aidl.rs
index acdb53a..0f16291 100644
--- a/android/virtualizationservice/src/aidl.rs
+++ b/android/virtualizationservice/src/aidl.rs
@@ -33,7 +33,6 @@
     self, wait_for_interface, BinderFeatures, ExceptionCode, Interface, IntoBinderResult,
     LazyServiceGuard, ParcelFileDescriptor, Status, Strong,
 };
-use lazy_static::lazy_static;
 use libc::{VMADDR_CID_HOST, VMADDR_CID_HYPERVISOR, VMADDR_CID_LOCAL};
 use log::{error, info, warn};
 use nix::unistd::{chown, Uid};
@@ -52,7 +51,7 @@
 use std::os::unix::fs::PermissionsExt;
 use std::os::unix::raw::{pid_t, uid_t};
 use std::path::{Path, PathBuf};
-use std::sync::{Arc, Condvar, Mutex, Weak};
+use std::sync::{Arc, Condvar, LazyLock, Mutex, Weak};
 use tombstoned_client::{DebuggerdDumpType, TombstonedConnection};
 use virtualizationcommon::Certificate::Certificate;
 use virtualizationmaintenance::{
@@ -157,18 +156,18 @@
     0xb9, 0x0f,
 ];
 
-lazy_static! {
-    static ref FAKE_PROVISIONED_KEY_BLOB_FOR_TESTING: Mutex<Option<Vec<u8>>> = Mutex::new(None);
-    static ref VFIO_SERVICE: Strong<dyn IVfioHandler> =
-        wait_for_interface(<BpVfioHandler as IVfioHandler>::get_descriptor())
-            .expect("Could not connect to VfioHandler");
-    static ref NETWORK_SERVICE: Strong<dyn IVmnic> =
-        wait_for_interface(<BpVmnic as IVmnic>::get_descriptor())
-            .expect("Could not connect to Vmnic");
-    static ref TETHERING_SERVICE: Strong<dyn IVmTethering> =
-        wait_for_interface(<BpVmTethering as IVmTethering>::get_descriptor())
-            .expect("Could not connect to VmTethering");
-}
+static FAKE_PROVISIONED_KEY_BLOB_FOR_TESTING: Mutex<Option<Vec<u8>>> = Mutex::new(None);
+static VFIO_SERVICE: LazyLock<Strong<dyn IVfioHandler>> = LazyLock::new(|| {
+    wait_for_interface(<BpVfioHandler as IVfioHandler>::get_descriptor())
+        .expect("Could not connect to VfioHandler")
+});
+static NETWORK_SERVICE: LazyLock<Strong<dyn IVmnic>> = LazyLock::new(|| {
+    wait_for_interface(<BpVmnic as IVmnic>::get_descriptor()).expect("Could not connect to Vmnic")
+});
+static TETHERING_SERVICE: LazyLock<Strong<dyn IVmTethering>> = LazyLock::new(|| {
+    wait_for_interface(<BpVmTethering as IVmTethering>::get_descriptor())
+        .expect("Could not connect to VmTethering")
+});
 
 fn is_valid_guest_cid(cid: Cid) -> bool {
     (GUEST_CID_MIN..=GUEST_CID_MAX).contains(&cid)
diff --git a/android/virtualizationservice/vfio_handler/Android.bp b/android/virtualizationservice/vfio_handler/Android.bp
index 66fc2ee..3635cf1 100644
--- a/android/virtualizationservice/vfio_handler/Android.bp
+++ b/android/virtualizationservice/vfio_handler/Android.bp
@@ -25,7 +25,6 @@
         "libandroid_logger",
         "libanyhow",
         "libbinder_rs",
-        "liblazy_static",
         "liblog_rust",
         "libnix",
         "librustutils",
diff --git a/android/virtualizationservice/vfio_handler/src/aidl.rs b/android/virtualizationservice/vfio_handler/src/aidl.rs
index b527260..3b4d0c5 100644
--- a/android/virtualizationservice/vfio_handler/src/aidl.rs
+++ b/android/virtualizationservice/vfio_handler/src/aidl.rs
@@ -20,11 +20,11 @@
 use android_system_virtualizationservice_internal::aidl::android::system::virtualizationservice_internal::IVfioHandler::VfioDev::VfioDev;
 use android_system_virtualizationservice_internal::binder::ParcelFileDescriptor;
 use binder::{self, BinderFeatures, ExceptionCode, Interface, IntoBinderResult, Strong};
-use lazy_static::lazy_static;
 use log::error;
 use std::fs::{read_link, write, File};
 use std::io::{Read, Seek, SeekFrom, Write};
 use std::mem::size_of;
+use std::sync::LazyLock;
 use std::path::{Path, PathBuf};
 use rustutils::system_properties;
 use zerocopy::{
@@ -169,10 +169,9 @@
     _custom: [U32<BigEndian>; 4],
 }
 
-lazy_static! {
-    static ref IS_VFIO_SUPPORTED: bool =
-        Path::new(DEV_VFIO_PATH).exists() && Path::new(VFIO_PLATFORM_DRIVER_PATH).exists();
-}
+static IS_VFIO_SUPPORTED: LazyLock<bool> = LazyLock::new(|| {
+    Path::new(DEV_VFIO_PATH).exists() && Path::new(VFIO_PLATFORM_DRIVER_PATH).exists()
+});
 
 fn check_platform_device(path: &Path) -> binder::Result<()> {
     if !path.exists() {
diff --git a/android/vm/src/main.rs b/android/vm/src/main.rs
index 3c0887c..f2c2fa4 100644
--- a/android/vm/src/main.rs
+++ b/android/vm/src/main.rs
@@ -109,6 +109,23 @@
     /// Note: this is only supported on Android kernels android14-5.15 and higher.
     #[arg(long)]
     gdb: Option<NonZeroU16>,
+
+    /// Whether to enable earlycon. Only supported for debuggable Linux-based VMs.
+    #[cfg(debuggable_vms_improvements)]
+    #[arg(long)]
+    enable_earlycon: bool,
+}
+
+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
+    }
 }
 
 #[derive(Args, Default)]
@@ -142,12 +159,12 @@
 
 impl MicrodroidConfig {
     #[cfg(vendor_modules)]
-    fn vendor(&self) -> &Option<PathBuf> {
-        &self.vendor
+    fn vendor(&self) -> Option<&PathBuf> {
+        self.vendor.as_ref()
     }
 
     #[cfg(not(vendor_modules))]
-    fn vendor(&self) -> Option<PathBuf> {
+    fn vendor(&self) -> Option<&PathBuf> {
         None
     }
 
@@ -162,13 +179,13 @@
     }
 
     #[cfg(device_assignment)]
-    fn devices(&self) -> &Vec<PathBuf> {
+    fn devices(&self) -> &[PathBuf] {
         &self.devices
     }
 
     #[cfg(not(device_assignment))]
-    fn devices(&self) -> Vec<PathBuf> {
-        Vec::new()
+    fn devices(&self) -> &[PathBuf] {
+        &[]
     }
 }
 
diff --git a/android/vm/src/run.rs b/android/vm/src/run.rs
index b3743ae..823546f 100644
--- a/android/vm/src/run.rs
+++ b/android/vm/src/run.rs
@@ -148,7 +148,7 @@
 
     let payload_config_str = format!("{:?}!{:?}", config.apk, payload);
 
-    let custom_config = CustomConfig {
+    let mut custom_config = CustomConfig {
         gdbPort: config.debug.gdb.map(u16::from).unwrap_or(0) as i32, // 0 means no gdb
         vendorImage: vendor,
         devices: config
@@ -163,6 +163,21 @@
         ..Default::default()
     };
 
+    if config.debug.enable_earlycon() {
+        if config.debug.debug != DebugLevel::FULL {
+            bail!("earlycon is only supported for debuggable VMs")
+        }
+        if cfg!(target_arch = "aarch64") {
+            custom_config
+                .extraKernelCmdlineParams
+                .push(String::from("earlycon=uart8250,mmio,0x3f8"));
+        } else if cfg!(target_arch = "x86_64") {
+            custom_config.extraKernelCmdlineParams.push(String::from("earlycon=uart8250,io,0x3f8"));
+        } else {
+            bail!("unexpected architecture!");
+        }
+    }
+
     let vm_config = VirtualMachineConfig::AppConfig(VirtualMachineAppConfig {
         name: config.common.name.unwrap_or_else(|| String::from("VmRunApp")),
         apk: apk_fd.into(),
diff --git a/build/Android.bp b/build/Android.bp
index 66cc626..6ab1d89 100644
--- a/build/Android.bp
+++ b/build/Android.bp
@@ -44,6 +44,9 @@
     }) + select(release_flag("RELEASE_AVF_ENABLE_VIRT_CPUFREQ"), {
         true: ["virt_cpufreq"],
         default: [],
+    }) + select(release_flag("RELEASE_AVF_IMPROVE_DEBUGGABLE_VMS"), {
+        true: ["debuggable_vms_improvements"],
+        default: [],
     }) + select(release_flag("RELEASE_AVF_SUPPORT_CUSTOM_VM_WITH_PARAVIRTUALIZED_DEVICES"), {
         true: ["paravirtualized_devices"],
         default: [],
diff --git a/build/apex/product_packages.mk b/build/apex/product_packages.mk
index e710021..a024192 100644
--- a/build/apex/product_packages.mk
+++ b/build/apex/product_packages.mk
@@ -62,3 +62,15 @@
     $(error RELEASE_AVF_ENABLE_LLPVM_CHANGES must also be enabled)
   endif
 endif
+
+ifdef RELEASE_AVF_ENABLE_EARLY_VM
+  # We can't query TARGET_RELEASE from here, so we use RELEASE_AIDL_USE_UNFROZEN as a proxy value of
+  # whether we are building -next release.
+  ifneq ($(RELEASE_AIDL_USE_UNFROZEN),true)
+    $(error RELEASE_AVF_ENABLE_EARLY_VM can only be enabled in trunk_staging until b/357025924 is fixed)
+  endif
+endif
+
+ifdef RELEASE_AVF_SUPPORT_CUSTOM_VM_WITH_PARAVIRTUALIZED_DEVICES
+  PRODUCT_PACKAGES += LinuxInstallerAppStub
+endif
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/pvm_dice_chain.md b/docs/pvm_dice_chain.md
new file mode 100644
index 0000000..11cdb6f
--- /dev/null
+++ b/docs/pvm_dice_chain.md
@@ -0,0 +1,52 @@
+# 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, 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
+
+## 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/guest/authfs/Android.bp b/guest/authfs/Android.bp
index b11da3d..d7a8322 100644
--- a/guest/authfs/Android.bp
+++ b/guest/authfs/Android.bp
@@ -13,7 +13,6 @@
         "libanyhow",
         "libauthfs_fsverity_metadata",
         "libbinder_rs",
-        "libcfg_if",
         "libclap",
         "libfsverity_digests_proto_rust",
         "libfuse_rust",
diff --git a/guest/authfs/src/fusefs.rs b/guest/authfs/src/fusefs.rs
index 618b8ac..fa4076d 100644
--- a/guest/authfs/src/fusefs.rs
+++ b/guest/authfs/src/fusefs.rs
@@ -385,15 +385,6 @@
     }
 }
 
-cfg_if::cfg_if! {
-    if #[cfg(all(any(target_arch = "aarch64", target_arch = "riscv64"),
-                 target_pointer_width = "64"))] {
-        fn blk_size() -> libc::c_int { CHUNK_SIZE as libc::c_int }
-    } else {
-        fn blk_size() -> libc::c_long { CHUNK_SIZE as libc::c_long }
-    }
-}
-
 #[allow(clippy::enum_variant_names)]
 enum AccessMode {
     ReadOnly,
@@ -421,7 +412,7 @@
     st.st_gid = 0;
     st.st_size = libc::off64_t::try_from(file_size)
         .map_err(|_| io::Error::from_raw_os_error(libc::EFBIG))?;
-    st.st_blksize = blk_size();
+    st.st_blksize = CHUNK_SIZE.try_into().unwrap();
     // Per man stat(2), st_blocks is "Number of 512B blocks allocated".
     st.st_blocks = libc::c_longlong::try_from(divide_roundup(file_size, 512))
         .map_err(|_| io::Error::from_raw_os_error(libc::EFBIG))?;
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/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..7352a2c 100644
--- a/guest/microdroid_manager/src/main.rs
+++ b/guest/microdroid_manager/src/main.rs
@@ -50,14 +50,13 @@
 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;
 use std::ffi::CString;
 use std::fs::{self, create_dir, File, OpenOptions};
 use std::io::{Read, Write};
-use std::os::unix::io::OwnedFd;
+use std::os::unix::io::{FromRawFd, OwnedFd};
 use std::os::unix::process::CommandExt;
 use std::os::unix::process::ExitStatusExt;
 use std::path::Path;
@@ -200,7 +199,13 @@
     );
     info!("started.");
 
-    let vm_payload_service_fd = prepare_vm_payload_service_socket()?;
+    // SAFETY: This is the only place we take the ownership of the fd of the vm payload service.
+    //
+    // To ensure that the CLOEXEC flag is set on the file descriptor as early as possible,
+    // it is necessary to fetch the socket corresponding to vm_payload_service at the
+    // very beginning, as android_get_control_socket() sets the CLOEXEC flag on the file
+    // descriptor.
+    let vm_payload_service_fd = unsafe { prepare_vm_payload_service_socket()? };
 
     load_crashkernel_if_supported().context("Failed to load crashkernel")?;
 
@@ -265,7 +270,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.
@@ -482,9 +487,22 @@
 }
 
 /// Prepares a socket file descriptor for the vm payload service.
-fn prepare_vm_payload_service_socket() -> Result<OwnedFd> {
+///
+/// # Safety
+///
+/// The caller must ensure that this function is the only place that claims ownership
+/// of the file descriptor and it is called only once.
+unsafe 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)?)
+
+    // Creating OwnedFd for stdio FDs is not safe.
+    if [libc::STDIN_FILENO, libc::STDOUT_FILENO, libc::STDERR_FILENO].contains(&raw_fd) {
+        bail!("File descriptor {raw_fd} is standard I/O descriptor");
+    }
+    // SAFETY: Initializing OwnedFd for a RawFd created by the init.
+    // We checked that the integer value corresponds to a valid FD and that the caller
+    // ensures that this is the only place to claim its ownership.
+    Ok(unsafe { OwnedFd::from_raw_fd(raw_fd) })
 }
 
 fn is_strict_boot() -> bool {
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/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 ce04317..8f9340b 100644
--- a/guest/pvmfw/src/entry.rs
+++ b/guest/pvmfw/src/entry.rs
@@ -276,8 +276,9 @@
     MEMORY.lock().as_mut().unwrap().unshare_all_memory();
 
     if let Some(mmio_guard) = get_mmio_guard() {
-        // Keep UART MMIO_GUARD-ed for debuggable payloads, to enable earlycon.
-        if !debuggable_payload {
+        if cfg!(debuggable_vms_improvements) && debuggable_payload {
+            // Keep UART MMIO_GUARD-ed for debuggable payloads, to enable earlycon.
+        } else {
             mmio_guard.unmap(UART_PAGE_ADDR).map_err(|e| {
                 error!("Failed to unshare the UART: {e}");
                 RebootReason::InternalError
diff --git a/guest/rialto/src/main.rs b/guest/rialto/src/main.rs
index 604f935..d35354c 100644
--- a/guest/rialto/src/main.rs
+++ b/guest/rialto/src/main.rs
@@ -109,31 +109,27 @@
     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
         })?;
     }
 
@@ -153,9 +149,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.
diff --git a/guest/rialto/tests/test.rs b/guest/rialto/tests/test.rs
index 7c0d9dc..582b69e 100644
--- a/guest/rialto/tests/test.rs
+++ b/guest/rialto/tests/test.rs
@@ -68,14 +68,9 @@
 
 #[test]
 fn process_requests_in_non_protected_vm() -> Result<()> {
-    check_processing_requests(VmType::NonProtectedVm, None)
-}
-
-#[ignore] // TODO(b/360077974): Figure out why this is flaky.
-#[test]
-fn process_requests_in_non_protected_vm_with_extra_ram() -> Result<()> {
     const MEMORY_MB: i32 = 300;
-    check_processing_requests(VmType::NonProtectedVm, Some(MEMORY_MB))
+    check_processing_requests(VmType::NonProtectedVm, Some(MEMORY_MB))?;
+    check_processing_requests(VmType::NonProtectedVm, None)
 }
 
 fn check_processing_requests(vm_type: VmType, vm_memory_mb: Option<i32>) -> Result<()> {
@@ -288,7 +283,9 @@
 }
 
 fn check_csr(csr: Vec<u8>) -> Result<()> {
-    let _csr = rkp::Csr::from_cbor(&Session::default(), &csr[..]).context("Failed to parse CSR")?;
+    let mut session = Session::default();
+    session.set_allow_any_mode(true);
+    let _csr = rkp::Csr::from_cbor(&session, &csr[..]).context("Failed to parse CSR")?;
     Ok(())
 }
 
diff --git a/libs/dice/open_dice/Android.bp b/libs/dice/open_dice/Android.bp
index 52b28b8..d1129fb 100644
--- a/libs/dice/open_dice/Android.bp
+++ b/libs/dice/open_dice/Android.bp
@@ -54,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/framework-virtualization/src/android/system/virtualmachine/VirtualMachineConfig.java b/libs/framework-virtualization/src/android/system/virtualmachine/VirtualMachineConfig.java
index 7ae4a55..cb21ccf 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;
@@ -724,6 +725,16 @@
                 Optional.ofNullable(customImageConfig.getAudioConfig())
                         .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;
     }
 
@@ -777,6 +788,7 @@
             VirtualMachineAppConfig.CustomConfig customConfig =
                     new VirtualMachineAppConfig.CustomConfig();
             customConfig.devices = EMPTY_STRING_ARRAY;
+            customConfig.extraKernelCmdlineParams = EMPTY_STRING_ARRAY;
             try {
                 customConfig.vendorImage =
                         ParcelFileDescriptor.open(mVendorDiskImage, MODE_READ_ONLY);
diff --git a/libs/framework-virtualization/src/android/system/virtualmachine/VirtualMachineCustomImageConfig.java b/libs/framework-virtualization/src/android/system/virtualmachine/VirtualMachineCustomImageConfig.java
index 37dc8fa..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;
@@ -360,7 +375,9 @@
         private boolean network;
         private GpuConfig gpuConfig;
         private boolean trackpad;
-        private boolean autoMemoryBalloon = true;
+        // TODO(b/363985291): balloon breaks Linux VM behavior
+        private boolean autoMemoryBalloon = false;
+        private UsbConfig usbConfig;
 
         /** @hide */
         public Builder() {}
@@ -462,6 +479,12 @@
         }
 
         /** @hide */
+        public Builder setUsbConfig(UsbConfig usbConfig) {
+            this.usbConfig = usbConfig;
+            return this;
+        }
+
+        /** @hide */
         public VirtualMachineCustomImageConfig build() {
             return new VirtualMachineCustomImageConfig(
                     this.name,
@@ -479,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/libcompos_common/Android.bp b/libs/libcompos_common/Android.bp
index 72cb5e1..01836ae 100644
--- a/libs/libcompos_common/Android.bp
+++ b/libs/libcompos_common/Android.bp
@@ -14,7 +14,6 @@
         "libanyhow",
         "libbinder_rs",
         "libglob",
-        "liblazy_static",
         "liblog_rust",
         "libnested_virt",
         "libnum_traits",
diff --git a/libs/libcompos_common/timeouts.rs b/libs/libcompos_common/timeouts.rs
index 7bd7679..d22f7f7 100644
--- a/libs/libcompos_common/timeouts.rs
+++ b/libs/libcompos_common/timeouts.rs
@@ -17,7 +17,7 @@
 //! Timeouts for common situations, with support for longer timeouts when using nested
 //! virtualization.
 
-use lazy_static::lazy_static;
+use std::sync::LazyLock;
 use std::time::Duration;
 
 /// Holder for the various timeouts we use.
@@ -31,15 +31,15 @@
     pub vm_max_time_to_exit: Duration,
 }
 
-lazy_static! {
 /// The timeouts that are appropriate on the current platform.
-pub static ref TIMEOUTS: Timeouts = if nested_virt::is_nested_virtualization().unwrap() {
-    // Nested virtualization is slow.
-    EXTENDED_TIMEOUTS
-} else {
-    NORMAL_TIMEOUTS
-};
-}
+pub static TIMEOUTS: LazyLock<Timeouts> = LazyLock::new(|| {
+    if nested_virt::is_nested_virtualization().unwrap() {
+        // Nested virtualization is slow.
+        EXTENDED_TIMEOUTS
+    } else {
+        NORMAL_TIMEOUTS
+    }
+});
 
 /// The timeouts that we use normally.
 const NORMAL_TIMEOUTS: Timeouts = Timeouts {
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/libinherited_fd/Android.bp b/libs/libinherited_fd/Android.bp
new file mode 100644
index 0000000..28ec2e5
--- /dev/null
+++ b/libs/libinherited_fd/Android.bp
@@ -0,0 +1,44 @@
+package {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+rust_defaults {
+    name: "libinherited_fd.defaults",
+    crate_name: "inherited_fd",
+    srcs: ["src/lib.rs"],
+    edition: "2021",
+    rustlibs: [
+        "libnix",
+        "libonce_cell",
+        "libthiserror",
+    ],
+}
+
+rust_library {
+    name: "libinherited_fd",
+    defaults: ["libinherited_fd.defaults"],
+    apex_available: [
+        "com.android.compos",
+        "com.android.virt",
+    ],
+}
+
+rust_test {
+    name: "libinherited_fd.test",
+    defaults: ["libinherited_fd.defaults"],
+    rustlibs: [
+        "libanyhow",
+        "libtempfile",
+    ],
+    host_supported: true,
+    test_suites: ["general-tests"],
+    test_options: {
+        unit_test: true,
+    },
+    // this is to run each test function in a separate process.
+    // note that they still run in parallel.
+    flags: [
+        "-C panic=abort",
+        "-Z panic_abort_tests",
+    ],
+}
diff --git a/libs/libinherited_fd/src/lib.rs b/libs/libinherited_fd/src/lib.rs
new file mode 100644
index 0000000..f5e2d6b
--- /dev/null
+++ b/libs/libinherited_fd/src/lib.rs
@@ -0,0 +1,270 @@
+// Copyright 2024, The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+//! Library for safely obtaining `OwnedFd` for inherited file descriptors.
+
+use nix::fcntl::{fcntl, FdFlag, F_SETFD};
+use nix::libc;
+use std::collections::HashMap;
+use std::fs::canonicalize;
+use std::fs::read_dir;
+use std::os::fd::FromRawFd;
+use std::os::fd::OwnedFd;
+use std::os::fd::RawFd;
+use std::sync::Mutex;
+use std::sync::OnceLock;
+use thiserror::Error;
+
+/// Errors that can occur while taking an ownership of `RawFd`
+#[derive(Debug, PartialEq, Error)]
+pub enum Error {
+    /// init_once() not called
+    #[error("init_once() not called")]
+    NotInitialized,
+
+    /// Ownership already taken
+    #[error("Ownership of FD {0} is already taken")]
+    OwnershipTaken(RawFd),
+
+    /// Not an inherited file descriptor
+    #[error("FD {0} is either invalid file descriptor or not an inherited one")]
+    FileDescriptorNotInherited(RawFd),
+
+    /// Failed to set CLOEXEC
+    #[error("Failed to set CLOEXEC on FD {0}")]
+    FailCloseOnExec(RawFd),
+}
+
+static INHERITED_FDS: OnceLock<Mutex<HashMap<RawFd, Option<OwnedFd>>>> = OnceLock::new();
+
+/// Take ownership of all open file descriptors in this process, which later can be obtained by
+/// calling `take_fd_ownership`.
+///
+/// # Safety
+/// This function has to be called very early in the program before the ownership of any file
+/// descriptors (except stdin/out/err) is taken.
+pub unsafe fn init_once() -> Result<(), std::io::Error> {
+    let mut fds = HashMap::new();
+
+    let fd_path = canonicalize("/proc/self/fd")?;
+
+    for entry in read_dir(&fd_path)? {
+        let entry = entry?;
+
+        // Files in /prod/self/fd are guaranteed to be numbers. So parsing is always successful.
+        let file_name = entry.file_name();
+        let raw_fd = file_name.to_str().unwrap().parse::<RawFd>().unwrap();
+
+        // We don't take ownership of the stdio FDs as the Rust runtime owns them.
+        if [libc::STDIN_FILENO, libc::STDOUT_FILENO, libc::STDERR_FILENO].contains(&raw_fd) {
+            continue;
+        }
+
+        // Exceptional case: /proc/self/fd/* may be a dir fd created by read_dir just above. Since
+        // the file descriptor is owned by read_dir (and thus closed by it), we shouldn't take
+        // ownership to it.
+        if entry.path().read_link()? == fd_path {
+            continue;
+        }
+
+        // SAFETY: /proc/self/fd/* are file descriptors that are open. If `init_once()` was called
+        // at the very beginning of the program execution (as requested by the safety requirement
+        // of this function), this is the first time to claim the ownership of these file
+        // descriptors.
+        let owned_fd = unsafe { OwnedFd::from_raw_fd(raw_fd) };
+        fds.insert(raw_fd, Some(owned_fd));
+    }
+
+    INHERITED_FDS
+        .set(Mutex::new(fds))
+        .or(Err(std::io::Error::other("Inherited fds were already initialized")))
+}
+
+/// Take the ownership of the given `RawFd` and returns `OwnedFd` for it. The returned FD is set
+/// CLOEXEC. `Error` is returned when the ownership was already taken (by a prior call to this
+/// function with the same `RawFd`) or `RawFd` is not an inherited file descriptor.
+pub fn take_fd_ownership(raw_fd: RawFd) -> Result<OwnedFd, Error> {
+    let mut fds = INHERITED_FDS.get().ok_or(Error::NotInitialized)?.lock().unwrap();
+
+    if let Some(value) = fds.get_mut(&raw_fd) {
+        if let Some(owned_fd) = value.take() {
+            fcntl(raw_fd, F_SETFD(FdFlag::FD_CLOEXEC)).or(Err(Error::FailCloseOnExec(raw_fd)))?;
+            Ok(owned_fd)
+        } else {
+            Err(Error::OwnershipTaken(raw_fd))
+        }
+    } else {
+        Err(Error::FileDescriptorNotInherited(raw_fd))
+    }
+}
+
+#[cfg(test)]
+mod test {
+    use super::*;
+    use anyhow::Result;
+    use nix::fcntl::{fcntl, FdFlag, F_GETFD, F_SETFD};
+    use nix::unistd::close;
+    use std::os::fd::{AsRawFd, IntoRawFd};
+    use tempfile::tempfile;
+
+    struct Fixture {
+        fds: Vec<RawFd>,
+    }
+
+    impl Fixture {
+        fn setup(num_fds: usize) -> Result<Self> {
+            let mut fds = Vec::new();
+            for _ in 0..num_fds {
+                fds.push(tempfile()?.into_raw_fd());
+            }
+            Ok(Fixture { fds })
+        }
+
+        fn open_new_file(&mut self) -> Result<RawFd> {
+            let raw_fd = tempfile()?.into_raw_fd();
+            self.fds.push(raw_fd);
+            Ok(raw_fd)
+        }
+    }
+
+    impl Drop for Fixture {
+        fn drop(&mut self) {
+            self.fds.iter().for_each(|fd| {
+                let _ = close(*fd);
+            });
+        }
+    }
+
+    fn is_fd_opened(raw_fd: RawFd) -> bool {
+        fcntl(raw_fd, F_GETFD).is_ok()
+    }
+
+    #[test]
+    fn happy_case() -> Result<()> {
+        let fixture = Fixture::setup(2)?;
+        let f0 = fixture.fds[0];
+        let f1 = fixture.fds[1];
+
+        // SAFETY: assume files opened by Fixture are inherited ones
+        unsafe {
+            init_once()?;
+        }
+
+        let f0_owned = take_fd_ownership(f0)?;
+        let f1_owned = take_fd_ownership(f1)?;
+        assert_eq!(f0, f0_owned.as_raw_fd());
+        assert_eq!(f1, f1_owned.as_raw_fd());
+
+        drop(f0_owned);
+        drop(f1_owned);
+        assert!(!is_fd_opened(f0));
+        assert!(!is_fd_opened(f1));
+        Ok(())
+    }
+
+    #[test]
+    fn access_non_inherited_fd() -> Result<()> {
+        let mut fixture = Fixture::setup(2)?;
+
+        // SAFETY: assume files opened by Fixture are inherited ones
+        unsafe {
+            init_once()?;
+        }
+
+        let f = fixture.open_new_file()?;
+        assert_eq!(Some(Error::FileDescriptorNotInherited(f)), take_fd_ownership(f).err());
+        Ok(())
+    }
+
+    #[test]
+    fn call_init_once_multiple_times() -> Result<()> {
+        let _ = Fixture::setup(2)?;
+
+        // SAFETY: assume files opened by Fixture are inherited ones
+        unsafe {
+            init_once()?;
+        }
+
+        // SAFETY: for testing
+        let res = unsafe { init_once() };
+        assert!(res.is_err());
+        Ok(())
+    }
+
+    #[test]
+    fn access_without_init_once() -> Result<()> {
+        let fixture = Fixture::setup(2)?;
+
+        let f = fixture.fds[0];
+        assert_eq!(Some(Error::NotInitialized), take_fd_ownership(f).err());
+        Ok(())
+    }
+
+    #[test]
+    fn double_ownership() -> Result<()> {
+        let fixture = Fixture::setup(2)?;
+        let f = fixture.fds[0];
+
+        // SAFETY: assume files opened by Fixture are inherited ones
+        unsafe {
+            init_once()?;
+        }
+
+        let f_owned = take_fd_ownership(f)?;
+        let f_double_owned = take_fd_ownership(f);
+        assert_eq!(Some(Error::OwnershipTaken(f)), f_double_owned.err());
+
+        // just to highlight that f_owned is kept alive when the second call to take_fd_ownership
+        // is made.
+        drop(f_owned);
+        Ok(())
+    }
+
+    #[test]
+    fn take_drop_retake() -> Result<()> {
+        let fixture = Fixture::setup(2)?;
+        let f = fixture.fds[0];
+
+        // SAFETY: assume files opened by Fixture are inherited ones
+        unsafe {
+            init_once()?;
+        }
+
+        let f_owned = take_fd_ownership(f)?;
+        drop(f_owned);
+
+        let f_double_owned = take_fd_ownership(f);
+        assert_eq!(Some(Error::OwnershipTaken(f)), f_double_owned.err());
+        Ok(())
+    }
+
+    #[test]
+    fn cloexec() -> Result<()> {
+        let fixture = Fixture::setup(2)?;
+        let f = fixture.fds[0];
+
+        // SAFETY: assume files opened by Fixture are inherited ones
+        unsafe {
+            init_once()?;
+        }
+
+        // Intentionally cleaar cloexec to see if it is set by take_fd_ownership
+        fcntl(f, F_SETFD(FdFlag::empty()))?;
+
+        let f_owned = take_fd_ownership(f)?;
+        let flags = fcntl(f_owned.as_raw_fd(), F_GETFD)?;
+        assert_eq!(flags, FdFlag::FD_CLOEXEC.bits());
+        Ok(())
+    }
+}
diff --git a/libs/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/libservice_vm_manager/Android.bp b/libs/libservice_vm_manager/Android.bp
index 6469212..b3618a6 100644
--- a/libs/libservice_vm_manager/Android.bp
+++ b/libs/libservice_vm_manager/Android.bp
@@ -12,7 +12,6 @@
         "android.system.virtualizationservice-rust",
         "libanyhow",
         "libciborium",
-        "liblazy_static",
         "liblog_rust",
         "libnix",
         "libservice_vm_comm",
diff --git a/libs/libservice_vm_manager/src/lib.rs b/libs/libservice_vm_manager/src/lib.rs
index d3d86e9..d7b4dd6 100644
--- a/libs/libservice_vm_manager/src/lib.rs
+++ b/libs/libservice_vm_manager/src/lib.rs
@@ -25,7 +25,6 @@
     binder::ParcelFileDescriptor,
 };
 use anyhow::{anyhow, ensure, Context, Result};
-use lazy_static::lazy_static;
 use log::{info, warn};
 use service_vm_comm::{Request, Response, ServiceVmRequest, VmType};
 use std::fs::{self, File, OpenOptions};
@@ -48,11 +47,10 @@
 const WRITE_BUFFER_CAPACITY: usize = 512;
 const READ_TIMEOUT: Duration = Duration::from_secs(10);
 const WRITE_TIMEOUT: Duration = Duration::from_secs(10);
-lazy_static! {
-    static ref PENDING_REQUESTS: AtomicCounter = AtomicCounter::default();
-    static ref SERVICE_VM: Mutex<Option<ServiceVm>> = Mutex::new(None);
-    static ref SERVICE_VM_SHUTDOWN: Condvar = Condvar::new();
-}
+
+static PENDING_REQUESTS: AtomicCounter = AtomicCounter::new();
+static SERVICE_VM: Mutex<Option<ServiceVm>> = Mutex::new(None);
+static SERVICE_VM_SHUTDOWN: Condvar = Condvar::new();
 
 /// Atomic counter with a condition variable that is used to wait for the counter
 /// to become positive within a timeout.
@@ -63,6 +61,10 @@
 }
 
 impl AtomicCounter {
+    const fn new() -> Self {
+        Self { num: Mutex::new(0), num_increased: Condvar::new() }
+    }
+
     /// Checks if the counter becomes positive within the given timeout.
     fn is_positive_within_timeout(&self, timeout: Duration) -> bool {
         let (guard, _wait_result) = self
diff --git a/libs/libvirtualization_jni/Android.bp b/libs/libvirtualization_jni/Android.bp
index 4a569d4..9dc86b0 100644
--- a/libs/libvirtualization_jni/Android.bp
+++ b/libs/libvirtualization_jni/Android.bp
@@ -16,7 +16,10 @@
         "liblog",
         "libnativehelper",
     ],
-    static_libs: ["libavf_cc_flags"],
+    static_libs: [
+        "libavf_cc_flags",
+        "libvmclient.ffi",
+    ],
 }
 
 cc_library_shared {
diff --git a/libs/libvirtualization_jni/android_system_virtualmachine_VirtualizationService.cpp b/libs/libvirtualization_jni/android_system_virtualmachine_VirtualizationService.cpp
index 0538c9e..f0c9b4f 100644
--- a/libs/libvirtualization_jni/android_system_virtualmachine_VirtualizationService.cpp
+++ b/libs/libvirtualization_jni/android_system_virtualmachine_VirtualizationService.cpp
@@ -19,6 +19,7 @@
 #include <android-base/unique_fd.h>
 #include <android/avf_cc_flags.h>
 #include <android/binder_ibinder_jni.h>
+#include <errno.h>
 #include <jni.h>
 #include <log/log.h>
 #include <poll.h>
@@ -29,57 +30,25 @@
 
 using namespace android::base;
 
-static constexpr const char VIRTMGR_PATH[] = "/apex/com.android.virt/bin/virtmgr";
 static constexpr size_t VIRTMGR_THREADS = 2;
 
+void error_callback(int code, const char* msg, void* ctx) {
+    JNIEnv* env = reinterpret_cast<JNIEnv*>(ctx);
+    if (code == EPERM || code == EACCES) {
+        env->ThrowNew(env->FindClass("java/lang/SecurityException"),
+                      "Virtmgr didn't send any data through pipe. Please consider checking if "
+                      "android.permission.MANAGE_VIRTUAL_MACHINE permission is granted");
+        return;
+    }
+    env->ThrowNew(env->FindClass("android/system/virtualmachine/VirtualMachineException"), msg);
+}
+
+extern "C" int get_virtualization_service(decltype(error_callback)*, void*);
+
 extern "C" JNIEXPORT jint JNICALL
 Java_android_system_virtualmachine_VirtualizationService_nativeSpawn(
         JNIEnv* env, [[maybe_unused]] jclass clazz) {
-    unique_fd serverFd, clientFd;
-    if (!Socketpair(SOCK_STREAM, &serverFd, &clientFd)) {
-        env->ThrowNew(env->FindClass("android/system/virtualmachine/VirtualMachineException"),
-                      ("Failed to create socketpair: " + std::string(strerror(errno))).c_str());
-        return -1;
-    }
-
-    unique_fd waitFd, readyFd;
-    if (!Pipe(&waitFd, &readyFd, 0)) {
-        env->ThrowNew(env->FindClass("android/system/virtualmachine/VirtualMachineException"),
-                      ("Failed to create pipe: " + std::string(strerror(errno))).c_str());
-        return -1;
-    }
-
-    if (fork() == 0) {
-        // Close client's FDs.
-        clientFd.reset();
-        waitFd.reset();
-
-        auto strServerFd = std::to_string(serverFd.get());
-        auto strReadyFd = std::to_string(readyFd.get());
-
-        execl(VIRTMGR_PATH, VIRTMGR_PATH, "--rpc-server-fd", strServerFd.c_str(), "--ready-fd",
-              strReadyFd.c_str(), NULL);
-    }
-
-    // Close virtmgr's FDs.
-    serverFd.reset();
-    readyFd.reset();
-
-    // Wait for the server to signal its readiness by closing its end of the pipe.
-    char buf;
-    int ret = read(waitFd.get(), &buf, sizeof(buf));
-    if (ret < 0) {
-        env->ThrowNew(env->FindClass("android/system/virtualmachine/VirtualMachineException"),
-                      "Failed to wait for VirtualizationService to be ready");
-        return -1;
-    } else if (ret < 1) {
-        env->ThrowNew(env->FindClass("java/lang/SecurityException"),
-                      "Virtmgr didn't send any data through pipe. Please consider checking if "
-                      "android.permission.MANAGE_VIRTUAL_MACHINE permission is granted");
-        return -1;
-    }
-
-    return clientFd.release();
+    return get_virtualization_service(error_callback, env);
 }
 
 extern "C" JNIEXPORT jobject JNICALL
diff --git a/libs/libvm_payload/Android.bp b/libs/libvm_payload/Android.bp
index cf2a002..bb91737 100644
--- a/libs/libvm_payload/Android.bp
+++ b/libs/libvm_payload/Android.bp
@@ -16,7 +16,6 @@
         "libandroid_logger",
         "libanyhow",
         "libbinder_rs",
-        "liblazy_static",
         "liblibc",
         "liblog_rust",
         "libopenssl",
diff --git a/libs/libvm_payload/src/lib.rs b/libs/libvm_payload/src/lib.rs
index 13c6e76..40f7b79 100644
--- a/libs/libvm_payload/src/lib.rs
+++ b/libs/libvm_payload/src/lib.rs
@@ -23,7 +23,6 @@
     unstable_api::{new_spibinder, AIBinder},
     Strong, ExceptionCode,
 };
-use lazy_static::lazy_static;
 use log::{error, info, LevelFilter};
 use rpcbinder::{RpcServer, RpcSession};
 use openssl::{ec::EcKey, sha::sha256, ecdsa::EcdsaSig};
@@ -35,6 +34,7 @@
 use std::ptr::{self, NonNull};
 use std::sync::{
     atomic::{AtomicBool, Ordering},
+    LazyLock,
     Mutex,
 };
 use vm_payload_status_bindgen::AVmAttestationStatus;
@@ -42,13 +42,11 @@
 /// Maximum size of an ECDSA signature for EC P-256 key is 72 bytes.
 const MAX_ECDSA_P256_SIGNATURE_SIZE: usize = 72;
 
-lazy_static! {
-    static ref VM_APK_CONTENTS_PATH_C: CString =
-        CString::new(VM_APK_CONTENTS_PATH).expect("CString::new failed");
-    static ref PAYLOAD_CONNECTION: Mutex<Option<Strong<dyn IVmPayloadService>>> = Mutex::default();
-    static ref VM_ENCRYPTED_STORAGE_PATH_C: CString =
-        CString::new(ENCRYPTEDSTORE_MOUNTPOINT).expect("CString::new failed");
-}
+static VM_APK_CONTENTS_PATH_C: LazyLock<CString> =
+    LazyLock::new(|| CString::new(VM_APK_CONTENTS_PATH).expect("CString::new failed"));
+static PAYLOAD_CONNECTION: Mutex<Option<Strong<dyn IVmPayloadService>>> = Mutex::new(None);
+static VM_ENCRYPTED_STORAGE_PATH_C: LazyLock<CString> =
+    LazyLock::new(|| CString::new(ENCRYPTEDSTORE_MOUNTPOINT).expect("CString::new failed"));
 
 static ALREADY_NOTIFIED: AtomicBool = AtomicBool::new(false);
 
diff --git a/libs/libvmclient/Android.bp b/libs/libvmclient/Android.bp
index 9fdeaf8..5bd59da 100644
--- a/libs/libvmclient/Android.bp
+++ b/libs/libvmclient/Android.bp
@@ -2,8 +2,8 @@
     default_applicable_licenses: ["Android-Apache-2.0"],
 }
 
-rust_library {
-    name: "libvmclient",
+rust_defaults {
+    name: "libvmclient.default",
     crate_name: "vmclient",
     defaults: ["avf_build_flags_rust"],
     srcs: ["src/lib.rs"],
@@ -25,3 +25,13 @@
         "com.android.virt",
     ],
 }
+
+rust_library {
+    name: "libvmclient",
+    defaults: ["libvmclient.default"],
+}
+
+rust_ffi_static {
+    name: "libvmclient.ffi",
+    defaults: ["libvmclient.default"],
+}
diff --git a/libs/libvmclient/src/lib.rs b/libs/libvmclient/src/lib.rs
index 7b576e6..bc9d683 100644
--- a/libs/libvmclient/src/lib.rs
+++ b/libs/libvmclient/src/lib.rs
@@ -43,7 +43,9 @@
 use log::warn;
 use rpcbinder::{FileDescriptorTransportMode, RpcSession};
 use shared_child::SharedChild;
+use std::ffi::{c_char, c_int, c_void, CString};
 use std::io::{self, Read};
+use std::os::fd::RawFd;
 use std::process::Command;
 use std::{
     fmt::{self, Debug, Formatter},
@@ -74,6 +76,40 @@
     Ok(socketpair(AddressFamily::Unix, SockType::Stream, None, SockFlag::SOCK_CLOEXEC)?)
 }
 
+/// Error handling function for `get_virtualization_service`.
+///
+/// # Safety
+/// `message` shouldn't be used outside of the lifetime of the function. Management of `ctx` is
+/// entirely up to the function.
+pub type ErrorCallback =
+    unsafe extern "C" fn(code: c_int, message: *const c_char, ctx: *mut c_void);
+
+/// Spawns a new instance of virtmgr and rerturns a file descriptor for the socket connection to
+/// the service. When error occurs, it is reported via the ErrorCallback function along with the
+/// error message and any context that is set by the client.
+///
+/// # Safety
+/// `cb` should be null or a valid function pointer of type `ErrorCallback`
+#[no_mangle]
+pub unsafe extern "C" fn get_virtualization_service(
+    cb: Option<ErrorCallback>,
+    ctx: *mut c_void,
+) -> RawFd {
+    match VirtualizationService::new() {
+        Ok(vs) => vs.client_fd.into_raw_fd(),
+        Err(e) => {
+            if let Some(cb) = cb {
+                let code = e.raw_os_error().unwrap_or(-1);
+                let msg = CString::new(e.to_string()).unwrap();
+                // SAFETY: `cb` doesn't use `msg` outside of the lifetime of the function.
+                // msg's lifetime is longer than `cb` as it is bound to a local variable.
+                unsafe { cb(code, msg.as_ptr(), ctx) };
+            }
+            -1
+        }
+    }
+}
+
 /// A running instance of virtmgr which is hosting a VirtualizationService
 /// RpcBinder server.
 pub struct VirtualizationService {
@@ -97,10 +133,11 @@
 
         SharedChild::spawn(&mut command)?;
 
-        // Wait for the child to signal that the RpcBinder server is ready
-        // by closing its end of the pipe.
-        let _ignored = File::from(wait_fd).read(&mut [0]);
-
+        // Wait for the child to signal that the RpcBinder server is read by closing its end of the
+        // pipe. Failing to read (especially EACCESS or EPERM) can happen if the client lacks the
+        // MANAGE_VIRTUAL_MACHINE permission. Therefore, such errors are propagated instead of
+        // being ignored.
+        let _ = File::from(wait_fd).read(&mut [0])?;
         Ok(VirtualizationService { client_fd })
     }
 
diff --git a/libs/vm_launcher_lib/java/com/android/virtualization/vmlauncher/VmLauncherServices.java b/libs/vm_launcher_lib/java/com/android/virtualization/vmlauncher/VmLauncherServices.java
index c5bc5fb..565b793 100644
--- a/libs/vm_launcher_lib/java/com/android/virtualization/vmlauncher/VmLauncherServices.java
+++ b/libs/vm_launcher_lib/java/com/android/virtualization/vmlauncher/VmLauncherServices.java
@@ -59,6 +59,11 @@
         return i;
     }
 
+    public static void stopVmLauncherService(Context context) {
+        Intent i = buildVmLauncherServiceIntent(context);
+        context.stopService(i);
+    }
+
     public static void startVmLauncherService(Context context, VmLauncherServiceCallback callback) {
         Intent i = buildVmLauncherServiceIntent(context);
         if (i == null) {
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/ComposHostTestCases/java/android/compos/test/ComposTestCase.java b/tests/ComposHostTestCases/java/android/compos/test/ComposTestCase.java
index 7a35829..6e583c0 100644
--- a/tests/ComposHostTestCases/java/android/compos/test/ComposTestCase.java
+++ b/tests/ComposHostTestCases/java/android/compos/test/ComposTestCase.java
@@ -200,6 +200,7 @@
                                 10000,
                                 validator.getAbsolutePath(),
                                 "dice-chain",
+                                "--allow-any-mode",
                                 bcc_file.getAbsolutePath());
         assertWithMessage("hwtrust failed").about(command_results()).that(result).isSuccess();
     }
diff --git a/tests/testapk/src/java/com/android/microdroid/test/HwTrustJni.java b/tests/testapk/src/java/com/android/microdroid/test/HwTrustJni.java
index 3b237aa..0cf0606 100644
--- a/tests/testapk/src/java/com/android/microdroid/test/HwTrustJni.java
+++ b/tests/testapk/src/java/com/android/microdroid/test/HwTrustJni.java
@@ -25,7 +25,8 @@
      * Validates a DICE chain.
      *
      * @param diceChain The dice chain to validate.
+     * @param allowAnyMode Allow the chain's certificates to have any mode.
      * @return true if the dice chain is valid, false otherwise.
      */
-    public static native boolean validateDiceChain(byte[] diceChain);
+    public static native boolean validateDiceChain(byte[] diceChain, boolean allowAnyMode);
 }
diff --git a/tests/testapk/src/java/com/android/microdroid/test/MicrodroidTests.java b/tests/testapk/src/java/com/android/microdroid/test/MicrodroidTests.java
index f21e18e..d38af45 100644
--- a/tests/testapk/src/java/com/android/microdroid/test/MicrodroidTests.java
+++ b/tests/testapk/src/java/com/android/microdroid/test/MicrodroidTests.java
@@ -1318,7 +1318,7 @@
         grantPermission(VirtualMachine.USE_CUSTOM_VIRTUAL_MACHINE_PERMISSION);
         VirtualMachineConfig config =
                 newVmConfigBuilderWithPayloadConfig("assets/vm_config.json")
-                        .setDebugLevel(DEBUG_LEVEL_FULL)
+                        .setDebugLevel(DEBUG_LEVEL_NONE)
                         .build();
         VirtualMachine vm = forceCreateNewVirtualMachine("bcc_vm_for_vsr", config);
         TestResults testResults =
@@ -1331,7 +1331,11 @@
         testResults.assertNoException();
         byte[] bccBytes = testResults.mBcc;
         assertThat(bccBytes).isNotNull();
-        assertThat(HwTrustJni.validateDiceChain(bccBytes)).isTrue();
+
+        String buildType = SystemProperties.get("ro.build.type");
+        boolean nonUserBuild = !buildType.isEmpty() && buildType != "user";
+
+        assertThat(HwTrustJni.validateDiceChain(bccBytes, nonUserBuild)).isTrue();
     }
 
     @Test
diff --git a/tests/testapk/src/native/hwtrust_jni.rs b/tests/testapk/src/native/hwtrust_jni.rs
index 3b00364..058b1a6 100644
--- a/tests/testapk/src/native/hwtrust_jni.rs
+++ b/tests/testapk/src/native/hwtrust_jni.rs
@@ -29,6 +29,7 @@
     env: JNIEnv,
     _class: JClass,
     dice_chain: JByteArray,
+    allow_any_mode: jboolean,
 ) -> jboolean {
     android_logger::init_once(
         android_logger::Config::default()
@@ -36,7 +37,7 @@
             .with_max_level(log::LevelFilter::Debug),
     );
     debug!("Starting the DICE chain validation ...");
-    match validate_dice_chain(env, dice_chain) {
+    match validate_dice_chain(env, dice_chain, allow_any_mode) {
         Ok(_) => {
             info!("DICE chain validated successfully");
             true
@@ -49,9 +50,14 @@
     .into()
 }
 
-fn validate_dice_chain(env: JNIEnv, jdice_chain: JByteArray) -> Result<()> {
+fn validate_dice_chain(
+    env: JNIEnv,
+    jdice_chain: JByteArray,
+    allow_any_mode: jboolean,
+) -> Result<()> {
     let dice_chain = env.convert_byte_array(jdice_chain)?;
-    let session = Session::default();
+    let mut session = Session::default();
+    session.set_allow_any_mode(allow_any_mode == jboolean::from(true));
     let _chain = dice::Chain::from_cbor(&session, &dice_chain)?;
     Ok(())
 }
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:?}",)
diff --git a/tests/vm_accessor/apex/accessor_demo.init.rc b/tests/vm_accessor/apex/accessor_demo.init.rc
index f3dfae9..1ebb038 100644
--- a/tests/vm_accessor/apex/accessor_demo.init.rc
+++ b/tests/vm_accessor/apex/accessor_demo.init.rc
@@ -16,5 +16,6 @@
 service accessor_demo /apex/com.android.virt.accessor_demo/bin/accessor_demo
     disabled
     oneshot
+    user root
     # MUST match with VINTF and accessor/src/main.rs
     interface aidl android.os.IAccessor/IAccessorVmService/default