Merge "MicrodroidHostTests: unset LD_LIBRARY_PATH" into main
diff --git a/Android.bp b/Android.bp
index 7cedfb7..dcf67dd 100644
--- a/Android.bp
+++ b/Android.bp
@@ -30,6 +30,7 @@
         "release_avf_enable_remote_attestation",
         "release_avf_enable_vendor_modules",
         "release_avf_enable_virt_cpufreq",
+        "release_avf_support_custom_vm_with_paravirtualized_devices",
     ],
     properties: [
         "cfgs",
@@ -60,6 +61,9 @@
         release_avf_enable_virt_cpufreq: {
             cfgs: ["virt_cpufreq"],
         },
+        release_avf_support_custom_vm_with_paravirtualized_devices: {
+            cfgs: ["paravirtualized_devices"],
+        },
     },
 }
 
@@ -69,6 +73,7 @@
     config_namespace: "ANDROID",
     bool_variables: [
         "release_avf_enable_dice_changes",
+        "release_avf_enable_vendor_modules",
         "release_avf_enable_virt_cpufreq",
     ],
     properties: [
@@ -82,6 +87,9 @@
         release_avf_enable_dice_changes: {
             cflags: ["-DAVF_OPEN_DICE_CHANGES=1"],
         },
+        release_avf_enable_vendor_modules: {
+            cflags: ["-DAVF_ENABLE_VENDOR_MODULES=1"],
+        },
         release_avf_enable_virt_cpufreq: {
             cflags: ["-DAVF_ENABLE_VIRT_CPUFREQ=1"],
         },
diff --git a/TEST_MAPPING b/TEST_MAPPING
index 58dcc06..1c4f5ca 100644
--- a/TEST_MAPPING
+++ b/TEST_MAPPING
@@ -36,15 +36,9 @@
       "name": "initrd_bootconfig.test"
     },
     {
-      "name": "libdice_policy.test"
-    },
-    {
       "name": "libapkzip.test"
     },
     {
-      "name": "libsecretkeeper_comm.test"
-    },
-    {
       "name": "libdice_driver_test"
     }
   ],
diff --git a/apex/Android.bp b/apex/Android.bp
index 3b5141e..e6c809c 100644
--- a/apex/Android.bp
+++ b/apex/Android.bp
@@ -42,6 +42,7 @@
         "release_avf_enable_remote_attestation",
         "release_avf_enable_vendor_modules",
         "release_avf_enable_virt_cpufreq",
+        "release_avf_support_custom_vm_with_paravirtualized_devices",
     ],
     properties: [
         "androidManifest",
@@ -50,6 +51,7 @@
         "prebuilts",
         "systemserverclasspath_fragments",
         "vintf_fragments",
+        "apps",
     ],
 }
 
@@ -96,6 +98,11 @@
                 canned_fs_config: "canned_fs_config",
             },
         },
+        release_avf_support_custom_vm_with_paravirtualized_devices: {
+            apps: [
+                "VmLauncherApp",
+            ],
+        },
     },
 }
 
diff --git a/apex/canned_fs_config_sys_nice b/apex/canned_fs_config_sys_nice
index 5b12eb5..90c9747 100644
--- a/apex/canned_fs_config_sys_nice
+++ b/apex/canned_fs_config_sys_nice
@@ -1,2 +1,3 @@
 /bin/virtualizationservice 0 2000 0755 capabilities=0x1000001  # CAP_CHOWN, CAP_SYS_RESOURCE
-/bin/crosvm 0 3013 0755 capabilities=0x800000  # SYS_NICE
+/bin/crosvm 0 3013 0755 capabilities=0x800000  # CAP_SYS_NICE
+/bin/virtmgr 0 3013 0755 capabilities=0x800000 # CAP_SYS_NICE
diff --git a/authfs/fd_server/src/aidl.rs b/authfs/fd_server/src/aidl.rs
index 8edd899..5f91987 100644
--- a/authfs/fd_server/src/aidl.rs
+++ b/authfs/fd_server/src/aidl.rs
@@ -289,7 +289,7 @@
             FdConfig::OutputDir(dir) => {
                 let mode = validate_file_mode(mode)?;
                 let new_fd = openat(
-                    dir.as_raw_fd(),
+                    Some(dir.as_raw_fd()),
                     basename,
                     // This function is supposed to be only called when FUSE/authfs thinks the file
                     // does not exist. However, if the file does exist from the view of fd_server
@@ -319,10 +319,14 @@
             FdConfig::InputDir(_) => Err(new_errno_error(Errno::EACCES)),
             FdConfig::OutputDir(_) => {
                 let mode = validate_file_mode(mode)?;
-                mkdirat(dir_fd, basename, mode).map_err(new_errno_error)?;
-                let new_dir_fd =
-                    openat(dir_fd, basename, OFlag::O_DIRECTORY | OFlag::O_RDONLY, Mode::empty())
-                        .map_err(new_errno_error)?;
+                mkdirat(Some(dir_fd), basename, mode).map_err(new_errno_error)?;
+                let new_dir_fd = openat(
+                    Some(dir_fd),
+                    basename,
+                    OFlag::O_DIRECTORY | OFlag::O_RDONLY,
+                    Mode::empty(),
+                )
+                .map_err(new_errno_error)?;
                 // SAFETY: new_dir_fd is just created and not an error.
                 let fd_owner = unsafe { OwnedFd::from_raw_fd(new_dir_fd) };
                 Ok((new_dir_fd, FdConfig::OutputDir(fd_owner)))
@@ -403,7 +407,7 @@
 }
 
 fn open_readonly_at(dir_fd: BorrowedFd, path: &Path) -> nix::Result<File> {
-    let new_fd = openat(dir_fd.as_raw_fd(), path, OFlag::O_RDONLY, Mode::empty())?;
+    let new_fd = openat(Some(dir_fd.as_raw_fd()), path, OFlag::O_RDONLY, Mode::empty())?;
     // SAFETY: new_fd is just created successfully and not owned.
     let new_file = unsafe { File::from_raw_fd(new_fd) };
     Ok(new_file)
diff --git a/compos/apex/Android.bp b/compos/apex/Android.bp
index 55cc446..9996e4e 100644
--- a/compos/apex/Android.bp
+++ b/compos/apex/Android.bp
@@ -51,6 +51,13 @@
         "compsvc",
     ],
 
+    native_shared_libs: [
+        // b/334192594: compsvc has a transitive dependency to libminijail.
+        // Adding it explicitly here is required because the existence of
+        // it in Microdroid cannot be guaranteed.
+        "libminijail",
+    ],
+
     systemserverclasspath_fragments: ["com.android.compos-systemserverclasspath-fragment"],
 
     apps: [
diff --git a/compos/composd/src/fd_server_helper.rs b/compos/composd/src/fd_server_helper.rs
index 24371b5..91860ab 100644
--- a/compos/composd/src/fd_server_helper.rs
+++ b/compos/composd/src/fd_server_helper.rs
@@ -23,7 +23,7 @@
 use nix::unistd::pipe2;
 use std::fs::File;
 use std::io::Read;
-use std::os::unix::io::{AsRawFd, FromRawFd, OwnedFd};
+use std::os::unix::io::{AsRawFd, OwnedFd};
 use std::path::Path;
 
 const FD_SERVER_BIN: &str = "/apex/com.android.virt/bin/fd_server";
@@ -106,12 +106,8 @@
 }
 
 fn create_pipe() -> Result<(File, File)> {
-    let (raw_read, raw_write) = pipe2(OFlag::O_CLOEXEC)?;
-    // SAFETY: We are the sole owner of raw_read and it is valid as it was just created.
-    let read_fd = unsafe { File::from_raw_fd(raw_read) };
-    // SAFETY: We are the sole owner of raw_write and it is valid as it was just created.
-    let write_fd = unsafe { File::from_raw_fd(raw_write) };
-    Ok((read_fd, write_fd))
+    let (read_fd, write_fd) = pipe2(OFlag::O_CLOEXEC)?;
+    Ok((read_fd.into(), write_fd.into()))
 }
 
 fn wait_for_fd_server_ready(mut ready_fd: File) -> Result<()> {
diff --git a/docs/vm_remote_attestation.md b/docs/vm_remote_attestation.md
index 093418b..ddb7adf 100644
--- a/docs/vm_remote_attestation.md
+++ b/docs/vm_remote_attestation.md
@@ -1,3 +1,98 @@
 # VM Remote Attestation
 
-(To be filled)
+## Introduction
+
+In today's digital landscape, where security threats are ever-evolving, ensuring
+the authenticity and integrity of VMs is paramount. This is particularly crucial
+for sensitive applications, such as those running machine learning models, where
+guaranteeing a trusted and secure execution environment is essential.
+
+VM remote attestation provides a powerful mechanism for *protected VMs* (pVMs)
+to prove their trustworthiness to a third party. This process allows a pVM to
+demonstrate that:
+
+-   All its components, including firmware, operating system, and software, are
+    valid and have not been tampered with.
+-   It is running on a valid device trusted by the
+    [Remote Key Provisioning][rkp] (RKP) backend, such as Google.
+
+[rkp]: https://source.android.com/docs/core/ota/modular-system/remote-key-provisioning
+
+## Design
+
+The process of pVM remote attestation involves the use of a lightweight
+intermediate VM known as the [RKP VM][rkpvm]. It allows us to divide the
+attestation process into two parts:
+
+1.  Attesting the RKP VM against the RKP server.
+2.  Attesting the pVM against the RKP VM.
+
+[rkpvm]: https://android.googlesource.com/platform/packages/modules/Virtualization/+/main/service_vm/README.md
+
+### RKP VM attestation
+
+The RKP VM is recognized and attested by the RKP server, which acts as a trusted
+entity responsible for verifying the [DICE chain][open-dice] of the RKP VM. This
+verification ensures that the RKP VM is operating on a genuine device.
+Additionally, the RKP VM is validated by the pVM Firmware, as part of the
+verified boot process.
+
+[open-dice]: https://android.googlesource.com/platform/external/open-dice/+/main/docs/android.md
+
+### pVM attestation
+
+Once the RKP VM is successfully attested, it acts as a trusted platform to
+attest pVMs. Leveraging its trusted status, the RKP VM validates the integrity
+of each pVM's DICE chain by comparing it against its own DICE chain. This
+validation process ensures that the pVMs are running in the expected VM
+environment and certifies the payload executed within each pVM. Currently, only
+Microdroid VMs are supported.
+
+## API
+
+To request remote attestation of a pVM, the [VM Payload API][api]
+`AVmPayload_requestAttestation(challenge)` can be invoked within the pVM
+payload.
+
+For detailed information and usage examples, please refer to the
+[demo app][demo].
+
+[api]: https://android.googlesource.com/platform/packages/modules/Virtualization/+/main/vm_payload/README.md
+[demo]: https://android.googlesource.com/platform/packages/modules/Virtualization/+/main/service_vm/demo_apk
+
+## Output
+
+Upon successful completion of the attestation process, a pVM receives an
+RKP-backed certificate chain and an attested private key that is exclusively
+known to the pVM. This certificate chain includes a leaf certificate covering
+the attested public key. Notably, the leaf certificate features a new extension
+with the OID `1.3.6.1.4.1.11129.2.1.29.1`, specifically designed to describe the
+pVM payload for third-party verification.
+
+The extension format is as follows:
+
+```
+AttestationExtension ::= SEQUENCE {
+    attestationChallenge       OCTET_STRING,
+    isVmSecure                 BOOLEAN,
+    vmComponents               SEQUENCE OF VmComponent,
+}
+
+VmComponent ::= SEQUENCE {
+    name               UTF8String,
+    securityVersion    INTEGER,
+    codeHash           OCTET STRING,
+    authorityHash      OCTET STRING,
+}
+```
+
+In `AttestationExtension`:
+
+-   The `attestationChallenge` field represents a challenge provided by the
+    third party. It is passed to `AVmPayload_requestAttestation()` to ensure
+    the freshness of the certificate.
+-   The `isVmSecure` field indicates whether the attested pVM is secure. It is
+    set to true only when all the DICE certificates in the pVM DICE chain are in
+    normal mode.
+-   The `vmComponents` field contains a list of all the APKs and apexes loaded
+    by the pVM.
diff --git a/flags/cpp/include/android/avf_cc_flags.h b/flags/cpp/include/android/avf_cc_flags.h
index 536ea9f..c922266 100644
--- a/flags/cpp/include/android/avf_cc_flags.h
+++ b/flags/cpp/include/android/avf_cc_flags.h
@@ -27,5 +27,13 @@
 #endif
 }
 
+inline bool IsVendorModulesFlagEnabled() {
+#ifdef AVF_ENABLE_VENDOR_MODULES
+    return AVF_ENABLE_VENDOR_MODULES;
+#else
+    return false;
+#endif
+}
+
 } // namespace virtualization
 } // namespace android
diff --git a/java/framework/src/android/system/virtualmachine/VirtualMachine.java b/java/framework/src/android/system/virtualmachine/VirtualMachine.java
index d746c7c..1076219 100644
--- a/java/framework/src/android/system/virtualmachine/VirtualMachine.java
+++ b/java/framework/src/android/system/virtualmachine/VirtualMachine.java
@@ -63,11 +63,14 @@
 import android.os.ParcelFileDescriptor;
 import android.os.RemoteException;
 import android.os.ServiceSpecificException;
+import android.view.KeyEvent;
+import android.view.MotionEvent;
 import android.system.virtualizationcommon.DeathReason;
 import android.system.virtualizationcommon.ErrorCode;
 import android.system.virtualizationservice.IVirtualMachine;
 import android.system.virtualizationservice.IVirtualMachineCallback;
 import android.system.virtualizationservice.IVirtualizationService;
+import android.system.virtualizationservice.InputDevice;
 import android.system.virtualizationservice.MemoryTrimLevel;
 import android.system.virtualizationservice.PartitionType;
 import android.system.virtualizationservice.VirtualMachineAppConfig;
@@ -79,6 +82,8 @@
 import com.android.internal.annotations.GuardedBy;
 import com.android.system.virtualmachine.flags.Flags;
 
+import libcore.io.IoBridge;
+
 import java.io.File;
 import java.io.FileInputStream;
 import java.io.FileNotFoundException;
@@ -89,6 +94,8 @@
 import java.io.OutputStream;
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
 import java.nio.channels.FileChannel;
 import java.nio.file.FileAlreadyExistsException;
 import java.nio.file.FileVisitResult;
@@ -96,6 +103,7 @@
 import java.nio.file.Path;
 import java.nio.file.SimpleFileVisitor;
 import java.nio.file.attribute.BasicFileAttributes;
+import java.util.Arrays;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
@@ -154,6 +162,9 @@
     @SuppressLint("MinMaxConstant") // Won't change: see man 7 vsock.
     public static final long MAX_VSOCK_PORT = (1L << 32) - 1;
 
+    private ParcelFileDescriptor mTouchSock;
+    private ParcelFileDescriptor mKeySock;
+
     /**
      * Status of a virtual machine
      *
@@ -431,11 +442,6 @@
                 VirtualMachineConfig config = VirtualMachineConfig.from(vmDescriptor.getConfigFd());
                 vm = new VirtualMachine(context, name, config, VirtualizationService.getInstance());
                 config.serialize(vm.mConfigFilePath);
-                if (vm.mInstanceIdPath != null) {
-                    vm.importInstanceIdFrom(vmDescriptor.getInstanceIdFd());
-                    vm.claimInstance();
-                }
-
                 try {
                     vm.mInstanceFilePath.createNewFile();
                 } catch (IOException e) {
@@ -452,12 +458,16 @@
                     }
                     vm.importEncryptedStoreFrom(vmDescriptor.getEncryptedStoreFd());
                 }
+                if (vm.mInstanceIdPath != null) {
+                    vm.importInstanceIdFrom(vmDescriptor.getInstanceIdFd());
+                    vm.claimInstance();
+                }
             }
             return vm;
         } catch (VirtualMachineException | RuntimeException e) {
             // If anything goes wrong, delete any files created so far and the VM's directory
             try {
-                vmInstanceCleanup(context, name);
+                deleteRecursively(vmDir);
             } catch (Exception innerException) {
                 e.addSuppressed(innerException);
             }
@@ -850,9 +860,102 @@
             createVirtualMachineConfigForRawFrom(VirtualMachineConfig vmConfig)
                     throws IllegalStateException, IOException {
         VirtualMachineRawConfig rawConfig = vmConfig.toVsRawConfig();
+
+        // Handle input devices here
+        List<InputDevice> inputDevices = new ArrayList<>();
+        if (vmConfig.getCustomImageConfig() != null
+                && rawConfig.displayConfig != null) {
+            if (vmConfig.getCustomImageConfig().useTouch()) {
+                ParcelFileDescriptor[] pfds = ParcelFileDescriptor.createSocketPair();
+                mTouchSock = pfds[0];
+                InputDevice.SingleTouch t = new InputDevice.SingleTouch();
+                t.width = rawConfig.displayConfig.width;
+                t.height = rawConfig.displayConfig.height;
+                t.pfd = pfds[1];
+                inputDevices.add(InputDevice.singleTouch(t));
+            }
+            if (vmConfig.getCustomImageConfig().useKeyboard()) {
+                ParcelFileDescriptor[] pfds = ParcelFileDescriptor.createSocketPair();
+                mKeySock = pfds[0];
+                InputDevice.Keyboard k = new InputDevice.Keyboard();
+                k.pfd = pfds[1];
+                inputDevices.add(InputDevice.keyboard(k));
+            }
+        }
+        rawConfig.inputDevices = inputDevices.toArray(new InputDevice[0]);
+
         return android.system.virtualizationservice.VirtualMachineConfig.rawConfig(rawConfig);
     }
 
+    private static record InputEvent(short type, short code, int value) {}
+
+    /** @hide */
+    public boolean sendKeyEvent(KeyEvent event) {
+        if (mKeySock == null) {
+            Log.d(TAG, "mKeySock == null");
+            return false;
+        }
+        // from include/uapi/linux/input-event-codes.h in the kernel.
+        short EV_SYN = 0x00;
+        short EV_KEY = 0x01;
+        short SYN_REPORT = 0x00;
+        boolean down = event.getAction() != MotionEvent.ACTION_UP;
+
+        return writeEventsToSock(
+                mKeySock,
+                Arrays.asList(
+                        new InputEvent(EV_KEY, (short) event.getScanCode(), down ? 1 : 0),
+                        new InputEvent(EV_SYN, SYN_REPORT, 0)));
+    }
+
+    /** @hide */
+    public boolean sendSingleTouchEvent(MotionEvent event) {
+        if (mTouchSock == null) {
+            Log.d(TAG, "mTouchSock == null");
+            return false;
+        }
+        // from include/uapi/linux/input-event-codes.h in the kernel.
+        short EV_SYN = 0x00;
+        short EV_ABS = 0x03;
+        short EV_KEY = 0x01;
+        short BTN_TOUCH = 0x14a;
+        short ABS_X = 0x00;
+        short ABS_Y = 0x01;
+        short SYN_REPORT = 0x00;
+
+        int x = (int) event.getX();
+        int y = (int) event.getY();
+        boolean down = event.getAction() != MotionEvent.ACTION_UP;
+
+        return writeEventsToSock(
+                mTouchSock,
+                Arrays.asList(
+                        new InputEvent(EV_ABS, ABS_X, x),
+                        new InputEvent(EV_ABS, ABS_Y, y),
+                        new InputEvent(EV_KEY, BTN_TOUCH, down ? 1 : 0),
+                        new InputEvent(EV_SYN, SYN_REPORT, 0)));
+    }
+
+    private boolean writeEventsToSock(ParcelFileDescriptor sock, List<InputEvent> evtList) {
+        ByteBuffer byteBuffer =
+                ByteBuffer.allocate(8 /* (type: u16 + code: u16 + value: i32) */ * evtList.size());
+        byteBuffer.clear();
+        byteBuffer.order(ByteOrder.LITTLE_ENDIAN);
+        for (InputEvent e : evtList) {
+            byteBuffer.putShort(e.type);
+            byteBuffer.putShort(e.code);
+            byteBuffer.putInt(e.value);
+        }
+        try {
+            IoBridge.write(
+                    sock.getFileDescriptor(), byteBuffer.array(), 0, byteBuffer.array().length);
+        } catch (IOException e) {
+            Log.d(TAG, "cannot send event", e);
+            return false;
+        }
+        return true;
+    }
+
     private android.system.virtualizationservice.VirtualMachineConfig
             createVirtualMachineConfigForAppFrom(
                     VirtualMachineConfig vmConfig, IVirtualizationService service)
diff --git a/java/framework/src/android/system/virtualmachine/VirtualMachineCustomImageConfig.java b/java/framework/src/android/system/virtualmachine/VirtualMachineCustomImageConfig.java
index 89df1f2..8d294fd 100644
--- a/java/framework/src/android/system/virtualmachine/VirtualMachineCustomImageConfig.java
+++ b/java/framework/src/android/system/virtualmachine/VirtualMachineCustomImageConfig.java
@@ -34,6 +34,8 @@
     private static final String KEY_DISK_WRITABLES = "disk_writables";
     private static final String KEY_DISK_IMAGES = "disk_images";
     private static final String KEY_DISPLAY_CONFIG = "display_config";
+    private static final String KEY_TOUCH = "touch";
+    private static final String KEY_KEYBOARD = "keyboard";
 
     @Nullable private final String name;
     @NonNull private final String kernelPath;
@@ -42,6 +44,8 @@
     @Nullable private final String[] params;
     @Nullable private final Disk[] disks;
     @Nullable private final DisplayConfig displayConfig;
+    private final boolean touch;
+    private final boolean keyboard;
 
     @Nullable
     public Disk[] getDisks() {
@@ -73,6 +77,14 @@
         return params;
     }
 
+    public boolean useTouch() {
+        return touch;
+    }
+
+    public boolean useKeyboard() {
+        return keyboard;
+    }
+
     /** @hide */
     public VirtualMachineCustomImageConfig(
             String name,
@@ -81,7 +93,9 @@
             String bootloaderPath,
             String[] params,
             Disk[] disks,
-            DisplayConfig displayConfig) {
+            DisplayConfig displayConfig,
+            boolean touch,
+            boolean keyboard) {
         this.name = name;
         this.kernelPath = kernelPath;
         this.initrdPath = initrdPath;
@@ -89,6 +103,8 @@
         this.params = params;
         this.disks = disks;
         this.displayConfig = displayConfig;
+        this.touch = touch;
+        this.keyboard = keyboard;
     }
 
     static VirtualMachineCustomImageConfig from(PersistableBundle customImageConfigBundle) {
@@ -116,7 +132,8 @@
         PersistableBundle displayConfigPb =
                 customImageConfigBundle.getPersistableBundle(KEY_DISPLAY_CONFIG);
         builder.setDisplayConfig(DisplayConfig.from(displayConfigPb));
-
+        builder.useTouch(customImageConfigBundle.getBoolean(KEY_TOUCH));
+        builder.useKeyboard(customImageConfigBundle.getBoolean(KEY_KEYBOARD));
         return builder.build();
     }
 
@@ -145,6 +162,8 @@
                 Optional.ofNullable(displayConfig)
                         .map(dc -> dc.toPersistableBundle())
                         .orElse(null));
+        pb.putBoolean(KEY_TOUCH, touch);
+        pb.putBoolean(KEY_KEYBOARD, keyboard);
         return pb;
     }
 
@@ -193,6 +212,8 @@
         private List<String> params = new ArrayList<>();
         private List<Disk> disks = new ArrayList<>();
         private DisplayConfig displayConfig;
+        private boolean touch;
+        private boolean keyboard;
 
         /** @hide */
         public Builder() {}
@@ -240,6 +261,18 @@
         }
 
         /** @hide */
+        public Builder useTouch(boolean touch) {
+            this.touch = touch;
+            return this;
+        }
+
+        /** @hide */
+        public Builder useKeyboard(boolean keyboard) {
+            this.keyboard = keyboard;
+            return this;
+        }
+
+        /** @hide */
         public VirtualMachineCustomImageConfig build() {
             return new VirtualMachineCustomImageConfig(
                     this.name,
@@ -248,7 +281,9 @@
                     this.bootloaderPath,
                     this.params.toArray(new String[0]),
                     this.disks.toArray(new Disk[0]),
-                    displayConfig);
+                    displayConfig,
+                    touch,
+                    keyboard);
         }
     }
 
diff --git a/java/framework/src/android/system/virtualmachine/VirtualMachineManager.java b/java/framework/src/android/system/virtualmachine/VirtualMachineManager.java
index 8c0c20e..4a9e943 100644
--- a/java/framework/src/android/system/virtualmachine/VirtualMachineManager.java
+++ b/java/framework/src/android/system/virtualmachine/VirtualMachineManager.java
@@ -38,9 +38,11 @@
 import com.android.internal.annotations.GuardedBy;
 import com.android.system.virtualmachine.flags.Flags;
 
+import java.io.File;
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
 import java.lang.ref.WeakReference;
+import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.List;
 import java.util.Map;
@@ -357,6 +359,30 @@
         return null;
     }
 
+    private static final String JSON_SUFFIX = ".json";
+    private static final List<String> SUPPORTED_OS_LIST_FROM_CFG =
+            extractSupportedOSListFromConfig();
+
+    private boolean isVendorModuleEnabled() {
+        return VirtualizationService.nativeIsVendorModulesFlagEnabled();
+    }
+
+    private static List<String> extractSupportedOSListFromConfig() {
+        List<String> supportedOsList = new ArrayList<>();
+        File directory = new File("/apex/com.android.virt/etc");
+        File[] files = directory.listFiles();
+        if (files != null) {
+            for (File file : files) {
+                String fileName = file.getName();
+                if (fileName.endsWith(JSON_SUFFIX)) {
+                    supportedOsList.add(
+                            fileName.substring(0, fileName.length() - JSON_SUFFIX.length()));
+                }
+            }
+        }
+        return supportedOsList;
+    }
+
     /**
      * Returns a list of supported OS names.
      *
@@ -366,13 +392,10 @@
     @FlaggedApi(Flags.FLAG_AVF_V_TEST_APIS)
     @NonNull
     public List<String> getSupportedOSList() throws VirtualMachineException {
-        synchronized (sCreateLock) {
-            VirtualizationService service = VirtualizationService.getInstance();
-            try {
-                return Arrays.asList(service.getBinder().getSupportedOSList());
-            } catch (RemoteException e) {
-                throw e.rethrowAsRuntimeException();
-            }
+        if (isVendorModuleEnabled()) {
+            return SUPPORTED_OS_LIST_FROM_CFG;
+        } else {
+            return Arrays.asList("microdroid");
         }
     }
 
diff --git a/java/framework/src/android/system/virtualmachine/VirtualizationService.java b/java/framework/src/android/system/virtualmachine/VirtualizationService.java
index 57990a9..83b64ee 100644
--- a/java/framework/src/android/system/virtualmachine/VirtualizationService.java
+++ b/java/framework/src/android/system/virtualmachine/VirtualizationService.java
@@ -51,6 +51,12 @@
     private native boolean nativeIsOk(int clientFd);
 
     /*
+     * Retrieve boolean value whether RELEASE_AVF_ENABLE_VENDOR_MODULES build flag is enabled or
+     * not.
+     */
+    static native boolean nativeIsVendorModulesFlagEnabled();
+
+    /*
      * Spawns a new virtmgr subprocess that will host a VirtualizationService
      * AIDL service.
      */
diff --git a/java/jni/Android.bp b/java/jni/Android.bp
index 74a1766..4a569d4 100644
--- a/java/jni/Android.bp
+++ b/java/jni/Android.bp
@@ -16,6 +16,7 @@
         "liblog",
         "libnativehelper",
     ],
+    static_libs: ["libavf_cc_flags"],
 }
 
 cc_library_shared {
diff --git a/java/jni/android_system_virtualmachine_VirtualizationService.cpp b/java/jni/android_system_virtualmachine_VirtualizationService.cpp
index fbd1fd5..ced2079 100644
--- a/java/jni/android_system_virtualmachine_VirtualizationService.cpp
+++ b/java/jni/android_system_virtualmachine_VirtualizationService.cpp
@@ -17,6 +17,7 @@
 #define LOG_TAG "VirtualizationService"
 
 #include <android-base/unique_fd.h>
+#include <android/avf_cc_flags.h>
 #include <android/binder_ibinder_jni.h>
 #include <jni.h>
 #include <log/log.h>
@@ -66,10 +67,16 @@
 
     // Wait for the server to signal its readiness by closing its end of the pipe.
     char buf;
-    if (read(waitFd.get(), &buf, sizeof(buf)) < 0) {
+    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();
@@ -101,3 +108,9 @@
     }
     return pfds[0].revents == 0;
 }
+
+extern "C" JNIEXPORT jboolean JNICALL
+Java_android_system_virtualmachine_VirtualizationService_nativeIsVendorModulesFlagEnabled(
+        [[maybe_unused]] JNIEnv* env, [[maybe_unused]] jobject obj) {
+    return android::virtualization::IsVendorModulesFlagEnabled();
+}
diff --git a/libs/android_display_backend/Android.bp b/libs/android_display_backend/Android.bp
index 6ad5fab..32587dd 100644
--- a/libs/android_display_backend/Android.bp
+++ b/libs/android_display_backend/Android.bp
@@ -11,6 +11,9 @@
     backend: {
         java: {
             enabled: true,
+            apex_available: [
+                "com.android.virt",
+            ],
         },
         cpp: {
             enabled: false,
@@ -42,7 +45,6 @@
         "android.system.virtualizationservice_internal-ndk",
         "android.system.virtualizationcommon-ndk",
         "android.system.virtualizationservice-ndk",
-        "libyuv",
     ],
     shared_libs: [
         "libbinder_ndk",
diff --git a/libs/android_display_backend/crosvm_android_display_client.cpp b/libs/android_display_backend/crosvm_android_display_client.cpp
index a16b7f2..66320f3 100644
--- a/libs/android_display_backend/crosvm_android_display_client.cpp
+++ b/libs/android_display_backend/crosvm_android_display_client.cpp
@@ -18,38 +18,27 @@
 #include <aidl/android/system/virtualizationservice_internal/IVirtualizationServiceInternal.h>
 #include <android/binder_manager.h>
 #include <android/binder_process.h>
-#include <android/native_window.h>
-#include <android/native_window_aidl.h>
-#include <libyuv.h>
-#include <stdint.h>
-#include <utils/Errors.h>
+#include <system/graphics.h> // for HAL_PIXEL_FORMAT_*
 
 #include <condition_variable>
 #include <memory>
 #include <mutex>
-#include <vector>
 
+using aidl::android::crosvm::BnCrosvmAndroidDisplayService;
 using aidl::android::system::virtualizationservice_internal::IVirtualizationServiceInternal;
-
-#define LIBEXPORT __attribute__((visibility("default"))) extern "C"
-
-typedef void (*android_display_log_callback_type)(const char* message);
-
-static void android_display_log_callback_stub(const char* message) {
-    (void)message;
-}
+using aidl::android::view::Surface;
 
 namespace {
 
-class DisplayService : public aidl::android::crosvm::BnCrosvmAndroidDisplayService {
+class DisplayService : public BnCrosvmAndroidDisplayService {
 public:
     DisplayService() = default;
     virtual ~DisplayService() = default;
 
-    ndk::ScopedAStatus setSurface(aidl::android::view::Surface* surface) override {
+    ndk::ScopedAStatus setSurface(Surface* surface) override {
         {
             std::lock_guard lk(mSurfaceReadyMutex);
-            mSurface = std::make_unique<aidl::android::view::Surface>(surface->release());
+            mSurface = std::make_unique<Surface>(surface->release());
         }
         mSurfaceReady.notify_one();
         return ::ndk::ScopedAStatus::ok();
@@ -64,7 +53,7 @@
         return ::ndk::ScopedAStatus::ok();
     }
 
-    aidl::android::view::Surface* getSurface() {
+    Surface* getSurface() {
         std::unique_lock lk(mSurfaceReadyMutex);
         mSurfaceReady.wait(lk, [this] { return mSurface != nullptr; });
         return mSurface.get();
@@ -73,214 +62,116 @@
 private:
     std::condition_variable mSurfaceReady;
     std::mutex mSurfaceReadyMutex;
-    std::unique_ptr<aidl::android::view::Surface> mSurface;
+    std::unique_ptr<Surface> mSurface;
 };
 
-void ErrorF(android_display_log_callback_type error_callback, const char* format, ...) {
-    char buffer[1024];
-
-    va_list vararg;
-    va_start(vararg, format);
-    vsnprintf(buffer, sizeof(buffer), format, vararg);
-    va_end(vararg);
-
-    error_callback(buffer);
-}
-
 } // namespace
 
-struct android_display_context {
-    uint32_t width;
-    uint32_t height;
-    std::shared_ptr<DisplayService> displayService;
+typedef void (*ErrorCallback)(const char* message);
+
+struct AndroidDisplayContext {
+    std::shared_ptr<IVirtualizationServiceInternal> virt_service;
+    std::shared_ptr<DisplayService> disp_service;
+    ErrorCallback error_callback;
+
+    AndroidDisplayContext(ErrorCallback cb) : error_callback(cb) {
+        auto disp_service = ::ndk::SharedRefBase::make<DisplayService>();
+
+        // Creates DisplayService and register it to the virtualizationservice. This is needed
+        // because this code is executed inside of crosvm which runs as an app. Apps are not allowed
+        // to register a service to the service manager.
+        auto virt_service = IVirtualizationServiceInternal::fromBinder(ndk::SpAIBinder(
+                AServiceManager_waitForService("android.system.virtualizationservice")));
+        if (virt_service == nullptr) {
+            errorf("Failed to find virtualization service");
+            return;
+        }
+        auto status = virt_service->setDisplayService(disp_service->asBinder());
+        if (!status.isOk()) {
+            errorf("Failed to register display service");
+            return;
+        }
+
+        this->virt_service = virt_service;
+        this->disp_service = disp_service;
+        ABinderProcess_startThreadPool();
+    }
+
+    ~AndroidDisplayContext() {
+        if (virt_service == nullptr) {
+            errorf("Not connected to virtualization service");
+            return;
+        }
+        auto status = this->virt_service->clearDisplayService();
+        if (!status.isOk()) {
+            errorf("Failed to clear display service");
+        }
+    }
+
+    void errorf(const char* format, ...) {
+        char buffer[1024];
+
+        va_list vararg;
+        va_start(vararg, format);
+        vsnprintf(buffer, sizeof(buffer), format, vararg);
+        va_end(vararg);
+
+        error_callback(buffer);
+    }
 };
 
-LIBEXPORT
-struct android_display_context* create_android_display_context(
-        const char* name, size_t name_len, android_display_log_callback_type error_callback) {
-    auto ctx = new android_display_context();
-
-    auto service = ::ndk::SharedRefBase::make<DisplayService>();
-
-    if (strlen(name) != name_len) {
-        ErrorF(error_callback, "Invalid service name length. Expected %u, actual %u", name_len,
-               strlen(name));
-        return nullptr;
-    }
-    ::ndk::SpAIBinder binder(
-            AServiceManager_waitForService("android.system.virtualizationservice"));
-
-    auto virt_service = IVirtualizationServiceInternal::fromBinder(binder);
-    if (virt_service == nullptr) {
-        ErrorF(error_callback, "Failed to find android.system.virtualizationservice");
-        return nullptr;
-    }
-    auto status = virt_service->setDisplayService(service->asBinder());
-    if (!status.isOk()) {
-        ErrorF(error_callback, "Failed to register %s",
-               aidl::android::crosvm::ICrosvmAndroidDisplayService::descriptor);
-        return nullptr;
-    }
-
-    ABinderProcess_startThreadPool();
-
-    auto surface = service->getSurface();
-    ctx->width = static_cast<uint32_t>(ANativeWindow_getWidth(surface->get()));
-    ctx->height = static_cast<uint32_t>(ANativeWindow_getHeight(surface->get()));
-    ctx->displayService = service;
-    return ctx;
+extern "C" struct AndroidDisplayContext* create_android_display_context(
+        const char*, ErrorCallback error_callback) {
+    return new AndroidDisplayContext(error_callback);
 }
 
-LIBEXPORT
-void destroy_android_display_context(android_display_log_callback_type error_callback,
-                                     struct android_display_context* ctx) {
-    auto service = ::ndk::SharedRefBase::make<DisplayService>();
-    ::ndk::SpAIBinder binder(
-            AServiceManager_waitForService("android.system.virtualizationservice"));
-    auto virt_service = IVirtualizationServiceInternal::fromBinder(binder);
-    if (virt_service != nullptr) {
-        auto status = virt_service->clearDisplayService();
-    } else {
-        ErrorF(error_callback, "Failed to find android.system.virtualizationservice");
-    }
-
-    if (!ctx) {
-        ErrorF(error_callback, "Invalid context.");
-        return;
-    }
-
+extern "C" void destroy_android_display_context(struct AndroidDisplayContext* ctx) {
     delete ctx;
 }
 
-LIBEXPORT
-uint32_t get_android_display_width(android_display_log_callback_type error_callback,
-                                   struct android_display_context* ctx) {
-    if (!ctx) {
-        ErrorF(error_callback, "Invalid context.");
-        return -1;
+extern "C" ANativeWindow* create_android_surface(struct AndroidDisplayContext* ctx, uint32_t width,
+                                                 uint32_t height) {
+    if (ctx->disp_service == nullptr) {
+        ctx->errorf("Display service was not created");
+        return nullptr;
     }
-    if (!ctx->displayService->getSurface()) {
-        ErrorF(error_callback, "Invalid context surface for ctx:%p.", ctx);
-        return -1;
+    // Note: crosvm always uses BGRA8888 or BGRX8888. See devices/src/virtio/gpu/mod.rs in crosvm
+    // where the SetScanoutBlob command is handled. Let's use BGRA not BGRX with a hope that we will
+    // need alpha blending for the cursor surface.
+    int format = HAL_PIXEL_FORMAT_BGRA_8888;
+    ANativeWindow* surface = ctx->disp_service->getSurface()->get(); // this can block
+    if (ANativeWindow_setBuffersGeometry(surface, width, height, format) != 0) {
+        ctx->errorf("Failed to set buffer gemoetry");
+        return nullptr;
     }
-    return ctx->width;
+    // TODO(b/332785161): if we know that surface can get destroyed dynamically while VM is running,
+    // consider calling ANativeWindow_acquire here and _release in destroy_android_surface, so that
+    // crosvm doesn't hold a dangling pointer.
+    return surface;
 }
 
-LIBEXPORT
-uint32_t get_android_display_height(android_display_log_callback_type error_callback,
-                                    struct android_display_context* ctx) {
-    if (!ctx) {
-        ErrorF(error_callback, "Invalid context.");
-        return -1;
-    }
-    if (!ctx->displayService->getSurface()) {
-        ErrorF(error_callback, "Invalid context surface for ctx:%p.", ctx);
-        return -1;
-    }
-    return ctx->height;
+extern "C" void destroy_android_surface(struct AndroidDisplayContext*, ANativeWindow*) {
+    // NOT IMPLEMENTED
 }
 
-uint16_t RGBA8888ToRGB565(uint8_t r, uint8_t g, uint8_t b, uint8_t a) {
-    (void)a;
-    return (static_cast<uint16_t>(r >> 3) << 11) | (static_cast<uint16_t>(g >> 2) << 5) |
-            (static_cast<uint16_t>(b >> 3) << 0);
+extern "C" bool get_android_surface_buffer(struct AndroidDisplayContext* ctx,
+                                           ANativeWindow* surface,
+                                           ANativeWindow_Buffer* out_buffer) {
+    if (out_buffer == nullptr) {
+        ctx->errorf("out_buffer is null");
+        return false;
+    }
+    if (ANativeWindow_lock(surface, out_buffer, nullptr) != 0) {
+        ctx->errorf("Failed to lock buffer");
+        return false;
+    }
+    return true;
 }
 
-LIBEXPORT
-void blit_android_display(android_display_log_callback_type error_callback,
-                          struct android_display_context* ctx, uint32_t width, uint32_t height,
-                          uint8_t* pixels, size_t pixels_num_bytes) {
-    if (!ctx) {
-        ErrorF(error_callback, "Invalid context.");
-        return;
-    }
-    if (!ctx->displayService->getSurface()) {
-        ErrorF(error_callback, "Invalid context surface.");
-        return;
-    }
-    if (pixels_num_bytes != width * height * 4) {
-        ErrorF(error_callback, "Invalid buffer size.");
-        return;
-    }
-    ANativeWindow* anw = ctx->displayService->getSurface()->get();
-    if (!anw) {
-        ErrorF(error_callback, "Invalid context surface.");
-        return;
-    }
-
-    ANativeWindow_Buffer anwBuffer = {};
-    if (ANativeWindow_lock(anw, &anwBuffer, nullptr) != android::OK) {
-        ErrorF(error_callback, "Failed to lock ANativeWindow.");
-        return;
-    }
-
-    // Source is always BGRA8888.
-    auto* src = reinterpret_cast<uint32_t*>(pixels);
-    auto srcWidth = static_cast<uint32_t>(width);
-    auto srcHeight = static_cast<uint32_t>(height);
-    auto srcStrideBytes = srcWidth * 4;
-    auto srcStridePixels = srcWidth;
-
-    auto dstWidth = static_cast<uint32_t>(anwBuffer.width);
-    auto dstHeight = static_cast<uint32_t>(anwBuffer.height);
-
-    // Scale to fit if needed.
-    std::vector<uint32_t> scaledSrc;
-    if (srcWidth != dstWidth || srcHeight != dstHeight) {
-        const float ratioWidth = static_cast<float>(dstWidth) / static_cast<float>(srcWidth);
-        const float ratioHeight = static_cast<float>(dstHeight) / static_cast<float>(srcHeight);
-        const float ratioUsed = std::min(ratioWidth, ratioHeight);
-
-        uint32_t scaledSrcWidth = static_cast<uint32_t>(static_cast<float>(srcWidth) * ratioUsed);
-        uint32_t scaledSrcHeight = static_cast<uint32_t>(static_cast<float>(srcHeight) * ratioUsed);
-        uint32_t scaledSrcStrideBytes = scaledSrcWidth * 4;
-        uint32_t scaledSrcStridePixels = scaledSrcWidth;
-
-        scaledSrc.resize(scaledSrcHeight * scaledSrcStridePixels);
-
-        libyuv::ARGBScale(reinterpret_cast<uint8_t*>(src), srcStrideBytes, srcWidth, srcHeight,
-                          reinterpret_cast<uint8_t*>(scaledSrc.data()), scaledSrcStrideBytes,
-                          scaledSrcWidth, scaledSrcHeight, libyuv::kFilterBilinear);
-
-        src = scaledSrc.data();
-        srcWidth = scaledSrcWidth;
-        srcHeight = scaledSrcHeight;
-        srcStrideBytes = scaledSrcStrideBytes;
-        srcStridePixels = scaledSrcStridePixels;
-    }
-
-    if (anwBuffer.format == AHARDWAREBUFFER_FORMAT_R8G8B8A8_UNORM) {
-        auto* dst = reinterpret_cast<uint32_t*>(anwBuffer.bits);
-        auto dstStridePixels = static_cast<uint32_t>(anwBuffer.stride);
-
-        for (uint32_t h = 0; h < std::min(srcHeight, dstHeight); h++) {
-            for (uint32_t w = 0; w < std::min(srcWidth, dstWidth); w++) {
-                dst[(h * dstStridePixels) + w] = src[(h * srcStridePixels) + w];
-            }
-        }
-    } else if (anwBuffer.format == AHARDWAREBUFFER_FORMAT_R5G6B5_UNORM) {
-        auto* dst = reinterpret_cast<uint16_t*>(anwBuffer.bits);
-        auto dstWidth = static_cast<uint32_t>(anwBuffer.width);
-        auto dstHeight = static_cast<uint32_t>(anwBuffer.height);
-        auto dstStridePixels = static_cast<uint32_t>(anwBuffer.stride);
-
-        for (uint32_t h = 0; h < std::min(srcHeight, dstHeight); h++) {
-            for (uint32_t w = 0; w < std::min(srcWidth, dstWidth); w++) {
-                uint32_t srcPixel = src[(h * srcStridePixels) + w];
-                uint8_t* srcPixelBytes = reinterpret_cast<uint8_t*>(&srcPixel);
-                uint8_t r = srcPixelBytes[2];
-                uint8_t g = srcPixelBytes[1];
-                uint8_t b = srcPixelBytes[0];
-                uint8_t a = srcPixelBytes[3];
-                dst[(h * dstStridePixels) + w] = RGBA8888ToRGB565(r, g, b, a);
-            }
-        }
-    } else {
-        ErrorF(error_callback, "Unhandled format: %d", anwBuffer.format);
-    }
-
-    if (ANativeWindow_unlockAndPost(anw) != android::OK) {
-        ErrorF(error_callback, "Failed to unlock and post ANativeWindow.");
+extern "C" void post_android_surface_buffer(struct AndroidDisplayContext* ctx,
+                                            ANativeWindow* surface) {
+    if (ANativeWindow_unlockAndPost(surface) != 0) {
+        ctx->errorf("Failed to unlock and post surface.");
         return;
     }
 }
diff --git a/libs/dice/driver/src/lib.rs b/libs/dice/driver/src/lib.rs
index 79edb51..b5c1f12 100644
--- a/libs/dice/driver/src/lib.rs
+++ b/libs/dice/driver/src/lib.rs
@@ -65,6 +65,7 @@
 
     /// Creates a new dice driver from the given driver_path.
     pub fn new(driver_path: &Path, is_strict_boot: bool) -> Result<Self> {
+        log::info!("Creating DiceDriver backed by {driver_path:?} driver");
         if driver_path.exists() {
             log::info!("Using DICE values from driver");
         } else if is_strict_boot {
@@ -107,6 +108,7 @@
 
     /// Create a new dice driver that reads dice_artifacts from the given file.
     pub fn from_file(file_path: &Path) -> Result<Self> {
+        log::info!("Creating DiceDriver backed by {file_path:?} file");
         let file =
             fs::File::open(file_path).map_err(|error| Error::new(error).context("open file"))?;
         let dice_artifacts = serde_cbor::from_reader(file)
@@ -149,11 +151,18 @@
             &input_values,
         )
         .context("DICE derive from driver")?;
-        if let Self::Real { driver_path, .. } = &self {
-            // Writing to the device wipes the artifacts. The string is ignored by the driver but
-            // included for documentation.
-            fs::write(driver_path, "wipe")
-                .map_err(|err| Error::new(err).context("Wiping driver"))?;
+        match &self {
+            Self::Real { driver_path, .. } => {
+                // Writing to the device wipes the artifacts. The string is ignored by the driver
+                // but included for documentation.
+                fs::write(driver_path, "wipe")
+                    .map_err(|err| Error::new(err).context("Wiping driver"))?;
+            }
+            Self::FromFile { file_path, .. } => {
+                fs::remove_file(file_path)
+                    .map_err(|err| Error::new(err).context("Deleting file"))?;
+            }
+            Self::Fake { .. } => (),
         }
         Ok(next_dice_artifacts)
     }
@@ -176,6 +185,11 @@
 #[cfg(test)]
 mod tests {
     use super::*;
+    use core::ffi::CStr;
+    use diced_open_dice::{
+        hash, retry_bcc_format_config_descriptor, DiceConfigValues, HIDDEN_SIZE,
+    };
+    use std::fs::File;
 
     fn assert_eq_bytes(expected: &[u8], actual: &[u8]) {
         assert_eq!(
@@ -204,4 +218,34 @@
 
         Ok(())
     }
+
+    #[test]
+    fn test_dice_driver_from_file_deletes_file_after_derive() -> Result<()> {
+        let tmp_dir = tempfile::tempdir()?;
+
+        let file_path = tmp_dir.path().join("test-dice-chain.raw");
+
+        {
+            let dice_artifacts = diced_sample_inputs::make_sample_bcc_and_cdis()?;
+            let file = File::create(&file_path)?;
+            serde_cbor::to_writer(file, &dice_artifacts)?;
+        }
+
+        let dice = DiceDriver::from_file(&file_path)?;
+
+        let values = DiceConfigValues {
+            component_name: Some(CStr::from_bytes_with_nul(b"test\0")?),
+            ..Default::default()
+        };
+        let desc = retry_bcc_format_config_descriptor(&values)?;
+        let code_hash = hash(&String::from("test code hash").into_bytes())?;
+        let authority_hash = hash(&String::from("test authority hash").into_bytes())?;
+        let hidden = [0; HIDDEN_SIZE];
+
+        let _ = dice.derive(code_hash, &desc, authority_hash, false, hidden)?;
+
+        assert!(!file_path.exists());
+
+        Ok(())
+    }
 }
diff --git a/libs/vbmeta/Android.bp b/libs/vbmeta/Android.bp
index 4fb6ae4..9a7375d 100644
--- a/libs/vbmeta/Android.bp
+++ b/libs/vbmeta/Android.bp
@@ -35,6 +35,8 @@
         ":avb_testkey_rsa2048",
         ":avb_testkey_rsa4096",
         ":avb_testkey_rsa8192",
+        ":test_microdroid_vendor_image",
+        ":test_microdroid_vendor_image_no_rollback_index",
     ],
     required: ["avbtool"],
     test_suites: ["general-tests"],
diff --git a/libs/vbmeta/src/lib.rs b/libs/vbmeta/src/lib.rs
index 1a40e45..a15f699 100644
--- a/libs/vbmeta/src/lib.rs
+++ b/libs/vbmeta/src/lib.rs
@@ -148,6 +148,11 @@
         Descriptors::from_image(&self.data)
     }
 
+    /// Returns the rollback_index of the VBMeta image.
+    pub fn rollback_index(&self) -> u64 {
+        self.header.rollback_index
+    }
+
     /// Get the raw VBMeta image.
     pub fn data(&self) -> &[u8] {
         &self.data
@@ -283,4 +288,19 @@
     fn test_rsa8192_signed_image() -> Result<()> {
         signed_image_has_valid_vbmeta("SHA256_RSA8192", "data/testkey_rsa8192.pem")
     }
+
+    #[test]
+    fn test_rollback_index() -> Result<()> {
+        let vbmeta = VbMetaImage::verify_path("test_microdroid_vendor_image.img")?;
+        assert_eq!(5, vbmeta.rollback_index());
+        Ok(())
+    }
+
+    #[test]
+    fn test_rollback_index_default_zero() -> Result<()> {
+        let vbmeta =
+            VbMetaImage::verify_path("test_microdroid_vendor_image_no_rollback_index.img")?;
+        assert_eq!(0, vbmeta.rollback_index());
+        Ok(())
+    }
 }
diff --git a/microdroid/Android.bp b/microdroid/Android.bp
index 169ecae..33d98dc 100644
--- a/microdroid/Android.bp
+++ b/microdroid/Android.bp
@@ -55,6 +55,7 @@
     properties: [
         "deps",
         "dirs",
+        "multilib",
     ],
 }
 
@@ -157,9 +158,11 @@
     // Below are dependencies that are conditionally enabled depending on value of build flags.
     soong_config_variables: {
         release_avf_enable_dice_changes: {
-            deps: [
-                "derive_microdroid_vendor_dice_node",
-            ],
+            multilib: {
+                lib64: {
+                    deps: ["derive_microdroid_vendor_dice_node"],
+                },
+            },
             dirs: [
                 "microdroid_resources",
             ],
diff --git a/microdroid/README.md b/microdroid/README.md
index baf41b0..c0cba97 100644
--- a/microdroid/README.md
+++ b/microdroid/README.md
@@ -41,7 +41,7 @@
 ## Building an app
 
 A [vm
-payload](https://android.googlesource.com/platform/packages/modules/Virtualization/+/refs/heads/master/vm_payload/)
+payload](https://android.googlesource.com/platform/packages/modules/Virtualization/+/refs/heads/main/vm_payload/)
 is a shared library file that gets executed in microdroid. It is packaged as
 part of an Android application.  The library should have an entry point
 `AVmPayload_main` as shown below:
@@ -132,12 +132,12 @@
 ### Using the APIs
 
 Use the [Android Virtualization Framework Java
-APIs](https://android.googlesource.com/platform/packages/modules/Virtualization/+/refs/heads/master/javalib/api/system-current.txt)
+APIs](https://android.googlesource.com/platform/packages/modules/Virtualization/+/refs/heads/main/java/framework/README.md)
 in your app to create a microdroid VM and run payload in it. The APIs are currently
 @SystemApi, and only available to preinstalled apps.
 
 If you are looking for an example usage of the APIs, you may refer to the [demo
-app](https://android.googlesource.com/platform/packages/modules/Virtualization/+/refs/heads/master/demo/).
+app](https://android.googlesource.com/platform/packages/modules/Virtualization/+/refs/heads/main/demo/).
 
 
 ## Running Microdroid with vendor image
diff --git a/microdroid/derive_microdroid_vendor_dice_node/Android.bp b/microdroid/derive_microdroid_vendor_dice_node/Android.bp
index de1bef7..8b79aad 100644
--- a/microdroid/derive_microdroid_vendor_dice_node/Android.bp
+++ b/microdroid/derive_microdroid_vendor_dice_node/Android.bp
@@ -10,9 +10,20 @@
     rustlibs: [
         "libanyhow",
         "libclap",
+        "libcstr",
+        "libdice_driver",
+        "libdiced_open_dice",
+        "libdm_rust",
+        "libserde_cbor",
+        "libvbmeta_rust",
     ],
     bootstrap: true,
     prefer_rlib: true,
+    multilib: {
+        lib32: {
+            enabled: false,
+        },
+    },
 }
 
 rust_binary {
diff --git a/microdroid/derive_microdroid_vendor_dice_node/src/main.rs b/microdroid/derive_microdroid_vendor_dice_node/src/main.rs
index 1d5db0d..c7bc3f5 100644
--- a/microdroid/derive_microdroid_vendor_dice_node/src/main.rs
+++ b/microdroid/derive_microdroid_vendor_dice_node/src/main.rs
@@ -14,9 +14,19 @@
 
 //! Derives microdroid vendor dice node.
 
-use anyhow::Error;
+use anyhow::{bail, Context, Result};
 use clap::Parser;
-use std::path::PathBuf;
+use cstr::cstr;
+use dice_driver::DiceDriver;
+use diced_open_dice::{
+    hash, retry_bcc_format_config_descriptor, DiceConfigValues, OwnedDiceArtifacts, HIDDEN_SIZE,
+};
+use dm::util::blkgetsize64;
+use std::fs::{read_link, File};
+use std::path::{Path, PathBuf};
+use vbmeta::VbMetaImage;
+
+const AVF_STRICT_BOOT: &str = "/proc/device-tree/chosen/avf,strict-boot";
 
 #[derive(Parser)]
 struct Args {
@@ -31,8 +41,74 @@
     output: PathBuf,
 }
 
-fn main() -> Result<(), Error> {
+// TODO(ioffe): move to a library to reuse same code here, in microdroid_manager and in
+// first_stage_init.
+fn is_strict_boot() -> bool {
+    Path::new(AVF_STRICT_BOOT).exists()
+}
+
+fn build_descriptor(vbmeta: &VbMetaImage) -> Result<Vec<u8>> {
+    let values = DiceConfigValues {
+        component_name: Some(cstr!("Microdroid vendor")),
+        security_version: Some(vbmeta.rollback_index()),
+        ..Default::default()
+    };
+    Ok(retry_bcc_format_config_descriptor(&values)?)
+}
+
+// TODO(ioffe): move to libvbmeta.
+fn find_root_digest(vbmeta: &VbMetaImage) -> Result<Option<Vec<u8>>> {
+    for descriptor in vbmeta.descriptors()?.iter() {
+        if let vbmeta::Descriptor::Hashtree(_) = descriptor {
+            return Ok(Some(descriptor.to_hashtree()?.root_digest().to_vec()));
+        }
+    }
+    Ok(None)
+}
+
+fn dice_derivation(dice: DiceDriver, vbmeta: &VbMetaImage) -> Result<OwnedDiceArtifacts> {
+    let authority_hash = if let Some(pubkey) = vbmeta.public_key() {
+        hash(pubkey).context("hash pubkey")?
+    } else {
+        bail!("no public key")
+    };
+    let code_hash = if let Some(root_digest) = find_root_digest(vbmeta)? {
+        hash(root_digest.as_ref()).context("hash root_digest")?
+    } else {
+        bail!("no hashtree")
+    };
+    let desc = build_descriptor(vbmeta).context("build descriptor")?;
+    let hidden = [0; HIDDEN_SIZE];
+    // The microdroid vendor partition doesn't contribute to the debuggability of the VM, and it is
+    // a bit tricky to propagate the info on whether the VM is debuggable to
+    // derive_microdroid_dice_node binary. Given these, we just always set `is_debuggable: false`
+    // for the "Microdroid vendor" dice node. The adjacent dice nodes (pvmfw & payload) provide the
+    // accurate information on whether VM is debuggable.
+    dice.derive(code_hash, &desc, authority_hash, /* debug= */ false, hidden)
+}
+
+fn extract_vbmeta(block_dev: &Path) -> Result<VbMetaImage> {
+    let size = blkgetsize64(block_dev).context("blkgetsize64  failed")?;
+    let file = File::open(block_dev).context("open failed")?;
+    let vbmeta = VbMetaImage::verify_reader_region(file, 0, size)?;
+    Ok(vbmeta)
+}
+
+fn try_main() -> Result<()> {
     let args = Args::parse();
-    eprintln!("{:?} {:?} {:?}", args.dice_driver, args.microdroid_vendor_disk_image, args.output);
+    let dice =
+        DiceDriver::new(&args.dice_driver, is_strict_boot()).context("Failed to load DICE")?;
+    let path = read_link(args.microdroid_vendor_disk_image).context("failed to read symlink")?;
+    let vbmeta = extract_vbmeta(&path).context("failed to extract vbmeta")?;
+    let dice_artifacts = dice_derivation(dice, &vbmeta).context("failed to derive dice chain")?;
+    let file = File::create(&args.output).context("failed to create output")?;
+    serde_cbor::to_writer(file, &dice_artifacts).context("failed to write dice artifacts")?;
     Ok(())
 }
+
+fn main() {
+    if let Err(e) = try_main() {
+        eprintln!("failed with {:?}", e);
+        std::process::exit(1);
+    }
+}
diff --git a/microdroid/kernel/README.md b/microdroid/kernel/README.md
index 92b7cfe..52df333 100644
--- a/microdroid/kernel/README.md
+++ b/microdroid/kernel/README.md
@@ -29,7 +29,7 @@
 ```
 
 Note that
-[`--config=fast`](https://android.googlesource.com/kernel/build/+/refs/heads/master/kleaf/docs/fast.md)
+[`--config=fast`](https://android.googlesource.com/kernel/build/+/refs/heads/main/kleaf/docs/fast.md)
 is not mandatory, but will make your build much faster.
 
 The build may fail in case you are doing an incremental build and the config has changed (b/257288175). Until that issue
diff --git a/microdroid_manager/src/main.rs b/microdroid_manager/src/main.rs
index 2386bd4..7da9ea4 100644
--- a/microdroid_manager/src/main.rs
+++ b/microdroid_manager/src/main.rs
@@ -43,6 +43,7 @@
 use log::{error, info};
 use microdroid_metadata::{Metadata, PayloadMetadata};
 use microdroid_payload_config::{ApkConfig, OsConfig, Task, TaskType, VmPayloadConfig};
+use nix::mount::{umount2, MntFlags};
 use nix::sys::signal::Signal;
 use payload::load_metadata;
 use rpcbinder::RpcSession;
@@ -86,6 +87,8 @@
 const ENCRYPTEDSTORE_BACKING_DEVICE: &str = "/dev/block/by-name/encryptedstore";
 const ENCRYPTEDSTORE_KEYSIZE: usize = 32;
 
+const DICE_CHAIN_FILE: &str = "/microdroid_resources/dice_chain.raw";
+
 #[derive(thiserror::Error, Debug)]
 enum MicrodroidError {
     #[error("Cannot connect to virtualization service: {0}")]
@@ -301,8 +304,13 @@
     vm_payload_service_fd: OwnedFd,
 ) -> Result<i32> {
     let metadata = load_metadata().context("Failed to load payload metadata")?;
-    let dice = DiceDriver::new(Path::new("/dev/open-dice0"), is_strict_boot())
-        .context("Failed to load DICE")?;
+    let dice = if Path::new(DICE_CHAIN_FILE).exists() {
+        DiceDriver::from_file(Path::new(DICE_CHAIN_FILE))
+            .context("Failed to load DICE from file")?
+    } else {
+        DiceDriver::new(Path::new("/dev/open-dice0"), is_strict_boot())
+            .context("Failed to load DICE from driver")?
+    };
 
     // Microdroid skips checking payload against instance image iff the device supports
     // secretkeeper. In that case Microdroid use VmSecret::V2, which provide protection against
@@ -328,6 +336,10 @@
 
         // Start apexd to activate APEXes. This may allow code within them to run.
         system_properties::write("ctl.start", "apexd-vm")?;
+
+        // Unmounting /microdroid_resources is a defence-in-depth effort to ensure that payload
+        // can't get hold of dice chain stored there.
+        umount2("/microdroid_resources", MntFlags::MNT_DETACH)?;
     }
 
     // Run encryptedstore binary to prepare the storage
diff --git a/pvmfw/src/entry.rs b/pvmfw/src/entry.rs
index 253604b..72212c3 100644
--- a/pvmfw/src/entry.rs
+++ b/pvmfw/src/entry.rs
@@ -403,12 +403,6 @@
     unsafe { slice::from_raw_parts_mut(range.start.0 as *mut u8, range.end - range.start) }
 }
 
-enum AppendedConfigType {
-    Valid,
-    Invalid,
-    NotFound,
-}
-
 enum AppendedPayload<'a> {
     /// Configuration data.
     Config(config::Config<'a>),
@@ -418,35 +412,29 @@
 
 impl<'a> AppendedPayload<'a> {
     fn new(data: &'a mut [u8]) -> Option<Self> {
-        match Self::guess_config_type(data) {
-            AppendedConfigType::Valid => {
-                let config = config::Config::new(data);
-                Some(Self::Config(config.unwrap()))
-            }
-            AppendedConfigType::NotFound if cfg!(feature = "legacy") => {
+        // The borrow checker gets confused about the ownership of data (see inline comments) so we
+        // intentionally obfuscate it using a raw pointer; see a similar issue (still not addressed
+        // in v1.77) in https://users.rust-lang.org/t/78467.
+        let data_ptr = data as *mut [u8];
+
+        // Config::new() borrows data as mutable ...
+        match config::Config::new(data) {
+            // ... so this branch has a mutable reference to data, from the Ok(Config<'a>). But ...
+            Ok(valid) => Some(Self::Config(valid)),
+            // ... if Config::new(data).is_err(), the Err holds no ref to data. However ...
+            Err(config::Error::InvalidMagic) if cfg!(feature = "legacy") => {
+                // ... the borrow checker still complains about a second mutable ref without this.
+                // SAFETY: Pointer to a valid mut (not accessed elsewhere), 'a lifetime re-used.
+                let data: &'a mut _ = unsafe { &mut *data_ptr };
+
                 const BCC_SIZE: usize = SIZE_4KB;
                 warn!("Assuming the appended data at {:?} to be a raw BCC", data.as_ptr());
                 Some(Self::LegacyBcc(&mut data[..BCC_SIZE]))
             }
-            _ => None,
-        }
-    }
-
-    fn guess_config_type(data: &mut [u8]) -> AppendedConfigType {
-        // This function is necessary to prevent the borrow checker from getting confused
-        // about the ownership of data in new(); see https://users.rust-lang.org/t/78467.
-        let addr = data.as_ptr();
-
-        match config::Config::new(data) {
-            Err(config::Error::InvalidMagic) => {
-                warn!("No configuration data found at {addr:?}");
-                AppendedConfigType::NotFound
-            }
             Err(e) => {
-                error!("Invalid configuration data at {addr:?}: {e}");
-                AppendedConfigType::Invalid
+                error!("Invalid configuration data at {data_ptr:?}: {e}");
+                None
             }
-            Ok(_) => AppendedConfigType::Valid,
         }
     }
 
diff --git a/pvmfw/src/fdt.rs b/pvmfw/src/fdt.rs
index d847ca2..9dca8af 100644
--- a/pvmfw/src/fdt.rs
+++ b/pvmfw/src/fdt.rs
@@ -199,11 +199,16 @@
     opp_node: FdtNode,
 ) -> libfdt::Result<ArrayVec<[u64; CpuInfo::MAX_OPPTABLES]>> {
     let mut table = ArrayVec::new();
-    for subnode in opp_node.subnodes()? {
+    let mut opp_nodes = opp_node.subnodes()?;
+    for subnode in opp_nodes.by_ref().take(table.capacity()) {
         let prop = subnode.getprop_u64(cstr!("opp-hz"))?.ok_or(FdtError::NotFound)?;
         table.push(prop);
     }
 
+    if opp_nodes.next().is_some() {
+        warn!("OPP table has more than {} entries: discarding extra nodes.", table.capacity());
+    }
+
     Ok(table)
 }
 
diff --git a/service_vm/README.md b/service_vm/README.md
index 3d94f78..ca03c1d 100644
--- a/service_vm/README.md
+++ b/service_vm/README.md
@@ -18,28 +18,12 @@
 
 ## RKP VM (Remote Key Provisioning Virtual Machine)
 
-The RKP VM is a key dependency of the Service VM. It is a virtual machine that
-undergoes validation by the [RKP][rkp] Server and acts as a remotely provisioned
-component for verifying the integrity of other virtual machines.
+Currently, the Service VM only supports VM remote attestation, and in that
+context we refer to it as the RKP VM. The RKP VM undergoes validation by the
+[RKP][rkp] Server and functions as a remotely provisioned component responsible
+for verifying the integrity of other virtual machines. See
+[VM remote attestation][vm-attestation] for more details about the role of RKP
+VM in remote attestation.
 
 [rkp]: https://source.android.com/docs/core/ota/modular-system/remote-key-provisioning
-
-### RKP VM attestation
-
-The RKP VM is recognized and attested by the RKP server, which acts as a trusted
-entity responsible for verifying the DICE chain of the RKP VM. This verification
-ensures that the RKP VM is operating on a genuine device.
-Additionally, the RKP VM is validated by the pVM Firmware, as part of the
-verified boot process.
-
-### Client VM attestation
-
-Once the RKP VM is successfully attested, it assumes the role of a trusted
-platform to attest client VMs. It leverages its trusted status to validate the
-integrity of the [DICE chain][open-dice] associated with each client VM. This
-validation process verifies that the client VMs are running in the expected
-[Microdroid][microdroid] VM environment, and certifies the payload executed
-within the VM. Currently, only Microdroid VMs are supported.
-
-[open-dice]: https://android.googlesource.com/platform/external/open-dice/+/main/docs/android.md
-[microdroid]: https://android.googlesource.com/platform/packages/modules/Virtualization/+/main/microdroid/README.md
+[vm-attestation]: https://android.googlesource.com/platform/packages/modules/Virtualization/+/main/docs/vm_remote_attestation.md
diff --git a/service_vm/manager/src/lib.rs b/service_vm/manager/src/lib.rs
index 273b54d..987325d 100644
--- a/service_vm/manager/src/lib.rs
+++ b/service_vm/manager/src/lib.rs
@@ -30,7 +30,6 @@
 use service_vm_comm::{Request, Response, ServiceVmRequest, VmType};
 use std::fs::{self, File, OpenOptions};
 use std::io::{self, BufRead, BufReader, BufWriter, Read, Write};
-use std::os::unix::io::FromRawFd;
 use std::path::{Path, PathBuf};
 use std::sync::{Condvar, Mutex};
 use std::thread;
@@ -294,10 +293,8 @@
 pub fn android_log_fd() -> io::Result<File> {
     let (reader_fd, writer_fd) = nix::unistd::pipe()?;
 
-    // SAFETY: These are new FDs with no previous owner.
-    let reader = unsafe { File::from_raw_fd(reader_fd) };
-    // SAFETY: These are new FDs with no previous owner.
-    let writer = unsafe { File::from_raw_fd(writer_fd) };
+    let reader = File::from(reader_fd);
+    let writer = File::from(writer_fd);
 
     thread::spawn(|| {
         for line in BufReader::new(reader).lines() {
diff --git a/service_vm/requests/src/cert.rs b/service_vm/requests/src/cert.rs
index 91281e7..e31d870 100644
--- a/service_vm/requests/src/cert.rs
+++ b/service_vm/requests/src/cert.rs
@@ -43,7 +43,7 @@
 /// Attestation extension contents
 ///
 /// ```asn1
-/// AttestationDescription ::= SEQUENCE {
+/// AttestationExtension ::= SEQUENCE {
 ///     attestationChallenge       OCTET_STRING,
 ///     isVmSecure                 BOOLEAN,
 ///     vmComponents               SEQUENCE OF VmComponent,
diff --git a/service_vm/test_apk/Android.bp b/service_vm/test_apk/Android.bp
index 450d475..72e411e 100644
--- a/service_vm/test_apk/Android.bp
+++ b/service_vm/test_apk/Android.bp
@@ -58,14 +58,8 @@
     manifest: "AndroidManifest.rkpd.xml",
     test_config: "AndroidTest.rkpd.xml",
     static_libs: [
-        "RkpdAppTestUtil",
         "VmAttestationTestUtil",
-        "androidx.work_work-testing",
     ],
-    instrumentation_for: "rkpdapp",
-    // This app is a variation of rkpdapp, with additional permissions to run
-    // a VM. It is defined in packages/modules/RemoteKeyProvisioning.
-    data: [":avf-rkpdapp"],
 }
 
 java_library {
diff --git a/service_vm/test_apk/AndroidManifest.rkpd.xml b/service_vm/test_apk/AndroidManifest.rkpd.xml
index 6ecc5a9..369a456 100644
--- a/service_vm/test_apk/AndroidManifest.rkpd.xml
+++ b/service_vm/test_apk/AndroidManifest.rkpd.xml
@@ -17,8 +17,15 @@
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
     package="com.android.virt.rkpd.vm_attestation.testapp">
 
+    <uses-permission android:name="android.permission.MANAGE_VIRTUAL_MACHINE" />
+    <uses-permission android:name="android.permission.USE_CUSTOM_VIRTUAL_MACHINE" />
+
+    <!-- Required to check the network access -->
+    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
+    <uses-permission android:name="android.permission.INTERNET" />
+
     <instrumentation
             android:name="androidx.test.runner.AndroidJUnitRunner"
-            android:targetPackage="com.android.rkpdapp"
+            android:targetPackage="com.android.virt.rkpd.vm_attestation.testapp"
             android:label="AVF rkpd app integration tests" />
 </manifest>
diff --git a/service_vm/test_apk/AndroidTest.rkpd.xml b/service_vm/test_apk/AndroidTest.rkpd.xml
index 39eca32..8ff7bb4 100644
--- a/service_vm/test_apk/AndroidTest.rkpd.xml
+++ b/service_vm/test_apk/AndroidTest.rkpd.xml
@@ -17,22 +17,18 @@
     <option name="test-suite-tag" value="apct" />
     <option name="test-suite-tag" value="apct-instrumentation" />
 
-    <!-- Need to disable SELinux policy to allow com.android.rkpdapp to run a VM. -->
-    <target_preparer class="com.android.tradefed.targetprep.DisableSELinuxTargetPreparer"/>
+    <target_preparer class="com.android.tradefed.targetprep.DeviceSetup">
+        <!-- The host name is needed for RKPD key provisioning -->
+        <option name="set-property" key="remote_provisioning.hostname"
+         value="remoteprovisioning.googleapis.com" />
+        <option name="restore-properties" value="true"/>
+    </target_preparer>
 
     <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
         <option name="test-file-name" value="AvfRkpdVmAttestationTestApp.apk" />
-        <option name="test-file-name" value="avf-rkpdapp.apk" />
     </target_preparer>
 
     <test class="com.android.tradefed.testtype.AndroidJUnitTest" >
         <option name="package" value="com.android.virt.rkpd.vm_attestation.testapp" />
     </test>
-
-    <!-- Only run if RKPD mainline module is installed -->
-    <object type="module_controller"
-            class="com.android.tradefed.testtype.suite.module.MainlineTestModuleController">
-        <option name="enable" value="true" />
-        <option name="mainline-module-package-name" value="com.android.rkpd" />
-    </object>
 </configuration>
diff --git a/service_vm/test_apk/src/java/com/android/virt/rkpd/vm_attestation/testapp/RkpdVmAttestationTest.java b/service_vm/test_apk/src/java/com/android/virt/rkpd/vm_attestation/testapp/RkpdVmAttestationTest.java
index ce7fc45..f456cb4 100644
--- a/service_vm/test_apk/src/java/com/android/virt/rkpd/vm_attestation/testapp/RkpdVmAttestationTest.java
+++ b/service_vm/test_apk/src/java/com/android/virt/rkpd/vm_attestation/testapp/RkpdVmAttestationTest.java
@@ -18,31 +18,20 @@
 
 import static android.system.virtualmachine.VirtualMachineConfig.DEBUG_LEVEL_FULL;
 
-import static com.google.common.truth.Truth.assertThat;
 import static com.google.common.truth.TruthJUnit.assume;
 
+import android.net.ConnectivityManager;
+import android.net.NetworkCapabilities;
+import android.net.Network;
 import android.content.Context;
-import android.hardware.security.keymint.IRemotelyProvisionedComponent;
-import android.os.SystemProperties;
 import android.system.virtualmachine.VirtualMachine;
 import android.system.virtualmachine.VirtualMachineConfig;
 
-import androidx.work.ListenableWorker;
-import androidx.work.testing.TestWorkerBuilder;
-
 import com.android.microdroid.test.device.MicrodroidDeviceTestBase;
-import com.android.rkpdapp.database.ProvisionedKeyDao;
-import com.android.rkpdapp.database.RkpdDatabase;
-import com.android.rkpdapp.interfaces.ServerInterface;
-import com.android.rkpdapp.interfaces.ServiceManagerInterface;
-import com.android.rkpdapp.interfaces.SystemInterface;
-import com.android.rkpdapp.provisioner.PeriodicProvisioner;
-import com.android.rkpdapp.testutil.SystemInterfaceSelector;
-import com.android.rkpdapp.utils.Settings;
 import com.android.virt.vm_attestation.testservice.IAttestationService.SigningResult;
 import com.android.virt.vm_attestation.util.X509Utils;
+import android.system.virtualmachine.VirtualMachineManager;
 
-import org.junit.After;
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -53,7 +42,6 @@
 import java.util.Arrays;
 import java.util.Collection;
 import java.util.List;
-import java.util.concurrent.Executors;
 
 /**
  * End-to-end test for the pVM remote attestation.
@@ -71,23 +59,16 @@
  * <p>- Have an arm64 device supporting protected VMs.
  *
  * <p>- Have a stable network connection on the device.
- *
- * <p>- Have the RKP server hostname configured in the device. If not, you can set it using: $ adb
- * shell setprop remote_provisioning.hostname remoteprovisioning.googleapis.com
  */
 @RunWith(Parameterized.class)
 public class RkpdVmAttestationTest extends MicrodroidDeviceTestBase {
     private static final String TAG = "RkpdVmAttestationTest";
 
-    private static final String SERVICE_NAME = IRemotelyProvisionedComponent.DESCRIPTOR + "/avf";
     private static final String VM_PAYLOAD_PATH = "libvm_attestation_test_payload.so";
     private static final String MESSAGE = "Hello RKP from AVF!";
     private static final String TEST_APP_PACKAGE_NAME =
             "com.android.virt.rkpd.vm_attestation.testapp";
 
-    private ProvisionedKeyDao mKeyDao;
-    private PeriodicProvisioner mProvisioner;
-
     @Parameterized.Parameter(0)
     public String mGki;
 
@@ -103,57 +84,33 @@
 
     @Before
     public void setUp() throws Exception {
-        assume().withMessage("The RKP server hostname is not configured -- assume RKP disabled.")
-                .that(SystemProperties.get("remote_provisioning.hostname"))
-                .isNotEmpty();
         assume().withMessage("RKP Integration tests rely on network availability.")
-                .that(ServerInterface.isNetworkConnected(getContext()))
+                .that(isNetworkConnected(getContext()))
                 .isTrue();
-        // TODO(b/329652894): Assume that pVM remote attestation feature is supported.
+        assumeFeatureEnabled(VirtualMachineManager.FEATURE_REMOTE_ATTESTATION);
+        assume().withMessage("Test needs Remote Attestation support")
+                .that(getVirtualMachineManager().isRemoteAttestationSupported())
+                .isTrue();
 
-        prepareTestSetup(true /* protectedVm */, mGki);
-
-        Settings.clearPreferences(getContext());
-        mKeyDao = RkpdDatabase.getDatabase(getContext()).provisionedKeyDao();
-        mKeyDao.deleteAllKeys();
-
-        mProvisioner =
-                TestWorkerBuilder.from(
-                                getContext(),
-                                PeriodicProvisioner.class,
-                                Executors.newSingleThreadExecutor())
-                        .build();
-
-        SystemInterface systemInterface =
-                SystemInterfaceSelector.getSystemInterfaceForServiceName(SERVICE_NAME);
-        ServiceManagerInterface.setInstances(new SystemInterface[] {systemInterface});
-
-        setMaxPerformanceTaskProfile();
-    }
-
-    @After
-    public void tearDown() throws Exception {
-        ServiceManagerInterface.setInstances(null);
-        if (mKeyDao != null) {
-            mKeyDao.deleteAllKeys();
+        if (mGki == null) {
+            // We don't need this permission to use the microdroid kernel.
+            revokePermission(VirtualMachine.USE_CUSTOM_VIRTUAL_MACHINE_PERMISSION);
+        } else {
+            // The permission is needed to use the GKI kernel.
+            // Granting the permission is needed as the microdroid kernel test setup
+            // can revoke the permission before the GKI kernel test.
+            grantPermission(VirtualMachine.USE_CUSTOM_VIRTUAL_MACHINE_PERMISSION);
         }
-        Settings.clearPreferences(getContext());
+        prepareTestSetup(true /* protectedVm */, mGki);
+        setMaxPerformanceTaskProfile();
     }
 
     @Test
     public void usingProvisionedKeyForVmAttestationSucceeds() throws Exception {
-        // Provision keys.
-        assertThat(mProvisioner.doWork()).isEqualTo(ListenableWorker.Result.success());
-        assertThat(mKeyDao.getTotalUnassignedKeysForIrpc(SERVICE_NAME)).isGreaterThan(0);
-
         // Arrange.
-        Context ctx = getContext();
-        Context otherAppCtx = ctx.createPackageContext(TEST_APP_PACKAGE_NAME, 0);
         VirtualMachineConfig config =
-                new VirtualMachineConfig.Builder(otherAppCtx)
-                        .setProtectedVm(true)
+                newVmConfigBuilderWithPayloadBinary(VM_PAYLOAD_PATH)
                         .setDebugLevel(DEBUG_LEVEL_FULL)
-                        .setPayloadBinaryName(VM_PAYLOAD_PATH)
                         .setVmOutputCaptured(true)
                         .build();
         VirtualMachine vm = forceCreateNewVirtualMachine("attestation_with_rkpd_client", config);
@@ -170,4 +127,13 @@
         X509Utils.verifyAvfRelatedCerts(certs, challenge, TEST_APP_PACKAGE_NAME);
         X509Utils.verifySignature(certs[0], MESSAGE.getBytes(), signingResult.signature);
     }
+
+    private static boolean isNetworkConnected(Context context) {
+        ConnectivityManager cm = context.getSystemService(ConnectivityManager.class);
+        Network network = cm.getActiveNetwork();
+        NetworkCapabilities capabilities = cm.getNetworkCapabilities(network);
+        return capabilities != null
+                && capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
+                && capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED);
+    }
 }
diff --git a/tests/helper/src/java/com/android/microdroid/test/device/MicrodroidDeviceTestBase.java b/tests/helper/src/java/com/android/microdroid/test/device/MicrodroidDeviceTestBase.java
index 6c82de8..f881909 100644
--- a/tests/helper/src/java/com/android/microdroid/test/device/MicrodroidDeviceTestBase.java
+++ b/tests/helper/src/java/com/android/microdroid/test/device/MicrodroidDeviceTestBase.java
@@ -155,6 +155,12 @@
     public VirtualMachine forceCreateNewVirtualMachine(String name, VirtualMachineConfig config)
             throws VirtualMachineException {
         final VirtualMachineManager vmm = getVirtualMachineManager();
+        deleteVirtualMachineIfExists(name);
+        return vmm.create(name, config);
+    }
+
+    protected void deleteVirtualMachineIfExists(String name) throws VirtualMachineException {
+        VirtualMachineManager vmm = getVirtualMachineManager();
         boolean deleteExisting;
         try {
             deleteExisting = vmm.get(name) != null;
@@ -166,7 +172,6 @@
         if (deleteExisting) {
             vmm.delete(name);
         }
-        return vmm.create(name, config);
     }
 
     public void prepareTestSetup(boolean protectedVm, String gki) {
diff --git a/tests/hostside/java/com/android/microdroid/test/MicrodroidHostTests.java b/tests/hostside/java/com/android/microdroid/test/MicrodroidHostTests.java
index 57f0c77..e7e9ded 100644
--- a/tests/hostside/java/com/android/microdroid/test/MicrodroidHostTests.java
+++ b/tests/hostside/java/com/android/microdroid/test/MicrodroidHostTests.java
@@ -828,6 +828,11 @@
         AtomsProto.VmExited atomVmExited = data.get(2).getAtom().getVmExited();
         assertThat(atomVmExited.getVmIdentifier()).isEqualTo("VmRunApp");
         assertThat(atomVmExited.getDeathReason()).isEqualTo(AtomsProto.VmExited.DeathReason.KILLED);
+        assertThat(atomVmExited.getExitSignal()).isEqualTo(9);
+        // In CPU & memory related fields, check whether positive values are collected or not.
+        assertThat(atomVmExited.getGuestTimeMillis()).isGreaterThan(0);
+        assertThat(atomVmExited.getRssVmKb()).isGreaterThan(0);
+        assertThat(atomVmExited.getRssCrosvmKb()).isGreaterThan(0);
 
         // Check UID and elapsed_time by comparing each other.
         assertThat(atomVmBooted.getUid()).isEqualTo(atomVmCreationRequested.getUid());
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 8f4df63..29e9014 100644
--- a/tests/testapk/src/java/com/android/microdroid/test/MicrodroidTests.java
+++ b/tests/testapk/src/java/com/android/microdroid/test/MicrodroidTests.java
@@ -37,6 +37,7 @@
 import static org.junit.Assume.assumeTrue;
 
 import static java.nio.file.StandardCopyOption.REPLACE_EXISTING;
+import static java.util.stream.Collectors.toList;
 
 import android.app.Instrumentation;
 import android.app.UiAutomation;
@@ -110,6 +111,7 @@
 import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.atomic.AtomicReference;
+import java.util.stream.Stream;
 
 import co.nstant.in.cbor.CborDecoder;
 import co.nstant.in.cbor.model.Array;
@@ -1055,6 +1057,51 @@
         changeDebugLevel(DEBUG_LEVEL_NONE, DEBUG_LEVEL_FULL);
     }
 
+    // Copy the Vm directory, creating the target Vm directory if it does not already exist.
+    private void copyVmDirectory(String sourceVmName, String targetVmName) throws IOException {
+        Path sourceVm = getVmDirectory(sourceVmName);
+        Path targetVm = getVmDirectory(targetVmName);
+        if (!Files.exists(targetVm)) {
+            Files.createDirectories(targetVm);
+        }
+
+        try (Stream<Path> stream = Files.list(sourceVm)) {
+            for (Path f : stream.collect(toList())) {
+                Files.copy(f, targetVm.resolve(f.getFileName()), REPLACE_EXISTING);
+            }
+        }
+    }
+
+    private Path getVmDirectory(String vmName) {
+        Context context = getContext();
+        Path filePath = Paths.get(context.getDataDir().getPath(), "vm", vmName);
+        return filePath;
+    }
+
+    // Create a fresh VM with the given `vmName`, instance_id & instance.img. This function creates
+    // a Vm with a different temporary name & copies it to target VM directory. This ensures this
+    // VM is not in cache of `VirtualMachineManager` which makes it possible to modify underlying
+    // files.
+    private void createUncachedVmWithName(
+            String vmName, VirtualMachineConfig config, File vmIdBackup, File vmInstanceBackup)
+            throws Exception {
+        deleteVirtualMachineIfExists(vmName);
+        forceCreateNewVirtualMachine("test_vm_tmp", config);
+        copyVmDirectory("test_vm_tmp", vmName);
+        if (vmInstanceBackup != null) {
+            Files.copy(
+                    vmInstanceBackup.toPath(),
+                    getVmFile(vmName, "instance.img").toPath(),
+                    REPLACE_EXISTING);
+        }
+        if (vmIdBackup != null) {
+            Files.copy(
+                    vmIdBackup.toPath(),
+                    getVmFile(vmName, "instance_id").toPath(),
+                    REPLACE_EXISTING);
+        }
+    }
+
     @Test
     @CddTest(requirements = {"9.17/C-1-1", "9.17/C-2-7"})
     public void changingDebuggableVmNonDebuggableInvalidatesVmIdentity() throws Exception {
@@ -1089,29 +1136,17 @@
             Files.copy(vmId.toPath(), vmIdBackup.toPath(), REPLACE_EXISTING);
         }
 
-        forceCreateNewVirtualMachine("test_vm", normalConfig);
-
-        if (vmInstanceBackup != null) {
-            Files.copy(vmInstanceBackup.toPath(), vmInstance.toPath(), REPLACE_EXISTING);
-        }
-        if (vmIdBackup != null) {
-            Files.copy(vmIdBackup.toPath(), vmId.toPath(), REPLACE_EXISTING);
-        }
-        assertThat(tryBootVm(TAG, "test_vm").payloadStarted).isTrue();
+        createUncachedVmWithName("test_vm_rerun", normalConfig, vmIdBackup, vmInstanceBackup);
+        assertThat(tryBootVm(TAG, "test_vm_rerun").payloadStarted).isTrue();
 
         // Launch the same VM with a different debug level. The Java API prohibits this
         // (thankfully).
         // For testing, we do that by creating a new VM with debug level, and overwriting the old
         // instance data to the new VM instance data.
         VirtualMachineConfig debugConfig = builder.setDebugLevel(toLevel).build();
-        forceCreateNewVirtualMachine("test_vm", debugConfig);
-        if (vmInstanceBackup != null) {
-            Files.copy(vmInstanceBackup.toPath(), vmInstance.toPath(), REPLACE_EXISTING);
-        }
-        if (vmIdBackup != null) {
-            Files.copy(vmIdBackup.toPath(), vmId.toPath(), REPLACE_EXISTING);
-        }
-        assertThat(tryBootVm(TAG, "test_vm").payloadStarted).isFalse();
+        createUncachedVmWithName(
+                "test_vm_changed_debug_level", debugConfig, vmIdBackup, vmInstanceBackup);
+        assertThat(tryBootVm(TAG, "test_vm_changed_debug_level").payloadStarted).isFalse();
     }
 
     private static class VmCdis {
@@ -1555,7 +1590,6 @@
             assertFileContentsAreEqualInTwoVms("storage.img", vmNameOrig, vmNameImport);
         }
         assertThat(vmImport).isNotEqualTo(vmOrig);
-        vmm.delete(vmNameOrig);
         assertThat(vmImport).isEqualTo(vmm.get(vmNameImport));
         TestResults testResults =
                 runVmTestService(
diff --git a/tests/vendor_images/Android.bp b/tests/vendor_images/Android.bp
index 26dbc01..66f0219 100644
--- a/tests/vendor_images/Android.bp
+++ b/tests/vendor_images/Android.bp
@@ -15,6 +15,16 @@
     file_contexts: ":microdroid_vendor_file_contexts.gen",
     use_avb: true,
     avb_private_key: ":vendor_sign_key",
+    rollback_index: 5,
+}
+
+android_filesystem {
+    name: "test_microdroid_vendor_image_no_rollback_index",
+    partition_name: "microdroid-vendor",
+    type: "ext4",
+    file_contexts: ":microdroid_vendor_file_contexts.gen",
+    use_avb: true,
+    avb_private_key: ":vendor_sign_key",
 }
 
 android_filesystem {
diff --git a/virtualizationmanager/src/aidl.rs b/virtualizationmanager/src/aidl.rs
index 279b4ec..99a0078 100644
--- a/virtualizationmanager/src/aidl.rs
+++ b/virtualizationmanager/src/aidl.rs
@@ -17,7 +17,7 @@
 use crate::{get_calling_pid, get_calling_uid};
 use crate::atom::{write_vm_booted_stats, write_vm_creation_stats};
 use crate::composite::make_composite_image;
-use crate::crosvm::{CrosvmConfig, DiskFile, DisplayConfig, PayloadState, VmContext, VmInstance, VmState};
+use crate::crosvm::{CrosvmConfig, DiskFile, DisplayConfig, InputDeviceOption, PayloadState, 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};
@@ -32,6 +32,7 @@
     AssignableDevice::AssignableDevice,
     CpuTopology::CpuTopology,
     DiskImage::DiskImage,
+    InputDevice::InputDevice,
     IVirtualMachine::{BnVirtualMachine, IVirtualMachine},
     IVirtualMachineCallback::IVirtualMachineCallback,
     IVirtualizationService::IVirtualizationService,
@@ -581,13 +582,27 @@
         } else {
             (vec![], None)
         };
+        let display_config = if cfg!(paravirtualized_devices) {
+            config
+                .displayConfig
+                .as_ref()
+                .map(DisplayConfig::new)
+                .transpose()
+                .or_binder_exception(ExceptionCode::ILLEGAL_ARGUMENT)?
+        } else {
+            None
+        };
 
-        let display_config = config
-            .displayConfig
-            .as_ref()
-            .map(DisplayConfig::new)
-            .transpose()
-            .or_binder_exception(ExceptionCode::ILLEGAL_ARGUMENT)?;
+        let input_device_options = if cfg!(paravirtualized_devices) {
+            config
+                .inputDevices
+                .iter()
+                .map(to_input_device_option_from)
+                .collect::<Result<Vec<InputDeviceOption>, _>>()
+                .or_binder_exception(ExceptionCode::ILLEGAL_ARGUMENT)?
+        } else {
+            vec![]
+        };
 
         // Actually start the VM.
         let crosvm_config = CrosvmConfig {
@@ -615,6 +630,7 @@
             dtbo,
             device_tree_overlay,
             display_config,
+            input_device_options,
         };
         let instance = Arc::new(
             VmInstance::new(
@@ -720,6 +736,26 @@
     (result / granularity) * granularity
 }
 
+fn to_input_device_option_from(input_device: &InputDevice) -> Result<InputDeviceOption> {
+    Ok(match input_device {
+        InputDevice::SingleTouch(single_touch) => InputDeviceOption::SingleTouch {
+            file: clone_file(single_touch.pfd.as_ref().ok_or(anyhow!("pfd should have value"))?)?,
+            height: u32::try_from(single_touch.height)?,
+            width: u32::try_from(single_touch.width)?,
+            name: if !single_touch.name.is_empty() {
+                Some(single_touch.name.clone())
+            } else {
+                None
+            },
+        },
+        InputDevice::EvDev(evdev) => InputDeviceOption::EvDev(clone_file(
+            evdev.pfd.as_ref().ok_or(anyhow!("pfd should have value"))?,
+        )?),
+        InputDevice::Keyboard(keyboard) => InputDeviceOption::Keyboard(clone_file(
+            keyboard.pfd.as_ref().ok_or(anyhow!("pfd should have value"))?,
+        )?),
+    })
+}
 /// Given the configuration for a disk image, assembles the `DiskFile` to pass to crosvm.
 ///
 /// This may involve assembling a composite disk from a set of partition images.
@@ -1410,13 +1446,11 @@
         return Ok(None);
     };
 
-    let (raw_read_fd, raw_write_fd) =
+    let (read_fd, write_fd) =
         pipe().context("Failed to create pipe").or_service_specific_exception(-1)?;
 
-    // SAFETY: We are the sole owner of this FD as we just created it, and it is valid and open.
-    let mut reader = BufReader::new(unsafe { File::from_raw_fd(raw_read_fd) });
-    // SAFETY: We are the sole owner of this FD as we just created it, and it is valid and open.
-    let write_fd = unsafe { File::from_raw_fd(raw_write_fd) };
+    let mut reader = BufReader::new(File::from(read_fd));
+    let write_fd = File::from(write_fd);
 
     std::thread::spawn(move || loop {
         let mut buf = vec![];
diff --git a/virtualizationmanager/src/crosvm.rs b/virtualizationmanager/src/crosvm.rs
index 9b0ec5f..040e552 100644
--- a/virtualizationmanager/src/crosvm.rs
+++ b/virtualizationmanager/src/crosvm.rs
@@ -34,7 +34,7 @@
 use std::io::{self, Read};
 use std::mem;
 use std::num::{NonZeroU16, NonZeroU32};
-use std::os::unix::io::{AsRawFd, RawFd, FromRawFd};
+use std::os::unix::io::{AsRawFd, RawFd};
 use std::os::unix::process::ExitStatusExt;
 use std::path::{Path, PathBuf};
 use std::process::{Command, ExitStatus};
@@ -120,6 +120,7 @@
     pub dtbo: Option<File>,
     pub device_tree_overlay: Option<File>,
     pub display_config: Option<DisplayConfig>,
+    pub input_device_options: Vec<InputDeviceOption>,
 }
 
 #[derive(Debug)]
@@ -154,6 +155,15 @@
     pub writable: bool,
 }
 
+/// virtio-input device configuration from `external/crosvm/src/crosvm/config.rs`
+#[derive(Debug)]
+#[allow(dead_code)]
+pub enum InputDeviceOption {
+    EvDev(File),
+    SingleTouch { file: File, width: u32, height: u32, name: Option<String> },
+    Keyboard(File),
+}
+
 type VfioDevice = Strong<dyn IBoundDevice>;
 
 /// The lifecycle state which the payload in the VM has reported itself to be in.
@@ -440,15 +450,15 @@
 
     /// Waits until payload is started, or timeout expires. When timeout occurs, kill
     /// the VM to prevent indefinite hangup and update the payload_state accordingly.
-    #[allow(let_underscore_lock)]
     fn monitor_payload_hangup(&self, child: Arc<SharedChild>) {
         debug!("Starting to monitor hangup for Microdroid({})", child.id());
-        let (_, result) = self
+        let (state, result) = self
             .payload_state_updated
             .wait_timeout_while(self.payload_state.lock().unwrap(), *BOOT_HANGUP_TIMEOUT, |s| {
                 *s < PayloadState::Started
             })
             .unwrap();
+        drop(state); // we are not interested in state
         let child_still_running = child.try_wait().ok() == Some(None);
         if result.timed_out() && child_still_running {
             error!(
@@ -955,14 +965,46 @@
     if let Some(dt_overlay) = &config.device_tree_overlay {
         command.arg("--device-tree-overlay").arg(add_preserved_fd(&mut preserved_fds, dt_overlay));
     }
-    if let Some(display_config) = &config.display_config {
-        command.arg("--gpu")
-        // TODO(b/331708504): support backend config as well
-        .arg("backend=virglrenderer,context-types=virgl2,egl=true,surfaceless=true,glx=false,gles=true")
-        .arg(format!("--gpu-display=mode=windowed[{},{}],dpi=[{},{}],refresh-rate={}", display_config.width, display_config.height, display_config.horizontal_dpi, display_config.vertical_dpi, display_config.refresh_rate))
-        .arg(format!("--android-display-service={}", config.name));
+
+    if cfg!(paravirtualized_devices) {
+        if let Some(display_config) = &config.display_config {
+            command.arg("--gpu")
+            // TODO(b/331708504): support backend config as well
+            .arg("backend=virglrenderer,context-types=virgl2,egl=true,surfaceless=true,glx=false,gles=true")
+            .arg(format!("--gpu-display=mode=windowed[{},{}],dpi=[{},{}],refresh-rate={}", display_config.width, display_config.height, display_config.horizontal_dpi, display_config.vertical_dpi, display_config.refresh_rate))
+            .arg(format!("--android-display-service={}", config.name));
+        }
     }
 
+    if cfg!(paravirtualized_devices) {
+        // TODO(b/325929096): Need to set up network from the config
+        if rustutils::system_properties::read_bool("ro.crosvm.network.setup.done", false)
+            .unwrap_or(false)
+        {
+            command.arg("--net").arg("tap-name=crosvm_tap");
+        }
+    }
+
+    if cfg!(paravirtualized_devices) {
+        for input_device_option in config.input_device_options.iter() {
+            command.arg("--input");
+            command.arg(match input_device_option {
+                InputDeviceOption::EvDev(file) => {
+                    format!("evdev[path={}]", add_preserved_fd(&mut preserved_fds, file))
+                }
+                InputDeviceOption::Keyboard(file) => {
+                    format!("keyboard[path={}]", add_preserved_fd(&mut preserved_fds, file))
+                }
+                InputDeviceOption::SingleTouch { file, width, height, name } => format!(
+                    "single-touch[path={},width={},height={}{}]",
+                    add_preserved_fd(&mut preserved_fds, file),
+                    width,
+                    height,
+                    name.as_ref().map_or("".into(), |n| format!(",name={}", n))
+                ),
+            });
+        }
+    }
     append_platform_devices(&mut command, &mut preserved_fds, &config)?;
 
     debug!("Preserving FDs {:?}", preserved_fds);
@@ -1041,10 +1083,6 @@
 
 /// Creates a new pipe with the `O_CLOEXEC` flag set, and returns the read side and write side.
 fn create_pipe() -> Result<(File, File), Error> {
-    let (raw_read, raw_write) = pipe2(OFlag::O_CLOEXEC)?;
-    // SAFETY: We are the sole owner of this FD as we just created it, and it is valid and open.
-    let read_fd = unsafe { File::from_raw_fd(raw_read) };
-    // SAFETY: We are the sole owner of this FD as we just created it, and it is valid and open.
-    let write_fd = unsafe { File::from_raw_fd(raw_write) };
-    Ok((read_fd, write_fd))
+    let (read_fd, write_fd) = pipe2(OFlag::O_CLOEXEC)?;
+    Ok((read_fd.into(), write_fd.into()))
 }
diff --git a/virtualizationmanager/src/main.rs b/virtualizationmanager/src/main.rs
index b2a734a..a31fd0a 100644
--- a/virtualizationmanager/src/main.rs
+++ b/virtualizationmanager/src/main.rs
@@ -30,10 +30,10 @@
 use lazy_static::lazy_static;
 use log::{info, LevelFilter};
 use rpcbinder::{FileDescriptorTransportMode, RpcServer};
-use std::os::unix::io::{FromRawFd, OwnedFd, RawFd};
+use std::os::unix::io::{AsFd, FromRawFd, OwnedFd, RawFd};
 use clap::Parser;
 use nix::fcntl::{fcntl, F_GETFD, F_SETFD, FdFlag};
-use nix::unistd::{Pid, Uid};
+use nix::unistd::{write, Pid, Uid};
 use std::os::unix::raw::{pid_t, uid_t};
 
 const LOG_TAG: &str = "virtmgr";
@@ -138,6 +138,8 @@
     info!("Started VirtualizationService RpcServer. Ready to accept connections");
 
     // Signal readiness to the caller by closing our end of the pipe.
+    write(ready_fd.as_fd(), "o".as_bytes())
+        .expect("Failed to write a single character through ready_fd");
     drop(ready_fd);
 
     server.join();
diff --git a/virtualizationservice/aidl/Android.bp b/virtualizationservice/aidl/Android.bp
index c479691..fb89772 100644
--- a/virtualizationservice/aidl/Android.bp
+++ b/virtualizationservice/aidl/Android.bp
@@ -48,7 +48,7 @@
         java: {
             sdk_version: "module_current",
             apex_available: [
-                "//apex_available:platform",
+                "com.android.virt",
             ],
         },
         rust: {
diff --git a/virtualizationservice/aidl/android/system/virtualizationservice/InputDevice.aidl b/virtualizationservice/aidl/android/system/virtualizationservice/InputDevice.aidl
new file mode 100644
index 0000000..712d6a9
--- /dev/null
+++ b/virtualizationservice/aidl/android/system/virtualizationservice/InputDevice.aidl
@@ -0,0 +1,40 @@
+/*
+ * 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;
+
+// Refer to https://crosvm.dev/book/devices/input.html
+union InputDevice {
+    // Add a single-touch touchscreen virtio-input device.
+    parcelable SingleTouch {
+        ParcelFileDescriptor pfd;
+        // Default values come from https://crosvm.dev/book/devices/input.html#single-touch
+        int width = 1280;
+        int height = 1080;
+        @utf8InCpp String name = "";
+    }
+    // Passes an event device node into the VM. The device will be grabbed (unusable from the host)
+    // and made available to the guest with the same configuration it shows on the host.
+    parcelable EvDev {
+        ParcelFileDescriptor pfd;
+    }
+    // Keyboard input
+    parcelable Keyboard {
+        ParcelFileDescriptor pfd;
+    }
+    SingleTouch singleTouch;
+    EvDev evDev;
+    Keyboard keyboard;
+}
diff --git a/virtualizationservice/aidl/android/system/virtualizationservice/VirtualMachineRawConfig.aidl b/virtualizationservice/aidl/android/system/virtualizationservice/VirtualMachineRawConfig.aidl
index 1a18bf8..86e26da 100644
--- a/virtualizationservice/aidl/android/system/virtualizationservice/VirtualMachineRawConfig.aidl
+++ b/virtualizationservice/aidl/android/system/virtualizationservice/VirtualMachineRawConfig.aidl
@@ -18,6 +18,7 @@
 import android.system.virtualizationservice.CpuTopology;
 import android.system.virtualizationservice.DiskImage;
 import android.system.virtualizationservice.DisplayConfig;
+import android.system.virtualizationservice.InputDevice;
 
 /** Raw configuration for running a VM. */
 parcelable VirtualMachineRawConfig {
@@ -73,4 +74,7 @@
     String[] devices;
 
     @nullable DisplayConfig displayConfig;
+
+    /** List of input devices to the VM */
+    InputDevice[] inputDevices;
 }
diff --git a/virtualizationservice/src/maintenance.rs b/virtualizationservice/src/maintenance.rs
index f950db9..8efc58d 100644
--- a/virtualizationservice/src/maintenance.rs
+++ b/virtualizationservice/src/maintenance.rs
@@ -40,6 +40,9 @@
 /// parcel fits within max AIDL message size.
 const DELETE_MAX_BATCH_SIZE: usize = 100;
 
+/// Maximum number of VM IDs that a single app can have.
+const MAX_VM_IDS_PER_APP: usize = 400;
+
 /// State related to VM secrets.
 pub struct State {
     sk: binder::Strong<dyn ISecretkeeper>,
@@ -101,6 +104,24 @@
     pub fn add_id(&mut self, vm_id: &VmId, user_id: u32, app_id: u32) -> Result<()> {
         let user_id: i32 = user_id.try_into().context(format!("user_id {user_id} out of range"))?;
         let app_id: i32 = app_id.try_into().context(format!("app_id {app_id} out of range"))?;
+
+        // To prevent unbounded growth of VM IDs (and the associated state) for an app, limit the
+        // number of VM IDs per app.
+        let count = self
+            .vm_id_db
+            .count_vm_ids_for_app(user_id, app_id)
+            .context("failed to determine VM count")?;
+        if count >= MAX_VM_IDS_PER_APP {
+            // The owner has too many VM IDs, so delete the oldest IDs so that the new VM ID
+            // creation can progress/succeed.
+            let purge = 1 + count - MAX_VM_IDS_PER_APP;
+            let old_vm_ids = self
+                .vm_id_db
+                .oldest_vm_ids_for_app(user_id, app_id, purge)
+                .context("failed to find oldest VM IDs")?;
+            error!("Deleting {purge} of {count} VM IDs for user_id={user_id}, app_id={app_id}");
+            self.delete_ids(&old_vm_ids);
+        }
         self.vm_id_db.add_vm_id(vm_id, user_id, app_id)
     }
 
@@ -396,6 +417,39 @@
         assert_eq!(vec![VM_ID5], sk_state.vm_id_db.vm_ids_for_user(USER3).unwrap());
     }
 
+    #[test]
+    fn test_sk_state_too_many_vms() {
+        let history = Arc::new(Mutex::new(Vec::new()));
+        let mut sk_state = new_test_state(history.clone(), 20);
+
+        // Every VM ID added up to the limit is kept.
+        for idx in 0..MAX_VM_IDS_PER_APP {
+            let mut vm_id = [0u8; 64];
+            vm_id[0..8].copy_from_slice(&(idx as u64).to_be_bytes());
+            sk_state.add_id(&vm_id, USER1 as u32, APP_A as u32).unwrap();
+            assert_eq!(idx + 1, sk_state.vm_id_db.count_vm_ids_for_app(USER1, APP_A).unwrap());
+        }
+        assert_eq!(
+            MAX_VM_IDS_PER_APP,
+            sk_state.vm_id_db.count_vm_ids_for_app(USER1, APP_A).unwrap()
+        );
+
+        // Beyond the limit it's one in, one out.
+        for idx in MAX_VM_IDS_PER_APP..MAX_VM_IDS_PER_APP + 10 {
+            let mut vm_id = [0u8; 64];
+            vm_id[0..8].copy_from_slice(&(idx as u64).to_be_bytes());
+            sk_state.add_id(&vm_id, USER1 as u32, APP_A as u32).unwrap();
+            assert_eq!(
+                MAX_VM_IDS_PER_APP,
+                sk_state.vm_id_db.count_vm_ids_for_app(USER1, APP_A).unwrap()
+            );
+        }
+        assert_eq!(
+            MAX_VM_IDS_PER_APP,
+            sk_state.vm_id_db.count_vm_ids_for_app(USER1, APP_A).unwrap()
+        );
+    }
+
     struct Irreconcilable;
 
     impl IVirtualizationReconciliationCallback for Irreconcilable {
diff --git a/virtualizationservice/src/maintenance/vmdb.rs b/virtualizationservice/src/maintenance/vmdb.rs
index 47704bc..273f340 100644
--- a/virtualizationservice/src/maintenance/vmdb.rs
+++ b/virtualizationservice/src/maintenance/vmdb.rs
@@ -272,6 +272,34 @@
         Ok(vm_ids)
     }
 
+    /// Determine the number of VM IDs associated with `(user_id, app_id)`.
+    pub fn count_vm_ids_for_app(&mut self, user_id: i32, app_id: i32) -> Result<usize> {
+        let mut stmt = self
+            .conn
+            .prepare("SELECT COUNT(vm_id) FROM main.vmids WHERE user_id = ? AND app_id = ?;")
+            .context("failed to prepare SELECT stmt")?;
+        stmt.query_row(params![user_id, app_id], |row| row.get(0)).context("query failed")
+    }
+
+    /// Return the `count` oldest VM IDs associated with `(user_id, app_id)`.
+    pub fn oldest_vm_ids_for_app(
+        &mut self,
+        user_id: i32,
+        app_id: i32,
+        count: usize,
+    ) -> Result<Vec<VmId>> {
+        // SQLite considers NULL columns to be smaller than values, so rows left over from a v0
+        // database will be listed first.
+        let mut stmt = self
+            .conn
+            .prepare(
+                "SELECT vm_id FROM main.vmids WHERE user_id = ? AND app_id = ? ORDER BY created LIMIT ?;",
+            )
+            .context("failed to prepare SELECT stmt")?;
+        let rows = stmt.query(params![user_id, app_id, count]).context("query failed")?;
+        Self::vm_ids_from_rows(rows)
+    }
+
     /// Return all of the `(user_id, app_id)` pairs present in the database.
     pub fn get_all_owners(&mut self) -> Result<Vec<(i32, i32)>> {
         let mut stmt = self
@@ -344,6 +372,19 @@
     fn show_contents(db: &VmIdDb) {
         let mut stmt = db.conn.prepare("SELECT * FROM main.vmids;").unwrap();
         let mut rows = stmt.query(()).unwrap();
+        println!("DB contents:");
+        while let Some(row) = rows.next().unwrap() {
+            println!("  {row:?}");
+        }
+    }
+
+    fn show_contents_for_app(db: &VmIdDb, user_id: i32, app_id: i32, count: usize) {
+        let mut stmt = db
+            .conn
+            .prepare("SELECT vm_id, created FROM main.vmids WHERE user_id = ? AND app_id = ? ORDER BY created LIMIT ?;")
+            .unwrap();
+        let mut rows = stmt.query(params![user_id, app_id, count]).unwrap();
+        println!("First (by created) {count} rows for app_id={app_id}");
         while let Some(row) = rows.next().unwrap() {
             println!("  {row:?}");
         }
@@ -457,31 +498,39 @@
 
         assert_eq!(vec![VM_ID1, VM_ID2, VM_ID3], db.vm_ids_for_user(USER1).unwrap());
         assert_eq!(vec![VM_ID1, VM_ID2, VM_ID3], db.vm_ids_for_app(USER1, APP_A).unwrap());
+        assert_eq!(3, db.count_vm_ids_for_app(USER1, APP_A).unwrap());
         assert_eq!(vec![VM_ID4], db.vm_ids_for_app(USER2, APP_B).unwrap());
+        assert_eq!(1, db.count_vm_ids_for_app(USER2, APP_B).unwrap());
         assert_eq!(vec![VM_ID5], db.vm_ids_for_user(USER3).unwrap());
         assert_eq!(empty, db.vm_ids_for_user(USER_UNKNOWN).unwrap());
         assert_eq!(empty, db.vm_ids_for_app(USER1, APP_UNKNOWN).unwrap());
+        assert_eq!(0, db.count_vm_ids_for_app(USER1, APP_UNKNOWN).unwrap());
 
         db.delete_vm_ids(&[VM_ID2, VM_ID3]).unwrap();
 
         assert_eq!(vec![VM_ID1], db.vm_ids_for_user(USER1).unwrap());
         assert_eq!(vec![VM_ID1], db.vm_ids_for_app(USER1, APP_A).unwrap());
+        assert_eq!(1, db.count_vm_ids_for_app(USER1, APP_A).unwrap());
 
         // OK to delete things that don't exist.
         db.delete_vm_ids(&[VM_ID2, VM_ID3]).unwrap();
 
         assert_eq!(vec![VM_ID1], db.vm_ids_for_user(USER1).unwrap());
         assert_eq!(vec![VM_ID1], db.vm_ids_for_app(USER1, APP_A).unwrap());
+        assert_eq!(1, db.count_vm_ids_for_app(USER1, APP_A).unwrap());
 
         db.add_vm_id(&VM_ID2, USER1, APP_A).unwrap();
         db.add_vm_id(&VM_ID3, USER1, APP_A).unwrap();
 
         assert_eq!(vec![VM_ID1, VM_ID2, VM_ID3], db.vm_ids_for_user(USER1).unwrap());
         assert_eq!(vec![VM_ID1, VM_ID2, VM_ID3], db.vm_ids_for_app(USER1, APP_A).unwrap());
+        assert_eq!(3, db.count_vm_ids_for_app(USER1, APP_A).unwrap());
         assert_eq!(vec![VM_ID4], db.vm_ids_for_app(USER2, APP_B).unwrap());
+        assert_eq!(1, db.count_vm_ids_for_app(USER2, APP_B).unwrap());
         assert_eq!(vec![VM_ID5], db.vm_ids_for_user(USER3).unwrap());
         assert_eq!(empty, db.vm_ids_for_user(USER_UNKNOWN).unwrap());
         assert_eq!(empty, db.vm_ids_for_app(USER1, APP_UNKNOWN).unwrap());
+        assert_eq!(0, db.count_vm_ids_for_app(USER1, APP_UNKNOWN).unwrap());
 
         assert_eq!(
             vec![(USER1, APP_A), (USER2, APP_B), (USER3, APP_C)],
@@ -513,4 +562,47 @@
         assert_eq!(vec![VM_ID1, VM_ID2, VM_ID3], db.vm_ids_for_user(USER1).unwrap());
         show_contents(&db);
     }
+
+    #[test]
+    fn test_remove_oldest_with_upgrade() {
+        let mut db = new_test_db_version(0);
+        let version = db.schema_version().unwrap();
+        assert_eq!(0, version);
+
+        let remove_count = 10;
+        let mut want = vec![];
+
+        // Manually insert rows before upgrade.
+        const V0_COUNT: usize = 5;
+        for idx in 0..V0_COUNT {
+            let mut vm_id = [0u8; 64];
+            vm_id[0..8].copy_from_slice(&(idx as u64).to_be_bytes());
+            if want.len() < remove_count {
+                want.push(vm_id);
+            }
+            db.conn
+                .execute(
+                    "REPLACE INTO main.vmids (vm_id, user_id, app_id) VALUES (?1, ?2, ?3);",
+                    params![&vm_id, &USER1, APP_A],
+                )
+                .unwrap();
+        }
+
+        // Now move to v1.
+        db.upgrade_tables_v0_v1().unwrap();
+        let version = db.schema_version().unwrap();
+        assert_eq!(1, version);
+
+        for idx in V0_COUNT..40 {
+            let mut vm_id = [0u8; 64];
+            vm_id[0..8].copy_from_slice(&(idx as u64).to_be_bytes());
+            if want.len() < remove_count {
+                want.push(vm_id);
+            }
+            db.add_vm_id(&vm_id, USER1, APP_A).unwrap();
+        }
+        show_contents_for_app(&db, USER1, APP_A, 10);
+        let got = db.oldest_vm_ids_for_app(USER1, APP_A, 10).unwrap();
+        assert_eq!(got, want);
+    }
 }
diff --git a/vm_payload/README.md b/vm_payload/README.md
index 419d854..4b1e6f3 100644
--- a/vm_payload/README.md
+++ b/vm_payload/README.md
@@ -2,7 +2,7 @@
 
 This directory contains the definition of the VM Payload API. This is a native
 API, exposed as a set of C functions, available to payload code running inside a
-[Microdroid](https://android.googlesource.com/platform/packages/modules/Virtualization/+/refs/heads/master/microdroid/README.md)
+[Microdroid](https://android.googlesource.com/platform/packages/modules/Virtualization/+/refs/heads/main/microdroid/README.md)
 VM.
 
 Note that only native code is supported in Microdroid, so no Java APIs are
@@ -17,7 +17,7 @@
 under the `lib/<ABI>` directory, like other JNI code.
 
 The primary .so, which is specified as part of the VM configuration via
-[VirtualMachineConfig.Builder#setPayloadBinaryPath](https://android.googlesource.com/platform/packages/modules/Virtualization/+/refs/heads/master/javalib/src/android/system/virtualmachine/VirtualMachineConfig.java),
+[VirtualMachineConfig.Builder#setPayloadBinaryPath](https://android.googlesource.com/platform/packages/modules/Virtualization/+/refs/heads/main/java/framework/src/android/system/virtualmachine/VirtualMachineConfig.java),
 must define the entry point for the payload.
 
 This entry point is a C function called `AVmPayload_main()`, as declared in
@@ -36,7 +36,7 @@
 runtime from the real `libvm_payload.so` hosted within the Microdroid VM.
 
 See `MicrodroidTestNativeLib` in the [test
-APK](https://android.googlesource.com/platform/packages/modules/Virtualization/+/refs/heads/master/tests/testapk/Android.bp)
+APK](https://android.googlesource.com/platform/packages/modules/Virtualization/+/refs/heads/main/tests/testapk/Android.bp)
 for an example.
 
 In other build systems a similar stub `libvm_payload.so` can be built using
diff --git a/vmbase/example/src/main.rs b/vmbase/example/src/main.rs
index 48b24be..da82b17 100644
--- a/vmbase/example/src/main.rs
+++ b/vmbase/example/src/main.rs
@@ -28,6 +28,7 @@
 use aarch64_paging::paging::MemoryRegion;
 use aarch64_paging::MapError;
 use alloc::{vec, vec::Vec};
+use core::ptr::addr_of_mut;
 use cstr::cstr;
 use fdtpci::PciInfo;
 use libfdt::Fdt;
@@ -138,14 +139,15 @@
 
     // SAFETY: Nowhere else in the program accesses this static mutable variable, so there is no
     // chance of concurrent access.
-    let zeroed_data = unsafe { &mut ZEROED_DATA };
+    let zeroed_data = unsafe { &mut *addr_of_mut!(ZEROED_DATA) };
     // SAFETY: Nowhere else in the program accesses this static mutable variable, so there is no
     // chance of concurrent access.
-    let mutable_data = unsafe { &mut MUTABLE_DATA };
+    let mutable_data = unsafe { &mut *addr_of_mut!(MUTABLE_DATA) };
 
     for element in zeroed_data.iter() {
         assert_eq!(*element, 0);
     }
+
     zeroed_data[0] = 13;
     assert_eq!(zeroed_data[0], 13);
     zeroed_data[0] = 0;
diff --git a/vmbase/example/tests/test.rs b/vmbase/example/tests/test.rs
index 2df5a80..4dc6aec 100644
--- a/vmbase/example/tests/test.rs
+++ b/vmbase/example/tests/test.rs
@@ -27,7 +27,6 @@
     collections::{HashSet, VecDeque},
     fs::File,
     io::{self, BufRead, BufReader, Read, Write},
-    os::unix::io::FromRawFd,
     panic, thread,
 };
 use vmclient::{DeathReason, VmInstance};
@@ -142,13 +141,7 @@
 
 fn pipe() -> io::Result<(File, File)> {
     let (reader_fd, writer_fd) = nix::unistd::pipe()?;
-
-    // SAFETY: These are new FDs with no previous owner.
-    let reader = unsafe { File::from_raw_fd(reader_fd) };
-    // SAFETY: These are new FDs with no previous owner.
-    let writer = unsafe { File::from_raw_fd(writer_fd) };
-
-    Ok((reader, writer))
+    Ok((reader_fd.into(), writer_fd.into()))
 }
 
 struct VmLogProcessor {
diff --git a/vmbase/src/bionic.rs b/vmbase/src/bionic.rs
index a049616..63a6894 100644
--- a/vmbase/src/bionic.rs
+++ b/vmbase/src/bionic.rs
@@ -14,17 +14,17 @@
 
 //! Low-level compatibility layer between baremetal Rust and Bionic C functions.
 
-use core::ffi::c_char;
-use core::ffi::c_int;
-use core::ffi::c_void;
-use core::ffi::CStr;
-use core::slice;
-use core::str;
-
 use crate::console;
 use crate::eprintln;
 use crate::rand::fill_with_entropy;
 use crate::read_sysreg;
+use core::ffi::c_char;
+use core::ffi::c_int;
+use core::ffi::c_void;
+use core::ffi::CStr;
+use core::ptr::addr_of_mut;
+use core::slice;
+use core::str;
 
 use cstr::cstr;
 
@@ -75,7 +75,7 @@
 unsafe extern "C" fn __errno() -> *mut c_int {
     // SAFETY: C functions which call this are only called from the main thread, not from exception
     // handlers.
-    unsafe { &mut ERRNO as *mut _ }
+    unsafe { addr_of_mut!(ERRNO) as *mut _ }
 }
 
 fn set_errno(value: c_int) {
diff --git a/vmclient/src/lib.rs b/vmclient/src/lib.rs
index a2a88d8..88072a7 100644
--- a/vmclient/src/lib.rs
+++ b/vmclient/src/lib.rs
@@ -48,7 +48,7 @@
 use std::{
     fmt::{self, Debug, Formatter},
     fs::File,
-    os::unix::io::{AsFd, AsRawFd, FromRawFd, IntoRawFd, OwnedFd},
+    os::unix::io::{AsFd, AsRawFd, IntoRawFd, OwnedFd},
     sync::Arc,
     time::Duration,
 };
@@ -62,10 +62,7 @@
 
     // Create new POSIX pipe. Make it O_CLOEXEC to align with how Rust creates
     // file descriptors (expected by SharedChild).
-    let (raw1, raw2) = pipe2(OFlag::O_CLOEXEC)?;
-
-    // SAFETY: Taking ownership of brand new FDs.
-    unsafe { Ok((OwnedFd::from_raw_fd(raw1), OwnedFd::from_raw_fd(raw2))) }
+    Ok(pipe2(OFlag::O_CLOEXEC)?)
 }
 
 fn posix_socketpair() -> Result<(OwnedFd, OwnedFd), io::Error> {
diff --git a/vmlauncher_app/Android.bp b/vmlauncher_app/Android.bp
index 06dcf7a..f9c325c 100644
--- a/vmlauncher_app/Android.bp
+++ b/vmlauncher_app/Android.bp
@@ -21,4 +21,7 @@
     ],
     platform_apis: true,
     privileged: true,
+    apex_available: [
+        "com.android.virt",
+    ],
 }
diff --git a/vmlauncher_app/AndroidManifest.xml b/vmlauncher_app/AndroidManifest.xml
index 860c03f..d800ec7 100644
--- a/vmlauncher_app/AndroidManifest.xml
+++ b/vmlauncher_app/AndroidManifest.xml
@@ -4,10 +4,13 @@
 
     <uses-permission android:name="android.permission.MANAGE_VIRTUAL_MACHINE" />
     <uses-permission android:name="android.permission.USE_CUSTOM_VIRTUAL_MACHINE" />
+    <uses-permission android:name="android.permission.INTERNET" />
     <uses-feature android:name="android.software.virtualization_framework" android:required="true" />
     <application
-        android:label="VmLauncherApp">
+        android:label="VmLauncherApp"
+        android:networkSecurityConfig="@xml/network_security_config">
         <activity android:name=".MainActivity"
+                  android:enabled="false"
                   android:screenOrientation="landscape"
                   android:configChanges="orientation|screenSize|keyboard|keyboardHidden|navigation|uiMode"
                   android:theme="@style/MyTheme"
diff --git a/vmlauncher_app/README.md b/vmlauncher_app/README.md
index 9175e57..0109f37 100644
--- a/vmlauncher_app/README.md
+++ b/vmlauncher_app/README.md
@@ -1,13 +1,16 @@
 # VM launcher app
 
-## Building & Installing
+## Building
 
-Add `VmLauncherApp` into `PRODUCT_PACKAGES` and then `m`
+This app is now part of the virt APEX.
 
-You can also explicitly grant or revoke the permission, e.g.
+## Enabling
+
+This app is disabled by default. To re-enable it, execute the following command.
+
 ```
-adb shell pm grant com.android.virtualization.vmlauncher android.permission.USE_CUSTOM_VIRTUAL_MACHINE
-adb shell pm grant com.android.virtualization.vmlauncher android.permission.MANAGE_VIRTUAL_MACHINE
+adb root
+adb shell pm enable com.android.virtualization.vmlauncher/.MainActivity
 ```
 
 ## Running
diff --git a/vmlauncher_app/java/com/android/virtualization/vmlauncher/MainActivity.java b/vmlauncher_app/java/com/android/virtualization/vmlauncher/MainActivity.java
index b5995b8..4c42bb4 100644
--- a/vmlauncher_app/java/com/android/virtualization/vmlauncher/MainActivity.java
+++ b/vmlauncher_app/java/com/android/virtualization/vmlauncher/MainActivity.java
@@ -37,6 +37,7 @@
 import android.view.Display;
 import android.view.SurfaceHolder;
 import android.view.SurfaceView;
+import android.view.KeyEvent;
 import android.view.WindowManager;
 import android.view.WindowInsets;
 import android.view.WindowInsetsController;
@@ -126,6 +127,9 @@
             }
 
             customImageConfigBuilder.setDisplayConfig(displayConfigBuilder.build());
+            customImageConfigBuilder.useTouch(true);
+            customImageConfigBuilder.useKeyboard(true);
+
             configBuilder.setCustomImageConfig(customImageConfigBuilder.build());
 
         } catch (JSONException | IOException e) {
@@ -135,6 +139,22 @@
     }
 
     @Override
+    public boolean onKeyDown(int keyCode, KeyEvent event) {
+        if (mVirtualMachine == null) {
+            return false;
+        }
+        return mVirtualMachine.sendKeyEvent(event);
+    }
+
+    @Override
+    public boolean onKeyUp(int keyCode, KeyEvent event) {
+        if (mVirtualMachine == null) {
+            return false;
+        }
+        return mVirtualMachine.sendKeyEvent(event);
+    }
+
+    @Override
     protected void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
         try {
@@ -224,6 +244,13 @@
         }
 
         SurfaceView surfaceView = findViewById(R.id.surface_view);
+        surfaceView.setOnTouchListener(
+                (v, event) -> {
+                    if (mVirtualMachine == null) {
+                        return false;
+                    }
+                    return mVirtualMachine.sendSingleTouchEvent(event);
+                });
         surfaceView
                 .getHolder()
                 .addCallback(
diff --git a/vmlauncher_app/res/values/themes.xml b/vmlauncher_app/res/values/themes.xml
index 395f089..c10b6d9 100644
--- a/vmlauncher_app/res/values/themes.xml
+++ b/vmlauncher_app/res/values/themes.xml
@@ -8,7 +8,7 @@
             @android:color/transparent
         </item>
         <item name="android:windowLayoutInDisplayCutoutMode">
-            shortEdges
+            always
         </item>
     </style>
 </resources>
diff --git a/vmlauncher_app/res/xml/network_security_config.xml b/vmlauncher_app/res/xml/network_security_config.xml
new file mode 100644
index 0000000..f27fa56
--- /dev/null
+++ b/vmlauncher_app/res/xml/network_security_config.xml
@@ -0,0 +1,22 @@
+<?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.
+  -->
+
+<network-security-config>
+    <domain-config cleartextTrafficPermitted="true">
+        <domain includeSubdomains="true">localhost</domain>
+    </domain-config>
+</network-security-config>