Merge "Add persistent backing for encrypted storage in VM"
diff --git a/.prebuilt_info/prebuilt_info_pvmfw_pvmfw_img.asciipb b/.prebuilt_info/prebuilt_info_pvmfw_pvmfw_img.asciipb
deleted file mode 100644
index 984625e..0000000
--- a/.prebuilt_info/prebuilt_info_pvmfw_pvmfw_img.asciipb
+++ /dev/null
@@ -1,13 +0,0 @@
-drops {
-  android_build_drop {
-    build_id: "9110780"
-    target: "u-boot_pvmfw"
-    source_file: "pvmfw.img"
-  }
-  dest_file: "pvmfw/pvmfw.img"
-  version: ""
-  version_group: ""
-  git_project: "platform/packages/modules/Virtualization"
-  git_branch: "master"
-  transform: TRANSFORM_NONE
-}
diff --git a/compos/Android.bp b/compos/Android.bp
index ea7c4d6..0890e9d 100644
--- a/compos/Android.bp
+++ b/compos/Android.bp
@@ -7,7 +7,6 @@
     edition: "2021",
     srcs: ["src/compsvc_main.rs"],
     rustlibs: [
-        "android.system.virtualmachineservice-rust",
         "authfs_aidl_interface-rust",
         "compos_aidl_interface-rust",
         "libandroid_logger",
@@ -24,6 +23,7 @@
         "librpcbinder_rs",
         "librustutils",
         "libscopeguard",
+        "libvm_payload_bindgen",
     ],
     prefer_rlib: true,
     shared_libs: [
diff --git a/compos/compos_key_helper/Android.bp b/compos/compos_key_helper/Android.bp
index fdfcfc1..c9480fc 100644
--- a/compos/compos_key_helper/Android.bp
+++ b/compos/compos_key_helper/Android.bp
@@ -26,9 +26,9 @@
 
     static_libs: [
         "libcompos_key",
-        "libvm_payload",
     ],
     shared_libs: [
+        "libvm_payload",
         "libbinder_ndk",
     ],
 }
diff --git a/compos/compos_key_helper/compos_key.cpp b/compos/compos_key_helper/compos_key.cpp
index 2e3252c..34b931a 100644
--- a/compos/compos_key_helper/compos_key.cpp
+++ b/compos/compos_key_helper/compos_key.cpp
@@ -24,25 +24,11 @@
 using android::base::Error;
 using android::base::Result;
 using compos_key::Ed25519KeyPair;
+using compos_key::Seed;
 using compos_key::Signature;
 
-// Used to ensure the key we derive is distinct from any other.
-constexpr const char* kSigningKeyInfo = "CompOS signing key";
-
 namespace compos_key {
-Result<Ed25519KeyPair> deriveKeyFromSecret(const uint8_t* secret, size_t secret_size) {
-    // Ed25519 private keys are derived from a 32 byte seed:
-    // https://datatracker.ietf.org/doc/html/rfc8032#section-5.1.5
-    std::array<uint8_t, 32> seed;
-
-    // We derive the seed from the secret using HKDF - see
-    // https://datatracker.ietf.org/doc/html/rfc5869#section-2.
-    if (!HKDF(seed.data(), seed.size(), EVP_sha256(), secret, secret_size, /*salt=*/nullptr,
-              /*salt_len=*/0, reinterpret_cast<const uint8_t*>(kSigningKeyInfo),
-              strlen(kSigningKeyInfo))) {
-        return Error() << "HKDF failed";
-    }
-
+Result<Ed25519KeyPair> keyFromSeed(const Seed& seed) {
     Ed25519KeyPair result;
     ED25519_keypair_from_seed(result.public_key.data(), result.private_key.data(), seed.data());
     return result;
diff --git a/compos/compos_key_helper/compos_key.h b/compos/compos_key_helper/compos_key.h
index e9c6061..8629cf6 100644
--- a/compos/compos_key_helper/compos_key.h
+++ b/compos/compos_key_helper/compos_key.h
@@ -22,8 +22,11 @@
 #include <array>
 
 namespace compos_key {
+constexpr size_t ED25519_SEED_LEN = 32;
+
 using PrivateKey = std::array<uint8_t, ED25519_PRIVATE_KEY_LEN>;
 using PublicKey = std::array<uint8_t, ED25519_PUBLIC_KEY_LEN>;
+using Seed = std::array<uint8_t, ED25519_SEED_LEN>;
 using Signature = std::array<uint8_t, ED25519_SIGNATURE_LEN>;
 
 struct Ed25519KeyPair {
@@ -31,8 +34,7 @@
     PublicKey public_key;
 };
 
-android::base::Result<Ed25519KeyPair> deriveKeyFromSecret(const uint8_t* secret,
-                                                          size_t secret_size);
+android::base::Result<Ed25519KeyPair> keyFromSeed(const Seed& seed);
 
 android::base::Result<Signature> sign(const PrivateKey& private_key, const uint8_t* data,
                                       size_t data_size);
diff --git a/compos/compos_key_helper/compos_key_main.cpp b/compos/compos_key_helper/compos_key_main.cpp
index 77a9cf9..4fb0762 100644
--- a/compos/compos_key_helper/compos_key_main.cpp
+++ b/compos/compos_key_helper/compos_key_main.cpp
@@ -20,6 +20,7 @@
 #include <vm_payload.h>
 
 #include <string_view>
+#include <vector>
 
 #include "compos_key.h"
 
@@ -29,22 +30,24 @@
 using android::base::WriteFully;
 using namespace std::literals;
 using compos_key::Ed25519KeyPair;
+using compos_key::Seed;
 
 namespace {
-Result<Ed25519KeyPair> deriveKeyFromDice() {
-    uint8_t cdi_seal[64];
-    size_t cdi_size = get_dice_sealing_cdi(cdi_seal, sizeof(cdi_seal));
-    if (cdi_size == 0) {
-        return Error() << "Failed to get sealing CDI";
-    }
 
-    // We use the sealing CDI because we want stability - the key needs to be the same
-    // for any instance of the "same" VM.
-    return compos_key::deriveKeyFromSecret(cdi_seal, cdi_size);
+constexpr const char* kSigningKeySeedIdentifier = "CompOS signing key seed";
+
+Result<Ed25519KeyPair> getSigningKey() {
+    Seed seed;
+    if (!AVmPayload_getVmInstanceSecret(kSigningKeySeedIdentifier,
+                                        strlen(kSigningKeySeedIdentifier), seed.data(),
+                                        seed.size())) {
+        return Error() << "Failed to get signing key seed";
+    }
+    return compos_key::keyFromSeed(seed);
 }
 
 int write_public_key() {
-    auto key_pair = deriveKeyFromDice();
+    auto key_pair = getSigningKey();
     if (!key_pair.ok()) {
         LOG(ERROR) << key_pair.error();
         return 1;
@@ -57,14 +60,18 @@
 }
 
 int write_bcc() {
-    uint8_t bcc[2048];
-    size_t bcc_size = get_dice_attestation_chain(bcc, sizeof(bcc));
-    if (bcc_size == 0) {
+    size_t bcc_size;
+    if (!AVmPayload_getDiceAttestationChain(nullptr, 0, &bcc_size)) {
+        LOG(ERROR) << "Failed to measure attestation chain";
+        return 1;
+    }
+    std::vector<uint8_t> bcc(bcc_size);
+    if (!AVmPayload_getDiceAttestationChain(bcc.data(), bcc.size(), &bcc_size)) {
         LOG(ERROR) << "Failed to get attestation chain";
         return 1;
     }
 
-    if (!WriteFully(STDOUT_FILENO, bcc, bcc_size)) {
+    if (!WriteFully(STDOUT_FILENO, bcc.data(), bcc.size())) {
         PLOG(ERROR) << "Write failed";
         return 1;
     }
@@ -79,7 +86,7 @@
         return 1;
     }
 
-    auto key_pair = deriveKeyFromDice();
+    auto key_pair = getSigningKey();
     if (!key_pair.ok()) {
         LOG(ERROR) << key_pair.error();
         return 1;
diff --git a/compos/compos_key_helper/compos_key_test.cpp b/compos/compos_key_helper/compos_key_test.cpp
index e4c3e8a..37aefcf 100644
--- a/compos/compos_key_helper/compos_key_test.cpp
+++ b/compos/compos_key_helper/compos_key_test.cpp
@@ -22,30 +22,32 @@
 
 using namespace compos_key;
 
-const std::vector<uint8_t> secret = {1, 2, 3};
-const std::vector<uint8_t> other_secret = {3, 2, 3};
+constexpr Seed seed = {1,  2,  3,  4,  5,  6,  7,  8,  9,  10, 11, 12, 13, 14, 15, 16,
+                       17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32};
+constexpr Seed other_seed = {3, 2, 3, 2, 3, 2, 3, 2, 3, 2, 3, 2, 3, 2, 3, 2,
+                             3, 2, 3, 2, 3, 2, 3, 2, 3, 2, 3, 2, 3, 2, 3, 2};
 const std::vector<uint8_t> data = {42, 180, 65, 0};
 
 struct ComposKeyTest : public testing::Test {
     Ed25519KeyPair key_pair;
 
     void SetUp() override {
-        auto key_pair = deriveKeyFromSecret(secret.data(), secret.size());
+        auto key_pair = keyFromSeed(seed);
         ASSERT_TRUE(key_pair.ok()) << key_pair.error();
         this->key_pair = *key_pair;
     }
 };
 
-TEST_F(ComposKeyTest, SameSecretSameKey) {
-    auto other_key_pair = deriveKeyFromSecret(secret.data(), secret.size());
+TEST_F(ComposKeyTest, SameSeedSameKey) {
+    auto other_key_pair = keyFromSeed(seed);
     ASSERT_TRUE(other_key_pair.ok()) << other_key_pair.error();
 
     ASSERT_EQ(key_pair.private_key, other_key_pair->private_key);
     ASSERT_EQ(key_pair.public_key, other_key_pair->public_key);
 }
 
-TEST_F(ComposKeyTest, DifferentSecretDifferentKey) {
-    auto other_key_pair = deriveKeyFromSecret(other_secret.data(), other_secret.size());
+TEST_F(ComposKeyTest, DifferentSeedDifferentKey) {
+    auto other_key_pair = keyFromSeed(other_seed);
     ASSERT_TRUE(other_key_pair.ok()) << other_key_pair.error();
 
     ASSERT_NE(key_pair.private_key, other_key_pair->private_key);
@@ -84,7 +86,7 @@
 TEST_F(ComposKeyTest, WrongKeyDoesNotVerify) {
     auto signature = sign(key_pair.private_key, data.data(), data.size());
 
-    auto other_key_pair = deriveKeyFromSecret(other_secret.data(), other_secret.size());
+    auto other_key_pair = keyFromSeed(other_seed);
     ASSERT_TRUE(other_key_pair.ok()) << other_key_pair.error();
 
     bool verified = verify(other_key_pair->public_key, *signature, data.data(), data.size());
diff --git a/compos/src/compsvc_main.rs b/compos/src/compsvc_main.rs
index 16d258e..991725d 100644
--- a/compos/src/compsvc_main.rs
+++ b/compos/src/compsvc_main.rs
@@ -22,20 +22,12 @@
 mod compsvc;
 mod fsverity;
 
-use android_system_virtualmachineservice::{
-    aidl::android::system::virtualmachineservice::IVirtualMachineService::{
-        IVirtualMachineService, VM_BINDER_SERVICE_PORT,
-    },
-    binder::Strong,
-};
-use anyhow::{bail, Context, Result};
+use anyhow::{bail, Result};
 use compos_common::COMPOS_VSOCK_PORT;
 use log::{debug, error};
-use rpcbinder::{get_vsock_rpc_interface, run_rpc_server};
+use rpcbinder::run_rpc_server;
 use std::panic;
-
-/// The CID representing the host VM
-const VMADDR_CID_HOST: u32 = 2;
+use vm_payload_bindgen::AVmPayload_notifyPayloadReady;
 
 fn main() {
     if let Err(e) = try_main() {
@@ -54,14 +46,10 @@
     }));
 
     let service = compsvc::new_binder()?.as_binder();
-    let vm_service = get_vm_service()?;
-
     debug!("compsvc is starting as a rpc service.");
-
-    let retval = run_rpc_server(service, COMPOS_VSOCK_PORT, || {
-        if let Err(e) = vm_service.notifyPayloadReady() {
-            error!("Unable to notify ready: {}", e);
-        }
+    // SAFETY: Invokes a method from the bindgen library `vm_payload_bindgen`.
+    let retval = run_rpc_server(service, COMPOS_VSOCK_PORT, || unsafe {
+        AVmPayload_notifyPayloadReady();
     });
     if retval {
         debug!("RPC server has shut down gracefully");
@@ -70,8 +58,3 @@
         bail!("Premature termination of RPC server");
     }
 }
-
-fn get_vm_service() -> Result<Strong<dyn IVirtualMachineService>> {
-    get_vsock_rpc_interface(VMADDR_CID_HOST, VM_BINDER_SERVICE_PORT as u32)
-        .context("Connecting to IVirtualMachineService")
-}
diff --git a/demo/assets/vm_config.json b/demo/assets/vm_config.json
deleted file mode 100644
index da420ff..0000000
--- a/demo/assets/vm_config.json
+++ /dev/null
@@ -1,14 +0,0 @@
-{
-  "os": {
-    "name": "microdroid"
-  },
-  "task": {
-    "type": "microdroid_launcher",
-    "command": "MicrodroidTestNativeLib.so",
-    "args": [
-      "hello",
-      "microdroid"
-    ]
-  },
-  "export_tombstones": true
-}
diff --git a/demo/java/com/android/microdroid/demo/MainActivity.java b/demo/java/com/android/microdroid/demo/MainActivity.java
index 6266c18..b52ef40 100644
--- a/demo/java/com/android/microdroid/demo/MainActivity.java
+++ b/demo/java/com/android/microdroid/demo/MainActivity.java
@@ -24,7 +24,6 @@
 import android.system.virtualmachine.VirtualMachine;
 import android.system.virtualmachine.VirtualMachineCallback;
 import android.system.virtualmachine.VirtualMachineConfig;
-import android.system.virtualmachine.VirtualMachineConfig.DebugLevel;
 import android.system.virtualmachine.VirtualMachineException;
 import android.system.virtualmachine.VirtualMachineManager;
 import android.util.Log;
@@ -49,7 +48,6 @@
 import java.io.InputStreamReader;
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Executors;
-import java.util.concurrent.Future;
 
 /**
  * This app is to demonstrate the use of APIs in the android.system.virtualmachine library.
@@ -75,10 +73,11 @@
         // When the button is clicked, run or stop the VM
         runStopButton.setOnClickListener(
                 v -> {
-                    if (model.getStatus().getValue() == VirtualMachine.Status.RUNNING) {
+                    Integer status = model.getStatus().getValue();
+                    if (status != null && status == VirtualMachine.STATUS_RUNNING) {
                         model.stop();
                     } else {
-                        CheckBox debugModeCheckBox = (CheckBox) findViewById(R.id.debugMode);
+                        CheckBox debugModeCheckBox = findViewById(R.id.debugMode);
                         final boolean debug = debugModeCheckBox.isChecked();
                         model.run(debug);
                     }
@@ -88,7 +87,7 @@
         model.getStatus()
                 .observeForever(
                         status -> {
-                            if (status == VirtualMachine.Status.RUNNING) {
+                            if (status == VirtualMachine.STATUS_RUNNING) {
                                 runStopButton.setText("Stop");
                                 // Clear the outputs from the previous run
                                 consoleView.setText("");
@@ -150,12 +149,12 @@
         private final MutableLiveData<String> mConsoleOutput = new MutableLiveData<>();
         private final MutableLiveData<String> mLogOutput = new MutableLiveData<>();
         private final MutableLiveData<String> mPayloadOutput = new MutableLiveData<>();
-        private final MutableLiveData<VirtualMachine.Status> mStatus = new MutableLiveData<>();
+        private final MutableLiveData<Integer> mStatus = new MutableLiveData<>();
         private ExecutorService mExecutorService;
 
         public VirtualMachineModel(Application app) {
             super(app);
-            mStatus.setValue(VirtualMachine.Status.DELETED);
+            mStatus.setValue(VirtualMachine.STATUS_DELETED);
         }
 
         /** Runs a VM */
@@ -169,8 +168,8 @@
                         private final ExecutorService mService = mExecutorService;
 
                         @Override
-                        public void onPayloadStarted(
-                                VirtualMachine vm, ParcelFileDescriptor stream) {
+                        public void onPayloadStarted(VirtualMachine vm,
+                                ParcelFileDescriptor stream) {
                             if (stream == null) {
                                 mPayloadOutput.postValue("(no output available)");
                                 return;
@@ -189,25 +188,13 @@
                             }
                             mPayloadOutput.postValue("(Payload is ready. Testing VM service...)");
 
-                            Future<IBinder> service;
-                            try {
-                                service = vm.connectToVsockServer(ITestService.SERVICE_PORT);
-                            } catch (VirtualMachineException e) {
-                                mPayloadOutput.postValue(
-                                        String.format(
-                                                "(Exception while connecting VM's binder"
-                                                        + " service: %s)",
-                                                e.getMessage()));
-                                return;
-                            }
-
-                            mService.execute(() -> testVMService(service));
+                            mService.execute(() -> testVmService(vm));
                         }
 
-                        private void testVMService(Future<IBinder> service) {
+                        private void testVmService(VirtualMachine vm) {
                             IBinder binder;
                             try {
-                                binder = service.get();
+                                binder = vm.connectToVsockServer(ITestService.SERVICE_PORT);
                             } catch (Exception e) {
                                 if (!Thread.interrupted()) {
                                     mPayloadOutput.postValue(
@@ -256,9 +243,9 @@
                         }
 
                         @Override
-                        public void onDied(VirtualMachine vm, int reason) {
+                        public void onStopped(VirtualMachine vm, int reason) {
                             mService.shutdownNow();
-                            mStatus.postValue(VirtualMachine.Status.STOPPED);
+                            mStatus.postValue(VirtualMachine.STATUS_STOPPED);
                         }
 
                         @Override
@@ -271,9 +258,11 @@
 
             try {
                 VirtualMachineConfig.Builder builder =
-                        new VirtualMachineConfig.Builder(getApplication(), "assets/vm_config.json");
+                        new VirtualMachineConfig.Builder(getApplication());
+                builder.setPayloadBinaryPath("MicrodroidTestNativeLib.so");
+                builder.setProtectedVm(true);
                 if (debug) {
-                    builder.debugLevel(DebugLevel.FULL);
+                    builder.setDebugLevel(VirtualMachineConfig.DEBUG_LEVEL_FULL);
                 }
                 VirtualMachineConfig config = builder.build();
                 VirtualMachineManager vmm = VirtualMachineManager.getInstance(getApplication());
@@ -306,7 +295,7 @@
             }
             mVirtualMachine = null;
             mExecutorService.shutdownNow();
-            mStatus.postValue(VirtualMachine.Status.STOPPED);
+            mStatus.postValue(VirtualMachine.STATUS_STOPPED);
         }
 
         /** Returns the console output from the VM */
@@ -325,7 +314,7 @@
         }
 
         /** Returns the status of the VM */
-        public LiveData<VirtualMachine.Status> getStatus() {
+        public LiveData<Integer> getStatus() {
             return mStatus;
         }
     }
diff --git a/javalib/src/android/system/virtualmachine/VirtualMachine.java b/javalib/src/android/system/virtualmachine/VirtualMachine.java
index cc99006..bdec164 100644
--- a/javalib/src/android/system/virtualmachine/VirtualMachine.java
+++ b/javalib/src/android/system/virtualmachine/VirtualMachine.java
@@ -18,16 +18,43 @@
 
 import static android.os.ParcelFileDescriptor.MODE_READ_ONLY;
 import static android.os.ParcelFileDescriptor.MODE_READ_WRITE;
+import static android.system.virtualmachine.VirtualMachineCallback.ERROR_PAYLOAD_CHANGED;
+import static android.system.virtualmachine.VirtualMachineCallback.ERROR_PAYLOAD_INVALID_CONFIG;
+import static android.system.virtualmachine.VirtualMachineCallback.ERROR_PAYLOAD_VERIFICATION_FAILED;
+import static android.system.virtualmachine.VirtualMachineCallback.ERROR_UNKNOWN;
+import static android.system.virtualmachine.VirtualMachineCallback.STOP_REASON_BOOTLOADER_INSTANCE_IMAGE_CHANGED;
+import static android.system.virtualmachine.VirtualMachineCallback.STOP_REASON_BOOTLOADER_PUBLIC_KEY_MISMATCH;
+import static android.system.virtualmachine.VirtualMachineCallback.STOP_REASON_CRASH;
+import static android.system.virtualmachine.VirtualMachineCallback.STOP_REASON_ERROR;
+import static android.system.virtualmachine.VirtualMachineCallback.STOP_REASON_HANGUP;
+import static android.system.virtualmachine.VirtualMachineCallback.STOP_REASON_INFRASTRUCTURE_ERROR;
+import static android.system.virtualmachine.VirtualMachineCallback.STOP_REASON_KILLED;
+import static android.system.virtualmachine.VirtualMachineCallback.STOP_REASON_MICRODROID_FAILED_TO_CONNECT_TO_VIRTUALIZATION_SERVICE;
+import static android.system.virtualmachine.VirtualMachineCallback.STOP_REASON_MICRODROID_INVALID_PAYLOAD_CONFIG;
+import static android.system.virtualmachine.VirtualMachineCallback.STOP_REASON_MICRODROID_PAYLOAD_HAS_CHANGED;
+import static android.system.virtualmachine.VirtualMachineCallback.STOP_REASON_MICRODROID_PAYLOAD_VERIFICATION_FAILED;
+import static android.system.virtualmachine.VirtualMachineCallback.STOP_REASON_MICRODROID_UNKNOWN_RUNTIME_ERROR;
+import static android.system.virtualmachine.VirtualMachineCallback.STOP_REASON_PVM_FIRMWARE_INSTANCE_IMAGE_CHANGED;
+import static android.system.virtualmachine.VirtualMachineCallback.STOP_REASON_PVM_FIRMWARE_PUBLIC_KEY_MISMATCH;
+import static android.system.virtualmachine.VirtualMachineCallback.STOP_REASON_REBOOT;
+import static android.system.virtualmachine.VirtualMachineCallback.STOP_REASON_SHUTDOWN;
+import static android.system.virtualmachine.VirtualMachineCallback.STOP_REASON_UNKNOWN;
+
+import static java.util.Objects.requireNonNull;
 
 import android.annotation.CallbackExecutor;
+import android.annotation.IntDef;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
+import android.annotation.RequiresPermission;
 import android.content.Context;
 import android.os.Binder;
 import android.os.IBinder;
 import android.os.ParcelFileDescriptor;
 import android.os.RemoteException;
 import android.os.ServiceManager;
+import android.system.virtualizationcommon.ErrorCode;
+import android.system.virtualizationservice.DeathReason;
 import android.system.virtualizationservice.IVirtualMachine;
 import android.system.virtualizationservice.IVirtualMachineCallback;
 import android.system.virtualizationservice.IVirtualizationService;
@@ -45,19 +72,19 @@
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.InputStreamReader;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
 import java.lang.ref.WeakReference;
 import java.nio.file.FileAlreadyExistsException;
 import java.nio.file.Files;
 import java.util.ArrayList;
+import java.util.Collections;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.Optional;
 import java.util.WeakHashMap;
 import java.util.concurrent.Executor;
-import java.util.concurrent.ExecutorService;
-import java.util.concurrent.Executors;
-import java.util.concurrent.Future;
 import java.util.concurrent.atomic.AtomicBoolean;
 import java.util.function.Consumer;
 import java.util.zip.ZipFile;
@@ -68,7 +95,7 @@
  *
  * @hide
  */
-public class VirtualMachine {
+public class VirtualMachine implements AutoCloseable {
     private static final Map<Context, Map<String, WeakReference<VirtualMachine>>> sInstances =
             new WeakHashMap<>();
 
@@ -92,18 +119,35 @@
     /** Name of the virtualization service. */
     private static final String SERVICE_NAME = "android.system.virtualizationservice";
 
-    /** Status of a virtual machine */
-    public enum Status {
-        /** The virtual machine has just been created, or {@link #stop()} was called on it. */
-        STOPPED,
-        /** The virtual machine is running. */
-        RUNNING,
-        /**
-         * The virtual machine is deleted. This is a irreversable state. Once a virtual machine is
-         * deleted, it can never be undone, which means all its secrets are permanently lost.
-         */
-        DELETED,
-    }
+    /** The permission needed to create or run a virtual machine. */
+    public static final String MANAGE_VIRTUAL_MACHINE_PERMISSION =
+            "android.permission.MANAGE_VIRTUAL_MACHINE";
+
+    /**
+     * Status of a virtual machine
+     *
+     * @hide
+     */
+    @Retention(RetentionPolicy.SOURCE)
+    @IntDef({
+            STATUS_STOPPED,
+            STATUS_RUNNING,
+            STATUS_DELETED
+    })
+    public @interface Status {}
+
+     /** 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. */
+    public static final int STATUS_RUNNING = 1;
+
+    /**
+     * The virtual machine has been deleted. This is an irreversible state. Once a virtual machine
+     * is deleted all its secrets are permanently lost, and it cannot be run. A new virtual machine
+     * with the same name and config may be created, with new and different secrets.
+     */
+    public static final int STATUS_DELETED = 2;
 
     /** Lock for internal synchronization. */
     private final Object mLock = new Object();
@@ -166,8 +210,6 @@
     @Nullable private ParcelFileDescriptor mLogReader;
     @Nullable private ParcelFileDescriptor mLogWriter;
 
-    private final ExecutorService mExecutorService = Executors.newCachedThreadPool();
-
     @NonNull private final Context mContext;
 
     static {
@@ -179,8 +221,8 @@
             throws VirtualMachineException {
         mContext = context;
         mPackageName = context.getPackageName();
-        mName = name;
-        mConfig = config;
+        mName = requireNonNull(name, "Name must not be null");
+        mConfig = requireNonNull(config, "Config must not be null");
         mConfigFilePath = getConfigFilePath(context, name);
 
         final File vmRoot = new File(context.getFilesDir(), VM_DIR);
@@ -193,15 +235,12 @@
     /**
      * Creates a virtual machine with the given name and config. Once a virtual machine is created
      * it is persisted until it is deleted by calling {@link #delete()}. The created virtual machine
-     * is in {@link Status#STOPPED} state. To run the VM, call {@link #run()}.
+     * is in {@link #STATUS_STOPPED} state. To run the VM, call {@link #run()}.
      */
     @NonNull
     static VirtualMachine create(
             @NonNull Context context, @NonNull String name, @NonNull VirtualMachineConfig config)
             throws VirtualMachineException {
-        if (config == null) {
-            throw new VirtualMachineException("null config");
-        }
         VirtualMachine vm = new VirtualMachine(context, name, config);
 
         try {
@@ -332,29 +371,29 @@
      *
      * @hide
      */
-    @NonNull
-    public Status getStatus() throws VirtualMachineException {
+    @Status
+    public int getStatus() throws VirtualMachineException {
         try {
             if (mVirtualMachine != null) {
                 switch (mVirtualMachine.getState()) {
                     case VirtualMachineState.NOT_STARTED:
-                        return Status.STOPPED;
+                        return STATUS_STOPPED;
                     case VirtualMachineState.STARTING:
                     case VirtualMachineState.STARTED:
                     case VirtualMachineState.READY:
                     case VirtualMachineState.FINISHED:
-                        return Status.RUNNING;
+                        return STATUS_RUNNING;
                     case VirtualMachineState.DEAD:
-                        return Status.STOPPED;
+                        return STATUS_STOPPED;
                 }
             }
         } catch (RemoteException e) {
             throw new VirtualMachineException(e);
         }
         if (!mConfigFilePath.exists()) {
-            return Status.DELETED;
+            return STATUS_DELETED;
         }
-        return Status.STOPPED;
+        return STATUS_STOPPED;
     }
 
     /**
@@ -363,8 +402,7 @@
      *
      * @hide
      */
-    public void setCallback(
-            @NonNull @CallbackExecutor Executor executor,
+    public void setCallback(@NonNull @CallbackExecutor Executor executor,
             @NonNull VirtualMachineCallback callback) {
         synchronized (mLock) {
             mCallback = callback;
@@ -406,12 +444,13 @@
     /**
      * 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 object (not implemented currently).
+     * registering a callback using {@link #setCallback(Executor, VirtualMachineCallback)}.
      *
      * @hide
      */
+    @RequiresPermission(MANAGE_VIRTUAL_MACHINE_PERMISSION)
     public void run() throws VirtualMachineException {
-        if (getStatus() != Status.STOPPED) {
+        if (getStatus() != STATUS_STOPPED) {
             throw new VirtualMachineException(this + " is not in stopped state");
         }
 
@@ -472,8 +511,8 @@
 
             IBinder.DeathRecipient deathRecipient = () -> {
                 if (onDiedCalled.compareAndSet(false, true)) {
-                    executeCallback((cb) -> cb.onDied(VirtualMachine.this,
-                            VirtualMachineCallback.DEATH_REASON_VIRTUALIZATIONSERVICE_DIED));
+                    executeCallback((cb) -> cb.onStopped(VirtualMachine.this,
+                            VirtualMachineCallback.STOP_REASON_VIRTUALIZATION_SERVICE_DIED));
                 }
             };
 
@@ -485,28 +524,37 @@
                             executeCallback(
                                     (cb) -> cb.onPayloadStarted(VirtualMachine.this, stream));
                         }
+
                         @Override
                         public void onPayloadReady(int cid) {
                             executeCallback((cb) -> cb.onPayloadReady(VirtualMachine.this));
                         }
+
                         @Override
                         public void onPayloadFinished(int cid, int exitCode) {
                             executeCallback(
                                     (cb) -> cb.onPayloadFinished(VirtualMachine.this, exitCode));
                         }
+
                         @Override
                         public void onError(int cid, int errorCode, String message) {
+                            int translatedError = getTranslatedError(errorCode);
                             executeCallback(
-                                    (cb) -> cb.onError(VirtualMachine.this, errorCode, message));
+                                    (cb) -> cb.onError(VirtualMachine.this, translatedError,
+                                            message));
                         }
+
                         @Override
                         public void onDied(int cid, int reason) {
-                            // TODO(b/236811123) translate `reason` into a stable reason numbers
                             service.asBinder().unlinkToDeath(deathRecipient, 0);
+                            int translatedReason = getTranslatedReason(reason);
                             if (onDiedCalled.compareAndSet(false, true)) {
-                                executeCallback((cb) -> cb.onDied(VirtualMachine.this, reason));
+                                executeCallback(
+                                        (cb) -> cb.onStopped(VirtualMachine.this,
+                                                translatedReason));
                             }
                         }
+
                         @Override
                         public void onRamdump(int cid, ParcelFileDescriptor ramdump) {
                             executeCallback(
@@ -521,6 +569,60 @@
         }
     }
 
+    @VirtualMachineCallback.ErrorCode
+    private int getTranslatedError(int reason) {
+        switch (reason) {
+            case ErrorCode.PAYLOAD_VERIFICATION_FAILED:
+                return ERROR_PAYLOAD_VERIFICATION_FAILED;
+            case ErrorCode.PAYLOAD_CHANGED:
+                return ERROR_PAYLOAD_CHANGED;
+            case ErrorCode.PAYLOAD_CONFIG_INVALID:
+                return ERROR_PAYLOAD_INVALID_CONFIG;
+            default:
+                return ERROR_UNKNOWN;
+        }
+    }
+
+    @VirtualMachineCallback.StopReason
+    private int getTranslatedReason(int reason) {
+        switch (reason) {
+            case DeathReason.INFRASTRUCTURE_ERROR:
+                return STOP_REASON_INFRASTRUCTURE_ERROR;
+            case DeathReason.KILLED:
+                return STOP_REASON_KILLED;
+            case DeathReason.SHUTDOWN:
+                return STOP_REASON_SHUTDOWN;
+            case DeathReason.ERROR:
+                return STOP_REASON_ERROR;
+            case DeathReason.REBOOT:
+                return STOP_REASON_REBOOT;
+            case DeathReason.CRASH:
+                return STOP_REASON_CRASH;
+            case DeathReason.PVM_FIRMWARE_PUBLIC_KEY_MISMATCH:
+                return STOP_REASON_PVM_FIRMWARE_PUBLIC_KEY_MISMATCH;
+            case DeathReason.PVM_FIRMWARE_INSTANCE_IMAGE_CHANGED:
+                return STOP_REASON_PVM_FIRMWARE_INSTANCE_IMAGE_CHANGED;
+            case DeathReason.BOOTLOADER_PUBLIC_KEY_MISMATCH:
+                return STOP_REASON_BOOTLOADER_PUBLIC_KEY_MISMATCH;
+            case DeathReason.BOOTLOADER_INSTANCE_IMAGE_CHANGED:
+                return STOP_REASON_BOOTLOADER_INSTANCE_IMAGE_CHANGED;
+            case DeathReason.MICRODROID_FAILED_TO_CONNECT_TO_VIRTUALIZATION_SERVICE:
+                return STOP_REASON_MICRODROID_FAILED_TO_CONNECT_TO_VIRTUALIZATION_SERVICE;
+            case DeathReason.MICRODROID_PAYLOAD_HAS_CHANGED:
+                return STOP_REASON_MICRODROID_PAYLOAD_HAS_CHANGED;
+            case DeathReason.MICRODROID_PAYLOAD_VERIFICATION_FAILED:
+                return STOP_REASON_MICRODROID_PAYLOAD_VERIFICATION_FAILED;
+            case DeathReason.MICRODROID_INVALID_PAYLOAD_CONFIG:
+                return STOP_REASON_MICRODROID_INVALID_PAYLOAD_CONFIG;
+            case DeathReason.MICRODROID_UNKNOWN_RUNTIME_ERROR:
+                return STOP_REASON_MICRODROID_UNKNOWN_RUNTIME_ERROR;
+            case DeathReason.HANGUP:
+                return STOP_REASON_HANGUP;
+            default:
+                return STOP_REASON_UNKNOWN;
+        }
+    }
+
     /**
      * Returns the stream object representing the console output from the virtual machine.
      *
@@ -550,7 +652,7 @@
     /**
      * 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 with the event. A stopped virtual machine can be re-started by calling {@link
+     * notified of the event. A stopped virtual machine can be re-started by calling {@link
      * #run()}.
      *
      * @hide
@@ -566,15 +668,25 @@
     }
 
     /**
+     * Stops this virtual machine. See {@link #stop()}.
+     *
+     * @hide
+     */
+    @Override
+    public void close() throws VirtualMachineException {
+        stop();
+    }
+
+    /**
      * Deletes this virtual machine. Deleting a virtual machine means deleting any persisted data
-     * associated with it including the per-VM secret. This is an irreversable action. A virtual
+     * associated with it including the per-VM secret. This is an irreversible action. A virtual
      * machine once deleted can never be restored. A new virtual machine created with the same name
      * and the same config is different from an already deleted virtual machine.
      *
      * @hide
      */
     public void delete() throws VirtualMachineException {
-        if (getStatus() != Status.STOPPED) {
+        if (getStatus() != STATUS_STOPPED) {
             throw new VirtualMachineException("Virtual machine is not stopped");
         }
         final File vmRootDir = mConfigFilePath.getParentFile();
@@ -599,7 +711,7 @@
      */
     @NonNull
     public Optional<Integer> getCid() throws VirtualMachineException {
-        if (getStatus() != Status.RUNNING) {
+        if (getStatus() != STATUS_RUNNING) {
             return Optional.empty();
         }
         try {
@@ -629,7 +741,7 @@
         if (!oldConfig.isCompatibleWith(newConfig)) {
             throw new VirtualMachineException("incompatible config");
         }
-        if (getStatus() != Status.STOPPED) {
+        if (getStatus() != STATUS_STOPPED) {
             throw new VirtualMachineException(
                     "can't change config while virtual machine is not stopped");
         }
@@ -649,19 +761,19 @@
     private static native IBinder nativeConnectToVsockServer(IBinder vmBinder, int port);
 
     /**
-     * Connects to a VM's RPC server via vsock, and returns a root IBinder object. Guest VMs are
+     * 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 an RPC session to the guest VMs.
+     * establish a connection to the guest VM.
      *
      * @hide
      */
-    public Future<IBinder> connectToVsockServer(int port) throws VirtualMachineException {
-        if (getStatus() != Status.RUNNING) {
+    @NonNull
+    public IBinder connectToVsockServer(int port) throws VirtualMachineException {
+        if (getStatus() != STATUS_RUNNING) {
             throw new VirtualMachineException("VM is not running");
         }
-        return mExecutorService.submit(
-                () -> nativeConnectToVsockServer(mVirtualMachine.asBinder(), port));
+        return nativeConnectToVsockServer(mVirtualMachine.asBinder(), port);
     }
 
     /**
@@ -669,21 +781,40 @@
      *
      * @hide
      */
+    @NonNull
     public ParcelFileDescriptor connectVsock(int port) throws VirtualMachineException {
         try {
             return mVirtualMachine.connectVsock(port);
         } catch (RemoteException e) {
-            throw new VirtualMachineException("failed to connect Vsock", e);
+            throw new VirtualMachineException("failed to connect vsock", e);
         }
     }
 
     @Override
     public String toString() {
-        return "VirtualMachine("
-                + "name:" + getName() + ", "
-                + "config:" + getConfig().getPayloadConfigPath() + ", "
-                + "package: " + mPackageName
-                + ")";
+        VirtualMachineConfig config = getConfig();
+        String payloadConfigPath = config.getPayloadConfigPath();
+        String payloadBinaryPath = config.getPayloadBinaryPath();
+
+        StringBuilder result = new StringBuilder();
+        result.append("VirtualMachine(")
+                .append("name:")
+                .append(getName())
+                .append(", ");
+        if (payloadBinaryPath != null) {
+            result.append("payload:")
+                    .append(payloadBinaryPath)
+                    .append(", ");
+        }
+        if (payloadConfigPath != null) {
+            result.append("config:")
+                    .append(payloadConfigPath)
+                    .append(", ");
+        }
+        result.append("package: ")
+                .append(mPackageName)
+                .append(")");
+        return result.toString();
     }
 
     private static List<String> parseExtraApkListFromPayloadConfig(JsonReader reader)
@@ -729,11 +860,14 @@
     private static List<ExtraApkSpec> setupExtraApks(
             @NonNull Context context, @NonNull VirtualMachineConfig config, @NonNull File vmDir)
             throws VirtualMachineException {
+        String configPath = config.getPayloadConfigPath();
+        if (configPath == null) {
+            return Collections.emptyList();
+        }
         try {
             ZipFile zipFile = new ZipFile(context.getPackageCodePath());
-            String payloadPath = config.getPayloadConfigPath();
             InputStream inputStream =
-                    zipFile.getInputStream(zipFile.getEntry(config.getPayloadConfigPath()));
+                    zipFile.getInputStream(zipFile.getEntry(configPath));
             List<String> apkList =
                     parseExtraApkListFromPayloadConfig(
                             new JsonReader(new InputStreamReader(inputStream)));
diff --git a/javalib/src/android/system/virtualmachine/VirtualMachineCallback.java b/javalib/src/android/system/virtualmachine/VirtualMachineCallback.java
index c802678..c89b8bb 100644
--- a/javalib/src/android/system/virtualmachine/VirtualMachineCallback.java
+++ b/javalib/src/android/system/virtualmachine/VirtualMachineCallback.java
@@ -19,6 +19,7 @@
 import android.annotation.IntDef;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
+import android.annotation.SuppressLint;
 import android.os.ParcelFileDescriptor;
 
 import java.lang.annotation.Retention;
@@ -30,6 +31,7 @@
  *
  * @hide
  */
+@SuppressLint("CallbackInterface")  // Guidance has changed, lint is out of date (b/245552641)
 public interface VirtualMachineCallback {
     /** @hide */
     @Retention(RetentionPolicy.SOURCE)
@@ -43,90 +45,95 @@
 
     /** Error code for all other errors not listed below. */
     int ERROR_UNKNOWN = 0;
-
     /**
      * Error code indicating that the payload can't be verified due to various reasons (e.g invalid
      * merkle tree, invalid formats, etc).
      */
     int ERROR_PAYLOAD_VERIFICATION_FAILED = 1;
-
     /** Error code indicating that the payload is verified, but has changed since the last boot. */
     int ERROR_PAYLOAD_CHANGED = 2;
-
     /** Error code indicating that the payload config is invalid. */
     int ERROR_PAYLOAD_INVALID_CONFIG = 3;
 
     /** @hide */
     @Retention(RetentionPolicy.SOURCE)
     @IntDef({
-        DEATH_REASON_VIRTUALIZATIONSERVICE_DIED,
-        DEATH_REASON_INFRASTRUCTURE_ERROR,
-        DEATH_REASON_KILLED,
-        DEATH_REASON_UNKNOWN,
-        DEATH_REASON_SHUTDOWN,
-        DEATH_REASON_ERROR,
-        DEATH_REASON_REBOOT,
-        DEATH_REASON_CRASH,
-        DEATH_REASON_HANGUP,
+        STOP_REASON_VIRTUALIZATION_SERVICE_DIED,
+        STOP_REASON_INFRASTRUCTURE_ERROR,
+        STOP_REASON_KILLED,
+        STOP_REASON_UNKNOWN,
+        STOP_REASON_SHUTDOWN,
+        STOP_REASON_ERROR,
+        STOP_REASON_REBOOT,
+        STOP_REASON_CRASH,
+        STOP_REASON_PVM_FIRMWARE_PUBLIC_KEY_MISMATCH,
+        STOP_REASON_PVM_FIRMWARE_INSTANCE_IMAGE_CHANGED,
+        STOP_REASON_BOOTLOADER_PUBLIC_KEY_MISMATCH,
+        STOP_REASON_BOOTLOADER_INSTANCE_IMAGE_CHANGED,
+        STOP_REASON_MICRODROID_FAILED_TO_CONNECT_TO_VIRTUALIZATION_SERVICE,
+        STOP_REASON_MICRODROID_PAYLOAD_HAS_CHANGED,
+        STOP_REASON_MICRODROID_PAYLOAD_VERIFICATION_FAILED,
+        STOP_REASON_MICRODROID_INVALID_PAYLOAD_CONFIG,
+        STOP_REASON_MICRODROID_UNKNOWN_RUNTIME_ERROR,
+        STOP_REASON_HANGUP,
     })
-    @interface DeathReason {}
+    @interface StopReason {}
 
-    /**
-     * virtualizationservice itself died, taking the VM down with it. This is a negative number to
-     * avoid conflicting with the other death reasons which match the ones in the AIDL interface.
-     */
-    int DEATH_REASON_VIRTUALIZATIONSERVICE_DIED = -1;
+    /** The virtualization service itself died, taking the VM down with it. */
+    //  This is a negative number to avoid conflicting with the other death reasons which match
+    //  the ones in the AIDL interface.
+    int STOP_REASON_VIRTUALIZATION_SERVICE_DIED = -1;
 
     /** There was an error waiting for the VM. */
-    int DEATH_REASON_INFRASTRUCTURE_ERROR = 0;
+    int STOP_REASON_INFRASTRUCTURE_ERROR = 0;
 
     /** The VM was killed. */
-    int DEATH_REASON_KILLED = 1;
+    int STOP_REASON_KILLED = 1;
 
     /** The VM died for an unknown reason. */
-    int DEATH_REASON_UNKNOWN = 2;
+    int STOP_REASON_UNKNOWN = 2;
 
     /** The VM requested to shut down. */
-    int DEATH_REASON_SHUTDOWN = 3;
+    int STOP_REASON_SHUTDOWN = 3;
 
     /** crosvm had an error starting the VM. */
-    int DEATH_REASON_ERROR = 4;
+    int STOP_REASON_ERROR = 4;
 
     /** The VM requested to reboot, possibly as the result of a kernel panic. */
-    int DEATH_REASON_REBOOT = 5;
+    int STOP_REASON_REBOOT = 5;
 
     /** The VM or crosvm crashed. */
-    int DEATH_REASON_CRASH = 6;
+    int STOP_REASON_CRASH = 6;
 
     /** The pVM firmware failed to verify the VM because the public key doesn't match. */
-    int DEATH_REASON_PVM_FIRMWARE_PUBLIC_KEY_MISMATCH = 7;
+    int STOP_REASON_PVM_FIRMWARE_PUBLIC_KEY_MISMATCH = 7;
 
     /** The pVM firmware failed to verify the VM because the instance image changed. */
-    int DEATH_REASON_PVM_FIRMWARE_INSTANCE_IMAGE_CHANGED = 8;
+    int STOP_REASON_PVM_FIRMWARE_INSTANCE_IMAGE_CHANGED = 8;
 
     /** The bootloader failed to verify the VM because the public key doesn't match. */
-    int DEATH_REASON_BOOTLOADER_PUBLIC_KEY_MISMATCH = 9;
+    int STOP_REASON_BOOTLOADER_PUBLIC_KEY_MISMATCH = 9;
 
     /** The bootloader failed to verify the VM because the instance image changed. */
-    int DEATH_REASON_BOOTLOADER_INSTANCE_IMAGE_CHANGED = 10;
+    int STOP_REASON_BOOTLOADER_INSTANCE_IMAGE_CHANGED = 10;
 
     /** The microdroid failed to connect to VirtualizationService's RPC server. */
-    int DEATH_REASON_MICRODROID_FAILED_TO_CONNECT_TO_VIRTUALIZATION_SERVICE = 11;
+    int STOP_REASON_MICRODROID_FAILED_TO_CONNECT_TO_VIRTUALIZATION_SERVICE = 11;
 
     /** The payload for microdroid is changed. */
-    int DEATH_REASON_MICRODROID_PAYLOAD_HAS_CHANGED = 12;
+    int STOP_REASON_MICRODROID_PAYLOAD_HAS_CHANGED = 12;
 
     /** The microdroid failed to verify given payload APK. */
-    int DEATH_REASON_MICRODROID_PAYLOAD_VERIFICATION_FAILED = 13;
+    int STOP_REASON_MICRODROID_PAYLOAD_VERIFICATION_FAILED = 13;
 
     /** The VM config for microdroid is invalid (e.g. missing tasks). */
-    int DEATH_REASON_MICRODROID_INVALID_PAYLOAD_CONFIG = 14;
+    int STOP_REASON_MICRODROID_INVALID_PAYLOAD_CONFIG = 14;
 
     /** There was a runtime error while running microdroid manager. */
-    int DEATH_REASON_MICRODROID_UNKNOWN_RUNTIME_ERROR = 15;
+    int STOP_REASON_MICRODROID_UNKNOWN_RUNTIME_ERROR = 15;
 
     /** The VM killed due to hangup */
-    int DEATH_REASON_HANGUP = 16;
+    int STOP_REASON_HANGUP = 16;
 
     /**
      * Called when the payload starts in the VM. The stream, if non-null, provides access
@@ -136,7 +143,7 @@
 
     /**
      * Called when the payload in the VM is ready to serve. See
-     * {@link VirtualMachine#connectToVsockServer(int)} ()}.
+     * {@link VirtualMachine#connectToVsockServer(int)}.
      */
     void onPayloadReady(@NonNull VirtualMachine vm);
 
@@ -146,8 +153,8 @@
     /** Called when an error occurs in the VM. */
     void onError(@NonNull VirtualMachine vm, @ErrorCode int errorCode, @NonNull String message);
 
-    /** Called when the VM has ended. */
-    void onDied(@NonNull VirtualMachine vm, @DeathReason int reason);
+    /** Called when the VM has stopped. */
+    void onStopped(@NonNull VirtualMachine vm, @StopReason int reason);
 
     /** Called when kernel panic occurs and as a result ramdump is generated from the VM. */
     void onRamdump(@NonNull VirtualMachine vm, @NonNull ParcelFileDescriptor ramdump);
diff --git a/javalib/src/android/system/virtualmachine/VirtualMachineConfig.java b/javalib/src/android/system/virtualmachine/VirtualMachineConfig.java
index 63a3f43..3061f65 100644
--- a/javalib/src/android/system/virtualmachine/VirtualMachineConfig.java
+++ b/javalib/src/android/system/virtualmachine/VirtualMachineConfig.java
@@ -18,24 +18,23 @@
 
 import static android.os.ParcelFileDescriptor.MODE_READ_ONLY;
 
+import android.annotation.IntDef;
 import android.annotation.NonNull;
+import android.annotation.Nullable;
 import android.content.Context;
-import android.content.pm.PackageManager;
-import android.content.pm.PackageManager.PackageInfoFlags;
-import android.content.pm.Signature; // This actually is certificate!
 import android.os.ParcelFileDescriptor;
 import android.os.PersistableBundle;
 import android.sysprop.HypervisorProperties;
 import android.system.virtualizationservice.VirtualMachineAppConfig;
+import android.system.virtualizationservice.VirtualMachinePayloadConfig;
 
 import java.io.File;
 import java.io.FileNotFoundException;
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.OutputStream;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.List;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
 import java.util.Objects;
 
 /**
@@ -47,11 +46,11 @@
  */
 public final class VirtualMachineConfig {
     // These defines the schema of the config file persisted on disk.
-    private static final int VERSION = 1;
+    private static final int VERSION = 2;
     private static final String KEY_VERSION = "version";
-    private static final String KEY_CERTS = "certs";
     private static final String KEY_APKPATH = "apkPath";
     private static final String KEY_PAYLOADCONFIGPATH = "payloadConfigPath";
+    private static final String KEY_PAYLOADBINARYPATH = "payloadBinaryPath";
     private static final String KEY_DEBUGLEVEL = "debugLevel";
     private static final String KEY_PROTECTED_VM = "protectedVm";
     private static final String KEY_MEMORY_MIB = "memoryMib";
@@ -59,34 +58,41 @@
 
     // Paths to the APK file of this application.
     @NonNull private final String mApkPath;
-    @NonNull private final Signature[] mCerts;
+
+    /** @hide */
+    @Retention(RetentionPolicy.SOURCE)
+    @IntDef({
+            DEBUG_LEVEL_NONE,
+            DEBUG_LEVEL_APP_ONLY,
+            DEBUG_LEVEL_FULL
+    })
+    public @interface DebugLevel {}
 
     /**
-     * A debug level defines the set of debug features that the VM can be configured to.
+     * Not debuggable at all. No log is exported from the VM. Debugger can't be attached to the
+     * app process running in the VM. This is the default level.
      *
      * @hide
      */
-    public enum DebugLevel {
-        /**
-         * Not debuggable at all. No log is exported from the VM. Debugger can't be attached to the
-         * app process running in the VM. This is the default level.
-         */
-        NONE,
+    public static final int DEBUG_LEVEL_NONE = 0;
 
-        /**
-         * Only the app is debuggable. Log from the app is exported from the VM. Debugger can be
-         * attached to the app process. Rest of the VM is not debuggable.
-         */
-        APP_ONLY,
+    /**
+     * Only the app is debuggable. Log from the app is exported from the VM. Debugger can be
+     * attached to the app process. Rest of the VM is not debuggable.
+     *
+     * @hide
+     */
+    public static final int DEBUG_LEVEL_APP_ONLY = 1;
 
-        /**
-         * Fully debuggable. All logs (both logcat and kernel message) are exported. All processes
-         * running in the VM can be attached to the debugger. Rooting is possible.
-         */
-        FULL,
-    }
+    /**
+     * Fully debuggable. All logs (both logcat and kernel message) are exported. All processes
+     * running in the VM can be attached to the debugger. Rooting is possible.
+     *
+     * @hide
+     */
+    public static final int DEBUG_LEVEL_FULL = 2;
 
-    private final DebugLevel mDebugLevel;
+    @DebugLevel private final int mDebugLevel;
 
     /**
      * Whether to run the VM in protected mode, so the host can't access its memory.
@@ -104,21 +110,26 @@
     private final int mNumCpus;
 
     /**
-     * Path within the APK to the payload config file that defines software aspects of this config.
+     * Path within the APK to the payload config file that defines software aspects of the VM.
      */
-    @NonNull private final String mPayloadConfigPath;
+    @Nullable private final String mPayloadConfigPath;
+
+    /**
+     * Path within the APK to the payload binary file that will be executed within the VM.
+     */
+    @Nullable private final String mPayloadBinaryPath;
 
     private VirtualMachineConfig(
             @NonNull String apkPath,
-            @NonNull Signature[] certs,
-            @NonNull String payloadConfigPath,
-            DebugLevel debugLevel,
+            @Nullable String payloadConfigPath,
+            @Nullable String payloadBinaryPath,
+            @DebugLevel int debugLevel,
             boolean protectedVm,
             int memoryMib,
             int numCpus) {
-        mApkPath = apkPath;
-        mCerts = certs;
+        mApkPath = Objects.requireNonNull(apkPath);
         mPayloadConfigPath = payloadConfigPath;
+        mPayloadBinaryPath = payloadBinaryPath;
         mDebugLevel = debugLevel;
         mProtectedVm = protectedVm;
         mMemoryMib = memoryMib;
@@ -130,33 +141,33 @@
     static VirtualMachineConfig from(@NonNull InputStream input)
             throws IOException, VirtualMachineException {
         PersistableBundle b = PersistableBundle.readFromStream(input);
-        final int version = b.getInt(KEY_VERSION);
+        int version = b.getInt(KEY_VERSION);
         if (version > VERSION) {
             throw new VirtualMachineException("Version too high");
         }
-        final String apkPath = b.getString(KEY_APKPATH);
+        String apkPath = b.getString(KEY_APKPATH);
         if (apkPath == null) {
             throw new VirtualMachineException("No apkPath");
         }
-        final String[] certStrings = b.getStringArray(KEY_CERTS);
-        if (certStrings == null || certStrings.length == 0) {
-            throw new VirtualMachineException("No certs");
+        String payloadBinaryPath = b.getString(KEY_PAYLOADBINARYPATH);
+        String payloadConfigPath = null;
+        if (payloadBinaryPath == null) {
+            payloadConfigPath = b.getString(KEY_PAYLOADCONFIGPATH);
+            if (payloadConfigPath == null) {
+                throw new VirtualMachineException("No payloadBinaryPath");
+            }
         }
-        List<Signature> certList = new ArrayList<>();
-        for (String s : certStrings) {
-            certList.add(new Signature(s));
+        @DebugLevel int debugLevel = b.getInt(KEY_DEBUGLEVEL);
+        if (debugLevel != DEBUG_LEVEL_NONE && debugLevel != DEBUG_LEVEL_APP_ONLY
+                && debugLevel != DEBUG_LEVEL_FULL) {
+            throw new VirtualMachineException("Invalid debugLevel: " + debugLevel);
         }
-        Signature[] certs = certList.toArray(new Signature[0]);
-        final String payloadConfigPath = b.getString(KEY_PAYLOADCONFIGPATH);
-        if (payloadConfigPath == null) {
-            throw new VirtualMachineException("No payloadConfigPath");
-        }
-        final DebugLevel debugLevel = DebugLevel.values()[b.getInt(KEY_DEBUGLEVEL)];
-        final boolean protectedVm = b.getBoolean(KEY_PROTECTED_VM);
-        final int memoryMib = b.getInt(KEY_MEMORY_MIB);
-        final int numCpus = b.getInt(KEY_NUM_CPUS);
-        return new VirtualMachineConfig(apkPath, certs, payloadConfigPath, debugLevel, protectedVm,
-                memoryMib, numCpus);
+        boolean protectedVm = b.getBoolean(KEY_PROTECTED_VM);
+        int memoryMib = b.getInt(KEY_MEMORY_MIB);
+        int numCpus = b.getInt(KEY_NUM_CPUS);
+
+        return new VirtualMachineConfig(apkPath, payloadConfigPath, payloadBinaryPath, debugLevel,
+                protectedVm, memoryMib, numCpus);
     }
 
     /** Persists this config to a stream, for example a file. */
@@ -164,14 +175,9 @@
         PersistableBundle b = new PersistableBundle();
         b.putInt(KEY_VERSION, VERSION);
         b.putString(KEY_APKPATH, mApkPath);
-        List<String> certList = new ArrayList<>();
-        for (Signature cert : mCerts) {
-            certList.add(cert.toCharsString());
-        }
-        String[] certs = certList.toArray(new String[0]);
-        b.putStringArray(KEY_CERTS, certs);
         b.putString(KEY_PAYLOADCONFIGPATH, mPayloadConfigPath);
-        b.putInt(KEY_DEBUGLEVEL, mDebugLevel.ordinal());
+        b.putString(KEY_PAYLOADBINARYPATH, mPayloadBinaryPath);
+        b.putInt(KEY_DEBUGLEVEL, mDebugLevel);
         b.putBoolean(KEY_PROTECTED_VM, mProtectedVm);
         b.putInt(KEY_NUM_CPUS, mNumCpus);
         if (mMemoryMib > 0) {
@@ -185,32 +191,74 @@
      *
      * @hide
      */
-    @NonNull
+    @Nullable
     public String getPayloadConfigPath() {
         return mPayloadConfigPath;
     }
 
     /**
+     * Returns the path within the APK to the payload binary file that will be executed within the
+     * VM.
+     *
+     * @hide
+     */
+    @Nullable
+    public String getPayloadBinaryPath() {
+        return mPayloadBinaryPath;
+    }
+
+    /**
+     * Returns the debug level for the VM.
+     *
+     * @hide
+     */
+    @NonNull
+    @DebugLevel
+    public int getDebugLevel() {
+        return mDebugLevel;
+    }
+
+    /**
+     * Returns whether the VM's memory will be protected from the host.
+     *
+     * @hide
+     */
+    public boolean isProtectedVm() {
+        return mProtectedVm;
+    }
+
+    /**
+     * Returns the amount of RAM that will be made available to the VM.
+     *
+     * @hide
+     */
+    public int getMemoryMib() {
+        return mMemoryMib;
+    }
+
+    /**
+     * Returns the number of vCPUs that the VM will have.
+     *
+     * @hide
+     */
+    public int getNumCpus() {
+        return mNumCpus;
+    }
+
+    /**
      * 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, and change of the payload as long as the payload is
-     * signed by the same signer. All other changes (e.g. using a payload from a different signer,
+     * 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.
      *
      * @hide
      */
     public boolean isCompatibleWith(@NonNull VirtualMachineConfig other) {
-        if (!Arrays.equals(this.mCerts, other.mCerts)) {
-            return false;
-        }
-        if (this.mDebugLevel != other.mDebugLevel) {
-            // TODO(jiyong): should we treat APP_ONLY and FULL the same?
-            return false;
-        }
-        if (this.mProtectedVm != other.mProtectedVm) {
-            return false;
-        }
-        return true;
+        return this.mDebugLevel == other.mDebugLevel
+                && this.mProtectedVm == other.mProtectedVm
+                && Objects.equals(this.mPayloadConfigPath, other.mPayloadConfigPath)
+                && Objects.equals(this.mPayloadBinaryPath, other.mPayloadBinaryPath)
+                && this.mApkPath.equals(other.mApkPath);
     }
 
     /**
@@ -219,20 +267,29 @@
      * service doesn't accept paths as it might not have permission to open app-owned files and that
      * could be abused to run a VM with software that the calling application doesn't own.
      */
-    /* package */ VirtualMachineAppConfig toParcel() throws FileNotFoundException {
+    VirtualMachineAppConfig toParcel() throws FileNotFoundException {
         VirtualMachineAppConfig parcel = new VirtualMachineAppConfig();
         parcel.apk = ParcelFileDescriptor.open(new File(mApkPath), MODE_READ_ONLY);
-        parcel.payload = VirtualMachineAppConfig.Payload.configPath(mPayloadConfigPath);
+        if (mPayloadBinaryPath != null) {
+            VirtualMachinePayloadConfig payloadConfig = new VirtualMachinePayloadConfig();
+            payloadConfig.payloadPath = mPayloadBinaryPath;
+            payloadConfig.args = new String[]{};
+            parcel.payload =
+                    VirtualMachineAppConfig.Payload.payloadConfig(payloadConfig);
+        } else {
+            parcel.payload =
+                    VirtualMachineAppConfig.Payload.configPath(mPayloadConfigPath);
+        }
         switch (mDebugLevel) {
-            case NONE:
-                parcel.debugLevel = VirtualMachineAppConfig.DebugLevel.NONE;
-                break;
-            case APP_ONLY:
+            case DEBUG_LEVEL_APP_ONLY:
                 parcel.debugLevel = VirtualMachineAppConfig.DebugLevel.APP_ONLY;
                 break;
-            case FULL:
+            case DEBUG_LEVEL_FULL:
                 parcel.debugLevel = VirtualMachineAppConfig.DebugLevel.FULL;
                 break;
+            default:
+                parcel.debugLevel = VirtualMachineAppConfig.DebugLevel.NONE;
+                break;
         }
         parcel.protectedVm = mProtectedVm;
         parcel.memoryMib = mMemoryMib;
@@ -248,69 +305,28 @@
      *
      * @hide
      */
-    public static class Builder {
+    public static final class Builder {
         private final Context mContext;
-        private final String mPayloadConfigPath;
-        private DebugLevel mDebugLevel;
+        @Nullable private String mPayloadConfigPath;
+        @Nullable private String mPayloadBinaryPath;
+        @DebugLevel private int mDebugLevel;
         private boolean mProtectedVm;
+        private boolean mProtectedVmSet;
         private int mMemoryMib;
         private int mNumCpus;
 
         /**
-         * Creates a builder for the given context (APK), and the payload config file in APK.
+         * Creates a builder for the given context (APK).
          *
          * @hide
          */
-        public Builder(@NonNull Context context, @NonNull String payloadConfigPath) {
+        public Builder(@NonNull Context context) {
             mContext = Objects.requireNonNull(context);
-            mPayloadConfigPath = Objects.requireNonNull(payloadConfigPath);
-            mDebugLevel = DebugLevel.NONE;
-            mProtectedVm = false;
+            mDebugLevel = DEBUG_LEVEL_NONE;
             mNumCpus = 1;
         }
 
         /**
-         * Sets the debug level
-         *
-         * @hide
-         */
-        public Builder debugLevel(DebugLevel debugLevel) {
-            mDebugLevel = debugLevel;
-            return this;
-        }
-
-        /**
-         *  Sets whether to protect the VM memory from the host. Defaults to false.
-         *
-         * @hide
-         */
-        public Builder protectedVm(boolean protectedVm) {
-            mProtectedVm = protectedVm;
-            return this;
-        }
-
-        /**
-         * Sets the amount of RAM to give the VM. If this is zero or negative then the default will
-         * be used.
-         *
-         * @hide
-         */
-        public Builder memoryMib(int memoryMib) {
-            mMemoryMib = memoryMib;
-            return this;
-        }
-
-        /**
-         * Sets the number of vCPUs in the VM. Defaults to 1.
-         *
-         * @hide
-         */
-        public Builder numCpus(int num) {
-            mNumCpus = num;
-            return this;
-        }
-
-        /**
          * Builds an immutable {@link VirtualMachineConfig}
          *
          * @hide
@@ -318,18 +334,6 @@
         @NonNull
         public VirtualMachineConfig build() {
             final String apkPath = mContext.getPackageCodePath();
-            final String packageName = mContext.getPackageName();
-            Signature[] certs;
-            try {
-                certs = mContext.getPackageManager()
-                        .getPackageInfo(packageName,
-                                PackageInfoFlags.of(PackageManager.GET_SIGNING_CERTIFICATES))
-                        .signingInfo
-                        .getSigningCertificateHistory();
-            } catch (PackageManager.NameNotFoundException e) {
-                // This cannot happen as `packageName` is from this app.
-                throw new RuntimeException(e);
-            }
 
             final int availableCpus = Runtime.getRuntime().availableProcessors();
             if (mNumCpus < 1 || mNumCpus > availableCpus) {
@@ -337,6 +341,21 @@
                         + "range [1, " + availableCpus + "]");
             }
 
+            if (mPayloadBinaryPath == null) {
+                if (mPayloadConfigPath == null) {
+                    throw new IllegalStateException("payloadBinaryPath must be set");
+                }
+            } else {
+                if (mPayloadConfigPath != null) {
+                    throw new IllegalStateException(
+                            "payloadBinaryPath and payloadConfigPath may not both be set");
+                }
+            }
+
+            if (!mProtectedVmSet) {
+                throw new IllegalStateException("protectedVm must be set explicitly");
+            }
+
             if (mProtectedVm
                     && !HypervisorProperties.hypervisor_protected_vm_supported().orElse(false)) {
                 throw new UnsupportedOperationException(
@@ -348,8 +367,79 @@
             }
 
             return new VirtualMachineConfig(
-                    apkPath, certs, mPayloadConfigPath, mDebugLevel, mProtectedVm, mMemoryMib,
-                    mNumCpus);
+                    apkPath, mPayloadConfigPath, mPayloadBinaryPath, mDebugLevel, mProtectedVm,
+                    mMemoryMib, mNumCpus);
+        }
+
+        /**
+         * Sets the path within the APK to the payload config file that defines software aspects
+         * of the VM.
+         *
+         * @hide
+         */
+        @NonNull
+        public Builder setPayloadConfigPath(@NonNull String payloadConfigPath) {
+            mPayloadConfigPath = Objects.requireNonNull(payloadConfigPath);
+            return this;
+        }
+
+        /**
+         * Sets the path within the APK to the payload binary file that will be executed within
+         * the VM.
+         *
+         * @hide
+         */
+        @NonNull
+        public Builder setPayloadBinaryPath(@NonNull String payloadBinaryPath) {
+            mPayloadBinaryPath = Objects.requireNonNull(payloadBinaryPath);
+            return this;
+        }
+
+        /**
+         * Sets the debug level
+         *
+         * @hide
+         */
+        @NonNull
+        public Builder setDebugLevel(@DebugLevel int debugLevel) {
+            mDebugLevel = debugLevel;
+            return this;
+        }
+
+        /**
+         * Sets whether to protect the VM memory from the host. No default is provided, this
+         * must be set explicitly.
+         *
+         * @hide
+         */
+        @NonNull
+        public Builder setProtectedVm(boolean protectedVm) {
+            mProtectedVm = protectedVm;
+            mProtectedVmSet = true;
+            return this;
+        }
+
+        /**
+         * Sets the amount of RAM to give the VM. If this is zero or negative then the default will
+         * be used.
+         *
+         * @hide
+         */
+        @NonNull
+        public Builder setMemoryMib(int memoryMib) {
+            mMemoryMib = memoryMib;
+            return this;
+        }
+
+        /**
+         * Sets the number of vCPUs in the VM. Defaults to 1.
+         *
+         * @hide
+         */
+        @NonNull
+        public Builder setNumCpus(int num) {
+            mNumCpus = num;
+            return this;
         }
     }
 }
diff --git a/javalib/src/android/system/virtualmachine/VirtualMachineException.java b/javalib/src/android/system/virtualmachine/VirtualMachineException.java
index d6aeab3..88b5ea3 100644
--- a/javalib/src/android/system/virtualmachine/VirtualMachineException.java
+++ b/javalib/src/android/system/virtualmachine/VirtualMachineException.java
@@ -16,21 +16,27 @@
 
 package android.system.virtualmachine;
 
-/** @hide */
+import android.annotation.Nullable;
+
+/**
+ * Exception thrown when operations on virtual machines fail.
+ *
+ * @hide
+ */
 public class VirtualMachineException extends Exception {
     public VirtualMachineException() {
         super();
     }
 
-    public VirtualMachineException(String message) {
+    public VirtualMachineException(@Nullable String message) {
         super(message);
     }
 
-    public VirtualMachineException(String message, Throwable cause) {
+    public VirtualMachineException(@Nullable String message, @Nullable Throwable cause) {
         super(message, cause);
     }
 
-    public VirtualMachineException(Throwable cause) {
+    public VirtualMachineException(@Nullable Throwable cause) {
         super(cause);
     }
 }
diff --git a/javalib/src/android/system/virtualmachine/VirtualMachineManager.java b/javalib/src/android/system/virtualmachine/VirtualMachineManager.java
index 1ffc6bb..ad5864e 100644
--- a/javalib/src/android/system/virtualmachine/VirtualMachineManager.java
+++ b/javalib/src/android/system/virtualmachine/VirtualMachineManager.java
@@ -16,13 +16,16 @@
 
 package android.system.virtualmachine;
 
+import static java.util.Objects.requireNonNull;
+
 import android.annotation.NonNull;
 import android.annotation.Nullable;
+import android.annotation.RequiresPermission;
+import android.annotation.SuppressLint;
 import android.content.Context;
 
 import java.lang.ref.WeakReference;
 import java.util.Map;
-import java.util.Objects;
 import java.util.WeakHashMap;
 
 /**
@@ -46,8 +49,9 @@
      * @hide
      */
     @NonNull
+    @SuppressLint("ManagerLookup") // Optional API
     public static VirtualMachineManager getInstance(@NonNull Context context) {
-        Objects.requireNonNull(context);
+        requireNonNull(context, "context must not be null");
         synchronized (sInstances) {
             VirtualMachineManager vmm =
                     sInstances.containsKey(context) ? sInstances.get(context).get() : null;
@@ -65,13 +69,17 @@
     /**
      * Creates a new {@link VirtualMachine} with the given name and config. Creating a virtual
      * machine with the same name as an existing virtual machine is an error. The existing virtual
-     * machine has to be deleted before its name can be reused. Every call to this methods creates a
-     * new (and different) virtual machine even if the name and the config are the same as the
-     * deleted one.
+     * machine has to be deleted before its name can be reused.
      *
+     * Each successful call to this method creates a new (and different) virtual machine even if the
+     * name and the config are the same as a deleted one. The new virtual machine will initially
+     * be stopped.
+     *
+     * @throws VirtualMachineException If there is an existing virtual machine with the given name
      * @hide
      */
     @NonNull
+    @RequiresPermission(VirtualMachine.MANAGE_VIRTUAL_MACHINE_PERMISSION)
     public VirtualMachine create(
             @NonNull String name, @NonNull VirtualMachineConfig config)
             throws VirtualMachineException {
diff --git a/microdroid/README.md b/microdroid/README.md
index 5cfa523..3523e9d 100644
--- a/microdroid/README.md
+++ b/microdroid/README.md
@@ -171,3 +171,17 @@
 ```
 
 Done. Now you are logged into Microdroid. Have fun!
+
+Once you have an adb connection with `vm_shell`, `localhost:8000` will be the
+serial of microdroid.
+
+## Debugging the payload on microdroid
+
+Like a normal adb device, you can debug native processes using `lldbclient.py`
+script, either by running a new process, or attaching to an existing process.
+Use `vm_shell` tool above, and then run `lldbclient.py`.
+
+```sh
+development/scripts/lldbclient.py -s localhost:8000 --chroot . --user '' \
+    (-p PID | -n NAME | -r ...)
+```
diff --git a/microdroid/vm_payload/Android.bp b/microdroid/vm_payload/Android.bp
index f7223ab..a68595f 100644
--- a/microdroid/vm_payload/Android.bp
+++ b/microdroid/vm_payload/Android.bp
@@ -2,7 +2,7 @@
     default_applicable_licenses: ["Android-Apache-2.0"],
 }
 
-rust_ffi_static {
+rust_ffi_shared {
     name: "libvm_payload",
     crate_name: "vm_payload",
     srcs: ["src/*.rs"],
@@ -19,3 +19,15 @@
         "com.android.compos",
     ],
 }
+
+rust_bindgen {
+    name: "libvm_payload_bindgen",
+    wrapper_src: "include/vm_payload.h",
+    crate_name: "vm_payload_bindgen",
+    source_stem: "bindings",
+    apex_available: ["com.android.compos"],
+    visibility: ["//packages/modules/Virtualization/compos"],
+    shared_libs: [
+        "libvm_payload",
+    ],
+}
diff --git a/microdroid/vm_payload/include/vm_payload.h b/microdroid/vm_payload/include/vm_payload.h
index 6dba760..05abdce 100644
--- a/microdroid/vm_payload/include/vm_payload.h
+++ b/microdroid/vm_payload/include/vm_payload.h
@@ -16,37 +16,60 @@
 
 #pragma once
 
+#include <stdbool.h>
+#include <stddef.h>
+
 #ifdef __cplusplus
 extern "C" {
 #endif
 
 /**
  * Notifies the host that the payload is ready.
- * Returns true if the notification succeeds else false.
+ *
+ * \return true if the notification succeeds else false.
  */
-bool notify_payload_ready();
+bool AVmPayload_notifyPayloadReady(void);
 
 /**
- * Get the VM's attestation chain.
- * Returns the size of data or 0 on failure.
+ * Get a secret that is uniquely bound to this VM instance. The secrets are 32-byte values and the
+ * value associated with an identifier will not change over the lifetime of the VM instance.
+ *
+ * \param identifier identifier of the secret to return.
+ * \param identifier_size size of the secret identifier.
+ * \param secret pointer to size bytes where the secret is written.
+ * \param size number of bytes of the secret to get, up to the secret size.
+ *
+ * \return true on success and false on failure.
+ */
+bool AVmPayload_getVmInstanceSecret(const void *identifier, size_t identifier_size, void *secret,
+                                    size_t size);
+
+/**
+ * Get the VM's DICE attestation chain.
+ *
  * TODO: don't expose the contained privacy breaking identifiers to the payload
  * TODO: keep the DICE chain as an internal detail for as long as possible
+ *
+ * \param data pointer to size bytes where the chain is written.
+ * \param size number of bytes that can be written to data.
+ * \param total outputs the total size of the chain if the function succeeds
+ *
+ * \return true on success and false on failure.
  */
-size_t get_dice_attestation_chain(void *data, size_t size);
+bool AVmPayload_getDiceAttestationChain(void *data, size_t size, size_t *total);
 
 /**
- * Get the VM's attestation CDI.
- * Returns the size of data or 0 on failure.
+ * Get the VM's DICE attestation CDI.
+ *
  * TODO: don't expose the raw CDI, only derived values
+ *
+ * \param data pointer to size bytes where the CDI is written.
+ * \param size number of bytes that can be written to data.
+ * \param total outputs the total size of the CDI if the function succeeds
+ *
+ * \return true on success and false on failure.
  */
-size_t get_dice_attestation_cdi(void *data, size_t size);
-
-/**
- * Get the VM's sealing CDI.
- * Returns the size of data or 0 on failure.
- * TODO: don't expose the raw CDI, only derived values
- */
-size_t get_dice_sealing_cdi(void *data, size_t size);
+bool AVmPayload_getDiceAttestationCdi(void *data, size_t size, size_t *total);
 
 #ifdef __cplusplus
 } // extern "C"
diff --git a/microdroid/vm_payload/src/lib.rs b/microdroid/vm_payload/src/lib.rs
index e3da227..ca0c17b 100644
--- a/microdroid/vm_payload/src/lib.rs
+++ b/microdroid/vm_payload/src/lib.rs
@@ -17,6 +17,6 @@
 mod vm_service;
 
 pub use vm_service::{
-    get_dice_attestation_cdi, get_dice_attestation_chain, get_dice_sealing_cdi,
-    notify_payload_ready,
+    AVmPayload_getDiceAttestationCdi, AVmPayload_getDiceAttestationChain,
+    AVmPayload_getVmInstanceSecret, AVmPayload_notifyPayloadReady,
 };
diff --git a/microdroid/vm_payload/src/vm_service.rs b/microdroid/vm_payload/src/vm_service.rs
index 18d8222..44013c9 100644
--- a/microdroid/vm_payload/src/vm_service.rs
+++ b/microdroid/vm_payload/src/vm_service.rs
@@ -23,7 +23,7 @@
 /// Notifies the host that the payload is ready.
 /// Returns true if the notification succeeds else false.
 #[no_mangle]
-pub extern "C" fn notify_payload_ready() -> bool {
+pub extern "C" fn AVmPayload_notifyPayloadReady() -> bool {
     android_logger::init_once(
         android_logger::Config::default().with_tag("vm_payload").with_min_level(Level::Debug),
     );
@@ -42,26 +42,71 @@
     get_vm_payload_service()?.notifyPayloadReady().context("Cannot notify payload ready")
 }
 
-/// Get the VM's attestation chain.
-/// Returns the size of data or 0 on failure.
+/// Get a secret that is uniquely bound to this VM instance.
 ///
 /// # Safety
 ///
-/// The data must be size bytes big.
+/// Behavior is undefined if any of the following conditions are violated:
+///
+/// * `identifier` must be [valid] for reads of `identifier_size` bytes.
+/// * `secret` must be [valid] for writes of `size` bytes.
+///
+/// [valid]: std::ptr#safety
 #[no_mangle]
-pub unsafe extern "C" fn get_dice_attestation_chain(data: *mut u8, size: usize) -> usize {
+pub unsafe extern "C" fn AVmPayload_getVmInstanceSecret(
+    identifier: *const u8,
+    identifier_size: usize,
+    secret: *mut u8,
+    size: usize,
+) -> bool {
+    let identifier = std::slice::from_raw_parts(identifier, identifier_size);
+    match try_get_vm_instance_secret(identifier, size) {
+        Err(e) => {
+            error!("{:?}", e);
+            false
+        }
+        Ok(vm_secret) => {
+            if vm_secret.len() != size {
+                return false;
+            }
+            std::ptr::copy_nonoverlapping(vm_secret.as_ptr(), secret, size);
+            true
+        }
+    }
+}
+
+fn try_get_vm_instance_secret(identifier: &[u8], size: usize) -> Result<Vec<u8>> {
+    get_vm_payload_service()?
+        .getVmInstanceSecret(identifier, i32::try_from(size)?)
+        .context("Cannot get VM instance secret")
+}
+
+/// Get the VM's attestation chain.
+/// Returns true on success, else false.
+///
+/// # Safety
+///
+/// Behavior is undefined if any of the following conditions are violated:
+///
+/// * `data` must be [valid] for writes of `size` bytes.
+/// * `total` must be [valid] for writes.
+///
+/// [valid]: std::ptr#safety
+#[no_mangle]
+pub unsafe extern "C" fn AVmPayload_getDiceAttestationChain(
+    data: *mut u8,
+    size: usize,
+    total: *mut usize,
+) -> bool {
     match try_get_dice_attestation_chain() {
         Err(e) => {
             error!("{:?}", e);
-            0
+            false
         }
         Ok(chain) => {
-            if size < chain.len() {
-                0
-            } else {
-                std::ptr::copy_nonoverlapping(chain.as_ptr(), data, chain.len());
-                chain.len()
-            }
+            total.write(chain.len());
+            std::ptr::copy_nonoverlapping(chain.as_ptr(), data, std::cmp::min(chain.len(), size));
+            true
         }
     }
 }
@@ -71,25 +116,31 @@
 }
 
 /// Get the VM's attestation CDI.
-/// Returns the size of data or 0 on failure.
+/// Returns true on success, else false.
 ///
 /// # Safety
 ///
-/// The data must be size bytes big.
+/// Behavior is undefined if any of the following conditions are violated:
+///
+/// * `data` must be [valid] for writes of `size` bytes.
+/// * `total` must be [valid] for writes.
+///
+/// [valid]: std::ptr#safety
 #[no_mangle]
-pub unsafe extern "C" fn get_dice_attestation_cdi(data: *mut u8, size: usize) -> usize {
+pub unsafe extern "C" fn AVmPayload_getDiceAttestationCdi(
+    data: *mut u8,
+    size: usize,
+    total: *mut usize,
+) -> bool {
     match try_get_dice_attestation_cdi() {
         Err(e) => {
             error!("{:?}", e);
-            0
+            false
         }
         Ok(cdi) => {
-            if size < cdi.len() {
-                0
-            } else {
-                std::ptr::copy_nonoverlapping(cdi.as_ptr(), data, cdi.len());
-                cdi.len()
-            }
+            total.write(cdi.len());
+            std::ptr::copy_nonoverlapping(cdi.as_ptr(), data, std::cmp::min(cdi.len(), size));
+            true
         }
     }
 }
@@ -98,34 +149,6 @@
     get_vm_payload_service()?.getDiceAttestationCdi().context("Cannot get attestation CDI")
 }
 
-/// Get the VM's sealing CDI.
-/// Returns the size of data or 0 on failure.
-///
-/// # Safety
-///
-/// The data must be size bytes big.
-#[no_mangle]
-pub unsafe extern "C" fn get_dice_sealing_cdi(data: *mut u8, size: usize) -> usize {
-    match try_get_dice_sealing_cdi() {
-        Err(e) => {
-            error!("{:?}", e);
-            0
-        }
-        Ok(cdi) => {
-            if size < cdi.len() {
-                0
-            } else {
-                std::ptr::copy_nonoverlapping(cdi.as_ptr(), data, cdi.len());
-                cdi.len()
-            }
-        }
-    }
-}
-
-fn try_get_dice_sealing_cdi() -> Result<Vec<u8>> {
-    get_vm_payload_service()?.getDiceSealingCdi().context("Cannot get sealing CDI")
-}
-
 fn get_vm_payload_service() -> Result<Strong<dyn IVmPayloadService>> {
     wait_for_interface(VM_PAYLOAD_SERVICE_NAME)
         .context(format!("Failed to connect to service: {}", VM_PAYLOAD_SERVICE_NAME))
diff --git a/microdroid_manager/aidl/android/system/virtualization/payload/IVmPayloadService.aidl b/microdroid_manager/aidl/android/system/virtualization/payload/IVmPayloadService.aidl
index 9f56957..d3ebe5c 100644
--- a/microdroid_manager/aidl/android/system/virtualization/payload/IVmPayloadService.aidl
+++ b/microdroid_manager/aidl/android/system/virtualization/payload/IVmPayloadService.aidl
@@ -28,6 +28,15 @@
     void notifyPayloadReady();
 
     /**
+     * Gets a secret that is uniquely bound to this VM instance.
+     *
+     * @param identifier the identifier of the secret to return.
+     * @param size the number of bytes of the secret to return.
+     * @return size bytes of the identified secret.
+     */
+    byte[] getVmInstanceSecret(in byte[] identifier, int size);
+
+    /**
      * Gets the DICE attestation chain for the VM.
      *
      * STOPSHIP:
@@ -44,13 +53,4 @@
      * of it leaking.
      */
     byte[] getDiceAttestationCdi();
-
-    /**
-     * Gets the DICE sealing CDI for the VM.
-     *
-     * TODO: A better API would handle key derivation on behalf of the payload so they can't forget
-     * to do it themselves. It also means the payload doesn't get the raw CDI so there's less chance
-     * of it leaking.
-     */
-    byte[] getDiceSealingCdi();
 }
diff --git a/microdroid_manager/src/main.rs b/microdroid_manager/src/main.rs
index 49dcacb..a6856d0 100644
--- a/microdroid_manager/src/main.rs
+++ b/microdroid_manager/src/main.rs
@@ -71,6 +71,7 @@
 
 const APEX_CONFIG_DONE_PROP: &str = "apex_config.done";
 const APP_DEBUGGABLE_PROP: &str = "ro.boot.microdroid.app_debuggable";
+const APK_MOUNT_DONE_PROP: &str = "microdroid_manager.apk.mounted";
 
 // SYNC WITH virtualizationservice/src/crosvm.rs
 const FAILURE_SERIAL_DEVICE: &str = "/dev/ttyS1";
@@ -337,12 +338,12 @@
     let dice = dice_derivation(dice, &verified_data, &payload_metadata)?;
 
     // Before reading a file from the APK, start zipfuse
-    let noexec = false;
     run_zipfuse(
-        noexec,
+        MountForExec::Allowed,
         "fscontext=u:object_r:zipfusefs:s0,context=u:object_r:system_file:s0",
         Path::new("/dev/block/mapper/microdroid-apk"),
         Path::new("/mnt/apk"),
+        Some(APK_MOUNT_DONE_PROP),
     )
     .context("Failed to run zipfuse")?;
 
@@ -378,6 +379,9 @@
         control_service("start", "authfs_service")?;
     }
 
+    // Wait until zipfuse has mounted the APK so we can access the payload
+    wait_for_property_true(APK_MOUNT_DONE_PROP).context("Failed waiting for APK mount done")?;
+
     system_properties::write("dev.bootcomplete", "1").context("set dev.bootcomplete")?;
     register_vm_payload_service(service.clone(), dice)?;
     ProcessState::start_thread_pool();
@@ -413,11 +417,25 @@
     cmd.spawn().context("Spawn apkdmverity")
 }
 
-fn run_zipfuse(noexec: bool, option: &str, zip_path: &Path, mount_dir: &Path) -> Result<Child> {
+enum MountForExec {
+    Allowed,
+    Disallowed,
+}
+
+fn run_zipfuse(
+    noexec: MountForExec,
+    option: &str,
+    zip_path: &Path,
+    mount_dir: &Path,
+    ready_prop: Option<&str>,
+) -> Result<Child> {
     let mut cmd = Command::new(ZIPFUSE_BIN);
-    if noexec {
+    if let MountForExec::Disallowed = noexec {
         cmd.arg("--noexec");
     }
+    if let Some(property_name) = ready_prop {
+        cmd.args(["-p", property_name]);
+    }
     cmd.arg("-o")
         .arg(option)
         .arg(zip_path)
@@ -603,12 +621,12 @@
         create_dir(Path::new(&mount_dir)).context("Failed to create mount dir for extra apks")?;
 
         // don't wait, just detach
-        let noexec = true;
         run_zipfuse(
-            noexec,
+            MountForExec::Disallowed,
             "fscontext=u:object_r:zipfusefs:s0,context=u:object_r:extra_apk_file:s0",
             Path::new(&format!("/dev/block/mapper/extra-apk-{}", i)),
             Path::new(&mount_dir),
+            None,
         )
         .context("Failed to zipfuse extra apks")?;
     }
@@ -618,10 +636,14 @@
 
 // Waits until linker config is generated
 fn wait_for_apex_config_done() -> Result<()> {
-    let mut prop = PropertyWatcher::new(APEX_CONFIG_DONE_PROP)?;
+    wait_for_property_true(APEX_CONFIG_DONE_PROP).context("Failed waiting for apex config done")
+}
+
+fn wait_for_property_true(property_name: &str) -> Result<()> {
+    let mut prop = PropertyWatcher::new(property_name)?;
     loop {
         prop.wait()?;
-        if system_properties::read_bool(APEX_CONFIG_DONE_PROP, false)? {
+        if system_properties::read_bool(property_name, false)? {
             break;
         }
     }
diff --git a/microdroid_manager/src/vm_payload_service.rs b/microdroid_manager/src/vm_payload_service.rs
index 70117c0..4b47ad9 100644
--- a/microdroid_manager/src/vm_payload_service.rs
+++ b/microdroid_manager/src/vm_payload_service.rs
@@ -19,7 +19,10 @@
     BnVmPayloadService, IVmPayloadService, VM_PAYLOAD_SERVICE_NAME};
 use android_system_virtualmachineservice::aidl::android::system::virtualmachineservice::IVirtualMachineService::IVirtualMachineService;
 use anyhow::{Context, Result};
-use binder::{Interface, BinderFeatures, Strong, add_service};
+use binder::{Interface, BinderFeatures, ExceptionCode, Status, Strong, add_service};
+use log::error;
+use openssl::hkdf::hkdf;
+use openssl::md::Md;
 
 /// Implementation of `IVmPayloadService`.
 struct VmPayloadService {
@@ -32,6 +35,24 @@
         self.virtual_machine_service.notifyPayloadReady()
     }
 
+    fn getVmInstanceSecret(&self, identifier: &[u8], size: i32) -> binder::Result<Vec<u8>> {
+        if !(0..=32).contains(&size) {
+            return Err(Status::new_exception(ExceptionCode::ILLEGAL_ARGUMENT, None));
+        }
+        // Use a fixed salt to scope the derivation to this API. It was randomly generated.
+        let salt = [
+            0x8B, 0x0F, 0xF0, 0xD3, 0xB1, 0x69, 0x2B, 0x95, 0x84, 0x2C, 0x9E, 0x3C, 0x99, 0x56,
+            0x7A, 0x22, 0x55, 0xF8, 0x08, 0x23, 0x81, 0x5F, 0xF5, 0x16, 0x20, 0x3E, 0xBE, 0xBA,
+            0xB7, 0xA8, 0x43, 0x92,
+        ];
+        let mut secret = vec![0; size.try_into().unwrap()];
+        hkdf(&mut secret, Md::sha256(), &self.dice.cdi_seal, &salt, identifier).map_err(|e| {
+            error!("Failed to derive VM instance secret: {:?}", e);
+            Status::new_service_specific_error(-1, None)
+        })?;
+        Ok(secret)
+    }
+
     fn getDiceAttestationChain(&self) -> binder::Result<Vec<u8>> {
         Ok(self.dice.bcc.clone())
     }
@@ -39,10 +60,6 @@
     fn getDiceAttestationCdi(&self) -> binder::Result<Vec<u8>> {
         Ok(self.dice.cdi_attest.to_vec())
     }
-
-    fn getDiceSealingCdi(&self) -> binder::Result<Vec<u8>> {
-        Ok(self.dice.cdi_seal.to_vec())
-    }
 }
 
 impl Interface for VmPayloadService {}
diff --git a/pvmfw/pvmfw.img b/pvmfw/pvmfw.img
deleted file mode 100644
index bacf213..0000000
--- a/pvmfw/pvmfw.img
+++ /dev/null
Binary files differ
diff --git a/tests/aidl/com/android/microdroid/testservice/ITestService.aidl b/tests/aidl/com/android/microdroid/testservice/ITestService.aidl
index 0913fe3..ebb2bcf 100644
--- a/tests/aidl/com/android/microdroid/testservice/ITestService.aidl
+++ b/tests/aidl/com/android/microdroid/testservice/ITestService.aidl
@@ -25,8 +25,8 @@
     /* read a system property. */
     String readProperty(String prop);
 
-    /* get the VM's stable secret, this is _only_ done for testing. */
-    byte[] insecurelyExposeSealingCdi();
+    /* get a VM instance secret, this is _only_ done for testing. */
+    byte[] insecurelyExposeVmInstanceSecret();
 
     /* get the VM's attestation secret, this is _only_ done for testing. */
     byte[] insecurelyExposeAttestationCdi();
diff --git a/tests/benchmark/Android.bp b/tests/benchmark/Android.bp
index f1aebd2..e3f4685 100644
--- a/tests/benchmark/Android.bp
+++ b/tests/benchmark/Android.bp
@@ -19,6 +19,7 @@
     libs: ["android.system.virtualmachine"],
     jni_libs: [
         "MicrodroidBenchmarkNativeLib",
+        "MicrodroidIdleNativeLib",
         "libiovsock_host_jni",
     ],
     platform_apis: true,
@@ -27,6 +28,14 @@
 }
 
 cc_library_shared {
+    name: "MicrodroidIdleNativeLib",
+    srcs: ["src/native/idlebinary.cpp"],
+    shared_libs: [
+        "libbase",
+    ],
+}
+
+cc_library_shared {
     name: "MicrodroidBenchmarkNativeLib",
     srcs: ["src/native/benchmarkbinary.cpp"],
     static_libs: ["libiobenchmark"],
@@ -37,6 +46,7 @@
         "libbinder_ndk",
         "libbinder_rpc_unstable",
         "liblog",
+        "libvm_payload",
     ],
 }
 
diff --git a/tests/benchmark/assets/vm_config.json b/tests/benchmark/assets/vm_config.json
index e8f43e0..5a604a9 100644
--- a/tests/benchmark/assets/vm_config.json
+++ b/tests/benchmark/assets/vm_config.json
@@ -4,10 +4,7 @@
   },
   "task": {
     "type": "microdroid_launcher",
-    "command": "MicrodroidBenchmarkNativeLib.so",
-    "args": [
-      "no_io"
-    ]
+    "command": "MicrodroidIdleNativeLib.so"
   },
   "export_tombstones": true
 }
diff --git a/tests/benchmark/assets/vm_config_io.json b/tests/benchmark/assets/vm_config_io.json
index 1a5a9e5..66046ba 100644
--- a/tests/benchmark/assets/vm_config_io.json
+++ b/tests/benchmark/assets/vm_config_io.json
@@ -4,10 +4,7 @@
   },
   "task": {
     "type": "microdroid_launcher",
-    "command": "MicrodroidBenchmarkNativeLib.so",
-    "args": [
-      "io"
-    ]
+    "command": "MicrodroidBenchmarkNativeLib.so"
   },
   "apexes": [
     {
diff --git a/tests/benchmark/src/java/com/android/microdroid/benchmark/BenchmarkVmListener.java b/tests/benchmark/src/java/com/android/microdroid/benchmark/BenchmarkVmListener.java
index da08a47..cbb9a0a 100644
--- a/tests/benchmark/src/java/com/android/microdroid/benchmark/BenchmarkVmListener.java
+++ b/tests/benchmark/src/java/com/android/microdroid/benchmark/BenchmarkVmListener.java
@@ -49,7 +49,7 @@
         try {
             IBenchmarkService benchmarkService =
                     IBenchmarkService.Stub.asInterface(
-                            vm.connectToVsockServer(IBenchmarkService.SERVICE_PORT).get());
+                            vm.connectToVsockServer(IBenchmarkService.SERVICE_PORT));
             assertThat(benchmarkService).isNotNull();
 
             mListener.onPayloadReady(vm, benchmarkService);
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 263956b..b1a1160 100644
--- a/tests/benchmark/src/java/com/android/microdroid/benchmark/MicrodroidBenchmarks.java
+++ b/tests/benchmark/src/java/com/android/microdroid/benchmark/MicrodroidBenchmarks.java
@@ -16,6 +16,9 @@
 
 package com.android.microdroid.benchmark;
 
+import static android.system.virtualmachine.VirtualMachineConfig.DEBUG_LEVEL_FULL;
+import static android.system.virtualmachine.VirtualMachineConfig.DEBUG_LEVEL_NONE;
+
 import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
 
 import static com.google.common.truth.Truth.assertThat;
@@ -27,7 +30,6 @@
 import android.os.RemoteException;
 import android.system.virtualmachine.VirtualMachine;
 import android.system.virtualmachine.VirtualMachineConfig;
-import android.system.virtualmachine.VirtualMachineConfig.DebugLevel;
 import android.system.virtualmachine.VirtualMachineException;
 import android.util.Log;
 
@@ -91,7 +93,7 @@
             VirtualMachineConfig.Builder builder =
                     mInner.newVmConfigBuilder("assets/vm_config.json");
             VirtualMachineConfig normalConfig =
-                    builder.debugLevel(DebugLevel.NONE).memoryMib(mem).build();
+                    builder.setDebugLevel(DEBUG_LEVEL_NONE).setMemoryMib(mem).build();
             mInner.forceCreateNewVirtualMachine("test_vm_minimum_memory", normalConfig);
 
             if (tryBootVm(TAG, "test_vm_minimum_memory").payloadStarted) return true;
@@ -140,12 +142,12 @@
         List<Double> userspaceBootTimeMetrics = new ArrayList<>();
 
         for (int i = 0; i < trialCount; i++) {
-            VirtualMachineConfig.Builder builder =
-                    mInner.newVmConfigBuilder("assets/vm_config.json");
 
             // To grab boot events from log, set debug mode to FULL
-            VirtualMachineConfig normalConfig =
-                    builder.debugLevel(DebugLevel.FULL).memoryMib(256).build();
+            VirtualMachineConfig normalConfig = mInner.newVmConfigBuilder("assets/vm_config.json")
+                    .setDebugLevel(DEBUG_LEVEL_FULL)
+                    .setMemoryMib(256)
+                    .build();
             mInner.forceCreateNewVirtualMachine("test_vm_boot_time", normalConfig);
 
             BootResult result = tryBootVm(TAG, "test_vm_boot_time");
@@ -189,10 +191,9 @@
 
     @Test
     public void testVsockTransferFromHostToVM() throws Exception {
-        VirtualMachineConfig config =
-                mInner.newVmConfigBuilder("assets/vm_config_io.json")
-                        .debugLevel(DebugLevel.FULL)
-                        .build();
+        VirtualMachineConfig config = mInner.newVmConfigBuilder("assets/vm_config_io.json")
+                .setDebugLevel(DEBUG_LEVEL_FULL)
+                .build();
         List<Double> transferRates = new ArrayList<>(IO_TEST_TRIAL_COUNT);
 
         for (int i = 0; i < IO_TEST_TRIAL_COUNT; ++i) {
@@ -216,10 +217,9 @@
     }
 
     private void testVirtioBlkReadRate(boolean isRand) throws Exception {
-        VirtualMachineConfig config =
-                mInner.newVmConfigBuilder("assets/vm_config_io.json")
-                        .debugLevel(DebugLevel.FULL)
-                        .build();
+        VirtualMachineConfig config = mInner.newVmConfigBuilder("assets/vm_config_io.json")
+                .setDebugLevel(DEBUG_LEVEL_FULL)
+                .build();
         List<Double> readRates = new ArrayList<>(IO_TEST_TRIAL_COUNT);
 
         for (int i = 0; i < IO_TEST_TRIAL_COUNT + 1; ++i) {
@@ -284,8 +284,8 @@
         final String vmName = "test_vm_mem_usage";
         VirtualMachineConfig config =
                 mInner.newVmConfigBuilder("assets/vm_config_io.json")
-                        .debugLevel(DebugLevel.NONE)
-                        .memoryMib(256)
+                        .setDebugLevel(DEBUG_LEVEL_NONE)
+                        .setMemoryMib(256)
                         .build();
         mInner.forceCreateNewVirtualMachine(vmName, config);
         VirtualMachine vm = mInner.getVirtualMachineManager().get(vmName);
@@ -406,7 +406,11 @@
             new Thread(() -> sendRate.set(runVsockClientAndSendData(vm))).start();
             benchmarkService.runVsockServerAndReceiveData(serverFd, NUM_BYTES_TO_TRANSFER);
 
-            mReadRates.add(sendRate.get());
+            Double rate = sendRate.get();
+            if (rate == null) {
+                throw new IllegalStateException("runVsockClientAndSendData() failed");
+            }
+            mReadRates.add(rate);
         }
 
         private double runVsockClientAndSendData(VirtualMachine vm) {
diff --git a/tests/benchmark/src/native/benchmarkbinary.cpp b/tests/benchmark/src/native/benchmarkbinary.cpp
index 58b4cf0..4e3c14d 100644
--- a/tests/benchmark/src/native/benchmarkbinary.cpp
+++ b/tests/benchmark/src/native/benchmarkbinary.cpp
@@ -26,6 +26,7 @@
 #include <stdio.h>
 #include <time.h>
 #include <unistd.h>
+#include <vm_payload.h>
 
 #include <binder_rpc_unstable.hpp>
 #include <fstream>
@@ -157,19 +158,8 @@
 Result<void> run_io_benchmark_tests() {
     auto test_service = ndk::SharedRefBase::make<IOBenchmarkService>();
     auto callback = []([[maybe_unused]] void* param) {
-        // Tell microdroid_manager that we're ready.
-        // If we can't, abort in order to fail fast - the host won't proceed without
-        // receiving the onReady signal.
-        ndk::SpAIBinder binder(
-                RpcClient(VMADDR_CID_HOST, IVirtualMachineService::VM_BINDER_SERVICE_PORT));
-        auto vm_service = IVirtualMachineService::fromBinder(binder);
-        if (vm_service == nullptr) {
-            LOG(ERROR) << "failed to connect VirtualMachineService\n";
-            abort();
-        }
-        if (auto status = vm_service->notifyPayloadReady(); !status.isOk()) {
-            LOG(ERROR) << "failed to notify payload ready to virtualizationservice: "
-                       << status.getDescription();
+        if (!AVmPayload_notifyPayloadReady()) {
+            LOG(ERROR) << "failed to notify payload ready to virtualizationservice";
             abort();
         }
     };
@@ -182,17 +172,10 @@
 }
 } // Anonymous namespace
 
-extern "C" int android_native_main([[maybe_unused]] int argc, char* argv[]) {
-    if (strcmp(argv[1], "no_io") == 0) {
-        // do nothing for now; just leave it alive. good night.
-        for (;;) {
-            sleep(1000);
-        }
-    } else if (strcmp(argv[1], "io") == 0) {
-        if (auto res = run_io_benchmark_tests(); !res.ok()) {
-            LOG(ERROR) << "IO benchmark test failed: " << res.error() << "\n";
-            return EXIT_FAILURE;
-        }
+extern "C" int android_native_main(int /* argc */, char* /* argv */[]) {
+    if (auto res = run_io_benchmark_tests(); !res.ok()) {
+        LOG(ERROR) << "IO benchmark test failed: " << res.error() << "\n";
+        return EXIT_FAILURE;
     }
     return EXIT_SUCCESS;
 }
diff --git a/tests/benchmark/src/native/idlebinary.cpp b/tests/benchmark/src/native/idlebinary.cpp
new file mode 100644
index 0000000..a74e7bf
--- /dev/null
+++ b/tests/benchmark/src/native/idlebinary.cpp
@@ -0,0 +1,24 @@
+/*
+ * Copyright (C) 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.
+ */
+
+#include <unistd.h>
+
+extern "C" int android_native_main(int /* argc */, char* /* argv */[]) {
+    // do nothing; just leave it alive. good night.
+    for (;;) {
+        sleep(1000);
+    }
+}
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 a1dee6d..c2fd295 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
@@ -77,10 +77,12 @@
             return mContext;
         }
 
-        /** Create a new VirtualMachineConfig.Builder with the parameterized protection mode. */
+        public VirtualMachineConfig.Builder newVmConfigBuilder() {
+            return new VirtualMachineConfig.Builder(mContext).setProtectedVm(mProtectedVm);
+        }
+
         public VirtualMachineConfig.Builder newVmConfigBuilder(String payloadConfigPath) {
-            return new VirtualMachineConfig.Builder(mContext, payloadConfigPath)
-                        .protectedVm(mProtectedVm);
+            return newVmConfigBuilder().setPayloadConfigPath(payloadConfigPath);
         }
 
         /**
@@ -148,7 +150,7 @@
             if (!mVcpuStartedNanoTime.isPresent()) {
                 mVcpuStartedNanoTime = OptionalLong.of(System.nanoTime());
             }
-            if (log.contains("Starting kernel") && !mKernelStartedNanoTime.isPresent()) {
+            if (log.contains("Starting payload...") && !mKernelStartedNanoTime.isPresent()) {
                 mKernelStartedNanoTime = OptionalLong.of(System.nanoTime());
             }
             if (log.contains("Run /init as init process") && !mInitStartedNanoTime.isPresent()) {
@@ -239,7 +241,7 @@
 
         @Override
         @CallSuper
-        public void onDied(VirtualMachine vm, int reason) {
+        public void onStopped(VirtualMachine vm, int reason) {
             vm.clearCallback();
             mExecutorService.shutdown();
         }
@@ -328,16 +330,16 @@
                     }
 
                     @Override
-                    public void onDied(VirtualMachine vm, int reason) {
+                    public void onStopped(VirtualMachine vm, int reason) {
                         deathReason.complete(reason);
-                        super.onDied(vm, reason);
+                        super.onStopped(vm, reason);
                     }
                 };
         long apiCallNanoTime = System.nanoTime();
         listener.runToFinish(logTag, vm);
         return new BootResult(
                 payloadStarted.getNow(false),
-                deathReason.getNow(VirtualMachineCallback.DEATH_REASON_INFRASTRUCTURE_ERROR),
+                deathReason.getNow(VmEventListener.STOP_REASON_INFRASTRUCTURE_ERROR),
                 apiCallNanoTime,
                 endTime.getNow(apiCallNanoTime) - apiCallNanoTime,
                 listener.getVcpuStartedNanoTime(),
diff --git a/tests/hostside/java/com/android/microdroid/test/MicrodroidTestCase.java b/tests/hostside/java/com/android/microdroid/test/MicrodroidTestCase.java
index 9dd2a15..a2deb19 100644
--- a/tests/hostside/java/com/android/microdroid/test/MicrodroidTestCase.java
+++ b/tests/hostside/java/com/android/microdroid/test/MicrodroidTestCase.java
@@ -426,6 +426,7 @@
     }
 
     @Test
+    @Ignore("b/245081929")
     @CddTest(requirements = {"9.17/C-2-1", "9.17/C-2-2", "9.17/C-2-6"})
     public void testBootFailsWhenProtectedVmStartsWithImagesSignedWithDifferentKey()
             throws Exception {
diff --git a/tests/testapk/Android.bp b/tests/testapk/Android.bp
index e1c8124..e738930 100644
--- a/tests/testapk/Android.bp
+++ b/tests/testapk/Android.bp
@@ -20,7 +20,10 @@
         "compatibility-common-util-devicesidelib",
     ],
     libs: ["android.system.virtualmachine"],
-    jni_libs: ["MicrodroidTestNativeLib"],
+    jni_libs: [
+        "MicrodroidTestNativeLib",
+        "MicrodroidTestNativeCrashLib",
+    ],
     platform_apis: true,
     use_embedded_native_libs: true,
     // We only support 64-bit ABI, but CTS demands all APKs to be multi-ABI.
@@ -37,16 +40,21 @@
         "libbinder_ndk",
         "libbinder_rpc_unstable",
         "MicrodroidTestNativeLibSub",
+        "libvm_payload",
     ],
     static_libs: [
         "libfsverity_digests_proto_cc",
         "liblog",
         "libprotobuf-cpp-lite-ndk",
-        "libvm_payload",
     ],
 }
 
 cc_library_shared {
+    name: "MicrodroidTestNativeCrashLib",
+    srcs: ["src/native/crashbinary.cpp"],
+}
+
+cc_library_shared {
     name: "MicrodroidTestNativeLibSub",
     srcs: ["src/native/testlib.cpp"],
 }
diff --git a/tests/testapk/assets/vm_config.json b/tests/testapk/assets/vm_config.json
index da420ff..d12eb5c 100644
--- a/tests/testapk/assets/vm_config.json
+++ b/tests/testapk/assets/vm_config.json
@@ -4,11 +4,7 @@
   },
   "task": {
     "type": "microdroid_launcher",
-    "command": "MicrodroidTestNativeLib.so",
-    "args": [
-      "hello",
-      "microdroid"
-    ]
+    "command": "MicrodroidTestNativeLib.so"
   },
   "export_tombstones": true
 }
diff --git a/tests/testapk/assets/vm_config_apex.json b/tests/testapk/assets/vm_config_apex.json
index 0f100aa..c00787f 100644
--- a/tests/testapk/assets/vm_config_apex.json
+++ b/tests/testapk/assets/vm_config_apex.json
@@ -4,11 +4,7 @@
   },
   "task": {
     "type": "microdroid_launcher",
-    "command": "MicrodroidTestNativeLib.so",
-    "args": [
-      "hello",
-      "microdroid"
-    ]
+    "command": "MicrodroidTestNativeLib.so"
   },
   "apexes": [
     {
diff --git a/tests/testapk/assets/vm_config_crash.json b/tests/testapk/assets/vm_config_crash.json
index 282f25c..2951fdf 100644
--- a/tests/testapk/assets/vm_config_crash.json
+++ b/tests/testapk/assets/vm_config_crash.json
@@ -4,10 +4,7 @@
     },
     "task": {
       "type": "microdroid_launcher",
-      "command": "MicrodroidTestNativeLib.so",
-      "args": [
-        "crash"
-      ]
+      "command": "MicrodroidTestNativeCrashLib.so"
     },
     "export_tombstones": true
   }
diff --git a/tests/testapk/assets/vm_config_crash_no_tombstone.json b/tests/testapk/assets/vm_config_crash_no_tombstone.json
index be0983d..583f87b 100644
--- a/tests/testapk/assets/vm_config_crash_no_tombstone.json
+++ b/tests/testapk/assets/vm_config_crash_no_tombstone.json
@@ -4,10 +4,7 @@
     },
     "task": {
       "type": "microdroid_launcher",
-      "command": "MicrodroidTestNativeLib.so",
-      "args": [
-        "crash"
-      ]
+      "command": "MicrodroidTestNativeCrashLib.so"
     },
     "export_tombstones": false
   }
diff --git a/tests/testapk/assets/vm_config_extra_apk.json b/tests/testapk/assets/vm_config_extra_apk.json
index d7d3dd7..b45e57d 100644
--- a/tests/testapk/assets/vm_config_extra_apk.json
+++ b/tests/testapk/assets/vm_config_extra_apk.json
@@ -4,11 +4,7 @@
   },
   "task": {
     "type": "microdroid_launcher",
-    "command": "MicrodroidTestNativeLib.so",
-    "args": [
-      "hello",
-      "microdroid"
-    ]
+    "command": "MicrodroidTestNativeLib.so"
   },
   "extra_apks": [
     {
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 4b40293..51c145e 100644
--- a/tests/testapk/src/java/com/android/microdroid/test/MicrodroidTests.java
+++ b/tests/testapk/src/java/com/android/microdroid/test/MicrodroidTests.java
@@ -15,6 +15,9 @@
  */
 package com.android.microdroid.test;
 
+import static android.system.virtualmachine.VirtualMachineConfig.DEBUG_LEVEL_FULL;
+import static android.system.virtualmachine.VirtualMachineConfig.DEBUG_LEVEL_NONE;
+
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.common.truth.TruthJUnit.assume;
 
@@ -26,8 +29,6 @@
 import android.system.virtualmachine.VirtualMachine;
 import android.system.virtualmachine.VirtualMachineCallback;
 import android.system.virtualmachine.VirtualMachineConfig;
-import android.system.virtualmachine.VirtualMachineConfig.DebugLevel;
-import android.system.virtualmachine.VirtualMachineException;
 import android.util.Log;
 
 import com.android.compatibility.common.util.CddTest;
@@ -35,6 +36,7 @@
 import com.android.microdroid.testservice.ITestService;
 
 import org.junit.Before;
+import org.junit.Ignore;
 import org.junit.Rule;
 import org.junit.Test;
 import org.junit.rules.Timeout;
@@ -53,7 +55,6 @@
 import java.util.concurrent.CompletableFuture;
 
 import co.nstant.in.cbor.CborDecoder;
-import co.nstant.in.cbor.CborException;
 import co.nstant.in.cbor.model.Array;
 import co.nstant.in.cbor.model.DataItem;
 import co.nstant.in.cbor.model.MajorType;
@@ -86,92 +87,50 @@
             "9.17/C-1-1",
             "9.17/C-2-1"
     })
-    public void connectToVmService() throws VirtualMachineException, InterruptedException {
-        assume()
-            .withMessage("SKip on 5.4 kernel. b/218303240")
-            .that(KERNEL_VERSION)
-            .isNotEqualTo("5.4");
+    public void connectToVmService() throws Exception {
+        assumeSupportedKernel();
 
-        VirtualMachineConfig.Builder builder =
-                mInner.newVmConfigBuilder("assets/vm_config_extra_apk.json");
-        if (Build.SUPPORTED_ABIS.length > 0) {
-            String primaryAbi = Build.SUPPORTED_ABIS[0];
-            switch(primaryAbi) {
-                case "x86_64":
-                    builder.memoryMib(MIN_MEM_X86_64);
-                    break;
-                case "arm64-v8a":
-                    builder.memoryMib(MIN_MEM_ARM64);
-                    break;
-            }
-        }
-        VirtualMachineConfig config = builder.build();
+        VirtualMachineConfig config = mInner.newVmConfigBuilder()
+                .setPayloadBinaryPath("MicrodroidTestNativeLib.so")
+                .setMemoryMib(minMemoryRequired())
+                .build();
         VirtualMachine vm = mInner.forceCreateNewVirtualMachine("test_vm_extra_apk", config);
 
-        class TestResults {
-            Exception mException;
-            Integer mAddInteger;
-            String mAppRunProp;
-            String mSublibRunProp;
-            String mExtraApkTestProp;
-        }
-        final CompletableFuture<Boolean> payloadStarted = new CompletableFuture<>();
-        final CompletableFuture<Boolean> payloadReady = new CompletableFuture<>();
-        final TestResults testResults = new TestResults();
-        VmEventListener listener =
-                new VmEventListener() {
-                    private void testVMService(VirtualMachine vm) {
-                        try {
-                            ITestService testService = ITestService.Stub.asInterface(
-                                    vm.connectToVsockServer(ITestService.SERVICE_PORT).get());
-                            testResults.mAddInteger = testService.addInteger(123, 456);
-                            testResults.mAppRunProp =
-                                    testService.readProperty("debug.microdroid.app.run");
-                            testResults.mSublibRunProp =
-                                    testService.readProperty("debug.microdroid.app.sublib.run");
-                            testResults.mExtraApkTestProp =
-                                    testService.readProperty("debug.microdroid.test.extra_apk");
-                        } 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, ParcelFileDescriptor stream) {
-                        Log.i(TAG, "onPayloadStarted");
-                        payloadStarted.complete(true);
-                        logVmOutput(TAG, new FileInputStream(stream.getFileDescriptor()),
-                                "Payload");
-                    }
-                };
-        listener.runToFinish(TAG, vm);
-        assertThat(payloadStarted.getNow(false)).isTrue();
-        assertThat(payloadReady.getNow(false)).isTrue();
+        TestResults testResults = runVmTestService(vm);
         assertThat(testResults.mException).isNull();
         assertThat(testResults.mAddInteger).isEqualTo(123 + 456);
         assertThat(testResults.mAppRunProp).isEqualTo("true");
         assertThat(testResults.mSublibRunProp).isEqualTo("true");
+    }
+
+    @Test
+    @CddTest(requirements = {
+            "9.17/C-1-1",
+            "9.17/C-2-1"
+    })
+    public void extraApk() throws Exception {
+        assumeSupportedKernel();
+
+        VirtualMachineConfig config = mInner.newVmConfigBuilder("assets/vm_config_extra_apk.json")
+                .setMemoryMib(minMemoryRequired())
+                .build();
+        VirtualMachine vm = mInner.forceCreateNewVirtualMachine("test_vm_extra_apk", config);
+
+        TestResults testResults = runVmTestService(vm);
         assertThat(testResults.mExtraApkTestProp).isEqualTo("PASS");
     }
 
     @Test
-    public void bootFailsWhenLowMem() throws VirtualMachineException, InterruptedException {
+    public void bootFailsWhenLowMem() throws Exception {
         for (int memMib : new int[]{ 10, 20, 40 }) {
-            VirtualMachineConfig lowMemConfig = mInner.newVmConfigBuilder("assets/vm_config.json")
-                    .memoryMib(memMib)
-                    .debugLevel(DebugLevel.NONE)
+            VirtualMachineConfig lowMemConfig = mInner.newVmConfigBuilder()
+                    .setPayloadBinaryPath("MicrodroidTestNativeLib.so")
+                    .setMemoryMib(memMib)
+                    .setDebugLevel(DEBUG_LEVEL_NONE)
                     .build();
             VirtualMachine vm = mInner.forceCreateNewVirtualMachine("low_mem", lowMemConfig);
             final CompletableFuture<Boolean> onPayloadReadyExecuted = new CompletableFuture<>();
-            final CompletableFuture<Boolean> onDiedExecuted = new CompletableFuture<>();
+            final CompletableFuture<Boolean> onStoppedExecuted = new CompletableFuture<>();
             VmEventListener listener =
                     new VmEventListener() {
                         @Override
@@ -180,14 +139,14 @@
                             super.onPayloadReady(vm);
                         }
                         @Override
-                        public void onDied(VirtualMachine vm,  int reason) {
-                            onDiedExecuted.complete(true);
-                            super.onDied(vm, reason);
+                        public void onStopped(VirtualMachine vm,  int reason) {
+                            onStoppedExecuted.complete(true);
+                            super.onStopped(vm, reason);
                         }
                     };
             listener.runToFinish(TAG, vm);
-            // Assert that onDied() was executed but onPayloadReady() was never run
-            assertThat(onDiedExecuted.getNow(false)).isTrue();
+            // Assert that onStopped() was executed but onPayloadReady() was never run
+            assertThat(onStoppedExecuted.getNow(false)).isTrue();
             assertThat(onPayloadReadyExecuted.getNow(false)).isFalse();
         }
     }
@@ -197,15 +156,13 @@
             "9.17/C-1-1",
             "9.17/C-2-7"
     })
-    public void changingDebugLevelInvalidatesVmIdentity()
-            throws VirtualMachineException, InterruptedException, IOException {
-        assume()
-            .withMessage("SKip on 5.4 kernel. b/218303240")
-            .that(KERNEL_VERSION)
-            .isNotEqualTo("5.4");
+    public void changingDebugLevelInvalidatesVmIdentity() throws Exception {
+        assumeSupportedKernel();
 
-        VirtualMachineConfig.Builder builder = mInner.newVmConfigBuilder("assets/vm_config.json");
-        VirtualMachineConfig normalConfig = builder.debugLevel(DebugLevel.NONE).build();
+        VirtualMachineConfig.Builder builder = mInner.newVmConfigBuilder()
+                .setPayloadBinaryPath("MicrodroidTestNativeLib.so")
+                .setDebugLevel(DEBUG_LEVEL_NONE);
+        VirtualMachineConfig normalConfig = builder.build();
         mInner.forceCreateNewVirtualMachine("test_vm", normalConfig);
         assertThat(tryBootVm(TAG, "test_vm").payloadStarted).isTrue();
 
@@ -223,19 +180,18 @@
         // Launch the same VM with different debug level. The Java API prohibits this (thankfully).
         // For testing, we do that by creating a new VM with debug level, and copy the old instance
         // image to the new VM instance image.
-        VirtualMachineConfig debugConfig = builder.debugLevel(DebugLevel.FULL).build();
+        VirtualMachineConfig debugConfig = builder.setDebugLevel(DEBUG_LEVEL_FULL).build();
         mInner.forceCreateNewVirtualMachine("test_vm", debugConfig);
         Files.copy(vmInstanceBackup.toPath(), vmInstance.toPath(), REPLACE_EXISTING);
         assertThat(tryBootVm(TAG, "test_vm").payloadStarted).isFalse();
     }
 
-    private class VmCdis {
+    private static class VmCdis {
         public byte[] cdiAttest;
-        public byte[] cdiSeal;
+        public byte[] instanceSecret;
     }
 
-    private VmCdis launchVmAndGetCdis(String instanceName)
-            throws VirtualMachineException, InterruptedException {
+    private VmCdis launchVmAndGetCdis(String instanceName) throws Exception {
         VirtualMachine vm = mInner.getVirtualMachineManager().get(instanceName);
         final VmCdis vmCdis = new VmCdis();
         final CompletableFuture<Exception> exception = new CompletableFuture<>();
@@ -245,9 +201,9 @@
                     public void onPayloadReady(VirtualMachine vm) {
                         try {
                             ITestService testService = ITestService.Stub.asInterface(
-                                    vm.connectToVsockServer(ITestService.SERVICE_PORT).get());
+                                    vm.connectToVsockServer(ITestService.SERVICE_PORT));
                             vmCdis.cdiAttest = testService.insecurelyExposeAttestationCdi();
-                            vmCdis.cdiSeal = testService.insecurelyExposeSealingCdi();
+                            vmCdis.instanceSecret = testService.insecurelyExposeVmInstanceSecret();
                             forceStop(vm);
                         } catch (Exception e) {
                             exception.complete(e);
@@ -264,15 +220,12 @@
             "9.17/C-1-1",
             "9.17/C-2-7"
     })
-    public void instancesOfSameVmHaveDifferentCdis()
-            throws VirtualMachineException, InterruptedException {
-        assume()
-            .withMessage("SKip on 5.4 kernel. b/218303240")
-            .that(KERNEL_VERSION)
-            .isNotEqualTo("5.4");
+    public void instancesOfSameVmHaveDifferentCdis() throws Exception {
+        assumeSupportedKernel();
 
-        VirtualMachineConfig normalConfig = mInner.newVmConfigBuilder("assets/vm_config.json")
-                .debugLevel(DebugLevel.FULL)
+        VirtualMachineConfig normalConfig = mInner.newVmConfigBuilder()
+                .setPayloadBinaryPath("MicrodroidTestNativeLib.so")
+                .setDebugLevel(DEBUG_LEVEL_FULL)
                 .build();
         mInner.forceCreateNewVirtualMachine("test_vm_a", normalConfig);
         mInner.forceCreateNewVirtualMachine("test_vm_b", normalConfig);
@@ -281,10 +234,9 @@
         assertThat(vm_a_cdis.cdiAttest).isNotNull();
         assertThat(vm_b_cdis.cdiAttest).isNotNull();
         assertThat(vm_a_cdis.cdiAttest).isNotEqualTo(vm_b_cdis.cdiAttest);
-        assertThat(vm_a_cdis.cdiSeal).isNotNull();
-        assertThat(vm_b_cdis.cdiSeal).isNotNull();
-        assertThat(vm_a_cdis.cdiSeal).isNotEqualTo(vm_b_cdis.cdiSeal);
-        assertThat(vm_a_cdis.cdiAttest).isNotEqualTo(vm_b_cdis.cdiSeal);
+        assertThat(vm_a_cdis.instanceSecret).isNotNull();
+        assertThat(vm_b_cdis.instanceSecret).isNotNull();
+        assertThat(vm_a_cdis.instanceSecret).isNotEqualTo(vm_b_cdis.instanceSecret);
     }
 
     @Test
@@ -292,24 +244,21 @@
             "9.17/C-1-1",
             "9.17/C-2-7"
     })
-    public void sameInstanceKeepsSameCdis()
-            throws VirtualMachineException, InterruptedException {
-        assume()
-            .withMessage("SKip on 5.4 kernel. b/218303240")
-            .that(KERNEL_VERSION)
-            .isNotEqualTo("5.4");
+    public void sameInstanceKeepsSameCdis() throws Exception {
+        assumeSupportedKernel();
 
-        VirtualMachineConfig normalConfig = mInner.newVmConfigBuilder("assets/vm_config.json")
-                .debugLevel(DebugLevel.FULL)
+        VirtualMachineConfig normalConfig = mInner.newVmConfigBuilder()
+                .setPayloadBinaryPath("MicrodroidTestNativeLib.so")
+                .setDebugLevel(DEBUG_LEVEL_FULL)
                 .build();
         mInner.forceCreateNewVirtualMachine("test_vm", normalConfig);
 
         VmCdis first_boot_cdis = launchVmAndGetCdis("test_vm");
         VmCdis second_boot_cdis = launchVmAndGetCdis("test_vm");
         // The attestation CDI isn't specified to be stable, though it might be
-        assertThat(first_boot_cdis.cdiSeal).isNotNull();
-        assertThat(second_boot_cdis.cdiSeal).isNotNull();
-        assertThat(first_boot_cdis.cdiSeal).isEqualTo(second_boot_cdis.cdiSeal);
+        assertThat(first_boot_cdis.instanceSecret).isNotNull();
+        assertThat(second_boot_cdis.instanceSecret).isNotNull();
+        assertThat(first_boot_cdis.instanceSecret).isEqualTo(second_boot_cdis.instanceSecret);
     }
 
     @Test
@@ -317,18 +266,14 @@
             "9.17/C-1-1",
             "9.17/C-2-7"
     })
-    public void bccIsSuperficiallyWellFormed()
-            throws VirtualMachineException, InterruptedException, CborException {
-        assume()
-            .withMessage("SKip on 5.4 kernel. b/218303240")
-            .that(KERNEL_VERSION)
-            .isNotEqualTo("5.4");
+    public void bccIsSuperficiallyWellFormed() throws Exception {
+        assumeSupportedKernel();
 
-        VirtualMachineConfig normalConfig = mInner.newVmConfigBuilder("assets/vm_config.json")
-                .debugLevel(DebugLevel.FULL)
+        VirtualMachineConfig normalConfig = mInner.newVmConfigBuilder()
+                .setPayloadBinaryPath("MicrodroidTestNativeLib.so")
+                .setDebugLevel(DEBUG_LEVEL_FULL)
                 .build();
         VirtualMachine vm = mInner.forceCreateNewVirtualMachine("bcc_vm", normalConfig);
-        final VmCdis vmCdis = new VmCdis();
         final CompletableFuture<byte[]> bcc = new CompletableFuture<>();
         final CompletableFuture<Exception> exception = new CompletableFuture<>();
         VmEventListener listener =
@@ -337,7 +282,7 @@
                     public void onPayloadReady(VirtualMachine vm) {
                         try {
                             ITestService testService = ITestService.Stub.asInterface(
-                                    vm.connectToVsockServer(ITestService.SERVICE_PORT).get());
+                                    vm.connectToVsockServer(ITestService.SERVICE_PORT));
                             bcc.complete(testService.getBcc());
                             forceStop(vm);
                         } catch (Exception e) {
@@ -398,10 +343,10 @@
         file.writeByte(b ^ 1);
     }
 
-    private RandomAccessFile prepareInstanceImage(String vmName)
-            throws VirtualMachineException, InterruptedException, IOException {
-        VirtualMachineConfig config = mInner.newVmConfigBuilder("assets/vm_config.json")
-                .debugLevel(DebugLevel.FULL)
+    private RandomAccessFile prepareInstanceImage(String vmName) throws Exception {
+        VirtualMachineConfig config = mInner.newVmConfigBuilder()
+                .setPayloadBinaryPath("MicrodroidTestNativeLib.so")
+                .setDebugLevel(DEBUG_LEVEL_FULL)
                 .build();
 
         mInner.forceCreateNewVirtualMachine(vmName, config);
@@ -413,8 +358,7 @@
         return new RandomAccessFile(instanceImgPath, "rw");
     }
 
-    private void assertThatPartitionIsMissing(UUID partitionUuid)
-            throws VirtualMachineException, InterruptedException, IOException {
+    private void assertThatPartitionIsMissing(UUID partitionUuid) throws Exception {
         RandomAccessFile instanceFile = prepareInstanceImage("test_vm_integrity");
         assertThat(findPartitionDataOffset(instanceFile, partitionUuid).isPresent())
                 .isFalse();
@@ -422,7 +366,7 @@
 
     // Flips a bit of given partition, and then see if boot fails.
     private void assertThatBootFailsAfterCompromisingPartition(UUID partitionUuid)
-            throws VirtualMachineException, InterruptedException, IOException {
+            throws Exception {
         RandomAccessFile instanceFile = prepareInstanceImage("test_vm_integrity");
         OptionalLong offset = findPartitionDataOffset(instanceFile, partitionUuid);
         assertThat(offset.isPresent()).isTrue();
@@ -433,7 +377,7 @@
         assertThat(result.payloadStarted).isFalse();
 
         // This failure should shut the VM down immediately and shouldn't trigger a hangup.
-        assertThat(result.deathReason).isNotEqualTo(VirtualMachineCallback.DEATH_REASON_HANGUP);
+        assertThat(result.deathReason).isNotEqualTo(VirtualMachineCallback.STOP_REASON_HANGUP);
     }
 
     @Test
@@ -441,18 +385,17 @@
             "9.17/C-1-1",
             "9.17/C-2-7"
     })
-    public void bootFailsWhenMicrodroidDataIsCompromised()
-            throws VirtualMachineException, InterruptedException, IOException {
+    public void bootFailsWhenMicrodroidDataIsCompromised() throws Exception {
         assertThatBootFailsAfterCompromisingPartition(MICRODROID_PARTITION_UUID);
     }
 
     @Test
+    @Ignore("b/249723852")
     @CddTest(requirements = {
             "9.17/C-1-1",
             "9.17/C-2-7"
     })
-    public void bootFailsWhenPvmFwDataIsCompromised()
-            throws VirtualMachineException, InterruptedException, IOException {
+    public void bootFailsWhenPvmFwDataIsCompromised() throws Exception {
         if (mProtectedVm) {
             assertThatBootFailsAfterCompromisingPartition(PVM_FW_PARTITION_UUID);
         } else {
@@ -462,33 +405,105 @@
     }
 
     @Test
-    public void bootFailsWhenConfigIsInvalid()
-            throws VirtualMachineException, InterruptedException, IOException {
+    public void bootFailsWhenConfigIsInvalid() throws Exception {
         VirtualMachineConfig.Builder builder =
                 mInner.newVmConfigBuilder("assets/vm_config_no_task.json");
-        VirtualMachineConfig normalConfig = builder.debugLevel(DebugLevel.FULL).build();
+        VirtualMachineConfig normalConfig = builder.setDebugLevel(DEBUG_LEVEL_FULL).build();
         mInner.forceCreateNewVirtualMachine("test_vm_invalid_config", normalConfig);
 
         BootResult bootResult = tryBootVm(TAG, "test_vm_invalid_config");
         assertThat(bootResult.payloadStarted).isFalse();
         assertThat(bootResult.deathReason).isEqualTo(
-                VirtualMachineCallback.DEATH_REASON_MICRODROID_INVALID_PAYLOAD_CONFIG);
+                VirtualMachineCallback.STOP_REASON_MICRODROID_INVALID_PAYLOAD_CONFIG);
     }
 
     @Test
-    public void sameInstancesShareTheSameVmObject()
-            throws VirtualMachineException, InterruptedException, IOException {
-        VirtualMachineConfig.Builder builder =
-                mInner.newVmConfigBuilder("assets/vm_config.json");
-        VirtualMachineConfig normalConfig = builder.debugLevel(DebugLevel.NONE).build();
-        VirtualMachine vm = mInner.forceCreateNewVirtualMachine("test_vm", normalConfig);
+    public void sameInstancesShareTheSameVmObject() throws Exception {
+        VirtualMachineConfig config = mInner.newVmConfigBuilder()
+                .setPayloadBinaryPath("MicrodroidTestNativeLib.so")
+                .setDebugLevel(DEBUG_LEVEL_NONE)
+                .build();
+
+        VirtualMachine vm = mInner.forceCreateNewVirtualMachine("test_vm", config);
         VirtualMachine vm2 = mInner.getVirtualMachineManager().get("test_vm");
         assertThat(vm).isEqualTo(vm2);
 
-        VirtualMachine newVm = mInner.forceCreateNewVirtualMachine("test_vm", normalConfig);
+        VirtualMachine newVm = mInner.forceCreateNewVirtualMachine("test_vm", config);
         VirtualMachine newVm2 = mInner.getVirtualMachineManager().get("test_vm");
         assertThat(newVm).isEqualTo(newVm2);
 
         assertThat(vm).isNotEqualTo(newVm);
     }
+
+    private int minMemoryRequired() {
+        if (Build.SUPPORTED_ABIS.length > 0) {
+            String primaryAbi = Build.SUPPORTED_ABIS[0];
+            switch (primaryAbi) {
+                case "x86_64":
+                    return MIN_MEM_X86_64;
+                case "arm64-v8a":
+                    return MIN_MEM_ARM64;
+            }
+        }
+        return 0;
+    }
+
+    private void assumeSupportedKernel() {
+        assume()
+                .withMessage("Skip on 5.4 kernel. b/218303240")
+                .that(KERNEL_VERSION)
+                .isNotEqualTo("5.4");
+    }
+
+    static class TestResults {
+        Exception mException;
+        Integer mAddInteger;
+        String mAppRunProp;
+        String mSublibRunProp;
+        String mExtraApkTestProp;
+    }
+
+    private TestResults runVmTestService(VirtualMachine vm) 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));
+                            testResults.mAddInteger = testService.addInteger(123, 456);
+                            testResults.mAppRunProp =
+                                    testService.readProperty("debug.microdroid.app.run");
+                            testResults.mSublibRunProp =
+                                    testService.readProperty("debug.microdroid.app.sublib.run");
+                            testResults.mExtraApkTestProp =
+                                    testService.readProperty("debug.microdroid.test.extra_apk");
+                        } 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, ParcelFileDescriptor stream) {
+                        Log.i(TAG, "onPayloadStarted");
+                        payloadStarted.complete(true);
+                        logVmOutput(TAG, new FileInputStream(stream.getFileDescriptor()),
+                                "Payload");
+                    }
+                };
+        listener.runToFinish(TAG, vm);
+        assertThat(payloadStarted.getNow(false)).isTrue();
+        assertThat(payloadReady.getNow(false)).isTrue();
+        return testResults;
+    }
 }
diff --git a/tests/testapk/src/native/crashbinary.cpp b/tests/testapk/src/native/crashbinary.cpp
new file mode 100644
index 0000000..9f80fd0
--- /dev/null
+++ b/tests/testapk/src/native/crashbinary.cpp
@@ -0,0 +1,24 @@
+/*
+ * 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.
+ */
+
+#include <stdio.h>
+#include <stdlib.h>
+
+// A VM payload that crashes as soon as it starts, to allow us to exercise that error path.
+extern "C" int android_native_main(int /* argc */, char* /* argv */[]) {
+    printf("test crash!!!!\n");
+    abort();
+}
diff --git a/tests/testapk/src/native/testbinary.cpp b/tests/testapk/src/native/testbinary.cpp
index d1cfc56..003aca8 100644
--- a/tests/testapk/src/native/testbinary.cpp
+++ b/tests/testapk/src/native/testbinary.cpp
@@ -73,43 +73,50 @@
             return ndk::ScopedAStatus::ok();
         }
 
-        ndk::ScopedAStatus insecurelyExposeSealingCdi(std::vector<uint8_t>* out) override {
-            uint8_t cdi[64];
-            size_t cdi_size = get_dice_sealing_cdi(cdi, sizeof(cdi));
-            if (cdi_size == 0) {
+        ndk::ScopedAStatus insecurelyExposeVmInstanceSecret(std::vector<uint8_t>* out) override {
+            const uint8_t identifier[] = {1, 2, 3, 4};
+            out->resize(32);
+            if (!AVmPayload_getVmInstanceSecret(identifier, sizeof(identifier), out->data(),
+                                                out->size())) {
                 return ndk::ScopedAStatus::
-                        fromServiceSpecificErrorWithMessage(0, "Failed to get sealing cdi");
+                        fromServiceSpecificErrorWithMessage(0, "Failed to VM instance secret");
             }
-            *out = {cdi, cdi + cdi_size};
             return ndk::ScopedAStatus::ok();
         }
 
         ndk::ScopedAStatus insecurelyExposeAttestationCdi(std::vector<uint8_t>* out) override {
-            uint8_t cdi[64];
-            size_t cdi_size = get_dice_attestation_cdi(cdi, sizeof(cdi));
-            if (cdi_size == 0) {
+            size_t cdi_size;
+            if (!AVmPayload_getDiceAttestationCdi(nullptr, 0, &cdi_size)) {
+                return ndk::ScopedAStatus::
+                        fromServiceSpecificErrorWithMessage(0, "Failed to measure attestation cdi");
+            }
+            out->resize(cdi_size);
+            if (!AVmPayload_getDiceAttestationCdi(out->data(), out->size(), &cdi_size)) {
                 return ndk::ScopedAStatus::
                         fromServiceSpecificErrorWithMessage(0, "Failed to get attestation cdi");
             }
-            *out = {cdi, cdi + cdi_size};
             return ndk::ScopedAStatus::ok();
         }
 
         ndk::ScopedAStatus getBcc(std::vector<uint8_t>* out) override {
-            uint8_t bcc[2048];
-            size_t bcc_size = get_dice_attestation_chain(bcc, sizeof(bcc));
-            if (bcc_size == 0) {
+            size_t bcc_size;
+            if (!AVmPayload_getDiceAttestationChain(nullptr, 0, &bcc_size)) {
+                return ndk::ScopedAStatus::
+                        fromServiceSpecificErrorWithMessage(0,
+                                                            "Failed to measure attestation chain");
+            }
+            out->resize(bcc_size);
+            if (!AVmPayload_getDiceAttestationChain(out->data(), out->size(), &bcc_size)) {
                 return ndk::ScopedAStatus::
                         fromServiceSpecificErrorWithMessage(0, "Failed to get attestation chain");
             }
-            *out = {bcc, bcc + bcc_size};
             return ndk::ScopedAStatus::ok();
         }
     };
     auto testService = ndk::SharedRefBase::make<TestService>();
 
     auto callback = []([[maybe_unused]] void* param) {
-        if (!notify_payload_ready()) {
+        if (!AVmPayload_notifyPayloadReady()) {
             std::cerr << "failed to notify payload ready to virtualizationservice" << std::endl;
             abort();
         }
@@ -145,11 +152,6 @@
     setvbuf(stdout, nullptr, _IONBF, 0);
     setvbuf(stderr, nullptr, _IONBF, 0);
 
-    if (strcmp(argv[1], "crash") == 0) {
-        printf("test crash!!!!\n");
-        abort();
-    }
-
     printf("Hello Microdroid ");
     for (int i = 0; i < argc; i++) {
         printf("%s", argv[i]);
diff --git a/virtualizationservice/aidl/android/system/virtualizationservice/DeathReason.aidl b/virtualizationservice/aidl/android/system/virtualizationservice/DeathReason.aidl
index dceabf1..416eb7b 100644
--- a/virtualizationservice/aidl/android/system/virtualizationservice/DeathReason.aidl
+++ b/virtualizationservice/aidl/android/system/virtualizationservice/DeathReason.aidl
@@ -54,4 +54,6 @@
     MICRODROID_UNKNOWN_RUNTIME_ERROR = 15,
     /** The VM killed due to hangup */
     HANGUP = 16,
+    /** The VCPU stalled */
+    WATCHDOG_REBOOT = 17,
 }
diff --git a/virtualizationservice/src/aidl.rs b/virtualizationservice/src/aidl.rs
index 10d70ec..f956062 100644
--- a/virtualizationservice/src/aidl.rs
+++ b/virtualizationservice/src/aidl.rs
@@ -63,7 +63,7 @@
 use std::sync::{Arc, Mutex, Weak};
 use tombstoned_client::{TombstonedConnection, DebuggerdDumpType};
 use vmconfig::VmConfig;
-use vsock::{SockAddr, VsockListener, VsockStream};
+use vsock::{VsockListener, VsockStream};
 use zip::ZipArchive;
 
 pub const BINDER_SERVICE_IDENTIFIER: &str = "android.system.virtualizationservice";
@@ -268,7 +268,7 @@
 }
 
 fn handle_tombstone(stream: &mut VsockStream) -> Result<()> {
-    if let Ok(SockAddr::Vsock(addr)) = stream.peer_addr() {
+    if let Ok(addr) = stream.peer_addr() {
         info!("Vsock Stream connected to cid={} for tombstones", addr.cid());
     }
     let tb_connection =
@@ -499,7 +499,7 @@
             }
             Ok(s) => s,
         };
-        if let Ok(SockAddr::Vsock(addr)) = stream.peer_addr() {
+        if let Ok(addr) = stream.peer_addr() {
             let cid = addr.cid();
             let port = addr.port();
             info!("payload stream connected from cid={}, port={}", cid, port);
diff --git a/virtualizationservice/src/crosvm.rs b/virtualizationservice/src/crosvm.rs
index ff1116a..54cdeb6 100644
--- a/virtualizationservice/src/crosvm.rs
+++ b/virtualizationservice/src/crosvm.rs
@@ -58,6 +58,8 @@
 const CROSVM_REBOOT_STATUS: i32 = 32;
 /// The exit status which crosvm returns when it crashes due to an error.
 const CROSVM_CRASH_STATUS: i32 = 33;
+/// The exit status which crosvm returns when vcpu is stalled.
+const CROSVM_WATCHDOG_REBOOT_STATUS: i32 = 36;
 
 lazy_static! {
     /// If the VM doesn't move to the Started state within this amount time, a hang-up error is
@@ -247,7 +249,14 @@
         let result = child.wait();
         match &result {
             Err(e) => error!("Error waiting for crosvm({}) instance to die: {}", child.id(), e),
-            Ok(status) => info!("crosvm({}) exited with status {}", child.id(), status),
+            Ok(status) => {
+                info!("crosvm({}) exited with status {}", child.id(), status);
+                if let Some(exit_status_code) = status.code() {
+                    if exit_status_code == CROSVM_WATCHDOG_REBOOT_STATUS {
+                        info!("detected vcpu stall on crosvm");
+                    }
+                }
+            }
         }
 
         let mut vm_state = self.vm_state.lock().unwrap();
@@ -421,6 +430,7 @@
             Some(CROSVM_ERROR_STATUS) => DeathReason::ERROR,
             Some(CROSVM_REBOOT_STATUS) => DeathReason::REBOOT,
             Some(CROSVM_CRASH_STATUS) => DeathReason::CRASH,
+            Some(CROSVM_WATCHDOG_REBOOT_STATUS) => DeathReason::WATCHDOG_REBOOT,
             Some(_) => DeathReason::UNKNOWN,
         }
     } else {
diff --git a/zipfuse/Android.bp b/zipfuse/Android.bp
index 3aba94a..1bdc5fe 100644
--- a/zipfuse/Android.bp
+++ b/zipfuse/Android.bp
@@ -13,9 +13,10 @@
         "libclap",
         "libfuse_rust",
         "liblibc",
-        "libzip",
-        "libscopeguard",
         "liblog_rust",
+        "librustutils",
+        "libscopeguard",
+        "libzip",
     ],
     // libfuse_rust, etc don't support 32-bit targets
     multilib: {
diff --git a/zipfuse/src/main.rs b/zipfuse/src/main.rs
index 8400a72..9411759 100644
--- a/zipfuse/src/main.rs
+++ b/zipfuse/src/main.rs
@@ -20,10 +20,11 @@
 
 mod inode;
 
-use anyhow::Result;
+use anyhow::{Context as AnyhowContext, Result};
 use clap::{App, Arg};
 use fuse::filesystem::*;
 use fuse::mount::*;
+use rustutils::system_properties;
 use std::collections::HashMap;
 use std::convert::TryFrom;
 use std::ffi::{CStr, CString};
@@ -52,6 +53,12 @@
                 .takes_value(false)
                 .help("Disallow the execution of binary files"),
         )
+        .arg(
+            Arg::with_name("readyprop")
+                .short('p')
+                .takes_value(true)
+                .help("Specify a property to be set when mount is ready"),
+        )
         .arg(Arg::with_name("ZIPFILE").required(true))
         .arg(Arg::with_name("MOUNTPOINT").required(true))
         .get_matches();
@@ -60,7 +67,8 @@
     let mount_point = matches.value_of("MOUNTPOINT").unwrap().as_ref();
     let options = matches.value_of("options");
     let noexec = matches.is_present("noexec");
-    run_fuse(zip_file, mount_point, options, noexec)?;
+    let ready_prop = matches.value_of("readyprop");
+    run_fuse(zip_file, mount_point, options, noexec, ready_prop)?;
     Ok(())
 }
 
@@ -70,6 +78,7 @@
     mount_point: &Path,
     extra_options: Option<&str>,
     noexec: bool,
+    ready_prop: Option<&str>,
 ) -> Result<()> {
     const MAX_READ: u32 = 1 << 20; // TODO(jiyong): tune this
     const MAX_WRITE: u32 = 1 << 13; // This is a read-only filesystem
@@ -94,6 +103,11 @@
     }
 
     fuse::mount(mount_point, "zipfuse", mount_flags, &mount_options)?;
+
+    if let Some(property_name) = ready_prop {
+        system_properties::write(property_name, "1").context("Failed to set readyprop")?;
+    }
+
     let mut config = fuse::FuseConfig::new();
     config.dev_fuse(dev_fuse).max_write(MAX_WRITE).max_read(MAX_READ);
     Ok(config.enter_message_loop(ZipFuse::new(zip_file)?)?)