Merge "Optimize vsock benchmark fixture"
diff --git a/TEST_MAPPING b/TEST_MAPPING
index 321dbf4..c37fd19 100644
--- a/TEST_MAPPING
+++ b/TEST_MAPPING
@@ -46,9 +46,18 @@
       "path": "packages/modules/Virtualization/virtualizationservice"
     },
     {
+      "path": "packages/modules/Virtualization/libs/apexutil"
+    },
+    {
       "path": "packages/modules/Virtualization/libs/apkverify"
     },
     {
+      "path": "packages/modules/Virtualization/libs/avb"
+    },
+    {
+      "path": "packages/modules/Virtualization/libs/capabilities"
+    },
+    {
       "path": "packages/modules/Virtualization/libs/devicemapper"
     },
     {
@@ -58,12 +67,18 @@
       "path": "packages/modules/Virtualization/authfs"
     },
     {
+      "path": "packages/modules/Virtualization/microdroid_manager"
+    },
+    {
       "path": "packages/modules/Virtualization/pvmfw"
     },
     {
       "path": "packages/modules/Virtualization/rialto"
     },
     {
+      "path": "packages/modules/Virtualization/vm"
+    },
+    {
       "path": "packages/modules/Virtualization/vmbase"
     },
     {
diff --git a/apex/canned_fs_config b/apex/canned_fs_config
index ce942d3..5afd9d6 100644
--- a/apex/canned_fs_config
+++ b/apex/canned_fs_config
@@ -1 +1 @@
-/bin/virtualizationservice 0 2000 0755 capabilities=0x1000000  # CAP_SYS_RESOURCE
+/bin/virtualizationservice 0 2000 0755 capabilities=0x1000001  # CAP_CHOWN, CAP_SYS_RESOURCE
diff --git a/compos/composd/src/composd_main.rs b/compos/composd/src/composd_main.rs
index 5315828..b558f06 100644
--- a/compos/composd/src/composd_main.rs
+++ b/compos/composd/src/composd_main.rs
@@ -45,8 +45,11 @@
 
     ProcessState::start_thread_pool();
 
+    let virtmgr =
+        vmclient::VirtualizationService::new().context("Failed to spawn VirtualizationService")?;
     let virtualization_service =
-        vmclient::connect().context("Failed to find VirtualizationService")?;
+        virtmgr.connect().context("Failed to connect to VirtualizationService")?;
+
     let instance_manager = Arc::new(InstanceManager::new(virtualization_service));
     let composd_service = service::new_binder(instance_manager);
     register_lazy_service("android.system.composd", composd_service.as_binder())
diff --git a/compos/tests/AndroidTest.xml b/compos/tests/AndroidTest.xml
index f9e6837..4b414f1 100644
--- a/compos/tests/AndroidTest.xml
+++ b/compos/tests/AndroidTest.xml
@@ -16,6 +16,10 @@
 <configuration description="Tests for CompOS">
     <option name="config-descriptor:metadata" key="mainline-param" value="com.google.android.art.apex" />
 
+    <!-- Only run tests if the device under test is SDK version 33 (Android 13) or above. -->
+    <object type="module_controller"
+            class="com.android.tradefed.testtype.suite.module.Sdk33ModuleController" />
+
     <target_preparer class="com.android.tradefed.targetprep.RootTargetPreparer">
         <option name="force-root" value="true" />
     </target_preparer>
diff --git a/compos/verify/verify.rs b/compos/verify/verify.rs
index 5b7a8ad..745d5e9 100644
--- a/compos/verify/verify.rs
+++ b/compos/verify/verify.rs
@@ -106,7 +106,8 @@
     // We need to start the thread pool to be able to receive Binder callbacks
     ProcessState::start_thread_pool();
 
-    let virtualization_service = vmclient::connect()?;
+    let virtmgr = vmclient::VirtualizationService::new()?;
+    let virtualization_service = virtmgr.connect()?;
     let vm_instance = ComposClient::start(
         &*virtualization_service,
         instance_image,
diff --git a/javalib/api/system-current.txt b/javalib/api/system-current.txt
index 592a751..30e437b 100644
--- a/javalib/api/system-current.txt
+++ b/javalib/api/system-current.txt
@@ -3,19 +3,19 @@
 
   public class VirtualMachine implements java.lang.AutoCloseable {
     method public void clearCallback();
-    method public void close();
-    method @NonNull public android.os.IBinder connectToVsockServer(@IntRange(from=android.system.virtualmachine.VirtualMachine.MIN_VSOCK_PORT, to=android.system.virtualmachine.VirtualMachine.MAX_VSOCK_PORT) long) throws android.system.virtualmachine.VirtualMachineException;
-    method @NonNull public android.os.ParcelFileDescriptor connectVsock(@IntRange(from=android.system.virtualmachine.VirtualMachine.MIN_VSOCK_PORT, to=android.system.virtualmachine.VirtualMachine.MAX_VSOCK_PORT) long) throws android.system.virtualmachine.VirtualMachineException;
-    method @NonNull public android.system.virtualmachine.VirtualMachineConfig getConfig();
-    method @NonNull public java.io.InputStream getConsoleOutput() throws android.system.virtualmachine.VirtualMachineException;
-    method @NonNull public java.io.InputStream getLogOutput() throws android.system.virtualmachine.VirtualMachineException;
+    method @WorkerThread public void close();
+    method @NonNull @WorkerThread public android.os.IBinder connectToVsockServer(@IntRange(from=android.system.virtualmachine.VirtualMachine.MIN_VSOCK_PORT, to=android.system.virtualmachine.VirtualMachine.MAX_VSOCK_PORT) long) throws android.system.virtualmachine.VirtualMachineException;
+    method @NonNull @WorkerThread public android.os.ParcelFileDescriptor connectVsock(@IntRange(from=android.system.virtualmachine.VirtualMachine.MIN_VSOCK_PORT, to=android.system.virtualmachine.VirtualMachine.MAX_VSOCK_PORT) long) throws android.system.virtualmachine.VirtualMachineException;
+    method @NonNull @WorkerThread public android.system.virtualmachine.VirtualMachineConfig getConfig();
+    method @NonNull @WorkerThread public java.io.InputStream getConsoleOutput() throws android.system.virtualmachine.VirtualMachineException;
+    method @NonNull @WorkerThread public java.io.InputStream getLogOutput() throws android.system.virtualmachine.VirtualMachineException;
     method @NonNull public String getName();
-    method public int getStatus();
-    method @RequiresPermission(android.system.virtualmachine.VirtualMachine.MANAGE_VIRTUAL_MACHINE_PERMISSION) public void run() throws android.system.virtualmachine.VirtualMachineException;
+    method @WorkerThread public int getStatus();
+    method @RequiresPermission(android.system.virtualmachine.VirtualMachine.MANAGE_VIRTUAL_MACHINE_PERMISSION) @WorkerThread public void run() throws android.system.virtualmachine.VirtualMachineException;
     method public void setCallback(@NonNull java.util.concurrent.Executor, @NonNull android.system.virtualmachine.VirtualMachineCallback);
-    method @NonNull public android.system.virtualmachine.VirtualMachineConfig setConfig(@NonNull android.system.virtualmachine.VirtualMachineConfig) throws android.system.virtualmachine.VirtualMachineException;
-    method public void stop() throws android.system.virtualmachine.VirtualMachineException;
-    method @NonNull public android.system.virtualmachine.VirtualMachineDescriptor toDescriptor() throws android.system.virtualmachine.VirtualMachineException;
+    method @NonNull @WorkerThread public android.system.virtualmachine.VirtualMachineConfig setConfig(@NonNull android.system.virtualmachine.VirtualMachineConfig) throws android.system.virtualmachine.VirtualMachineException;
+    method @WorkerThread public void stop() throws android.system.virtualmachine.VirtualMachineException;
+    method @NonNull @WorkerThread public android.system.virtualmachine.VirtualMachineDescriptor toDescriptor() throws android.system.virtualmachine.VirtualMachineException;
     field public static final String MANAGE_VIRTUAL_MACHINE_PERMISSION = "android.permission.MANAGE_VIRTUAL_MACHINE";
     field public static final long MAX_VSOCK_PORT = 4294967295L; // 0xffffffffL
     field public static final long MIN_VSOCK_PORT = 1024L; // 0x400L
@@ -91,12 +91,12 @@
   }
 
   public class VirtualMachineManager {
-    method @NonNull @RequiresPermission(android.system.virtualmachine.VirtualMachine.MANAGE_VIRTUAL_MACHINE_PERMISSION) public android.system.virtualmachine.VirtualMachine create(@NonNull String, @NonNull android.system.virtualmachine.VirtualMachineConfig) throws android.system.virtualmachine.VirtualMachineException;
-    method public void delete(@NonNull String) throws android.system.virtualmachine.VirtualMachineException;
-    method @Nullable public android.system.virtualmachine.VirtualMachine get(@NonNull String) throws android.system.virtualmachine.VirtualMachineException;
+    method @NonNull @RequiresPermission(android.system.virtualmachine.VirtualMachine.MANAGE_VIRTUAL_MACHINE_PERMISSION) @WorkerThread public android.system.virtualmachine.VirtualMachine create(@NonNull String, @NonNull android.system.virtualmachine.VirtualMachineConfig) throws android.system.virtualmachine.VirtualMachineException;
+    method @WorkerThread public void delete(@NonNull String) throws android.system.virtualmachine.VirtualMachineException;
+    method @Nullable @WorkerThread public android.system.virtualmachine.VirtualMachine get(@NonNull String) throws android.system.virtualmachine.VirtualMachineException;
     method public int getCapabilities();
-    method @NonNull public android.system.virtualmachine.VirtualMachine getOrCreate(@NonNull String, @NonNull android.system.virtualmachine.VirtualMachineConfig) throws android.system.virtualmachine.VirtualMachineException;
-    method @NonNull public android.system.virtualmachine.VirtualMachine importFromDescriptor(@NonNull String, @NonNull android.system.virtualmachine.VirtualMachineDescriptor) throws android.system.virtualmachine.VirtualMachineException;
+    method @NonNull @WorkerThread public android.system.virtualmachine.VirtualMachine getOrCreate(@NonNull String, @NonNull android.system.virtualmachine.VirtualMachineConfig) throws android.system.virtualmachine.VirtualMachineException;
+    method @NonNull @WorkerThread public android.system.virtualmachine.VirtualMachine importFromDescriptor(@NonNull String, @NonNull android.system.virtualmachine.VirtualMachineDescriptor) throws android.system.virtualmachine.VirtualMachineException;
     field public static final int CAPABILITY_NON_PROTECTED_VM = 2; // 0x2
     field public static final int CAPABILITY_PROTECTED_VM = 1; // 0x1
   }
diff --git a/javalib/jni/android_system_virtualmachine_VirtualMachine.cpp b/javalib/jni/android_system_virtualmachine_VirtualMachine.cpp
index 3230af4..afdc944 100644
--- a/javalib/jni/android_system_virtualmachine_VirtualMachine.cpp
+++ b/javalib/jni/android_system_virtualmachine_VirtualMachine.cpp
@@ -27,7 +27,8 @@
 
 #include "common.h"
 
-JNIEXPORT jobject JNICALL android_system_virtualmachine_VirtualMachine_connectToVsockServer(
+extern "C" JNIEXPORT jobject JNICALL
+Java_android_system_virtualmachine_VirtualMachine_nativeConnectToVsockServer(
         JNIEnv* env, [[maybe_unused]] jclass clazz, jobject vmBinder, jint port) {
     using aidl::android::system::virtualizationservice::IVirtualMachine;
     using ndk::ScopedFileDescriptor;
@@ -59,32 +60,3 @@
     auto client = ARpcSession_setupPreconnectedClient(session.get(), requestFunc, &args);
     return AIBinder_toJavaBinder(env, client);
 }
-
-JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void* /*reserved*/) {
-    JNIEnv* env;
-    if (vm->GetEnv(reinterpret_cast<void**>(&env), JNI_VERSION_1_6) != JNI_OK) {
-        ALOGE("%s: Failed to get the environment", __FUNCTION__);
-        return JNI_ERR;
-    }
-
-    jclass c = env->FindClass("android/system/virtualmachine/VirtualMachine");
-    if (c == nullptr) {
-        ALOGE("%s: Failed to find class android.system.virtualmachine.VirtualMachine",
-              __FUNCTION__);
-        return JNI_ERR;
-    }
-
-    // Register your class' native methods.
-    static const JNINativeMethod methods[] = {
-            {"nativeConnectToVsockServer", "(Landroid/os/IBinder;I)Landroid/os/IBinder;",
-             reinterpret_cast<void*>(
-                     android_system_virtualmachine_VirtualMachine_connectToVsockServer)},
-    };
-    int rc = env->RegisterNatives(c, methods, sizeof(methods) / sizeof(JNINativeMethod));
-    if (rc != JNI_OK) {
-        ALOGE("%s: Failed to register natives", __FUNCTION__);
-        return rc;
-    }
-
-    return JNI_VERSION_1_6;
-}
diff --git a/javalib/jni/android_system_virtualmachine_VirtualizationService.cpp b/javalib/jni/android_system_virtualmachine_VirtualizationService.cpp
index b9d2ca4..bd80880 100644
--- a/javalib/jni/android_system_virtualmachine_VirtualizationService.cpp
+++ b/javalib/jni/android_system_virtualmachine_VirtualizationService.cpp
@@ -20,6 +20,7 @@
 #include <android/binder_ibinder_jni.h>
 #include <jni.h>
 #include <log/log.h>
+#include <poll.h>
 
 #include <string>
 
@@ -30,7 +31,8 @@
 static constexpr const char VIRTMGR_PATH[] = "/apex/com.android.virt/bin/virtmgr";
 static constexpr size_t VIRTMGR_THREADS = 16;
 
-JNIEXPORT jint JNICALL android_system_virtualmachine_VirtualizationService_spawn(
+extern "C" JNIEXPORT jint JNICALL
+Java_android_system_virtualmachine_VirtualizationService_nativeSpawn(
         JNIEnv* env, [[maybe_unused]] jclass clazz) {
     unique_fd serverFd, clientFd;
     if (!Socketpair(SOCK_STREAM, &serverFd, &clientFd)) {
@@ -73,8 +75,10 @@
     return clientFd.release();
 }
 
-JNIEXPORT jobject JNICALL android_system_virtualmachine_VirtualizationService_connect(
-        JNIEnv* env, [[maybe_unused]] jobject obj, int clientFd) {
+extern "C" JNIEXPORT jobject JNICALL
+Java_android_system_virtualmachine_VirtualizationService_nativeConnect(JNIEnv* env,
+                                                                       [[maybe_unused]] jobject obj,
+                                                                       int clientFd) {
     RpcSessionHandle session;
     ARpcSession_setFileDescriptorTransportMode(session.get(),
                                                ARpcSession_FileDescriptorTransportMode::Unix);
@@ -85,32 +89,16 @@
     return AIBinder_toJavaBinder(env, client);
 }
 
-JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void* /*reserved*/) {
-    JNIEnv* env;
-    if (vm->GetEnv(reinterpret_cast<void**>(&env), JNI_VERSION_1_6) != JNI_OK) {
-        ALOGE("%s: Failed to get the environment", __FUNCTION__);
-        return JNI_ERR;
+extern "C" JNIEXPORT jboolean JNICALL
+Java_android_system_virtualmachine_VirtualizationService_nativeIsOk(JNIEnv* env,
+                                                                    [[maybe_unused]] jobject obj,
+                                                                    int clientFd) {
+    /* Setting events=0 only returns POLLERR, POLLHUP or POLLNVAL. */
+    struct pollfd pfds[] = {{.fd = clientFd, .events = 0}};
+    if (poll(pfds, /*nfds*/ 1, /*timeout*/ 0) < 0) {
+        env->ThrowNew(env->FindClass("android/system/virtualmachine/VirtualMachineException"),
+                      ("Failed to poll client FD: " + std::string(strerror(errno))).c_str());
+        return false;
     }
-
-    jclass c = env->FindClass("android/system/virtualmachine/VirtualizationService");
-    if (c == nullptr) {
-        ALOGE("%s: Failed to find class android.system.virtualmachine.VirtualizationService",
-              __FUNCTION__);
-        return JNI_ERR;
-    }
-
-    // Register your class' native methods.
-    static const JNINativeMethod methods[] = {
-            {"nativeSpawn", "()I",
-             reinterpret_cast<void*>(android_system_virtualmachine_VirtualizationService_spawn)},
-            {"nativeConnect", "(I)Landroid/os/IBinder;",
-             reinterpret_cast<void*>(android_system_virtualmachine_VirtualizationService_connect)},
-    };
-    int rc = env->RegisterNatives(c, methods, sizeof(methods) / sizeof(JNINativeMethod));
-    if (rc != JNI_OK) {
-        ALOGE("%s: Failed to register natives", __FUNCTION__);
-        return rc;
-    }
-
-    return JNI_VERSION_1_6;
+    return pfds[0].revents == 0;
 }
diff --git a/javalib/src/android/system/virtualmachine/VirtualMachine.java b/javalib/src/android/system/virtualmachine/VirtualMachine.java
index b040cc0..34851e4 100644
--- a/javalib/src/android/system/virtualmachine/VirtualMachine.java
+++ b/javalib/src/android/system/virtualmachine/VirtualMachine.java
@@ -52,6 +52,7 @@
 import android.annotation.SuppressLint;
 import android.annotation.SystemApi;
 import android.annotation.TestApi;
+import android.annotation.WorkerThread;
 import android.content.ComponentCallbacks2;
 import android.content.Context;
 import android.content.res.Configuration;
@@ -59,7 +60,6 @@
 import android.os.IBinder;
 import android.os.ParcelFileDescriptor;
 import android.os.RemoteException;
-import android.os.ServiceManager;
 import android.os.ServiceSpecificException;
 import android.system.virtualizationcommon.DeathReason;
 import android.system.virtualizationcommon.ErrorCode;
@@ -108,6 +108,15 @@
  * received using {@link #setCallback}. The app can communicate with the VM using {@link
  * #connectToVsockServer} or {@link #connectVsock}.
  *
+ * <p>The payload code running inside the VM has access to a set of native APIs; see the <a
+ * href="https://cs.android.com/android/platform/superproject/+/master:packages/modules/Virtualization/vm_payload/README.md">README
+ * file</a> for details.
+ *
+ * <p>Each VM has a unique secret, computed from the APK that contains the code running in it, the
+ * VM configuration, and a random per-instance salt. The secret can be accessed by the payload code
+ * running inside the VM (using {@code AVmPayload_getVmInstanceSecret}) but is not made available
+ * outside it.
+ *
  * @hide
  */
 @SystemApi
@@ -153,7 +162,7 @@
     })
     public @interface Status {}
 
-     /** The virtual machine has just been created, or {@link #stop()} was called on it. */
+    /** The virtual machine has just been created, or {@link #stop} was called on it. */
     public static final int STATUS_STOPPED = 0;
 
     /** The virtual machine is running. */
@@ -269,6 +278,9 @@
         }
     }
 
+    /** Running instance of virtmgr that hosts VirtualizationService for this VM. */
+    @NonNull private final VirtualizationService mVirtualizationService;
+
     @NonNull private final MemoryManagementCallbacks mMemoryManagementCallbacks;
 
     @NonNull private final Context mContext;
@@ -339,11 +351,15 @@
     }
 
     private VirtualMachine(
-            @NonNull Context context, @NonNull String name, @NonNull VirtualMachineConfig config)
+            @NonNull Context context,
+            @NonNull String name,
+            @NonNull VirtualMachineConfig config,
+            @NonNull VirtualizationService service)
             throws VirtualMachineException {
         mPackageName = context.getPackageName();
         mName = requireNonNull(name, "Name must not be null");
         mConfig = requireNonNull(config, "Config must not be null");
+        mVirtualizationService = service;
 
         File thisVmDir = getVmDir(context, mName);
         mVmRootPath = thisVmDir;
@@ -357,6 +373,7 @@
                 (config.isEncryptedStorageEnabled())
                         ? new File(thisVmDir, ENCRYPTED_STORE_FILE)
                         : null;
+
     }
 
     /**
@@ -379,7 +396,8 @@
         VirtualMachineConfig config = VirtualMachineConfig.from(vmDescriptor.getConfigFd());
         File vmDir = createVmDir(context, name);
         try {
-            VirtualMachine vm = new VirtualMachine(context, name, config);
+            VirtualMachine vm =
+                    new VirtualMachine(context, name, config, VirtualizationService.getInstance());
             config.serialize(vm.mConfigFilePath);
             try {
                 vm.mInstanceFilePath.createNewFile();
@@ -422,7 +440,8 @@
         File vmDir = createVmDir(context, name);
 
         try {
-            VirtualMachine vm = new VirtualMachine(context, name, config);
+            VirtualMachine vm =
+                    new VirtualMachine(context, name, config, VirtualizationService.getInstance());
             config.serialize(vm.mConfigFilePath);
             try {
                 vm.mInstanceFilePath.createNewFile();
@@ -438,9 +457,7 @@
                 }
             }
 
-            IVirtualizationService service =
-                    IVirtualizationService.Stub.asInterface(
-                            ServiceManager.waitForService(SERVICE_NAME));
+            IVirtualizationService service = vm.mVirtualizationService.connect();
 
             try {
                 service.initializeWritablePartition(
@@ -494,7 +511,8 @@
         }
         File configFilePath = new File(thisVmDir, CONFIG_FILE);
         VirtualMachineConfig config = VirtualMachineConfig.from(configFilePath);
-        VirtualMachine vm = new VirtualMachine(context, name, config);
+        VirtualMachine vm =
+                new VirtualMachine(context, name, config, VirtualizationService.getInstance());
 
         if (!vm.mInstanceFilePath.exists()) {
             throw new VirtualMachineException("instance image missing");
@@ -512,8 +530,8 @@
             // Once we explicitly delete a VM it must remain permanently in the deleted state;
             // if a new VM is created with the same name (and files) that's unrelated.
             mWasDeleted = true;
-            deleteVmDirectory(context, name);
         }
+        deleteVmDirectory(context, name);
     }
 
     static void deleteVmDirectory(Context context, String name) throws VirtualMachineException {
@@ -569,13 +587,15 @@
     /**
      * Returns the currently selected config of this virtual machine. There can be multiple virtual
      * machines sharing the same config. Even in that case, the virtual machines are completely
-     * isolated from each other; one cannot share its secret to another virtual machine even if they
-     * share the same config. It is also possible that a virtual machine can switch its config,
-     * which can be done by calling {@link #setConfig(VirtualMachineConfig)}.
+     * isolated from each other; they have different secrets. It is also possible that a virtual
+     * machine can change its config, which can be done by calling {@link #setConfig}.
+     *
+     * <p>NOTE: This method may block and should not be called on the main thread.
      *
      * @hide
      */
     @SystemApi
+    @WorkerThread
     @NonNull
     public VirtualMachineConfig getConfig() {
         synchronized (mLock) {
@@ -586,9 +606,12 @@
     /**
      * Returns the current status of this virtual machine.
      *
+     * <p>NOTE: This method may block and should not be called on the main thread.
+     *
      * @hide
      */
     @SystemApi
+    @WorkerThread
     @Status
     public int getStatus() {
         IVirtualMachine virtualMachine;
@@ -724,14 +747,16 @@
     /**
      * Runs this virtual machine. The returning of this method however doesn't mean that the VM has
      * actually started running or the OS has booted there. Such events can be notified by
-     * registering a callback using {@link #setCallback(Executor, VirtualMachineCallback)} before
-     * calling {@code run()}.
+     * registering a callback using {@link #setCallback} before calling {@code run()}.
+     *
+     * <p>NOTE: This method may block and should not be called on the main thread.
      *
      * @throws VirtualMachineException if the virtual machine is not stopped or could not be
      *     started.
      * @hide
      */
     @SystemApi
+    @WorkerThread
     @RequiresPermission(MANAGE_VIRTUAL_MACHINE_PERMISSION)
     public void run() throws VirtualMachineException {
         synchronized (mLock) {
@@ -747,9 +772,7 @@
                 throw new VirtualMachineException("failed to create idsig file", e);
             }
 
-            IVirtualizationService service =
-                    IVirtualizationService.Stub.asInterface(
-                            ServiceManager.waitForService(SERVICE_NAME));
+            IVirtualizationService service = mVirtualizationService.connect();
 
             try {
                 createVmPipes();
@@ -872,10 +895,13 @@
     /**
      * Returns the stream object representing the console output from the virtual machine.
      *
+     * <p>NOTE: This method may block and should not be called on the main thread.
+     *
      * @throws VirtualMachineException if the stream could not be created.
      * @hide
      */
     @SystemApi
+    @WorkerThread
     @NonNull
     public InputStream getConsoleOutput() throws VirtualMachineException {
         synchronized (mLock) {
@@ -887,10 +913,13 @@
     /**
      * Returns the stream object representing the log output from the virtual machine.
      *
+     * <p>NOTE: This method may block and should not be called on the main thread.
+     *
      * @throws VirtualMachineException if the stream could not be created.
      * @hide
      */
     @SystemApi
+    @WorkerThread
     @NonNull
     public InputStream getLogOutput() throws VirtualMachineException {
         synchronized (mLock) {
@@ -902,13 +931,24 @@
     /**
      * Stops this virtual machine. Stopping a virtual machine is like pulling the plug on a real
      * computer; the machine halts immediately. Software running on the virtual machine is not
-     * notified of the event. A stopped virtual machine can be re-started by calling {@link #run()}.
+     * notified of the event. Writes to {@linkplain
+     * VirtualMachineConfig.Builder#setEncryptedStorageKib encrypted storage} might not be
+     * persisted, and the instance might be left in an inconsistent state.
+     *
+     * <p>For a graceful shutdown, you could request the payload to call {@code exit()}, e.g. via a
+     * {@linkplain #connectToVsockServer binder request}, and wait for {@link
+     * VirtualMachineCallback#onPayloadFinished} to be called.
+     *
+     * <p>A stopped virtual machine can be re-started by calling {@link #run()}.
+     *
+     * <p>NOTE: This method may block and should not be called on the main thread.
      *
      * @throws VirtualMachineException if the virtual machine is not running or could not be
      *     stopped.
      * @hide
      */
     @SystemApi
+    @WorkerThread
     public void stop() throws VirtualMachineException {
         synchronized (mLock) {
             if (mVirtualMachine == null) {
@@ -928,10 +968,13 @@
     /**
      * Stops this virtual machine, if it is running.
      *
+     * <p>NOTE: This method may block and should not be called on the main thread.
+     *
      * @see #stop()
      * @hide
      */
     @SystemApi
+    @WorkerThread
     @Override
     public void close() {
         synchronized (mLock) {
@@ -979,8 +1022,10 @@
      * like the number of CPU and size of the RAM, depending on the situation (e.g. the size of the
      * application to run on the virtual machine, etc.)
      *
-     * <p>The new config must be {@link VirtualMachineConfig#isCompatibleWith compatible with} the
-     * existing config.
+     * <p>The new config must be {@linkplain VirtualMachineConfig#isCompatibleWith compatible with}
+     * the existing config.
+     *
+     * <p>NOTE: This method may block and should not be called on the main thread.
      *
      * @return the old config
      * @throws VirtualMachineException if the virtual machine is not stopped, or the new config is
@@ -988,6 +1033,7 @@
      * @hide
      */
     @SystemApi
+    @WorkerThread
     @NonNull
     public VirtualMachineConfig setConfig(@NonNull VirtualMachineConfig newConfig)
             throws VirtualMachineException {
@@ -1013,14 +1059,17 @@
     /**
      * Connect to a VM's binder service via vsock and return the root IBinder object. Guest VMs are
      * expected to set up vsock servers in their payload. After the host app receives the {@link
-     * VirtualMachineCallback#onPayloadReady(VirtualMachine)}, it can use this method to establish a
-     * connection to the guest VM.
+     * VirtualMachineCallback#onPayloadReady}, it can use this method to establish a connection to
+     * the guest VM.
+     *
+     * <p>NOTE: This method may block and should not be called on the main thread.
      *
      * @throws VirtualMachineException if the virtual machine is not running or the connection
      *     failed.
      * @hide
      */
     @SystemApi
+    @WorkerThread
     @NonNull
     public IBinder connectToVsockServer(
             @IntRange(from = MIN_VSOCK_PORT, to = MAX_VSOCK_PORT) long port)
@@ -1039,10 +1088,15 @@
     /**
      * Opens a vsock connection to the VM on the given port.
      *
+     * <p>The caller is responsible for closing the returned {@code ParcelFileDescriptor}.
+     *
+     * <p>NOTE: This method may block and should not be called on the main thread.
+     *
      * @throws VirtualMachineException if connecting fails.
      * @hide
      */
     @SystemApi
+    @WorkerThread
     @NonNull
     public ParcelFileDescriptor connectVsock(
             @IntRange(from = MIN_VSOCK_PORT, to = MAX_VSOCK_PORT) long port)
@@ -1088,12 +1142,15 @@
      * VirtualMachineManager#importFromDescriptor} is called. It is recommended that the VM not be
      * started until that operation is complete.
      *
+     * <p>NOTE: This method may block and should not be called on the main thread.
+     *
      * @return a {@link VirtualMachineDescriptor} instance that represents the VM's state.
      * @throws VirtualMachineException if the virtual machine is not stopped, or the state could not
      *     be captured.
      * @hide
      */
     @SystemApi
+    @WorkerThread
     @NonNull
     public VirtualMachineDescriptor toDescriptor() throws VirtualMachineException {
         synchronized (mLock) {
diff --git a/javalib/src/android/system/virtualmachine/VirtualMachineCallback.java b/javalib/src/android/system/virtualmachine/VirtualMachineCallback.java
index 9aaecf0..d72ba14 100644
--- a/javalib/src/android/system/virtualmachine/VirtualMachineCallback.java
+++ b/javalib/src/android/system/virtualmachine/VirtualMachineCallback.java
@@ -141,8 +141,8 @@
     void onPayloadStarted(@NonNull VirtualMachine vm);
 
     /**
-     * Called when the payload in the VM is ready to serve. See
-     * {@link VirtualMachine#connectToVsockServer(int)}.
+     * Called when the payload in the VM is ready to serve. See {@link
+     * VirtualMachine#connectToVsockServer}.
      */
     void onPayloadReady(@NonNull VirtualMachine vm);
 
diff --git a/javalib/src/android/system/virtualmachine/VirtualMachineConfig.java b/javalib/src/android/system/virtualmachine/VirtualMachineConfig.java
index 75e5414..7c16798 100644
--- a/javalib/src/android/system/virtualmachine/VirtualMachineConfig.java
+++ b/javalib/src/android/system/virtualmachine/VirtualMachineConfig.java
@@ -349,9 +349,10 @@
 
     /**
      * Tests if this config is compatible with other config. Being compatible means that the configs
-     * can be interchangeably used for the same virtual machine. Compatible changes includes the
-     * number of CPUs and the size of the RAM. All other changes (e.g. using a different payload,
-     * change of the debug mode, etc.) are considered as incompatible.
+     * can be interchangeably used for the same virtual machine; they do not change the VM identity
+     * or secrets. Such changes include varying the number of CPUs or the size of the RAM. Changes
+     * that would alter the identity of the VM (e.g. using a different payload or changing the debug
+     * mode) are considered incompatible.
      *
      * @hide
      */
@@ -586,7 +587,7 @@
 
         /**
          * Sets the number of vCPUs in the VM. Defaults to 1. Cannot be more than the number of real
-         * CPUs (as returned by {@link Runtime#availableProcessors()}).
+         * CPUs (as returned by {@link Runtime#availableProcessors}).
          *
          * @hide
          */
diff --git a/javalib/src/android/system/virtualmachine/VirtualMachineDescriptor.java b/javalib/src/android/system/virtualmachine/VirtualMachineDescriptor.java
index c9718aa..483779a 100644
--- a/javalib/src/android/system/virtualmachine/VirtualMachineDescriptor.java
+++ b/javalib/src/android/system/virtualmachine/VirtualMachineDescriptor.java
@@ -29,7 +29,7 @@
  * A VM descriptor that captures the state of a Virtual Machine.
  *
  * <p>You can capture the current state of VM by creating an instance of this class with {@link
- * VirtualMachine#toDescriptor()}, optionally pass it to another App, and then build an identical VM
+ * VirtualMachine#toDescriptor}, optionally pass it to another App, and then build an identical VM
  * with the descriptor received.
  *
  * @hide
diff --git a/javalib/src/android/system/virtualmachine/VirtualMachineManager.java b/javalib/src/android/system/virtualmachine/VirtualMachineManager.java
index 5b30617..7773cb5 100644
--- a/javalib/src/android/system/virtualmachine/VirtualMachineManager.java
+++ b/javalib/src/android/system/virtualmachine/VirtualMachineManager.java
@@ -24,6 +24,7 @@
 import android.annotation.RequiresFeature;
 import android.annotation.RequiresPermission;
 import android.annotation.SystemApi;
+import android.annotation.WorkerThread;
 import android.content.Context;
 import android.content.pm.PackageManager;
 import android.sysprop.HypervisorProperties;
@@ -37,9 +38,9 @@
 import java.util.Map;
 
 /**
- * Manages {@link VirtualMachine virtual machine} instances created by an app. Each instance is
- * created from a {@link VirtualMachineConfig configuration} that defines the shape of the VM (RAM,
- * CPUs), the code to execute within it, etc.
+ * Manages {@linkplain VirtualMachine virtual machine} instances created by an app. Each instance is
+ * created from a {@linkplain VirtualMachineConfig configuration} that defines the shape of the VM
+ * (RAM, CPUs), the code to execute within it, etc.
  *
  * <p>Each virtual machine instance is named; the configuration and related state of each is
  * persisted in the app's private data directory and an instance can be retrieved given the name.
@@ -123,12 +124,15 @@
      * the name and the config are the same as a deleted one. The new virtual machine will initially
      * be stopped.
      *
+     * <p>NOTE: This method may block and should not be called on the main thread.
+     *
      * @throws VirtualMachineException if the VM cannot be created, or there is an existing VM with
      *     the given name.
      * @hide
      */
     @SystemApi
     @NonNull
+    @WorkerThread
     @RequiresPermission(VirtualMachine.MANAGE_VIRTUAL_MACHINE_PERMISSION)
     public VirtualMachine create(@NonNull String name, @NonNull VirtualMachineConfig config)
             throws VirtualMachineException {
@@ -154,12 +158,15 @@
      * machine instance. Multiple calls to get() passing the same name will get the same object
      * returned, until the virtual machine is deleted (via {@link #delete}) and then recreated.
      *
+     * <p>NOTE: This method may block and should not be called on the main thread.
+     *
      * @see #getOrCreate
      * @throws VirtualMachineException if the virtual machine exists but could not be successfully
      *     retrieved.
      * @hide
      */
     @SystemApi
+    @WorkerThread
     @Nullable
     public VirtualMachine get(@NonNull String name) throws VirtualMachineException {
         synchronized (sCreateLock) {
@@ -186,11 +193,14 @@
      *
      * <p>The new virtual machine will be in the same state as the descriptor indicates.
      *
+     * <p>NOTE: This method may block and should not be called on the main thread.
+     *
      * @throws VirtualMachineException if the VM cannot be imported.
      * @hide
      */
     @NonNull
     @SystemApi
+    @WorkerThread
     public VirtualMachine importFromDescriptor(
             @NonNull String name, @NonNull VirtualMachineDescriptor vmDescriptor)
             throws VirtualMachineException {
@@ -205,10 +215,13 @@
      * Returns an existing {@link VirtualMachine} if it exists, or create a new one. The config
      * parameter is used only when a new virtual machine is created.
      *
+     * <p>NOTE: This method may block and should not be called on the main thread.
+     *
      * @throws VirtualMachineException if the virtual machine could not be created or retrieved.
      * @hide
      */
     @SystemApi
+    @WorkerThread
     @NonNull
     public VirtualMachine getOrCreate(@NonNull String name, @NonNull VirtualMachineConfig config)
             throws VirtualMachineException {
@@ -229,11 +242,14 @@
      * with the same name is different from an already deleted virtual machine even if it has the
      * same config.
      *
+     * <p>NOTE: This method may block and should not be called on the main thread.
+     *
      * @throws VirtualMachineException if the virtual machine does not exist, is not stopped, or
      *     cannot be deleted.
      * @hide
      */
     @SystemApi
+    @WorkerThread
     public void delete(@NonNull String name) throws VirtualMachineException {
         synchronized (sCreateLock) {
             VirtualMachine vm = getVmByName(name);
diff --git a/javalib/src/android/system/virtualmachine/VirtualizationService.java b/javalib/src/android/system/virtualmachine/VirtualizationService.java
index 78d0c9c..75e359f 100644
--- a/javalib/src/android/system/virtualmachine/VirtualizationService.java
+++ b/javalib/src/android/system/virtualmachine/VirtualizationService.java
@@ -16,16 +16,25 @@
 
 package android.system.virtualmachine;
 
+import android.annotation.NonNull;
 import android.os.IBinder;
 import android.os.ParcelFileDescriptor;
 import android.system.virtualizationservice.IVirtualizationService;
 
+import com.android.internal.annotations.GuardedBy;
+
+import java.lang.ref.WeakReference;
+
 /** A running instance of virtmgr that is hosting a VirtualizationService AIDL service. */
 class VirtualizationService {
     static {
         System.loadLibrary("virtualizationservice_jni");
     }
 
+    /* Soft reference caching the last created instance of this class. */
+    @GuardedBy("VirtualMachineManager.sCreateLock")
+    private static WeakReference<VirtualizationService> sInstance;
+
     /*
      * Client FD for UDS connection to virtmgr's RpcBinder server. Closing it
      * will make virtmgr shut down.
@@ -36,11 +45,13 @@
 
     private native IBinder nativeConnect(int clientFd);
 
+    private native boolean nativeIsOk(int clientFd);
+
     /*
      * Spawns a new virtmgr subprocess that will host a VirtualizationService
      * AIDL service.
      */
-    public VirtualizationService() throws VirtualMachineException {
+    private VirtualizationService() throws VirtualMachineException {
         int clientFd = nativeSpawn();
         if (clientFd < 0) {
             throw new VirtualMachineException("Could not spawn VirtualizationService");
@@ -56,4 +67,27 @@
         }
         return IVirtualizationService.Stub.asInterface(binder);
     }
+
+    /*
+     * Checks the state of the client FD. Returns false if the FD is in erroneous state
+     * or if the other endpoint had closed its FD.
+     */
+    private boolean isOk() {
+        return nativeIsOk(mClientFd.getFd());
+    }
+
+    /*
+     * Returns an instance of this class. Might spawn a new instance if one doesn't exist, or
+     * if the previous instance had crashed.
+     */
+    @GuardedBy("VirtualMachineManager.sCreateLock")
+    @NonNull
+    static VirtualizationService getInstance() throws VirtualMachineException {
+        VirtualizationService service = (sInstance == null) ? null : sInstance.get();
+        if (service == null || !service.isOk()) {
+            service = new VirtualizationService();
+            sInstance = new WeakReference(service);
+        }
+        return service;
+    }
 }
diff --git a/libs/apexutil/TEST_MAPPING b/libs/apexutil/TEST_MAPPING
new file mode 100644
index 0000000..f66a400
--- /dev/null
+++ b/libs/apexutil/TEST_MAPPING
@@ -0,0 +1,7 @@
+{
+  "avf-presubmit" : [
+    {
+      "name" : "libapexutil_rust.test"
+    }
+  ]
+}
diff --git a/libs/avb/TEST_MAPPING b/libs/avb/TEST_MAPPING
new file mode 100644
index 0000000..2f4bccc
--- /dev/null
+++ b/libs/avb/TEST_MAPPING
@@ -0,0 +1,7 @@
+{
+  "avf-presubmit" : [
+    {
+      "name" : "libavb_bindgen_test"
+    }
+  ]
+}
diff --git a/libs/capabilities/TEST_MAPPING b/libs/capabilities/TEST_MAPPING
new file mode 100644
index 0000000..568a771
--- /dev/null
+++ b/libs/capabilities/TEST_MAPPING
@@ -0,0 +1,10 @@
+{
+  "avf-presubmit" : [
+    {
+      "name" : "libcap_rust.test"
+    },
+    {
+      "name" : "libcap_bindgen_test"
+    }
+  ]
+}
diff --git a/libs/fdtpci/Android.bp b/libs/fdtpci/Android.bp
new file mode 100644
index 0000000..f368b08
--- /dev/null
+++ b/libs/fdtpci/Android.bp
@@ -0,0 +1,18 @@
+package {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+rust_library_rlib {
+    name: "libfdtpci",
+    edition: "2021",
+    no_stdlibs: true,
+    host_supported: false,
+    crate_name: "fdtpci",
+    srcs: ["src/lib.rs"],
+    rustlibs: [
+        "liblibfdt",
+        "liblog_rust_nostd",
+        "libvirtio_drivers",
+    ],
+    apex_available: ["com.android.virt"],
+}
diff --git a/libs/fdtpci/TEST_MAPPING b/libs/fdtpci/TEST_MAPPING
new file mode 100644
index 0000000..c315b4a
--- /dev/null
+++ b/libs/fdtpci/TEST_MAPPING
@@ -0,0 +1,7 @@
+{
+  "avf-presubmit": [
+    {
+      "name": "vmbase_example.integration_test"
+    }
+  ]
+}
diff --git a/libs/fdtpci/src/lib.rs b/libs/fdtpci/src/lib.rs
new file mode 100644
index 0000000..1ddda9f
--- /dev/null
+++ b/libs/fdtpci/src/lib.rs
@@ -0,0 +1,231 @@
+// Copyright 2022, The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+//! Library for working with (VirtIO) PCI devices discovered from a device tree.
+
+#![no_std]
+
+use core::{
+    ffi::CStr,
+    fmt::{self, Display, Formatter},
+    ops::Range,
+};
+use libfdt::{AddressRange, Fdt, FdtError, FdtNode};
+use log::debug;
+use virtio_drivers::transport::pci::bus::{Cam, PciRoot};
+
+/// PCI MMIO configuration region size.
+const PCI_CFG_SIZE: usize = 0x100_0000;
+
+/// An error parsing a PCI node from an FDT.
+#[derive(Clone, Debug, Eq, PartialEq)]
+pub enum PciError {
+    /// Error getting PCI node from FDT.
+    FdtErrorPci(FdtError),
+    /// Failed to find PCI bus in FDT.
+    FdtNoPci,
+    /// Error getting `reg` property from PCI node.
+    FdtErrorReg(FdtError),
+    /// PCI node missing `reg` property.
+    FdtMissingReg,
+    /// Empty `reg property on PCI node.
+    FdtRegEmpty,
+    /// PCI `reg` property missing size.
+    FdtRegMissingSize,
+    /// PCI CAM size reported by FDT is not what we expected.
+    CamWrongSize(usize),
+    /// Error getting `ranges` property from PCI node.
+    FdtErrorRanges(FdtError),
+    /// PCI node missing `ranges` property.
+    FdtMissingRanges,
+    /// Bus address is not equal to CPU physical address in `ranges` property.
+    RangeAddressMismatch {
+        /// A bus address from the `ranges` property.
+        bus_address: u64,
+        /// The corresponding CPU physical address from the `ranges` property.
+        cpu_physical: u64,
+    },
+    /// No suitable PCI memory range found.
+    NoSuitableRange,
+}
+
+impl Display for PciError {
+    fn fmt(&self, f: &mut Formatter) -> fmt::Result {
+        match self {
+            Self::FdtErrorPci(e) => write!(f, "Error getting PCI node from FDT: {}", e),
+            Self::FdtNoPci => write!(f, "Failed to find PCI bus in FDT."),
+            Self::FdtErrorReg(e) => write!(f, "Error getting reg property from PCI node: {}", e),
+            Self::FdtMissingReg => write!(f, "PCI node missing reg property."),
+            Self::FdtRegEmpty => write!(f, "Empty reg property on PCI node."),
+            Self::FdtRegMissingSize => write!(f, "PCI reg property missing size."),
+            Self::CamWrongSize(cam_size) => write!(
+                f,
+                "FDT says PCI CAM is {} bytes but we expected {}.",
+                cam_size, PCI_CFG_SIZE
+            ),
+            Self::FdtErrorRanges(e) => {
+                write!(f, "Error getting ranges property from PCI node: {}", e)
+            }
+            Self::FdtMissingRanges => write!(f, "PCI node missing ranges property."),
+            Self::RangeAddressMismatch { bus_address, cpu_physical } => {
+                write!(
+                    f,
+                    "bus address {:#018x} != CPU physical address {:#018x}",
+                    bus_address, cpu_physical
+                )
+            }
+            Self::NoSuitableRange => write!(f, "No suitable PCI memory range found."),
+        }
+    }
+}
+
+/// Information about the PCI bus parsed from the device tree.
+#[derive(Debug)]
+pub struct PciInfo {
+    /// The MMIO range used by the memory-mapped PCI CAM.
+    pub cam_range: Range<usize>,
+    /// The MMIO range from which 32-bit PCI BARs should be allocated.
+    pub bar_range: Range<u32>,
+}
+
+impl PciInfo {
+    /// Finds the PCI node in the FDT, parses its properties and validates it.
+    pub fn from_fdt(fdt: &Fdt) -> Result<Self, PciError> {
+        let pci_node = pci_node(fdt)?;
+
+        let cam_range = parse_cam_range(&pci_node)?;
+        let bar_range = parse_ranges(&pci_node)?;
+
+        Ok(Self { cam_range, bar_range })
+    }
+
+    /// Returns the `PciRoot` for the memory-mapped CAM found in the FDT. The CAM should be mapped
+    /// before this is called, by calling [`PciInfo::map`].
+    ///
+    /// # Safety
+    ///
+    /// To prevent concurrent access, only one `PciRoot` should exist in the program. Thus this
+    /// method must only be called once, and there must be no other `PciRoot` constructed using the
+    /// same CAM.
+    pub unsafe fn make_pci_root(&self) -> PciRoot {
+        PciRoot::new(self.cam_range.start as *mut u8, Cam::MmioCam)
+    }
+}
+
+/// Finds an FDT node with compatible=pci-host-cam-generic.
+fn pci_node(fdt: &Fdt) -> Result<FdtNode, PciError> {
+    fdt.compatible_nodes(CStr::from_bytes_with_nul(b"pci-host-cam-generic\0").unwrap())
+        .map_err(PciError::FdtErrorPci)?
+        .next()
+        .ok_or(PciError::FdtNoPci)
+}
+
+/// Parses the "reg" property of the given PCI FDT node to find the MMIO CAM range.
+fn parse_cam_range(pci_node: &FdtNode) -> Result<Range<usize>, PciError> {
+    let pci_reg = pci_node
+        .reg()
+        .map_err(PciError::FdtErrorReg)?
+        .ok_or(PciError::FdtMissingReg)?
+        .next()
+        .ok_or(PciError::FdtRegEmpty)?;
+    let cam_addr = pci_reg.addr as usize;
+    let cam_size = pci_reg.size.ok_or(PciError::FdtRegMissingSize)? as usize;
+    debug!("Found PCI CAM at {:#x}-{:#x}", cam_addr, cam_addr + cam_size);
+    // Check that the CAM is the size we expect, so we don't later try accessing it beyond its
+    // bounds. If it is a different size then something is very wrong and we shouldn't continue to
+    // access it; maybe there is some new version of PCI we don't know about.
+    if cam_size != PCI_CFG_SIZE {
+        return Err(PciError::CamWrongSize(cam_size));
+    }
+
+    Ok(cam_addr..cam_addr + cam_size)
+}
+
+/// Parses the "ranges" property of the given PCI FDT node, and returns the largest suitable range
+/// to use for non-prefetchable 32-bit memory BARs.
+fn parse_ranges(pci_node: &FdtNode) -> Result<Range<u32>, PciError> {
+    let mut memory_address = 0;
+    let mut memory_size = 0;
+
+    for AddressRange { addr: (flags, bus_address), parent_addr: cpu_physical, size } in pci_node
+        .ranges::<(u32, u64), u64, u64>()
+        .map_err(PciError::FdtErrorRanges)?
+        .ok_or(PciError::FdtMissingRanges)?
+    {
+        let flags = PciMemoryFlags(flags);
+        let prefetchable = flags.prefetchable();
+        let range_type = flags.range_type();
+        debug!(
+            "range: {:?} {}prefetchable bus address: {:#018x} CPU physical address: {:#018x} size: {:#018x}",
+            range_type,
+            if prefetchable { "" } else { "non-" },
+            bus_address,
+            cpu_physical,
+            size,
+        );
+
+        // Use a 64-bit range for 32-bit memory, if it is low enough, because crosvm doesn't
+        // currently provide any 32-bit ranges.
+        if !prefetchable
+            && matches!(range_type, PciRangeType::Memory32 | PciRangeType::Memory64)
+            && size > memory_size.into()
+            && bus_address + size < u32::MAX.into()
+        {
+            if bus_address != cpu_physical {
+                return Err(PciError::RangeAddressMismatch { bus_address, cpu_physical });
+            }
+            memory_address = u32::try_from(cpu_physical).unwrap();
+            memory_size = u32::try_from(size).unwrap();
+        }
+    }
+
+    if memory_size == 0 {
+        return Err(PciError::NoSuitableRange);
+    }
+
+    Ok(memory_address..memory_address + memory_size)
+}
+
+#[derive(Copy, Clone, Debug, Eq, PartialEq)]
+struct PciMemoryFlags(u32);
+
+impl PciMemoryFlags {
+    pub fn prefetchable(self) -> bool {
+        self.0 & 0x80000000 != 0
+    }
+
+    pub fn range_type(self) -> PciRangeType {
+        PciRangeType::from((self.0 & 0x3000000) >> 24)
+    }
+}
+
+#[derive(Copy, Clone, Debug, Eq, PartialEq)]
+enum PciRangeType {
+    ConfigurationSpace,
+    IoSpace,
+    Memory32,
+    Memory64,
+}
+
+impl From<u32> for PciRangeType {
+    fn from(value: u32) -> Self {
+        match value {
+            0 => Self::ConfigurationSpace,
+            1 => Self::IoSpace,
+            2 => Self::Memory32,
+            3 => Self::Memory64,
+            _ => panic!("Tried to convert invalid range type {}", value),
+        }
+    }
+}
diff --git a/microdroid_manager/TEST_MAPPING b/microdroid_manager/TEST_MAPPING
new file mode 100644
index 0000000..af0abe7
--- /dev/null
+++ b/microdroid_manager/TEST_MAPPING
@@ -0,0 +1,7 @@
+{
+  "avf-presubmit" : [
+    {
+      "name" : "microdroid_manager_test"
+    }
+  ]
+}
diff --git a/pvmfw/Android.bp b/pvmfw/Android.bp
index 2912c91..ed3ef8d 100644
--- a/pvmfw/Android.bp
+++ b/pvmfw/Android.bp
@@ -15,9 +15,9 @@
         "libaarch64_paging",
         "libbuddy_system_allocator",
         "libdice_nostd",
+        "libfdtpci",
         "liblibfdt",
         "liblog_rust_nostd",
-        "libpvmfw_avb_nostd",
         "libpvmfw_embedded_key",
         "libtinyvec_nostd",
         "libvirtio_drivers",
diff --git a/pvmfw/avb/Android.bp b/pvmfw/avb/Android.bp
index 65259a5..3026d20 100644
--- a/pvmfw/avb/Android.bp
+++ b/pvmfw/avb/Android.bp
@@ -10,6 +10,9 @@
     rustlibs: [
         "libavb_bindgen",
     ],
+    whole_static_libs: [
+        "libavb",
+    ],
 }
 
 rust_library_rlib {
@@ -25,4 +28,31 @@
     name: "libpvmfw_avb.test",
     defaults: ["libpvmfw_avb_nostd_defaults"],
     test_suites: ["general-tests"],
+    data: [
+        ":avb_testkey_rsa2048_pub_bin",
+        ":avb_testkey_rsa4096_pub_bin",
+        ":microdroid_kernel_signed",
+        ":unsigned_test_image",
+    ],
+    rustlibs: [
+        "libanyhow",
+    ],
+    enabled: false,
+    arch: {
+        // Microdroid kernel is only available in these architectures.
+        arm64: {
+            enabled: true,
+        },
+        x86_64: {
+            enabled: true,
+        },
+    },
+}
+
+// Generates a 16KB unsigned image for testing.
+genrule {
+    name: "unsigned_test_image",
+    tools: ["avbtool"],
+    out: ["unsigned_test.img"],
+    cmd: "$(location avbtool) generate_test_image --image_size 16384 --output $(out)",
 }
diff --git a/pvmfw/avb/src/lib.rs b/pvmfw/avb/src/lib.rs
index eb1f918..1f39076 100644
--- a/pvmfw/avb/src/lib.rs
+++ b/pvmfw/avb/src/lib.rs
@@ -15,6 +15,8 @@
 //! A library implementing the payload verification for pvmfw with libavb
 
 #![cfg_attr(not(test), no_std)]
+// For usize.checked_add_signed(isize), available in Rust 1.66.0
+#![feature(mixed_integer_ops)]
 
 mod verify;
 
diff --git a/pvmfw/avb/src/verify.rs b/pvmfw/avb/src/verify.rs
index 7f3ba3d..b6db601 100644
--- a/pvmfw/avb/src/verify.rs
+++ b/pvmfw/avb/src/verify.rs
@@ -14,11 +14,19 @@
 
 //! This module handles the pvmfw payload verification.
 
-use avb_bindgen::AvbSlotVerifyResult;
-use core::fmt;
+use avb_bindgen::{
+    avb_slot_verify, AvbHashtreeErrorMode, AvbIOResult, AvbOps, AvbSlotVerifyFlags,
+    AvbSlotVerifyResult,
+};
+use core::{
+    ffi::{c_char, c_void, CStr},
+    fmt,
+    ptr::{self, NonNull},
+    slice,
+};
 
 /// Error code from AVB image verification.
-#[derive(Clone, Debug)]
+#[derive(Clone, Debug, PartialEq, Eq)]
 pub enum AvbImageVerifyError {
     /// AVB_SLOT_VERIFY_RESULT_ERROR_INVALID_ARGUMENT
     InvalidArgument,
@@ -82,31 +90,398 @@
     }
 }
 
+enum AvbIOError {
+    /// AVB_IO_RESULT_ERROR_OOM,
+    #[allow(dead_code)]
+    Oom,
+    /// AVB_IO_RESULT_ERROR_IO,
+    #[allow(dead_code)]
+    Io,
+    /// AVB_IO_RESULT_ERROR_NO_SUCH_PARTITION,
+    NoSuchPartition,
+    /// AVB_IO_RESULT_ERROR_RANGE_OUTSIDE_PARTITION,
+    RangeOutsidePartition,
+    /// AVB_IO_RESULT_ERROR_NO_SUCH_VALUE,
+    NoSuchValue,
+    /// AVB_IO_RESULT_ERROR_INVALID_VALUE_SIZE,
+    InvalidValueSize,
+    /// AVB_IO_RESULT_ERROR_INSUFFICIENT_SPACE,
+    #[allow(dead_code)]
+    InsufficientSpace,
+}
+
+impl From<AvbIOError> for AvbIOResult {
+    fn from(error: AvbIOError) -> Self {
+        match error {
+            AvbIOError::Oom => AvbIOResult::AVB_IO_RESULT_ERROR_OOM,
+            AvbIOError::Io => AvbIOResult::AVB_IO_RESULT_ERROR_IO,
+            AvbIOError::NoSuchPartition => AvbIOResult::AVB_IO_RESULT_ERROR_NO_SUCH_PARTITION,
+            AvbIOError::RangeOutsidePartition => {
+                AvbIOResult::AVB_IO_RESULT_ERROR_RANGE_OUTSIDE_PARTITION
+            }
+            AvbIOError::NoSuchValue => AvbIOResult::AVB_IO_RESULT_ERROR_NO_SUCH_VALUE,
+            AvbIOError::InvalidValueSize => AvbIOResult::AVB_IO_RESULT_ERROR_INVALID_VALUE_SIZE,
+            AvbIOError::InsufficientSpace => AvbIOResult::AVB_IO_RESULT_ERROR_INSUFFICIENT_SPACE,
+        }
+    }
+}
+
+fn to_avb_io_result(result: Result<(), AvbIOError>) -> AvbIOResult {
+    result.map_or_else(|e| e.into(), |_| AvbIOResult::AVB_IO_RESULT_OK)
+}
+
+extern "C" fn read_is_device_unlocked(
+    _ops: *mut AvbOps,
+    out_is_unlocked: *mut bool,
+) -> AvbIOResult {
+    if let Err(e) = is_not_null(out_is_unlocked) {
+        return e.into();
+    }
+    // SAFETY: It is safe as the raw pointer `out_is_unlocked` is a valid pointer.
+    unsafe {
+        *out_is_unlocked = false;
+    }
+    AvbIOResult::AVB_IO_RESULT_OK
+}
+
+extern "C" fn read_from_partition(
+    ops: *mut AvbOps,
+    partition: *const c_char,
+    offset: i64,
+    num_bytes: usize,
+    buffer: *mut c_void,
+    out_num_read: *mut usize,
+) -> AvbIOResult {
+    to_avb_io_result(try_read_from_partition(
+        ops,
+        partition,
+        offset,
+        num_bytes,
+        buffer,
+        out_num_read,
+    ))
+}
+
+fn try_read_from_partition(
+    ops: *mut AvbOps,
+    partition: *const c_char,
+    offset: i64,
+    num_bytes: usize,
+    buffer: *mut c_void,
+    out_num_read: *mut usize,
+) -> Result<(), AvbIOError> {
+    let ops = as_avbops_ref(ops)?;
+    let partition = ops.as_ref().get_partition(partition)?;
+    let buffer = to_nonnull(buffer)?;
+    // SAFETY: It is safe to copy the requested number of bytes to `buffer` as `buffer`
+    // is created to point to the `num_bytes` of bytes in memory.
+    let buffer_slice = unsafe { slice::from_raw_parts_mut(buffer.as_ptr() as *mut u8, num_bytes) };
+    copy_data_to_dst(partition, offset, buffer_slice)?;
+    let out_num_read = to_nonnull(out_num_read)?;
+    // SAFETY: The raw pointer `out_num_read` was created to point to a valid a `usize`
+    // and we checked it is nonnull.
+    unsafe {
+        *out_num_read.as_ptr() = buffer_slice.len();
+    }
+    Ok(())
+}
+
+fn copy_data_to_dst(src: &[u8], offset: i64, dst: &mut [u8]) -> Result<(), AvbIOError> {
+    let start = to_copy_start(offset, src.len()).ok_or(AvbIOError::InvalidValueSize)?;
+    let end = start.checked_add(dst.len()).ok_or(AvbIOError::InvalidValueSize)?;
+    dst.copy_from_slice(src.get(start..end).ok_or(AvbIOError::RangeOutsidePartition)?);
+    Ok(())
+}
+
+fn to_copy_start(offset: i64, len: usize) -> Option<usize> {
+    usize::try_from(offset)
+        .ok()
+        .or_else(|| isize::try_from(offset).ok().and_then(|v| len.checked_add_signed(v)))
+}
+
+extern "C" fn get_size_of_partition(
+    ops: *mut AvbOps,
+    partition: *const c_char,
+    out_size_num_bytes: *mut u64,
+) -> AvbIOResult {
+    to_avb_io_result(try_get_size_of_partition(ops, partition, out_size_num_bytes))
+}
+
+fn try_get_size_of_partition(
+    ops: *mut AvbOps,
+    partition: *const c_char,
+    out_size_num_bytes: *mut u64,
+) -> Result<(), AvbIOError> {
+    let ops = as_avbops_ref(ops)?;
+    let partition = ops.as_ref().get_partition(partition)?;
+    let partition_size =
+        u64::try_from(partition.len()).map_err(|_| AvbIOError::InvalidValueSize)?;
+    let out_size_num_bytes = to_nonnull(out_size_num_bytes)?;
+    // SAFETY: The raw pointer `out_size_num_bytes` was created to point to a valid a `u64`
+    // and we checked it is nonnull.
+    unsafe {
+        *out_size_num_bytes.as_ptr() = partition_size;
+    }
+    Ok(())
+}
+
+extern "C" fn read_rollback_index(
+    _ops: *mut AvbOps,
+    _rollback_index_location: usize,
+    _out_rollback_index: *mut u64,
+) -> AvbIOResult {
+    // Rollback protection is not yet implemented, but
+    // this method is required by `avb_slot_verify()`.
+    AvbIOResult::AVB_IO_RESULT_OK
+}
+
+extern "C" fn get_unique_guid_for_partition(
+    _ops: *mut AvbOps,
+    _partition: *const c_char,
+    _guid_buf: *mut c_char,
+    _guid_buf_size: usize,
+) -> AvbIOResult {
+    // This method is required by `avb_slot_verify()`.
+    AvbIOResult::AVB_IO_RESULT_OK
+}
+
+extern "C" fn validate_public_key_for_partition(
+    ops: *mut AvbOps,
+    partition: *const c_char,
+    public_key_data: *const u8,
+    public_key_length: usize,
+    public_key_metadata: *const u8,
+    public_key_metadata_length: usize,
+    out_is_trusted: *mut bool,
+    out_rollback_index_location: *mut u32,
+) -> AvbIOResult {
+    to_avb_io_result(try_validate_public_key_for_partition(
+        ops,
+        partition,
+        public_key_data,
+        public_key_length,
+        public_key_metadata,
+        public_key_metadata_length,
+        out_is_trusted,
+        out_rollback_index_location,
+    ))
+}
+
+#[allow(clippy::too_many_arguments)]
+fn try_validate_public_key_for_partition(
+    ops: *mut AvbOps,
+    partition: *const c_char,
+    public_key_data: *const u8,
+    public_key_length: usize,
+    _public_key_metadata: *const u8,
+    _public_key_metadata_length: usize,
+    out_is_trusted: *mut bool,
+    _out_rollback_index_location: *mut u32,
+) -> Result<(), AvbIOError> {
+    is_not_null(public_key_data)?;
+    // SAFETY: It is safe to create a slice with the given pointer and length as
+    // `public_key_data` is a valid pointer and it points to an array of length
+    // `public_key_length`.
+    let public_key = unsafe { slice::from_raw_parts(public_key_data, public_key_length) };
+    let ops = as_avbops_ref(ops)?;
+    // Verifies the public key for the known partitions only.
+    ops.as_ref().get_partition(partition)?;
+    let trusted_public_key = ops.as_ref().trusted_public_key;
+    let out_is_trusted = to_nonnull(out_is_trusted)?;
+    // SAFETY: It is safe as the raw pointer `out_is_trusted` is a nonnull pointer.
+    unsafe {
+        *out_is_trusted.as_ptr() = public_key == trusted_public_key;
+    }
+    Ok(())
+}
+
+fn as_avbops_ref<'a>(ops: *mut AvbOps) -> Result<&'a AvbOps, AvbIOError> {
+    let ops = to_nonnull(ops)?;
+    // SAFETY: It is safe as the raw pointer `ops` is a nonnull pointer.
+    unsafe { Ok(ops.as_ref()) }
+}
+
+fn to_nonnull<T>(p: *mut T) -> Result<NonNull<T>, AvbIOError> {
+    NonNull::new(p).ok_or(AvbIOError::NoSuchValue)
+}
+
+fn is_not_null<T>(ptr: *const T) -> Result<(), AvbIOError> {
+    if ptr.is_null() {
+        Err(AvbIOError::NoSuchValue)
+    } else {
+        Ok(())
+    }
+}
+
+struct Payload<'a> {
+    kernel: &'a [u8],
+    trusted_public_key: &'a [u8],
+}
+
+impl<'a> AsRef<Payload<'a>> for AvbOps {
+    fn as_ref(&self) -> &Payload<'a> {
+        let payload = self.user_data as *const Payload;
+        // SAFETY: It is safe to cast the `AvbOps.user_data` to Payload as we have saved a
+        // pointer to a valid value of Payload in user_data when creating AvbOps, and
+        // assume that the Payload isn't used beyond the lifetime of the AvbOps that it
+        // belongs to.
+        unsafe { &*payload }
+    }
+}
+
+impl<'a> Payload<'a> {
+    const KERNEL_PARTITION_NAME: &[u8] = b"bootloader\0";
+
+    fn kernel_partition_name(&self) -> &CStr {
+        CStr::from_bytes_with_nul(Self::KERNEL_PARTITION_NAME).unwrap()
+    }
+
+    fn get_partition(&self, partition_name: *const c_char) -> Result<&[u8], AvbIOError> {
+        is_not_null(partition_name)?;
+        // SAFETY: It is safe as the raw pointer `partition_name` is a nonnull pointer.
+        let partition_name = unsafe { CStr::from_ptr(partition_name) };
+        match partition_name.to_bytes_with_nul() {
+            Self::KERNEL_PARTITION_NAME => Ok(self.kernel),
+            _ => Err(AvbIOError::NoSuchPartition),
+        }
+    }
+}
+
 /// Verifies the payload (signed kernel + initrd) against the trusted public key.
-pub fn verify_payload(_public_key: &[u8]) -> Result<(), AvbImageVerifyError> {
-    // TODO(b/256148034): Verify the kernel image with avb_slot_verify()
-    // let result = unsafe {
-    //     avb_slot_verify(
-    //         &mut avb_ops,
-    //         requested_partitions.as_ptr(),
-    //         ab_suffix.as_ptr(),
-    //         flags,
-    //         hashtree_error_mode,
-    //         null_mut(),
-    //     )
-    // };
-    let result = AvbSlotVerifyResult::AVB_SLOT_VERIFY_RESULT_OK;
+pub fn verify_payload(kernel: &[u8], trusted_public_key: &[u8]) -> Result<(), AvbImageVerifyError> {
+    let mut payload = Payload { kernel, trusted_public_key };
+    let mut avb_ops = AvbOps {
+        user_data: &mut payload as *mut _ as *mut c_void,
+        ab_ops: ptr::null_mut(),
+        atx_ops: ptr::null_mut(),
+        read_from_partition: Some(read_from_partition),
+        get_preloaded_partition: None,
+        write_to_partition: None,
+        validate_vbmeta_public_key: None,
+        read_rollback_index: Some(read_rollback_index),
+        write_rollback_index: None,
+        read_is_device_unlocked: Some(read_is_device_unlocked),
+        get_unique_guid_for_partition: Some(get_unique_guid_for_partition),
+        get_size_of_partition: Some(get_size_of_partition),
+        read_persistent_value: None,
+        write_persistent_value: None,
+        validate_public_key_for_partition: Some(validate_public_key_for_partition),
+    };
+    // NULL is needed to mark the end of the array.
+    let requested_partitions: [*const c_char; 2] =
+        [payload.kernel_partition_name().as_ptr(), ptr::null()];
+    let ab_suffix = CStr::from_bytes_with_nul(b"\0").unwrap();
+
+    // SAFETY: It is safe to call `avb_slot_verify()` as the pointer arguments (`ops`,
+    // `requested_partitions` and `ab_suffix`) passed to the method are all valid and
+    // initialized. The last argument `out_data` is allowed to be null so that nothing
+    // will be written to it.
+    let result = unsafe {
+        avb_slot_verify(
+            &mut avb_ops,
+            requested_partitions.as_ptr(),
+            ab_suffix.as_ptr(),
+            AvbSlotVerifyFlags::AVB_SLOT_VERIFY_FLAGS_NO_VBMETA_PARTITION,
+            AvbHashtreeErrorMode::AVB_HASHTREE_ERROR_MODE_RESTART_AND_INVALIDATE,
+            /*out_data=*/ ptr::null_mut(),
+        )
+    };
     to_avb_verify_result(result)
 }
 
 #[cfg(test)]
 mod tests {
     use super::*;
+    use anyhow::Result;
+    use avb_bindgen::AvbFooter;
+    use std::{fs, mem::size_of};
 
-    // TODO(b/256148034): Test verification succeeds with valid payload later.
+    const PUBLIC_KEY_RSA2048_PATH: &str = "data/testkey_rsa2048_pub.bin";
+    const PUBLIC_KEY_RSA4096_PATH: &str = "data/testkey_rsa4096_pub.bin";
+    const RANDOM_FOOTER_POS: usize = 30;
+
+    /// This test uses the Microdroid payload compiled on the fly to check that
+    /// the latest payload can be verified successfully.
     #[test]
-    fn verification_succeeds_with_placeholder_input() {
-        let fake_public_key = [0u8; 2];
-        assert!(verify_payload(&fake_public_key).is_ok());
+    fn latest_valid_payload_is_verified_successfully() -> Result<()> {
+        let kernel = load_latest_signed_kernel()?;
+        let public_key = fs::read(PUBLIC_KEY_RSA4096_PATH)?;
+
+        assert_eq!(Ok(()), verify_payload(&kernel, &public_key));
+        Ok(())
+    }
+
+    #[test]
+    fn payload_with_empty_public_key_fails_verification() -> Result<()> {
+        assert_payload_verification_fails(
+            &load_latest_signed_kernel()?,
+            /*trusted_public_key=*/ &[0u8; 0],
+            AvbImageVerifyError::PublicKeyRejected,
+        )
+    }
+
+    #[test]
+    fn payload_with_an_invalid_public_key_fails_verification() -> Result<()> {
+        assert_payload_verification_fails(
+            &load_latest_signed_kernel()?,
+            /*trusted_public_key=*/ &[0u8; 512],
+            AvbImageVerifyError::PublicKeyRejected,
+        )
+    }
+
+    #[test]
+    fn payload_with_a_different_valid_public_key_fails_verification() -> Result<()> {
+        assert_payload_verification_fails(
+            &load_latest_signed_kernel()?,
+            &fs::read(PUBLIC_KEY_RSA2048_PATH)?,
+            AvbImageVerifyError::PublicKeyRejected,
+        )
+    }
+
+    #[test]
+    fn unsigned_kernel_fails_verification() -> Result<()> {
+        assert_payload_verification_fails(
+            &fs::read("unsigned_test.img")?,
+            &fs::read(PUBLIC_KEY_RSA4096_PATH)?,
+            AvbImageVerifyError::Io,
+        )
+    }
+
+    #[test]
+    fn tampered_kernel_fails_verification() -> Result<()> {
+        let mut kernel = load_latest_signed_kernel()?;
+        kernel[1] = !kernel[1]; // Flip the bits
+
+        assert_payload_verification_fails(
+            &kernel,
+            &fs::read(PUBLIC_KEY_RSA4096_PATH)?,
+            AvbImageVerifyError::Verification,
+        )
+    }
+
+    #[test]
+    fn tampered_kernel_footer_fails_verification() -> Result<()> {
+        let mut kernel = load_latest_signed_kernel()?;
+        let avb_footer_index = kernel.len() - size_of::<AvbFooter>() + RANDOM_FOOTER_POS;
+        kernel[avb_footer_index] = !kernel[avb_footer_index];
+
+        assert_payload_verification_fails(
+            &kernel,
+            &fs::read(PUBLIC_KEY_RSA4096_PATH)?,
+            AvbImageVerifyError::InvalidMetadata,
+        )
+    }
+
+    fn assert_payload_verification_fails(
+        kernel: &[u8],
+        trusted_public_key: &[u8],
+        expected_error: AvbImageVerifyError,
+    ) -> Result<()> {
+        assert_eq!(Err(expected_error), verify_payload(kernel, trusted_public_key));
+        Ok(())
+    }
+
+    fn load_latest_signed_kernel() -> Result<Vec<u8>> {
+        Ok(fs::read("microdroid_kernel")?)
     }
 }
diff --git a/pvmfw/src/entry.rs b/pvmfw/src/entry.rs
index 1b35c79..e979a95 100644
--- a/pvmfw/src/entry.rs
+++ b/pvmfw/src/entry.rs
@@ -47,6 +47,7 @@
     /// The provided ramdisk was invalid.
     InvalidRamdisk,
     /// Failed to verify the payload.
+    #[allow(dead_code)]
     PayloadVerificationError,
 }
 
diff --git a/pvmfw/src/heap.rs b/pvmfw/src/heap.rs
index bfa8320..eab3bc4 100644
--- a/pvmfw/src/heap.rs
+++ b/pvmfw/src/heap.rs
@@ -14,6 +14,14 @@
 
 //! Heap implementation.
 
+use core::alloc::GlobalAlloc as _;
+use core::alloc::Layout;
+use core::ffi::c_void;
+use core::mem;
+use core::num::NonZeroUsize;
+use core::ptr;
+use core::ptr::NonNull;
+
 use buddy_system_allocator::LockedHeap;
 
 #[global_allocator]
@@ -24,3 +32,32 @@
 pub unsafe fn init() {
     HEAP_ALLOCATOR.lock().init(HEAP.as_mut_ptr() as usize, HEAP.len());
 }
+
+#[no_mangle]
+unsafe extern "C" fn malloc(size: usize) -> *mut c_void {
+    malloc_(size).map_or(ptr::null_mut(), |p| p.cast::<c_void>().as_ptr())
+}
+
+#[no_mangle]
+unsafe extern "C" fn free(ptr: *mut c_void) {
+    if let Some(ptr) = NonNull::new(ptr).map(|p| p.cast::<usize>().as_ptr().offset(-1)) {
+        if let Some(size) = NonZeroUsize::new(*ptr) {
+            if let Some(layout) = malloc_layout(size) {
+                HEAP_ALLOCATOR.dealloc(ptr as *mut u8, layout);
+            }
+        }
+    }
+}
+
+unsafe fn malloc_(size: usize) -> Option<NonNull<usize>> {
+    let size = NonZeroUsize::new(size)?.checked_add(mem::size_of::<usize>())?;
+    let ptr = HEAP_ALLOCATOR.alloc(malloc_layout(size)?);
+    let ptr = NonNull::new(ptr)?.cast::<usize>().as_ptr();
+    *ptr = size.get();
+    NonNull::new(ptr.offset(1))
+}
+
+fn malloc_layout(size: NonZeroUsize) -> Option<Layout> {
+    const ALIGN: usize = mem::size_of::<u64>();
+    Layout::from_size_align(size.get(), ALIGN).ok()
+}
diff --git a/pvmfw/src/main.rs b/pvmfw/src/main.rs
index 7d64bf0..4d1ddfe 100644
--- a/pvmfw/src/main.rs
+++ b/pvmfw/src/main.rs
@@ -34,15 +34,15 @@
 mod smccc;
 
 use crate::{
-    avb::PUBLIC_KEY,
+    avb::PUBLIC_KEY, // Keep the public key here otherwise the signing script will be broken.
     entry::RebootReason,
     memory::MemoryTracker,
-    pci::{find_virtio_devices, PciError, PciInfo},
+    pci::{find_virtio_devices, map_mmio},
 };
 use dice::bcc;
+use fdtpci::{PciError, PciInfo};
 use libfdt::Fdt;
 use log::{debug, error, info, trace};
-use pvmfw_avb::verify_payload;
 
 fn main(
     fdt: &Fdt,
@@ -54,6 +54,7 @@
     info!("pVM firmware");
     debug!("FDT: {:?}", fdt as *const libfdt::Fdt);
     debug!("Signed kernel: {:?} ({:#x} bytes)", signed_kernel.as_ptr(), signed_kernel.len());
+    debug!("AVB public key: addr={:?}, size={:#x} ({1})", PUBLIC_KEY.as_ptr(), PUBLIC_KEY.len());
     if let Some(rd) = ramdisk {
         debug!("Ramdisk: {:?} ({:#x} bytes)", rd.as_ptr(), rd.len());
     } else {
@@ -64,16 +65,12 @@
     // Set up PCI bus for VirtIO devices.
     let pci_info = PciInfo::from_fdt(fdt).map_err(handle_pci_error)?;
     debug!("PCI: {:#x?}", pci_info);
-    pci_info.map(memory)?;
+    map_mmio(&pci_info, memory)?;
     // Safety: This is the only place where we call make_pci_root, and this main function is only
     // called once.
     let mut pci_root = unsafe { pci_info.make_pci_root() };
     find_virtio_devices(&mut pci_root).map_err(handle_pci_error)?;
 
-    verify_payload(PUBLIC_KEY).map_err(|e| {
-        error!("Failed to verify the payload: {e}");
-        RebootReason::PayloadVerificationError
-    })?;
     info!("Starting payload...");
     Ok(())
 }
diff --git a/pvmfw/src/pci.rs b/pvmfw/src/pci.rs
index 301ecfc..2b81772 100644
--- a/pvmfw/src/pci.rs
+++ b/pvmfw/src/pci.rs
@@ -14,225 +14,26 @@
 
 //! Functions to scan the PCI bus for VirtIO devices.
 
-use crate::{
-    entry::RebootReason,
-    memory::{MemoryRange, MemoryTracker},
-};
-use core::{
-    ffi::CStr,
-    fmt::{self, Display, Formatter},
-    ops::Range,
-};
-use libfdt::{AddressRange, Fdt, FdtError, FdtNode};
+use crate::{entry::RebootReason, memory::MemoryTracker};
+use fdtpci::{PciError, PciInfo};
 use log::{debug, error};
-use virtio_drivers::pci::{
-    bus::{Cam, PciRoot},
-    virtio_device_type,
-};
+use virtio_drivers::transport::pci::{bus::PciRoot, virtio_device_type};
 
-/// PCI MMIO configuration region size.
-const PCI_CFG_SIZE: usize = 0x100_0000;
+/// Maps the CAM and BAR range in the page table and MMIO guard.
+pub fn map_mmio(pci_info: &PciInfo, memory: &mut MemoryTracker) -> Result<(), RebootReason> {
+    memory.map_mmio_range(pci_info.cam_range.clone()).map_err(|e| {
+        error!("Failed to map PCI CAM: {}", e);
+        RebootReason::InternalError
+    })?;
 
-#[derive(Clone, Debug, Eq, PartialEq)]
-pub enum PciError {
-    FdtErrorPci(FdtError),
-    FdtNoPci,
-    FdtErrorReg(FdtError),
-    FdtMissingReg,
-    FdtRegEmpty,
-    FdtRegMissingSize,
-    CamWrongSize(usize),
-    FdtErrorRanges(FdtError),
-    FdtMissingRanges,
-    RangeAddressMismatch { bus_address: u64, cpu_physical: u64 },
-    NoSuitableRange,
-}
-
-impl Display for PciError {
-    fn fmt(&self, f: &mut Formatter) -> fmt::Result {
-        match self {
-            Self::FdtErrorPci(e) => write!(f, "Error getting PCI node from FDT: {}", e),
-            Self::FdtNoPci => write!(f, "Failed to find PCI bus in FDT."),
-            Self::FdtErrorReg(e) => write!(f, "Error getting reg property from PCI node: {}", e),
-            Self::FdtMissingReg => write!(f, "PCI node missing reg property."),
-            Self::FdtRegEmpty => write!(f, "Empty reg property on PCI node."),
-            Self::FdtRegMissingSize => write!(f, "PCI reg property missing size."),
-            Self::CamWrongSize(cam_size) => write!(
-                f,
-                "FDT says PCI CAM is {} bytes but we expected {}.",
-                cam_size, PCI_CFG_SIZE
-            ),
-            Self::FdtErrorRanges(e) => {
-                write!(f, "Error getting ranges property from PCI node: {}", e)
-            }
-            Self::FdtMissingRanges => write!(f, "PCI node missing ranges property."),
-            Self::RangeAddressMismatch { bus_address, cpu_physical } => {
-                write!(
-                    f,
-                    "bus address {:#018x} != CPU physical address {:#018x}",
-                    bus_address, cpu_physical
-                )
-            }
-            Self::NoSuitableRange => write!(f, "No suitable PCI memory range found."),
-        }
-    }
-}
-
-/// Information about the PCI bus parsed from the device tree.
-#[derive(Debug)]
-pub struct PciInfo {
-    /// The MMIO range used by the memory-mapped PCI CAM.
-    cam_range: MemoryRange,
-    /// The MMIO range from which 32-bit PCI BARs should be allocated.
-    bar_range: Range<u32>,
-}
-
-impl PciInfo {
-    /// Finds the PCI node in the FDT, parses its properties and validates it.
-    pub fn from_fdt(fdt: &Fdt) -> Result<Self, PciError> {
-        let pci_node = pci_node(fdt)?;
-
-        let cam_range = parse_cam_range(&pci_node)?;
-        let bar_range = parse_ranges(&pci_node)?;
-
-        Ok(Self { cam_range, bar_range })
-    }
-
-    /// Maps the CAM and BAR range in the page table and MMIO guard.
-    pub fn map(&self, memory: &mut MemoryTracker) -> Result<(), RebootReason> {
-        memory.map_mmio_range(self.cam_range.clone()).map_err(|e| {
-            error!("Failed to map PCI CAM: {}", e);
+    memory
+        .map_mmio_range(pci_info.bar_range.start as usize..pci_info.bar_range.end as usize)
+        .map_err(|e| {
+            error!("Failed to map PCI MMIO range: {}", e);
             RebootReason::InternalError
         })?;
 
-        memory.map_mmio_range(self.bar_range.start as usize..self.bar_range.end as usize).map_err(
-            |e| {
-                error!("Failed to map PCI MMIO range: {}", e);
-                RebootReason::InternalError
-            },
-        )?;
-
-        Ok(())
-    }
-
-    /// Returns the `PciRoot` for the memory-mapped CAM found in the FDT. The CAM should be mapped
-    /// before this is called, by calling [`PciInfo::map`].
-    ///
-    /// # Safety
-    ///
-    /// To prevent concurrent access, only one `PciRoot` should exist in the program. Thus this
-    /// method must only be called once, and there must be no other `PciRoot` constructed using the
-    /// same CAM.
-    pub unsafe fn make_pci_root(&self) -> PciRoot {
-        PciRoot::new(self.cam_range.start as *mut u8, Cam::MmioCam)
-    }
-}
-
-/// Finds an FDT node with compatible=pci-host-cam-generic.
-fn pci_node(fdt: &Fdt) -> Result<FdtNode, PciError> {
-    fdt.compatible_nodes(CStr::from_bytes_with_nul(b"pci-host-cam-generic\0").unwrap())
-        .map_err(PciError::FdtErrorPci)?
-        .next()
-        .ok_or(PciError::FdtNoPci)
-}
-
-/// Parses the "reg" property of the given PCI FDT node to find the MMIO CAM range.
-fn parse_cam_range(pci_node: &FdtNode) -> Result<MemoryRange, PciError> {
-    let pci_reg = pci_node
-        .reg()
-        .map_err(PciError::FdtErrorReg)?
-        .ok_or(PciError::FdtMissingReg)?
-        .next()
-        .ok_or(PciError::FdtRegEmpty)?;
-    let cam_addr = pci_reg.addr as usize;
-    let cam_size = pci_reg.size.ok_or(PciError::FdtRegMissingSize)? as usize;
-    debug!("Found PCI CAM at {:#x}-{:#x}", cam_addr, cam_addr + cam_size);
-    // Check that the CAM is the size we expect, so we don't later try accessing it beyond its
-    // bounds. If it is a different size then something is very wrong and we shouldn't continue to
-    // access it; maybe there is some new version of PCI we don't know about.
-    if cam_size != PCI_CFG_SIZE {
-        return Err(PciError::CamWrongSize(cam_size));
-    }
-
-    Ok(cam_addr..cam_addr + cam_size)
-}
-
-/// Parses the "ranges" property of the given PCI FDT node, and returns the largest suitable range
-/// to use for non-prefetchable 32-bit memory BARs.
-fn parse_ranges(pci_node: &FdtNode) -> Result<Range<u32>, PciError> {
-    let mut memory_address = 0;
-    let mut memory_size = 0;
-
-    for AddressRange { addr: (flags, bus_address), parent_addr: cpu_physical, size } in pci_node
-        .ranges::<(u32, u64), u64, u64>()
-        .map_err(PciError::FdtErrorRanges)?
-        .ok_or(PciError::FdtMissingRanges)?
-    {
-        let flags = PciMemoryFlags(flags);
-        let prefetchable = flags.prefetchable();
-        let range_type = flags.range_type();
-        debug!(
-            "range: {:?} {}prefetchable bus address: {:#018x} CPU physical address: {:#018x} size: {:#018x}",
-            range_type,
-            if prefetchable { "" } else { "non-" },
-            bus_address,
-            cpu_physical,
-            size,
-        );
-
-        // Use a 64-bit range for 32-bit memory, if it is low enough, because crosvm doesn't
-        // currently provide any 32-bit ranges.
-        if !prefetchable
-            && matches!(range_type, PciRangeType::Memory32 | PciRangeType::Memory64)
-            && size > memory_size.into()
-            && bus_address + size < u32::MAX.into()
-        {
-            if bus_address != cpu_physical {
-                return Err(PciError::RangeAddressMismatch { bus_address, cpu_physical });
-            }
-            memory_address = u32::try_from(cpu_physical).unwrap();
-            memory_size = u32::try_from(size).unwrap();
-        }
-    }
-
-    if memory_size == 0 {
-        return Err(PciError::NoSuitableRange);
-    }
-
-    Ok(memory_address..memory_address + memory_size)
-}
-
-#[derive(Copy, Clone, Debug, Eq, PartialEq)]
-struct PciMemoryFlags(u32);
-
-impl PciMemoryFlags {
-    pub fn prefetchable(self) -> bool {
-        self.0 & 0x80000000 != 0
-    }
-
-    pub fn range_type(self) -> PciRangeType {
-        PciRangeType::from((self.0 & 0x3000000) >> 24)
-    }
-}
-
-#[derive(Copy, Clone, Debug, Eq, PartialEq)]
-enum PciRangeType {
-    ConfigurationSpace,
-    IoSpace,
-    Memory32,
-    Memory64,
-}
-
-impl From<u32> for PciRangeType {
-    fn from(value: u32) -> Self {
-        match value {
-            0 => Self::ConfigurationSpace,
-            1 => Self::IoSpace,
-            2 => Self::Memory32,
-            3 => Self::Memory64,
-            _ => panic!("Tried to convert invalid range type {}", value),
-        }
-    }
+    Ok(())
 }
 
 /// Finds VirtIO PCI devices.
diff --git a/rialto/tests/test.rs b/rialto/tests/test.rs
index 687ce86..0447cd3 100644
--- a/rialto/tests/test.rs
+++ b/rialto/tests/test.rs
@@ -48,7 +48,10 @@
     // We need to start the thread pool for Binder to work properly, especially link_to_death.
     ProcessState::start_thread_pool();
 
-    let service = vmclient::connect().context("Failed to find VirtualizationService")?;
+    let virtmgr =
+        vmclient::VirtualizationService::new().context("Failed to spawn VirtualizationService")?;
+    let service = virtmgr.connect().context("Failed to connect to VirtualizationService")?;
+
     let rialto = File::open(RIALTO_PATH).context("Failed to open Rialto kernel binary")?;
     let console = android_log_fd()?;
     let log = android_log_fd()?;
diff --git a/tests/aidl/com/android/microdroid/testservice/ITestService.aidl b/tests/aidl/com/android/microdroid/testservice/ITestService.aidl
index c936e1b..7ee1f01 100644
--- a/tests/aidl/com/android/microdroid/testservice/ITestService.aidl
+++ b/tests/aidl/com/android/microdroid/testservice/ITestService.aidl
@@ -49,4 +49,10 @@
 
     /** Returns a mask of effective capabilities that the process running the payload binary has. */
     String[] getEffectiveCapabilities();
+
+    /* write the content into the specified file. */
+    void writeToFile(String content, String path);
+
+    /* get the content of the specified file. */
+    String readFromFile(String path);
 }
diff --git a/tests/benchmark/src/java/com/android/microdroid/benchmark/MicrodroidBenchmarks.java b/tests/benchmark/src/java/com/android/microdroid/benchmark/MicrodroidBenchmarks.java
index 3c3faf2..9cfac4e 100644
--- a/tests/benchmark/src/java/com/android/microdroid/benchmark/MicrodroidBenchmarks.java
+++ b/tests/benchmark/src/java/com/android/microdroid/benchmark/MicrodroidBenchmarks.java
@@ -65,6 +65,7 @@
 
     private static final String APEX_ETC_FS = "/apex/com.android.virt/etc/fs/";
     private static final double SIZE_MB = 1024.0 * 1024.0;
+    private static final double NANO_TO_MILLI = 1000000.0;
     private static final String MICRODROID_IMG_PREFIX = "microdroid_";
     private static final String MICRODROID_IMG_SUFFIX = ".img";
 
@@ -80,10 +81,11 @@
     private Instrumentation mInstrumentation;
 
     @Before
-    public void setup() {
+    public void setup() throws IOException {
         grantPermission(VirtualMachine.MANAGE_VIRTUAL_MACHINE_PERMISSION);
         grantPermission(VirtualMachine.USE_CUSTOM_VIRTUAL_MACHINE_PERMISSION);
         prepareTestSetup(mProtectedVm);
+        setMaxPerformanceTaskProfile();
         mInstrumentation = getInstrumentation();
     }
 
@@ -140,19 +142,12 @@
 
         final int trialCount = 10;
 
-        List<Double> vmStartingTimeMetrics = new ArrayList<>();
         List<Double> bootTimeMetrics = new ArrayList<>();
-        List<Double> bootloaderTimeMetrics = new ArrayList<>();
-        List<Double> kernelBootTimeMetrics = new ArrayList<>();
-        List<Double> userspaceBootTimeMetrics = new ArrayList<>();
-
         for (int i = 0; i < trialCount; i++) {
-
-            // To grab boot events from log, set debug mode to FULL
             VirtualMachineConfig normalConfig =
                     newVmConfigBuilder()
                             .setPayloadBinaryPath("MicrodroidIdleNativeLib.so")
-                            .setDebugLevel(DEBUG_LEVEL_FULL)
+                            .setDebugLevel(DEBUG_LEVEL_NONE)
                             .setMemoryMib(256)
                             .build();
             forceCreateNewVirtualMachine("test_vm_boot_time", normalConfig);
@@ -160,12 +155,74 @@
             BootResult result = tryBootVm(TAG, "test_vm_boot_time");
             assertThat(result.payloadStarted).isTrue();
 
-            final double nanoToMilli = 1000000.0;
-            vmStartingTimeMetrics.add(result.getVMStartingElapsedNanoTime() / nanoToMilli);
-            bootTimeMetrics.add(result.endToEndNanoTime / nanoToMilli);
-            bootloaderTimeMetrics.add(result.getBootloaderElapsedNanoTime() / nanoToMilli);
-            kernelBootTimeMetrics.add(result.getKernelElapsedNanoTime() / nanoToMilli);
-            userspaceBootTimeMetrics.add(result.getUserspaceElapsedNanoTime() / nanoToMilli);
+            bootTimeMetrics.add(result.endToEndNanoTime / NANO_TO_MILLI);
+        }
+
+        reportMetrics(bootTimeMetrics, "boot_time", "ms");
+    }
+
+    @Test
+    public void testMicrodroidMulticoreBootTime()
+            throws VirtualMachineException, InterruptedException, IOException {
+        assume().withMessage("Skip on CF; too slow").that(isCuttlefish()).isFalse();
+
+        final int trialCount = 10;
+        final int[] trialNumCpus = {2, 4, 8};
+
+        for (int numCpus : trialNumCpus) {
+            List<Double> bootTimeMetrics = new ArrayList<>();
+            for (int i = 0; i < trialCount; i++) {
+                VirtualMachineConfig normalConfig =
+                        newVmConfigBuilder()
+                                .setPayloadBinaryPath("MicrodroidIdleNativeLib.so")
+                                .setDebugLevel(DEBUG_LEVEL_NONE)
+                                .setMemoryMib(256)
+                                .setNumCpus(numCpus)
+                                .build();
+                forceCreateNewVirtualMachine("test_vm_boot_time_multicore", normalConfig);
+
+                BootResult result = tryBootVm(TAG, "test_vm_boot_time_multicore");
+                assertThat(result.payloadStarted).isTrue();
+
+                bootTimeMetrics.add(result.endToEndNanoTime / NANO_TO_MILLI);
+            }
+
+            String metricName = "boot_time_" + numCpus + "cpus";
+            reportMetrics(bootTimeMetrics, metricName, "ms");
+        }
+    }
+
+    @Test
+    public void testMicrodroidDebugBootTime()
+            throws VirtualMachineException, InterruptedException, IOException {
+        assume().withMessage("Skip on CF; too slow").that(isCuttlefish()).isFalse();
+
+        final int trialCount = 10;
+
+        List<Double> vmStartingTimeMetrics = new ArrayList<>();
+        List<Double> bootTimeMetrics = new ArrayList<>();
+        List<Double> bootloaderTimeMetrics = new ArrayList<>();
+        List<Double> kernelBootTimeMetrics = new ArrayList<>();
+        List<Double> userspaceBootTimeMetrics = new ArrayList<>();
+
+        for (int i = 0; i < trialCount; i++) {
+            // To grab boot events from log, set debug mode to FULL
+            VirtualMachineConfig normalConfig =
+                    newVmConfigBuilder()
+                            .setPayloadBinaryPath("MicrodroidIdleNativeLib.so")
+                            .setDebugLevel(DEBUG_LEVEL_FULL)
+                            .setMemoryMib(256)
+                            .build();
+            forceCreateNewVirtualMachine("test_vm_boot_time_debug", normalConfig);
+
+            BootResult result = tryBootVm(TAG, "test_vm_boot_time_debug");
+            assertThat(result.payloadStarted).isTrue();
+
+            vmStartingTimeMetrics.add(result.getVMStartingElapsedNanoTime() / NANO_TO_MILLI);
+            bootTimeMetrics.add(result.endToEndNanoTime / NANO_TO_MILLI);
+            bootloaderTimeMetrics.add(result.getBootloaderElapsedNanoTime() / NANO_TO_MILLI);
+            kernelBootTimeMetrics.add(result.getKernelElapsedNanoTime() / NANO_TO_MILLI);
+            userspaceBootTimeMetrics.add(result.getUserspaceElapsedNanoTime() / NANO_TO_MILLI);
         }
 
         reportMetrics(vmStartingTimeMetrics, "vm_starting_time", "ms");
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 9aed34d..d762310 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
@@ -24,6 +24,7 @@
 import android.content.Context;
 import android.os.ParcelFileDescriptor;
 import android.os.SystemProperties;
+import android.system.Os;
 import android.system.virtualmachine.VirtualMachine;
 import android.system.virtualmachine.VirtualMachineCallback;
 import android.system.virtualmachine.VirtualMachineConfig;
@@ -50,6 +51,8 @@
 import java.util.concurrent.TimeUnit;
 
 public abstract class MicrodroidDeviceTestBase {
+    private final String MAX_PERFORMANCE_TASK_PROFILE = "CPUSET_SP_TOP_APP";
+
     public static boolean isCuttlefish() {
         return DeviceProperties.create(SystemProperties::get).isCuttlefish();
     }
@@ -73,6 +76,17 @@
                 permission);
     }
 
+    protected final void setMaxPerformanceTaskProfile() throws IOException {
+        Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation();
+        UiAutomation uiAutomation = instrumentation.getUiAutomation();
+        String cmd = "settaskprofile " + Os.gettid() + " " + MAX_PERFORMANCE_TASK_PROFILE;
+        String out = runInShell("MicrodroidDeviceTestBase", uiAutomation, cmd).trim();
+        String expect = "Profile " + MAX_PERFORMANCE_TASK_PROFILE + " is applied successfully!";
+        if (!expect.equals(out)) {
+            throw new IOException("Could not apply max performance task profile: " + out);
+        }
+    }
+
     private Context mCtx;
     private boolean mProtectedVm;
 
@@ -126,6 +140,12 @@
         }
     }
 
+    protected enum EncryptedStoreOperation {
+        NONE,
+        READ,
+        WRITE,
+    }
+
     public abstract static class VmEventListener implements VirtualMachineCallback {
         private ExecutorService mExecutorService = Executors.newSingleThreadExecutor();
         private OptionalLong mVcpuStartedNanoTime = OptionalLong.empty();
diff --git a/tests/testapk/assets/file.txt b/tests/testapk/assets/file.txt
new file mode 100644
index 0000000..4de897c
--- /dev/null
+++ b/tests/testapk/assets/file.txt
@@ -0,0 +1 @@
+Hello, I am a file!
\ No newline at end of file
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 160b679..0e94ba2 100644
--- a/tests/testapk/src/java/com/android/microdroid/test/MicrodroidTests.java
+++ b/tests/testapk/src/java/com/android/microdroid/test/MicrodroidTests.java
@@ -20,6 +20,8 @@
 import static android.system.virtualmachine.VirtualMachine.STATUS_STOPPED;
 import static android.system.virtualmachine.VirtualMachineConfig.DEBUG_LEVEL_FULL;
 import static android.system.virtualmachine.VirtualMachineConfig.DEBUG_LEVEL_NONE;
+import static android.system.virtualmachine.VirtualMachineManager.CAPABILITY_NON_PROTECTED_VM;
+import static android.system.virtualmachine.VirtualMachineManager.CAPABILITY_PROTECTED_VM;
 
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.common.truth.TruthJUnit.assume;
@@ -48,6 +50,8 @@
 import com.android.microdroid.test.device.MicrodroidDeviceTestBase;
 import com.android.microdroid.testservice.ITestService;
 
+import com.google.common.truth.BooleanSubject;
+
 import org.junit.After;
 import org.junit.Before;
 import org.junit.Ignore;
@@ -113,6 +117,7 @@
 
     private static final int MIN_MEM_ARM64 = 150;
     private static final int MIN_MEM_X86_64 = 196;
+    private static final String EXAMPLE_STRING = "Literally any string!! :)";
 
     @Test
     @CddTest(requirements = {"9.17/C-1-1", "9.17/C-2-1"})
@@ -313,8 +318,8 @@
 
     @Test
     @CddTest(requirements = {"9.17/C-1-1"})
-    public void vmConfigUnitTests() {
-
+    public void vmConfigGetAndSetTests() {
+        // Minimal has as little as specified as possible; everything that can be is defaulted.
         VirtualMachineConfig.Builder minimalBuilder = newVmConfigBuilder();
         VirtualMachineConfig minimal = minimalBuilder.setPayloadBinaryPath("binary/path").build();
 
@@ -328,6 +333,8 @@
         assertThat(minimal.isEncryptedStorageEnabled()).isFalse();
         assertThat(minimal.getEncryptedStorageKib()).isEqualTo(0);
 
+        // Maximal has everything that can be set to some non-default value. (And has different
+        // values than minimal for the required fields.)
         int maxCpus = Runtime.getRuntime().availableProcessors();
         VirtualMachineConfig.Builder maximalBuilder =
                 newVmConfigBuilder()
@@ -352,18 +359,11 @@
         assertThat(minimal.isCompatibleWith(maximal)).isFalse();
         assertThat(minimal.isCompatibleWith(minimal)).isTrue();
         assertThat(maximal.isCompatibleWith(maximal)).isTrue();
-
-        VirtualMachineConfig compatible = maximalBuilder.setNumCpus(1).setMemoryMib(99).build();
-        assertThat(compatible.isCompatibleWith(maximal)).isTrue();
-
-        // Assert that different encrypted storage size would imply the configs are incompatible
-        VirtualMachineConfig incompatible = minimalBuilder.setEncryptedStorageKib(1048).build();
-        assertThat(incompatible.isCompatibleWith(minimal)).isFalse();
     }
 
     @Test
     @CddTest(requirements = {"9.17/C-1-1"})
-    public void vmConfigBuilderUnitTests() {
+    public void vmConfigBuilderValidationTests() {
         VirtualMachineConfig.Builder builder = newVmConfigBuilder();
 
         // All your null are belong to me.
@@ -394,6 +394,44 @@
 
     @Test
     @CddTest(requirements = {"9.17/C-1-1"})
+    public void compatibleConfigTests() throws Exception {
+        int maxCpus = Runtime.getRuntime().availableProcessors();
+
+        VirtualMachineConfig.Builder builder =
+                newVmConfigBuilder().setPayloadBinaryPath("binary/path").setApkPath("/apk/path");
+        VirtualMachineConfig baseline = builder.build();
+
+        // A config must be compatible with itself
+        assertConfigCompatible(baseline, builder).isTrue();
+
+        // Changes that must always be compatible
+        assertConfigCompatible(baseline, builder.setMemoryMib(99)).isTrue();
+        if (maxCpus > 1) {
+            assertConfigCompatible(baseline, builder.setNumCpus(2)).isTrue();
+        }
+
+        // Changes that must be incompatible, since they must change the VM identity.
+        assertConfigCompatible(baseline, builder.setDebugLevel(DEBUG_LEVEL_FULL)).isFalse();
+        assertConfigCompatible(baseline, builder.setPayloadBinaryPath("different")).isFalse();
+        int capabilities = getVirtualMachineManager().getCapabilities();
+        if ((capabilities & CAPABILITY_PROTECTED_VM) != 0
+                && (capabilities & CAPABILITY_NON_PROTECTED_VM) != 0) {
+            assertConfigCompatible(baseline, builder.setProtectedVm(!isProtectedVm())).isFalse();
+        }
+
+        // Changes that are currently incompatible for ease of implementation, but this might change
+        // in the future.
+        assertConfigCompatible(baseline, builder.setApkPath("/different")).isFalse();
+        assertConfigCompatible(baseline, builder.setEncryptedStorageKib(100)).isFalse();
+    }
+
+    private BooleanSubject assertConfigCompatible(
+            VirtualMachineConfig baseline, VirtualMachineConfig.Builder builder) {
+        return assertThat(builder.build().isCompatibleWith(baseline));
+    }
+
+    @Test
+    @CddTest(requirements = {"9.17/C-1-1"})
     public void vmUnitTests() throws Exception {
         VirtualMachineConfig.Builder builder =
                 newVmConfigBuilder().setPayloadBinaryPath("binary/path");
@@ -1142,6 +1180,53 @@
         assertThat(testResults.mEffectiveCapabilities).isEmpty();
     }
 
+    @Test
+    @CddTest(requirements = {"9.17/C-1-1", "9.17/C-2-1"})
+    public void encryptedStorageIsPersistent() throws Exception {
+        assumeSupportedKernel();
+
+        VirtualMachineConfig config =
+                newVmConfigBuilder()
+                        .setPayloadBinaryPath("MicrodroidTestNativeLib.so")
+                        .setMemoryMib(minMemoryRequired())
+                        .setEncryptedStorageKib(4096)
+                        .setDebugLevel(DEBUG_LEVEL_FULL)
+                        .build();
+        VirtualMachine vm = forceCreateNewVirtualMachine("test_vm_a", config);
+        TestResults testResults = runVmTestService(vm, EncryptedStoreOperation.WRITE);
+        assertThat(testResults.mException).isNull();
+
+        // Re-run the same VM & verify the file persisted. Note, the previous `runVmTestService`
+        // stopped the VM
+        testResults = runVmTestService(vm, EncryptedStoreOperation.READ);
+        assertThat(testResults.mException).isNull();
+        assertThat(testResults.mFileContent).isEqualTo(EXAMPLE_STRING);
+    }
+
+    @Test
+    @CddTest(requirements = {"9.17/C-1-1", "9.17/C-2-1"})
+    public void canReadFileFromAssets_debugFull() throws Exception {
+        assumeSupportedKernel();
+
+        VirtualMachineConfig config =
+                newVmConfigBuilder()
+                        .setPayloadBinaryPath("MicrodroidTestNativeLib.so")
+                        .setMemoryMib(minMemoryRequired())
+                        .setDebugLevel(DEBUG_LEVEL_FULL)
+                        .build();
+        VirtualMachine vm = forceCreateNewVirtualMachine("test_vm_read_from_assets", config);
+
+        TestResults testResults =
+                runVmTestService(
+                        vm,
+                        (testService, ts) -> {
+                            ts.mFileContent = testService.readFromFile("/mnt/apk/assets/file.txt");
+                        });
+
+        assertThat(testResults.mException).isNull();
+        assertThat(testResults.mFileContent).isEqualTo("Hello, I am a file!");
+    }
+
     private void assertFileContentsAreEqualInTwoVms(String fileName, String vmName1, String vmName2)
             throws IOException {
         File file1 = getVmFile(vmName1, fileName);
@@ -1197,9 +1282,15 @@
         String mApkContentsPath;
         String mEncryptedStoragePath;
         String[] mEffectiveCapabilities;
+        String mFileContent;
     }
 
     private TestResults runVmTestService(VirtualMachine vm) throws Exception {
+        return runVmTestService(vm, EncryptedStoreOperation.NONE);
+    }
+
+    private TestResults runVmTestService(VirtualMachine vm, EncryptedStoreOperation mode)
+            throws Exception {
         CompletableFuture<Boolean> payloadStarted = new CompletableFuture<>();
         CompletableFuture<Boolean> payloadReady = new CompletableFuture<>();
         TestResults testResults = new TestResults();
@@ -1222,6 +1313,14 @@
                                     testService.getEncryptedStoragePath();
                             testResults.mEffectiveCapabilities =
                                     testService.getEffectiveCapabilities();
+                            if (mode == EncryptedStoreOperation.WRITE) {
+                                testService.writeToFile(
+                                        /*content*/ EXAMPLE_STRING,
+                                        /*path*/ "/mnt/encryptedstore/test_file");
+                            } else if (mode == EncryptedStoreOperation.READ) {
+                                testResults.mFileContent =
+                                        testService.readFromFile("/mnt/encryptedstore/test_file");
+                            }
                         } catch (Exception e) {
                             testResults.mException = e;
                         }
@@ -1246,4 +1345,47 @@
         assertThat(payloadReady.getNow(false)).isTrue();
         return testResults;
     }
+
+    private TestResults runVmTestService(VirtualMachine vm, RunTestsAgainstTestService testsToRun)
+            throws Exception {
+        CompletableFuture<Boolean> payloadStarted = new CompletableFuture<>();
+        CompletableFuture<Boolean> payloadReady = new CompletableFuture<>();
+        TestResults testResults = new TestResults();
+        VmEventListener listener =
+                new VmEventListener() {
+                    private void testVMService(VirtualMachine vm) {
+                        try {
+                            ITestService testService =
+                                    ITestService.Stub.asInterface(
+                                            vm.connectToVsockServer(ITestService.SERVICE_PORT));
+                            testsToRun.runTests(testService, testResults);
+                        } catch (Exception e) {
+                            testResults.mException = e;
+                        }
+                    }
+
+                    @Override
+                    public void onPayloadReady(VirtualMachine vm) {
+                        Log.i(TAG, "onPayloadReady");
+                        payloadReady.complete(true);
+                        testVMService(vm);
+                        forceStop(vm);
+                    }
+
+                    @Override
+                    public void onPayloadStarted(VirtualMachine vm) {
+                        Log.i(TAG, "onPayloadStarted");
+                        payloadStarted.complete(true);
+                    }
+                };
+        listener.runToFinish(TAG, vm);
+        assertThat(payloadStarted.getNow(false)).isTrue();
+        assertThat(payloadReady.getNow(false)).isTrue();
+        return testResults;
+    }
+
+    @FunctionalInterface
+    interface RunTestsAgainstTestService {
+        void runTests(ITestService testService, TestResults testResults) throws Exception;
+    }
 }
diff --git a/tests/testapk/src/native/testbinary.cpp b/tests/testapk/src/native/testbinary.cpp
index da408e4..4ba502a 100644
--- a/tests/testapk/src/native/testbinary.cpp
+++ b/tests/testapk/src/native/testbinary.cpp
@@ -232,6 +232,29 @@
                 return ScopedAStatus::fromServiceSpecificErrorWithMessage(-1, message.c_str());
             }
         }
+
+        ScopedAStatus writeToFile(const std::string& content, const std::string& path) override {
+            if (!android::base::WriteStringToFile(content, path)) {
+                std::string msg = "Failed to write " + content + " to file " + path +
+                        ". Errono: " + std::to_string(errno);
+                return ScopedAStatus::fromExceptionCodeWithMessage(EX_SERVICE_SPECIFIC,
+                                                                   msg.c_str());
+            }
+            // TODO(b/264520098): Remove sync() once TestService supports quit() method
+            // and Microdroid manager flushes filesystem caches on shutdown.
+            sync();
+            return ScopedAStatus::ok();
+        }
+
+        ScopedAStatus readFromFile(const std::string& path, std::string* out) override {
+            if (!android::base::ReadFileToString(path, out)) {
+                std::string msg =
+                        "Failed to read " + path + " to string. Errono: " + std::to_string(errno);
+                return ScopedAStatus::fromExceptionCodeWithMessage(EX_SERVICE_SPECIFIC,
+                                                                   msg.c_str());
+            }
+            return ScopedAStatus::ok();
+        }
     };
     auto testService = ndk::SharedRefBase::make<TestService>();
 
diff --git a/virtualizationservice/aidl/android/system/virtualizationservice_internal/IGlobalVmContext.aidl b/virtualizationservice/aidl/android/system/virtualizationservice_internal/IGlobalVmContext.aidl
index 1a7aa4a..a4d5d19 100644
--- a/virtualizationservice/aidl/android/system/virtualizationservice_internal/IGlobalVmContext.aidl
+++ b/virtualizationservice/aidl/android/system/virtualizationservice_internal/IGlobalVmContext.aidl
@@ -18,4 +18,7 @@
 interface IGlobalVmContext {
     /** Get the CID allocated to the VM. */
     int getCid();
+
+    /** Get the path to the temporary folder of the VM. */
+    String getTemporaryDirectory();
 }
diff --git a/virtualizationservice/aidl/android/system/virtualizationservice_internal/IVirtualizationServiceInternal.aidl b/virtualizationservice/aidl/android/system/virtualizationservice_internal/IVirtualizationServiceInternal.aidl
index ff56b68..d6b3536 100644
--- a/virtualizationservice/aidl/android/system/virtualizationservice_internal/IVirtualizationServiceInternal.aidl
+++ b/virtualizationservice/aidl/android/system/virtualizationservice_internal/IVirtualizationServiceInternal.aidl
@@ -22,6 +22,13 @@
 
 interface IVirtualizationServiceInternal {
     /**
+     * Removes the memlock rlimit of the calling process.
+     *
+     * The SELinux policy only allows this to succeed for virtmgr callers.
+     */
+    void removeMemlockRlimit();
+
+    /**
      * Allocates global context for a new VM.
      *
      * This allocates VM's globally unique resources such as the CID.
diff --git a/virtualizationservice/src/aidl.rs b/virtualizationservice/src/aidl.rs
index d7c7125..733d9c5 100644
--- a/virtualizationservice/src/aidl.rs
+++ b/virtualizationservice/src/aidl.rs
@@ -47,7 +47,7 @@
     AtomVmCreationRequested::AtomVmCreationRequested,
     AtomVmExited::AtomVmExited,
     IGlobalVmContext::{BnGlobalVmContext, IGlobalVmContext},
-    IVirtualizationServiceInternal::{BnVirtualizationServiceInternal, IVirtualizationServiceInternal},
+    IVirtualizationServiceInternal::IVirtualizationServiceInternal,
 };
 use android_system_virtualmachineservice::aidl::android::system::virtualmachineservice::IVirtualMachineService::{
         BnVirtualMachineService, IVirtualMachineService, VM_TOMBSTONES_SERVICE_PORT,
@@ -55,8 +55,8 @@
 use anyhow::{anyhow, bail, Context, Result};
 use apkverify::{HashAlgorithm, V4Signature};
 use binder::{
-    self, BinderFeatures, ExceptionCode, Interface, LazyServiceGuard, ParcelFileDescriptor,
-    Status, StatusCode, Strong,
+    self, wait_for_interface, BinderFeatures, ExceptionCode, Interface, LazyServiceGuard,
+    ParcelFileDescriptor, Status, StatusCode, Strong,
 };
 use disk::QcowFile;
 use lazy_static::lazy_static;
@@ -69,9 +69,10 @@
 use std::collections::HashMap;
 use std::convert::TryInto;
 use std::ffi::CStr;
-use std::fs::{create_dir, File, OpenOptions};
+use std::fs::{create_dir, read_dir, remove_dir, remove_file, set_permissions, File, OpenOptions, Permissions};
 use std::io::{Error, ErrorKind, Read, Write};
 use std::num::NonZeroU32;
+use std::os::unix::fs::PermissionsExt;
 use std::os::unix::io::{FromRawFd, IntoRawFd};
 use std::path::{Path, PathBuf};
 use std::sync::{Arc, Mutex, Weak};
@@ -79,10 +80,13 @@
 use vmconfig::VmConfig;
 use vsock::{VsockListener, VsockStream};
 use zip::ZipArchive;
+use nix::unistd::{chown, Uid};
 
 /// The unique ID of a VM used (together with a port number) for vsock communication.
 pub type Cid = u32;
 
+pub const BINDER_SERVICE_IDENTIFIER: &str = "android.system.virtualizationservice";
+
 /// Directory in which to write disk image files used while running VMs.
 pub const TEMPORARY_DIRECTORY: &str = "/data/misc/virtualizationservice";
 
@@ -110,10 +114,9 @@
 const UNFORMATTED_STORAGE_MAGIC: &str = "UNFORMATTED-STORAGE";
 
 lazy_static! {
-    pub static ref GLOBAL_SERVICE: Strong<dyn IVirtualizationServiceInternal> = {
-        let service = VirtualizationServiceInternal::init();
-        BnVirtualizationServiceInternal::new_binder(service, BinderFeatures::default())
-    };
+    pub static ref GLOBAL_SERVICE: Strong<dyn IVirtualizationServiceInternal> =
+        wait_for_interface(BINDER_SERVICE_IDENTIFIER)
+            .expect("Could not connect to VirtualizationServiceInternal");
 }
 
 fn is_valid_guest_cid(cid: Cid) -> bool {
@@ -155,6 +158,9 @@
 }
 
 impl VirtualizationServiceInternal {
+    // TODO(b/245727626): Remove after the source files for virtualizationservice
+    // and virtmgr binaries are split from each other.
+    #[allow(dead_code)]
     pub fn init() -> VirtualizationServiceInternal {
         let service = VirtualizationServiceInternal::default();
 
@@ -171,12 +177,32 @@
 impl Interface for VirtualizationServiceInternal {}
 
 impl IVirtualizationServiceInternal for VirtualizationServiceInternal {
+    fn removeMemlockRlimit(&self) -> binder::Result<()> {
+        let pid = get_calling_pid();
+        let lim = libc::rlimit { rlim_cur: libc::RLIM_INFINITY, rlim_max: libc::RLIM_INFINITY };
+
+        // SAFETY - borrowing the new limit struct only
+        let ret = unsafe { libc::prlimit(pid, libc::RLIMIT_MEMLOCK, &lim, std::ptr::null_mut()) };
+
+        match ret {
+            0 => Ok(()),
+            -1 => Err(Status::new_exception_str(
+                ExceptionCode::ILLEGAL_STATE,
+                Some(std::io::Error::last_os_error().to_string()),
+            )),
+            n => Err(Status::new_exception_str(
+                ExceptionCode::ILLEGAL_STATE,
+                Some(format!("Unexpected return value from prlimit(): {n}")),
+            )),
+        }
+    }
+
     fn allocateGlobalVmContext(&self) -> binder::Result<Strong<dyn IGlobalVmContext>> {
+        let client_uid = Uid::from_raw(get_calling_uid());
         let state = &mut *self.state.lock().unwrap();
-        let cid = state.allocate_cid().map_err(|e| {
+        state.allocate_vm_context(client_uid).map_err(|e| {
             Status::new_exception_str(ExceptionCode::ILLEGAL_STATE, Some(e.to_string()))
-        })?;
-        Ok(GlobalVmContext::create(cid))
+        })
     }
 
     fn atomVmBooted(&self, atom: &AtomVmBooted) -> Result<(), Status> {
@@ -258,6 +284,50 @@
     {
         range.find(|cid| !self.held_cids.contains_key(cid))
     }
+
+    fn allocate_vm_context(&mut self, client_uid: Uid) -> Result<Strong<dyn IGlobalVmContext>> {
+        let cid = self.allocate_cid()?;
+        let temp_dir = create_vm_directory(client_uid, *cid)?;
+        let binder = GlobalVmContext { cid, temp_dir, ..Default::default() };
+        Ok(BnGlobalVmContext::new_binder(binder, BinderFeatures::default()))
+    }
+}
+
+fn create_vm_directory(client_uid: Uid, cid: Cid) -> Result<PathBuf> {
+    let path: PathBuf = format!("{}/{}", TEMPORARY_DIRECTORY, cid).into();
+    if path.as_path().exists() {
+        remove_temporary_dir(&path).unwrap_or_else(|e| {
+            warn!("Could not delete temporary directory {:?}: {}", path, e);
+        });
+    }
+    // Create a directory that is owned by client's UID but system's GID, and permissions 0700.
+    // If the chown() fails, this will leave behind an empty directory that will get removed
+    // at the next attempt, or if virtualizationservice is restarted.
+    create_dir(&path)
+        .with_context(|| format!("Could not create temporary directory {:?}", path))?;
+    chown(&path, Some(client_uid), None)
+        .with_context(|| format!("Could not set ownership of temporary directory {:?}", path))?;
+    Ok(path)
+}
+
+/// Removes a directory owned by a different user by first changing its owner back
+/// to VirtualizationService.
+pub fn remove_temporary_dir(path: &PathBuf) -> Result<()> {
+    if !path.as_path().is_dir() {
+        bail!("Path {:?} is not a directory", path);
+    }
+    chown(path, Some(Uid::current()), None)?;
+    set_permissions(path, Permissions::from_mode(0o700))?;
+    remove_temporary_files(path)?;
+    remove_dir(path)?;
+    Ok(())
+}
+
+pub fn remove_temporary_files(path: &PathBuf) -> Result<()> {
+    for dir_entry in read_dir(path)? {
+        remove_file(dir_entry?.path())?;
+    }
+    Ok(())
 }
 
 /// Implementation of the AIDL `IGlobalVmContext` interface.
@@ -265,24 +335,23 @@
 struct GlobalVmContext {
     /// The unique CID assigned to the VM for vsock communication.
     cid: Arc<Cid>,
+    /// The temporary folder created for the VM and owned by the creator's UID.
+    temp_dir: PathBuf,
     /// Keeps our service process running as long as this VM context exists.
     #[allow(dead_code)]
     lazy_service_guard: LazyServiceGuard,
 }
 
-impl GlobalVmContext {
-    fn create(cid: Arc<Cid>) -> Strong<dyn IGlobalVmContext> {
-        let binder = GlobalVmContext { cid, ..Default::default() };
-        BnGlobalVmContext::new_binder(binder, BinderFeatures::default())
-    }
-}
-
 impl Interface for GlobalVmContext {}
 
 impl IGlobalVmContext for GlobalVmContext {
     fn getCid(&self) -> binder::Result<i32> {
         Ok(*self.cid as i32)
     }
+
+    fn getTemporaryDirectory(&self) -> binder::Result<String> {
+        Ok(self.temp_dir.to_string_lossy().to_string())
+    }
 }
 
 /// Implementation of `IVirtualizationService`, the entry point of the AIDL service.
@@ -488,16 +557,20 @@
 }
 
 impl VirtualizationService {
+    // TODO(b/245727626): Remove after the source files for virtualizationservice
+    // and virtmgr binaries are split from each other.
+    #[allow(dead_code)]
     pub fn init() -> VirtualizationService {
         VirtualizationService::default()
     }
 
-    fn create_vm_context(&self) -> Result<(VmContext, Cid)> {
+    fn create_vm_context(&self) -> Result<(VmContext, Cid, PathBuf)> {
         const NUM_ATTEMPTS: usize = 5;
 
         for _ in 0..NUM_ATTEMPTS {
             let global_context = GLOBAL_SERVICE.allocateGlobalVmContext()?;
             let cid = global_context.getCid()? as Cid;
+            let temp_dir: PathBuf = global_context.getTemporaryDirectory()?.into();
             let service = VirtualMachineService::new_binder(self.state.clone(), cid).as_binder();
 
             // Start VM service listening for connections from the new CID on port=CID.
@@ -505,7 +578,7 @@
             match RpcServer::new_vsock(service, cid, port) {
                 Ok(vm_server) => {
                     vm_server.start();
-                    return Ok((VmContext::new(global_context, vm_server), cid));
+                    return Ok((VmContext::new(global_context, vm_server), cid, temp_dir));
                 }
                 Err(err) => {
                     warn!("Could not start RpcServer on port {}: {}", port, err);
@@ -538,7 +611,7 @@
             check_use_custom_virtual_machine()?;
         }
 
-        let (vm_context, cid) = self.create_vm_context().map_err(|e| {
+        let (vm_context, cid, temporary_directory) = self.create_vm_context().map_err(|e| {
             error!("Failed to create VmContext: {:?}", e);
             Status::new_service_specific_error_str(
                 -1,
@@ -558,24 +631,6 @@
         // child process, and not closed before it is started.
         let mut indirect_files = vec![];
 
-        // Make directory for temporary files.
-        let temporary_directory: PathBuf = format!("{}/{}", TEMPORARY_DIRECTORY, cid).into();
-        create_dir(&temporary_directory).map_err(|e| {
-            // At this point, we do not know the protected status of Vm
-            // setting it to false, though this may not be correct.
-            error!(
-                "Failed to create temporary directory {:?} for VM files: {:?}",
-                temporary_directory, e
-            );
-            Status::new_service_specific_error_str(
-                -1,
-                Some(format!(
-                    "Failed to create temporary directory {:?} for VM files: {:?}",
-                    temporary_directory, e
-                )),
-            )
-        })?;
-
         let (is_app_config, config) = match config {
             VirtualMachineConfig::RawConfig(config) => (false, BorrowedOrOwned::Borrowed(config)),
             VirtualMachineConfig::AppConfig(config) => {
@@ -947,15 +1002,11 @@
 #[derive(Debug)]
 struct VirtualMachine {
     instance: Arc<VmInstance>,
-    /// Keeps our service process running as long as this VM instance exists.
-    #[allow(dead_code)]
-    lazy_service_guard: LazyServiceGuard,
 }
 
 impl VirtualMachine {
     fn create(instance: Arc<VmInstance>) -> Strong<dyn IVirtualMachine> {
-        let binder = VirtualMachine { instance, lazy_service_guard: Default::default() };
-        BnVirtualMachine::new_binder(binder, BinderFeatures::default())
+        BnVirtualMachine::new_binder(VirtualMachine { instance }, BinderFeatures::default())
     }
 }
 
diff --git a/virtualizationservice/src/crosvm.rs b/virtualizationservice/src/crosvm.rs
index 94248f8..98e7d99 100644
--- a/virtualizationservice/src/crosvm.rs
+++ b/virtualizationservice/src/crosvm.rs
@@ -14,7 +14,7 @@
 
 //! Functions for running instances of `crosvm`.
 
-use crate::aidl::{Cid, VirtualMachineCallbacks};
+use crate::aidl::{remove_temporary_files, Cid, VirtualMachineCallbacks};
 use crate::atom::write_vm_exited_stats;
 use anyhow::{anyhow, bail, Context, Error, Result};
 use command_fds::CommandFdExt;
@@ -29,7 +29,7 @@
 use std::borrow::Cow;
 use std::cmp::max;
 use std::fmt;
-use std::fs::{read_to_string, remove_dir_all, File};
+use std::fs::{read_to_string, File};
 use std::io::{self, Read};
 use std::mem;
 use std::num::NonZeroU32;
@@ -379,10 +379,10 @@
             &*vm_metric,
         );
 
-        // Delete temporary files.
-        if let Err(e) = remove_dir_all(&self.temporary_directory) {
-            error!("Error removing temporary directory {:?}: {}", self.temporary_directory, e);
-        }
+        // Delete temporary files. The folder itself is removed by VirtualizationServiceInternal.
+        remove_temporary_files(&self.temporary_directory).unwrap_or_else(|e| {
+            error!("Error removing temporary files from {:?}: {}", self.temporary_directory, e);
+        });
     }
 
     /// Waits until payload is started, or timeout expires. When timeout occurs, kill
diff --git a/virtualizationservice/src/main.rs b/virtualizationservice/src/main.rs
index 9272e65..35eeff3 100644
--- a/virtualizationservice/src/main.rs
+++ b/virtualizationservice/src/main.rs
@@ -21,19 +21,17 @@
 mod payload;
 mod selinux;
 
-use crate::aidl::{VirtualizationService, TEMPORARY_DIRECTORY};
+use crate::aidl::{remove_temporary_dir, BINDER_SERVICE_IDENTIFIER, TEMPORARY_DIRECTORY, VirtualizationServiceInternal};
 use android_logger::{Config, FilterBuilder};
-use android_system_virtualizationservice::aidl::android::system::virtualizationservice::IVirtualizationService::BnVirtualizationService;
-use anyhow::{bail, Context, Error};
+use android_system_virtualizationservice_internal::aidl::android::system::virtualizationservice_internal::IVirtualizationServiceInternal::BnVirtualizationServiceInternal;
+use anyhow::Error;
 use binder::{register_lazy_service, BinderFeatures, ProcessState, ThreadState};
 use log::{info, Level};
-use std::fs::{remove_dir_all, remove_file, read_dir};
+use std::fs::read_dir;
 use std::os::unix::raw::{pid_t, uid_t};
 
 const LOG_TAG: &str = "VirtualizationService";
 
-const BINDER_SERVICE_IDENTIFIER: &str = "android.system.virtualizationservice";
-
 fn get_calling_pid() -> pid_t {
     ThreadState::get_calling_pid()
 }
@@ -55,38 +53,19 @@
             ),
     );
 
-    remove_memlock_rlimit().expect("Failed to remove memlock rlimit");
     clear_temporary_files().expect("Failed to delete old temporary files");
 
-    let service = VirtualizationService::init();
-    let service = BnVirtualizationService::new_binder(service, BinderFeatures::default());
+    let service = VirtualizationServiceInternal::init();
+    let service = BnVirtualizationServiceInternal::new_binder(service, BinderFeatures::default());
     register_lazy_service(BINDER_SERVICE_IDENTIFIER, service.as_binder()).unwrap();
     info!("Registered Binder service, joining threadpool.");
     ProcessState::join_thread_pool();
 }
 
-/// Set this PID's RLIMIT_MEMLOCK to RLIM_INFINITY to allow crosvm (a child process) to mlock()
-/// arbitrary amounts of memory. This is necessary for spawning protected VMs.
-fn remove_memlock_rlimit() -> Result<(), Error> {
-    let lim = libc::rlimit { rlim_cur: libc::RLIM_INFINITY, rlim_max: libc::RLIM_INFINITY };
-    // SAFETY - borrowing the new limit struct only
-    match unsafe { libc::setrlimit(libc::RLIMIT_MEMLOCK, &lim) } {
-        0 => Ok(()),
-        -1 => Err(std::io::Error::last_os_error()).context("setrlimit failed"),
-        n => bail!("Unexpected return value from setrlimit(): {}", n),
-    }
-}
-
 /// Remove any files under `TEMPORARY_DIRECTORY`.
 fn clear_temporary_files() -> Result<(), Error> {
     for dir_entry in read_dir(TEMPORARY_DIRECTORY)? {
-        let dir_entry = dir_entry?;
-        let path = dir_entry.path();
-        if dir_entry.file_type()?.is_dir() {
-            remove_dir_all(path)?;
-        } else {
-            remove_file(path)?;
-        }
+        remove_temporary_dir(&dir_entry?.path())?
     }
     Ok(())
 }
diff --git a/virtualizationservice/src/virtmgr.rs b/virtualizationservice/src/virtmgr.rs
index 1aa3df9..5616097 100644
--- a/virtualizationservice/src/virtmgr.rs
+++ b/virtualizationservice/src/virtmgr.rs
@@ -21,15 +21,16 @@
 mod payload;
 mod selinux;
 
-use crate::aidl::VirtualizationService;
+use crate::aidl::{GLOBAL_SERVICE, VirtualizationService};
 use android_system_virtualizationservice::aidl::android::system::virtualizationservice::IVirtualizationService::BnVirtualizationService;
 use anyhow::{bail, Context};
-use binder::BinderFeatures;
+use binder::{BinderFeatures, ProcessState};
 use lazy_static::lazy_static;
 use log::{info, Level};
 use rpcbinder::{FileDescriptorTransportMode, RpcServer};
 use std::os::unix::io::{FromRawFd, OwnedFd, RawFd};
 use clap::Parser;
+use nix::fcntl::{fcntl, F_GETFD, F_SETFD, FdFlag};
 use nix::unistd::{Pid, Uid};
 use std::os::unix::raw::{pid_t, uid_t};
 
@@ -66,8 +67,12 @@
 
 fn take_fd_ownership(raw_fd: RawFd, owned_fds: &mut Vec<RawFd>) -> Result<OwnedFd, anyhow::Error> {
     // Basic check that the integer value does correspond to a file descriptor.
-    nix::fcntl::fcntl(raw_fd, nix::fcntl::F_GETFD)
-        .with_context(|| format!("Invalid file descriptor {raw_fd}"))?;
+    fcntl(raw_fd, F_GETFD).with_context(|| format!("Invalid file descriptor {raw_fd}"))?;
+
+    // The file descriptor had CLOEXEC disabled to be inherited from the parent.
+    // Re-enable it to make sure it is not accidentally inherited further.
+    fcntl(raw_fd, F_SETFD(FdFlag::FD_CLOEXEC))
+        .with_context(|| format!("Could not set CLOEXEC on file descriptor {raw_fd}"))?;
 
     // Creating OwnedFd for stdio FDs is not safe.
     if [libc::STDIN_FILENO, libc::STDOUT_FILENO, libc::STDERR_FILENO].contains(&raw_fd) {
@@ -102,6 +107,11 @@
     let ready_fd = take_fd_ownership(args.ready_fd, &mut owned_fds)
         .expect("Failed to take ownership of ready_fd");
 
+    // Start thread pool for kernel Binder connection to VirtualizationServiceInternal.
+    ProcessState::start_thread_pool();
+
+    GLOBAL_SERVICE.removeMemlockRlimit().expect("Failed to remove memlock rlimit");
+
     let service = VirtualizationService::init();
     let service =
         BnVirtualizationService::new_binder(service, BinderFeatures::default()).as_binder();
diff --git a/vm/TEST_MAPPING b/vm/TEST_MAPPING
new file mode 100644
index 0000000..a8d1fa6
--- /dev/null
+++ b/vm/TEST_MAPPING
@@ -0,0 +1,7 @@
+{
+  "avf-presubmit" : [
+    {
+      "name" : "vm.test"
+    }
+  ]
+}
diff --git a/vm/src/main.rs b/vm/src/main.rs
index 3d2fc00..002e505 100644
--- a/vm/src/main.rs
+++ b/vm/src/main.rs
@@ -245,7 +245,9 @@
     // We need to start the thread pool for Binder to work properly, especially link_to_death.
     ProcessState::start_thread_pool();
 
-    let service = vmclient::connect().context("Failed to find VirtualizationService")?;
+    let virtmgr =
+        vmclient::VirtualizationService::new().context("Failed to spawn VirtualizationService")?;
+    let service = virtmgr.connect().context("Failed to connect to VirtualizationService")?;
 
     match opt {
         Opt::RunApp {
diff --git a/vmbase/example/Android.bp b/vmbase/example/Android.bp
index fbad8f4..94eb21a 100644
--- a/vmbase/example/Android.bp
+++ b/vmbase/example/Android.bp
@@ -12,6 +12,7 @@
         "libaarch64_paging",
         "libbuddy_system_allocator",
         "libdice_nostd",
+        "libfdtpci",
         "liblibfdt",
         "liblog_rust_nostd",
         "libvirtio_drivers",
diff --git a/vmbase/example/src/main.rs b/vmbase/example/src/main.rs
index 888f273..ec28a11 100644
--- a/vmbase/example/src/main.rs
+++ b/vmbase/example/src/main.rs
@@ -28,18 +28,16 @@
     bionic_tls, dtb_range, print_addresses, rodata_range, stack_chk_guard, text_range,
     writable_region, DEVICE_REGION,
 };
-use crate::pci::{check_pci, pci_node, PciMemory32Allocator};
+use crate::pci::{check_pci, get_bar_region};
 use aarch64_paging::{idmap::IdMap, paging::Attributes};
 use alloc::{vec, vec::Vec};
 use buddy_system_allocator::LockedHeap;
 use core::ffi::CStr;
+use fdtpci::PciInfo;
 use libfdt::Fdt;
 use log::{debug, info, trace, LevelFilter};
 use vmbase::{logger, main, println};
 
-/// PCI MMIO configuration region size.
-const AARCH64_PCI_CFG_SIZE: u64 = 0x1000000;
-
 static INITIALISED_DATA: [u32; 4] = [1, 2, 3, 4];
 static mut ZEROED_DATA: [u32; 10] = [0; 10];
 static mut MUTABLE_DATA: [u32; 4] = [1, 2, 3, 4];
@@ -73,16 +71,8 @@
     info!("FDT passed verification.");
     check_fdt(fdt);
 
-    let pci_node = pci_node(fdt);
-    // Parse reg property to find CAM.
-    let pci_reg = pci_node.reg().unwrap().unwrap().next().unwrap();
-    debug!("Found PCI CAM at {:#x}-{:#x}", pci_reg.addr, pci_reg.addr + pci_reg.size.unwrap());
-    // Check that the CAM is the size we expect, so we don't later try accessing it beyond its
-    // bounds. If it is a different size then something is very wrong and we shouldn't continue to
-    // access it; maybe there is some new version of PCI we don't know about.
-    assert_eq!(pci_reg.size.unwrap(), AARCH64_PCI_CFG_SIZE);
-    // Parse ranges property to find memory ranges from which to allocate PCI BARs.
-    let pci_allocator = PciMemory32Allocator::for_pci_ranges(&pci_node);
+    let pci_info = PciInfo::from_fdt(fdt).unwrap();
+    debug!("Found PCI CAM at {:#x}-{:#x}", pci_info.cam_range.start, pci_info.cam_range.end);
 
     modify_fdt(fdt);
 
@@ -125,10 +115,7 @@
         )
         .unwrap();
     idmap
-        .map_range(
-            &pci_allocator.get_region(),
-            Attributes::DEVICE_NGNRE | Attributes::EXECUTE_NEVER,
-        )
+        .map_range(&get_bar_region(&pci_info), Attributes::DEVICE_NGNRE | Attributes::EXECUTE_NEVER)
         .unwrap();
 
     info!("Activating IdMap...");
@@ -139,7 +126,8 @@
     check_data();
     check_dice();
 
-    check_pci(pci_reg);
+    let mut pci_root = unsafe { pci_info.make_pci_root() };
+    check_pci(&mut pci_root);
 }
 
 fn check_stack_guard() {
diff --git a/vmbase/example/src/pci.rs b/vmbase/example/src/pci.rs
index bd5b5ba..438ff9e 100644
--- a/vmbase/example/src/pci.rs
+++ b/vmbase/example/src/pci.rs
@@ -16,15 +16,16 @@
 
 use aarch64_paging::paging::MemoryRegion;
 use alloc::alloc::{alloc, dealloc, Layout};
-use core::{ffi::CStr, mem::size_of};
-use libfdt::{AddressRange, Fdt, FdtNode, Reg};
+use core::{mem::size_of, ptr::NonNull};
+use fdtpci::PciInfo;
 use log::{debug, info};
 use virtio_drivers::{
-    pci::{
-        bus::{Cam, PciRoot},
-        virtio_device_type, PciTransport,
+    device::blk::VirtIOBlk,
+    transport::{
+        pci::{bus::PciRoot, virtio_device_type, PciTransport},
+        DeviceType, Transport,
     },
-    DeviceType, Hal, PhysAddr, Transport, VirtAddr, VirtIOBlk, PAGE_SIZE,
+    BufferDirection, Hal, PhysAddr, VirtAddr, PAGE_SIZE,
 };
 
 /// The standard sector size of a VirtIO block device, in bytes.
@@ -33,24 +34,14 @@
 /// The size in sectors of the test block device we expect.
 const EXPECTED_SECTOR_COUNT: usize = 4;
 
-/// Finds an FDT node with compatible=pci-host-cam-generic.
-pub fn pci_node(fdt: &Fdt) -> FdtNode {
-    fdt.compatible_nodes(CStr::from_bytes_with_nul(b"pci-host-cam-generic\0").unwrap())
-        .unwrap()
-        .next()
-        .unwrap()
-}
-
-pub fn check_pci(reg: Reg<u64>) {
-    let mut pci_root = unsafe { PciRoot::new(reg.addr as *mut u8, Cam::MmioCam) };
+pub fn check_pci(pci_root: &mut PciRoot) {
     let mut checked_virtio_device_count = 0;
     for (device_function, info) in pci_root.enumerate_bus(0) {
         let (status, command) = pci_root.get_status_command(device_function);
         info!("Found {} at {}, status {:?} command {:?}", info, device_function, status, command);
         if let Some(virtio_type) = virtio_device_type(&info) {
             info!("  VirtIO {:?}", virtio_type);
-            let mut transport =
-                PciTransport::new::<HalImpl>(&mut pci_root, device_function).unwrap();
+            let mut transport = PciTransport::new::<HalImpl>(pci_root, device_function).unwrap();
             info!(
                 "Detected virtio PCI device with device type {:?}, features {:#018x}",
                 transport.device_type(),
@@ -88,89 +79,9 @@
     }
 }
 
-#[derive(Copy, Clone, Debug, Eq, PartialEq)]
-struct PciMemoryFlags(u32);
-
-impl PciMemoryFlags {
-    pub fn prefetchable(self) -> bool {
-        self.0 & 0x80000000 != 0
-    }
-
-    pub fn range_type(self) -> PciRangeType {
-        PciRangeType::from((self.0 & 0x3000000) >> 24)
-    }
-}
-
-/// Allocates 32-bit memory addresses for PCI BARs.
-pub struct PciMemory32Allocator {
-    start: u32,
-    end: u32,
-}
-
-impl PciMemory32Allocator {
-    /// Creates a new allocator based on the ranges property of the given PCI node.
-    pub fn for_pci_ranges(pci_node: &FdtNode) -> Self {
-        let mut memory_32_address = 0;
-        let mut memory_32_size = 0;
-        for AddressRange { addr: (flags, bus_address), parent_addr: cpu_physical, size } in pci_node
-            .ranges::<(u32, u64), u64, u64>()
-            .expect("Error getting ranges property from PCI node")
-            .expect("PCI node missing ranges property.")
-        {
-            let flags = PciMemoryFlags(flags);
-            let prefetchable = flags.prefetchable();
-            let range_type = flags.range_type();
-            info!(
-                "range: {:?} {}prefetchable bus address: {:#018x} host physical address: {:#018x} size: {:#018x}",
-                range_type,
-                if prefetchable { "" } else { "non-" },
-                bus_address,
-                cpu_physical,
-                size,
-            );
-            if !prefetchable
-                && ((range_type == PciRangeType::Memory32 && size > memory_32_size.into())
-                    || (range_type == PciRangeType::Memory64
-                        && size > memory_32_size.into()
-                        && bus_address + size < u32::MAX.into()))
-            {
-                // Use the 64-bit range for 32-bit memory, if it is low enough.
-                assert_eq!(bus_address, cpu_physical);
-                memory_32_address = u32::try_from(cpu_physical).unwrap();
-                memory_32_size = u32::try_from(size).unwrap();
-            }
-        }
-        if memory_32_size == 0 {
-            panic!("No PCI memory regions found.");
-        }
-
-        Self { start: memory_32_address, end: memory_32_address + memory_32_size }
-    }
-
-    /// Gets a memory region covering the address space from which this allocator will allocate.
-    pub fn get_region(&self) -> MemoryRegion {
-        MemoryRegion::new(self.start as usize, self.end as usize)
-    }
-}
-
-#[derive(Copy, Clone, Debug, Eq, PartialEq)]
-enum PciRangeType {
-    ConfigurationSpace,
-    IoSpace,
-    Memory32,
-    Memory64,
-}
-
-impl From<u32> for PciRangeType {
-    fn from(value: u32) -> Self {
-        match value {
-            0 => Self::ConfigurationSpace,
-            1 => Self::IoSpace,
-            2 => Self::Memory32,
-            3 => Self::Memory64,
-            _ => panic!("Tried to convert invalid range type {}", value),
-        }
-    }
+/// Gets the memory region in which BARs are allocated.
+pub fn get_bar_region(pci_info: &PciInfo) -> MemoryRegion {
+    MemoryRegion::new(pci_info.bar_range.start as usize, pci_info.bar_range.end as usize)
 }
 
 struct HalImpl;
@@ -181,7 +92,7 @@
         let layout = Layout::from_size_align(pages * PAGE_SIZE, PAGE_SIZE).unwrap();
         // Safe because the layout has a non-zero size.
         let vaddr = unsafe { alloc(layout) } as VirtAddr;
-        Self::virt_to_phys(vaddr)
+        virt_to_phys(vaddr)
     }
 
     fn dma_dealloc(paddr: PhysAddr, pages: usize) -> i32 {
@@ -200,7 +111,18 @@
         paddr
     }
 
-    fn virt_to_phys(vaddr: VirtAddr) -> PhysAddr {
-        vaddr
+    fn share(buffer: NonNull<[u8]>, _direction: BufferDirection) -> PhysAddr {
+        let vaddr = buffer.as_ptr() as *mut u8 as usize;
+        // Nothing to do, as the host already has access to all memory.
+        virt_to_phys(vaddr)
     }
+
+    fn unshare(_paddr: PhysAddr, _buffer: NonNull<[u8]>, _direction: BufferDirection) {
+        // Nothing to do, as the host already has access to all memory and we didn't copy the buffer
+        // anywhere else.
+    }
+}
+
+fn virt_to_phys(vaddr: VirtAddr) -> PhysAddr {
+    vaddr
 }
diff --git a/vmbase/example/tests/test.rs b/vmbase/example/tests/test.rs
index d58c8e6..c6aea8c 100644
--- a/vmbase/example/tests/test.rs
+++ b/vmbase/example/tests/test.rs
@@ -50,7 +50,9 @@
     // We need to start the thread pool for Binder to work properly, especially link_to_death.
     ProcessState::start_thread_pool();
 
-    let service = vmclient::connect().context("Failed to find VirtualizationService")?;
+    let virtmgr =
+        vmclient::VirtualizationService::new().context("Failed to spawn VirtualizationService")?;
+    let service = virtmgr.connect().context("Failed to connect to VirtualizationService")?;
 
     // Start example VM.
     let bootloader = ParcelFileDescriptor::new(
diff --git a/vmbase/src/bionic.rs b/vmbase/src/bionic.rs
index 8b3a076..b4a2f7b 100644
--- a/vmbase/src/bionic.rs
+++ b/vmbase/src/bionic.rs
@@ -14,6 +14,11 @@
 
 //! Low-level compatibility layer between baremetal Rust and Bionic C functions.
 
+use core::ffi::c_char;
+use core::ffi::c_int;
+use core::ffi::CStr;
+
+use crate::eprintln;
 use crate::linker;
 
 /// Reference to __stack_chk_guard.
@@ -23,3 +28,37 @@
 extern "C" fn __stack_chk_fail() -> ! {
     panic!("stack guard check failed");
 }
+
+/// Called from C to cause abnormal program termination.
+#[no_mangle]
+extern "C" fn abort() -> ! {
+    panic!("C code called abort()")
+}
+
+/// Error number set and read by C functions.
+pub static mut ERRNO: c_int = 0;
+
+#[no_mangle]
+unsafe extern "C" fn __errno() -> *mut c_int {
+    &mut ERRNO as *mut _
+}
+
+/// Reports a fatal error detected by Bionic.
+///
+/// # Safety
+///
+/// Input strings `prefix` and `format` must be properly NULL-terminated.
+///
+/// # Note
+///
+/// This Rust functions is missing the last argument of its C/C++ counterpart, a va_list.
+#[no_mangle]
+unsafe extern "C" fn async_safe_fatal_va_list(prefix: *const c_char, format: *const c_char) {
+    let prefix = CStr::from_ptr(prefix);
+    let format = CStr::from_ptr(format);
+
+    if let (Ok(prefix), Ok(format)) = (prefix.to_str(), format.to_str()) {
+        // We don't bother with printf formatting.
+        eprintln!("FATAL BIONIC ERROR: {prefix}: \"{format}\" (unformatted)");
+    }
+}