Merge "Enable crosvm balloon device based on new system property"
diff --git a/compos/compos_key_helper/compos_key_main.cpp b/compos/compos_key_helper/compos_key_main.cpp
index 9417584..f1637e0 100644
--- a/compos/compos_key_helper/compos_key_main.cpp
+++ b/compos/compos_key_helper/compos_key_main.cpp
@@ -38,11 +38,8 @@
 
 Result<Ed25519KeyPair> getSigningKey() {
     Seed seed;
-    if (!AVmPayload_getVmInstanceSecret(kSigningKeySeedIdentifier,
-                                        strlen(kSigningKeySeedIdentifier), seed.data(),
-                                        seed.size())) {
-        return Error() << "Failed to get signing key seed";
-    }
+    AVmPayload_getVmInstanceSecret(kSigningKeySeedIdentifier, strlen(kSigningKeySeedIdentifier),
+                                   seed.data(), seed.size());
     return compos_key::keyFromSeed(seed);
 }
 
@@ -60,16 +57,9 @@
 }
 
 int write_bcc() {
-    size_t bcc_size;
-    if (!AVmPayload_getDiceAttestationChain(nullptr, 0, &bcc_size)) {
-        LOG(ERROR) << "Failed to measure attestation chain";
-        return 1;
-    }
+    size_t bcc_size = AVmPayload_getDiceAttestationChain(nullptr, 0);
     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;
-    }
+    AVmPayload_getDiceAttestationChain(bcc.data(), bcc.size());
 
     if (!WriteFully(STDOUT_FILENO, bcc.data(), bcc.size())) {
         PLOG(ERROR) << "Write failed";
diff --git a/demo/Android.bp b/demo/Android.bp
index 2b234a6..a291ee1 100644
--- a/demo/Android.bp
+++ b/demo/Android.bp
@@ -12,14 +12,9 @@
         "com.android.microdroid.testservice-java",
         "com.google.android.material_material",
     ],
-    libs: [
-        // We need to compile against the .impl library which includes the hidden
-        // APIs. Once the APIs are promoted to @SystemApi we can switch to
-        // framework-virtualization, which contains API stubs.
-        "framework-virtualization.impl",
-    ],
+    sdk_version: "system_current",
     jni_libs: ["MicrodroidTestNativeLib"],
-    platform_apis: true,
+    jni_uses_platform_apis: true,
     use_embedded_native_libs: true,
     v4_signature: true,
     min_sdk_version: "33",
diff --git a/javalib/api/system-current.txt b/javalib/api/system-current.txt
index d802177..ea2d23e 100644
--- a/javalib/api/system-current.txt
+++ b/javalib/api/system-current.txt
@@ -1 +1,109 @@
 // Signature format: 2.0
+package android.system.virtualmachine {
+
+  public class VirtualMachine implements java.lang.AutoCloseable {
+    method public void clearCallback();
+    method public void close() throws android.system.virtualmachine.VirtualMachineException;
+    method @NonNull public android.os.IBinder connectToVsockServer(int) throws android.system.virtualmachine.VirtualMachineException;
+    method @NonNull public android.os.ParcelFileDescriptor connectVsock(int) throws android.system.virtualmachine.VirtualMachineException;
+    method public int getCid() throws android.system.virtualmachine.VirtualMachineException;
+    method @NonNull public android.system.virtualmachine.VirtualMachineConfig getConfig();
+    method @NonNull public java.io.InputStream getConsoleOutput() throws android.system.virtualmachine.VirtualMachineException;
+    method @NonNull public java.io.InputStream getLogOutput() throws android.system.virtualmachine.VirtualMachineException;
+    method @NonNull public String getName();
+    method public int getStatus();
+    method @RequiresPermission(android.system.virtualmachine.VirtualMachine.MANAGE_VIRTUAL_MACHINE_PERMISSION) public void run() throws android.system.virtualmachine.VirtualMachineException;
+    method public void setCallback(@NonNull java.util.concurrent.Executor, @NonNull android.system.virtualmachine.VirtualMachineCallback);
+    method @NonNull public android.system.virtualmachine.VirtualMachineConfig setConfig(@NonNull android.system.virtualmachine.VirtualMachineConfig) throws android.system.virtualmachine.VirtualMachineException;
+    method public void stop() throws android.system.virtualmachine.VirtualMachineException;
+    method @NonNull public android.system.virtualmachine.VirtualMachineDescriptor toDescriptor() throws android.system.virtualmachine.VirtualMachineException;
+    field public static final String MANAGE_VIRTUAL_MACHINE_PERMISSION = "android.permission.MANAGE_VIRTUAL_MACHINE";
+    field public static final int STATUS_DELETED = 2; // 0x2
+    field public static final int STATUS_RUNNING = 1; // 0x1
+    field public static final int STATUS_STOPPED = 0; // 0x0
+    field public static final String USE_CUSTOM_VIRTUAL_MACHINE_PERMISSION = "android.permission.USE_CUSTOM_VIRTUAL_MACHINE";
+  }
+
+  public interface VirtualMachineCallback {
+    method public void onError(@NonNull android.system.virtualmachine.VirtualMachine, int, @NonNull String);
+    method public void onPayloadFinished(@NonNull android.system.virtualmachine.VirtualMachine, int);
+    method public void onPayloadReady(@NonNull android.system.virtualmachine.VirtualMachine);
+    method public void onPayloadStarted(@NonNull android.system.virtualmachine.VirtualMachine);
+    method public void onRamdump(@NonNull android.system.virtualmachine.VirtualMachine, @NonNull android.os.ParcelFileDescriptor);
+    method public void onStopped(@NonNull android.system.virtualmachine.VirtualMachine, int);
+    field public static final int ERROR_PAYLOAD_CHANGED = 2; // 0x2
+    field public static final int ERROR_PAYLOAD_INVALID_CONFIG = 3; // 0x3
+    field public static final int ERROR_PAYLOAD_VERIFICATION_FAILED = 1; // 0x1
+    field public static final int ERROR_UNKNOWN = 0; // 0x0
+    field public static final int STOP_REASON_BOOTLOADER_INSTANCE_IMAGE_CHANGED = 10; // 0xa
+    field public static final int STOP_REASON_BOOTLOADER_PUBLIC_KEY_MISMATCH = 9; // 0x9
+    field public static final int STOP_REASON_CRASH = 6; // 0x6
+    field public static final int STOP_REASON_ERROR = 4; // 0x4
+    field public static final int STOP_REASON_HANGUP = 16; // 0x10
+    field public static final int STOP_REASON_INFRASTRUCTURE_ERROR = 0; // 0x0
+    field public static final int STOP_REASON_KILLED = 1; // 0x1
+    field public static final int STOP_REASON_MICRODROID_FAILED_TO_CONNECT_TO_VIRTUALIZATION_SERVICE = 11; // 0xb
+    field public static final int STOP_REASON_MICRODROID_INVALID_PAYLOAD_CONFIG = 14; // 0xe
+    field public static final int STOP_REASON_MICRODROID_PAYLOAD_HAS_CHANGED = 12; // 0xc
+    field public static final int STOP_REASON_MICRODROID_PAYLOAD_VERIFICATION_FAILED = 13; // 0xd
+    field public static final int STOP_REASON_MICRODROID_UNKNOWN_RUNTIME_ERROR = 15; // 0xf
+    field public static final int STOP_REASON_PVM_FIRMWARE_INSTANCE_IMAGE_CHANGED = 8; // 0x8
+    field public static final int STOP_REASON_PVM_FIRMWARE_PUBLIC_KEY_MISMATCH = 7; // 0x7
+    field public static final int STOP_REASON_REBOOT = 5; // 0x5
+    field public static final int STOP_REASON_SHUTDOWN = 3; // 0x3
+    field public static final int STOP_REASON_UNKNOWN = 2; // 0x2
+    field public static final int STOP_REASON_VIRTUALIZATION_SERVICE_DIED = -1; // 0xffffffff
+  }
+
+  public final class VirtualMachineConfig {
+    method @NonNull public String getApkPath();
+    method @NonNull public int getDebugLevel();
+    method @IntRange(from=0) public int getMemoryMib();
+    method @IntRange(from=1) public int getNumCpus();
+    method @Nullable public String getPayloadBinaryPath();
+    method @Nullable public String getPayloadConfigPath();
+    method public boolean isCompatibleWith(@NonNull android.system.virtualmachine.VirtualMachineConfig);
+    method public boolean isProtectedVm();
+    field public static final int DEBUG_LEVEL_APP_ONLY = 1; // 0x1
+    field public static final int DEBUG_LEVEL_FULL = 2; // 0x2
+    field public static final int DEBUG_LEVEL_NONE = 0; // 0x0
+  }
+
+  public static final class VirtualMachineConfig.Builder {
+    ctor public VirtualMachineConfig.Builder(@NonNull android.content.Context);
+    method @NonNull public android.system.virtualmachine.VirtualMachineConfig build();
+    method @NonNull public android.system.virtualmachine.VirtualMachineConfig.Builder setApkPath(@NonNull String);
+    method @NonNull public android.system.virtualmachine.VirtualMachineConfig.Builder setDebugLevel(int);
+    method @NonNull public android.system.virtualmachine.VirtualMachineConfig.Builder setMemoryMib(@IntRange(from=0) int);
+    method @NonNull public android.system.virtualmachine.VirtualMachineConfig.Builder setNumCpus(@IntRange(from=1) int);
+    method @NonNull public android.system.virtualmachine.VirtualMachineConfig.Builder setPayloadBinaryPath(@NonNull String);
+    method @NonNull @RequiresPermission(android.system.virtualmachine.VirtualMachine.USE_CUSTOM_VIRTUAL_MACHINE_PERMISSION) public android.system.virtualmachine.VirtualMachineConfig.Builder setPayloadConfigPath(@NonNull String);
+    method @NonNull public android.system.virtualmachine.VirtualMachineConfig.Builder setProtectedVm(boolean);
+  }
+
+  public final class VirtualMachineDescriptor implements android.os.Parcelable {
+    method public int describeContents();
+    method public void writeToParcel(@NonNull android.os.Parcel, int);
+    field @NonNull public static final android.os.Parcelable.Creator<android.system.virtualmachine.VirtualMachineDescriptor> CREATOR;
+  }
+
+  public class VirtualMachineException extends java.lang.Exception {
+    ctor public VirtualMachineException(@Nullable String);
+    ctor public VirtualMachineException(@Nullable String, @Nullable Throwable);
+    ctor public VirtualMachineException(@Nullable Throwable);
+  }
+
+  public class VirtualMachineManager {
+    method @NonNull @RequiresPermission(android.system.virtualmachine.VirtualMachine.MANAGE_VIRTUAL_MACHINE_PERMISSION) public android.system.virtualmachine.VirtualMachine create(@NonNull String, @NonNull android.system.virtualmachine.VirtualMachineConfig) throws android.system.virtualmachine.VirtualMachineException;
+    method public void delete(@NonNull String) throws android.system.virtualmachine.VirtualMachineException;
+    method @Nullable public android.system.virtualmachine.VirtualMachine get(@NonNull String) throws android.system.virtualmachine.VirtualMachineException;
+    method public int getCapabilities();
+    method @NonNull public static android.system.virtualmachine.VirtualMachineManager getInstance(@NonNull android.content.Context);
+    method @NonNull public android.system.virtualmachine.VirtualMachine getOrCreate(@NonNull String, @NonNull android.system.virtualmachine.VirtualMachineConfig) throws android.system.virtualmachine.VirtualMachineException;
+    method @NonNull public android.system.virtualmachine.VirtualMachine importFromDescriptor(@NonNull String, @NonNull android.system.virtualmachine.VirtualMachineDescriptor) throws android.system.virtualmachine.VirtualMachineException;
+    field public static final int CAPABILITY_NON_PROTECTED_VM = 2; // 0x2
+    field public static final int CAPABILITY_PROTECTED_VM = 1; // 0x1
+  }
+
+}
+
diff --git a/javalib/src/android/system/virtualmachine/VirtualMachine.java b/javalib/src/android/system/virtualmachine/VirtualMachine.java
index 4435576..193d213 100644
--- a/javalib/src/android/system/virtualmachine/VirtualMachine.java
+++ b/javalib/src/android/system/virtualmachine/VirtualMachine.java
@@ -48,6 +48,7 @@
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.annotation.RequiresPermission;
+import android.annotation.SystemApi;
 import android.content.Context;
 import android.os.Binder;
 import android.os.IBinder;
@@ -94,14 +95,15 @@
 /**
  * Represents an VM instance, with its own configuration and state. Instances are persistent and are
  * created or retrieved via {@link VirtualMachineManager}.
- * <p>
- * The {@link #run} method actually starts up the VM and allows the payload code to execute. It
- * will continue until it exits or {@link #stop} is called. Updates on the state of the VM can
- * be received using {@link #setCallback}. The app can communicate with the VM using
- * {@link #connectToVsockServer} or {@link #connectVsock}.
+ *
+ * <p>The {@link #run} method actually starts up the VM and allows the payload code to execute. It
+ * will continue until it exits or {@link #stop} is called. Updates on the state of the VM can be
+ * received using {@link #setCallback}. The app can communicate with the VM using {@link
+ * #connectToVsockServer} or {@link #connectVsock}.
  *
  * @hide
  */
+@SystemApi
 public class VirtualMachine implements AutoCloseable {
     /** Name of the directory under the files directory where all VMs created for the app exist. */
     private static final String VM_DIR = "vm";
@@ -379,9 +381,8 @@
     void delete(Context context, String name) throws VirtualMachineException {
         synchronized (mLock) {
             checkStopped();
+            deleteVmDirectory(context, name);
         }
-
-        deleteVmDirectory(context, name);
     }
 
     static void deleteVmDirectory(Context context, String name) throws VirtualMachineException {
@@ -425,6 +426,7 @@
      *
      * @hide
      */
+    @SystemApi
     @NonNull
     public String getName() {
         return mName;
@@ -439,6 +441,7 @@
      *
      * @hide
      */
+    @SystemApi
     @NonNull
     public VirtualMachineConfig getConfig() {
         synchronized (mLock) {
@@ -451,6 +454,7 @@
      *
      * @hide
      */
+    @SystemApi
     @Status
     public int getStatus() {
         IVirtualMachine virtualMachine;
@@ -525,7 +529,9 @@
      *
      * @hide
      */
-    public void setCallback(@NonNull @CallbackExecutor Executor executor,
+    @SystemApi
+    public void setCallback(
+            @NonNull @CallbackExecutor Executor executor,
             @NonNull VirtualMachineCallback callback) {
         synchronized (mCallbackLock) {
             mCallback = callback;
@@ -538,6 +544,7 @@
      *
      * @hide
      */
+    @SystemApi
     public void clearCallback() {
         synchronized (mCallbackLock) {
             mCallback = null;
@@ -571,9 +578,10 @@
      * calling {@code run()}.
      *
      * @throws VirtualMachineException if the virtual machine is not stopped or could not be
-     *         started.
+     *     started.
      * @hide
      */
+    @SystemApi
     @RequiresPermission(MANAGE_VIRTUAL_MACHINE_PERMISSION)
     public void run() throws VirtualMachineException {
         synchronized (mLock) {
@@ -717,6 +725,7 @@
      * @throws VirtualMachineException if the stream could not be created.
      * @hide
      */
+    @SystemApi
     @NonNull
     public InputStream getConsoleOutput() throws VirtualMachineException {
         synchronized (mLock) {
@@ -731,6 +740,7 @@
      * @throws VirtualMachineException if the stream could not be created.
      * @hide
      */
+    @SystemApi
     @NonNull
     public InputStream getLogOutput() throws VirtualMachineException {
         synchronized (mLock) {
@@ -742,12 +752,12 @@
     /**
      * Stops this virtual machine. Stopping a virtual machine is like pulling the plug on a real
      * computer; the machine halts immediately. Software running on the virtual machine is not
-     * notified of the event. A stopped virtual machine can be re-started by calling {@link
-     * #run()}.
+     * notified of the event. A stopped virtual machine can be re-started by calling {@link #run()}.
      *
      * @throws VirtualMachineException if the virtual machine could not be stopped.
      * @hide
      */
+    @SystemApi
     public void stop() throws VirtualMachineException {
         synchronized (mLock) {
             if (mVirtualMachine == null) {
@@ -770,6 +780,7 @@
      * @throws VirtualMachineException if the virtual machine could not be stopped.
      * @hide
      */
+    @SystemApi
     @Override
     public void close() throws VirtualMachineException {
         stop();
@@ -802,6 +813,7 @@
      * @throws VirtualMachineException if the virtual machine is not running.
      * @hide
      */
+    @SystemApi
     public int getCid() throws VirtualMachineException {
         synchronized (mLock) {
             try {
@@ -817,14 +829,15 @@
      * like the number of CPU and size of the RAM, depending on the situation (e.g. the size of the
      * application to run on the virtual machine, etc.)
      *
-     * The new config must be {@link VirtualMachineConfig#isCompatibleWith compatible with} the
+     * <p>The new config must be {@link VirtualMachineConfig#isCompatibleWith compatible with} the
      * existing config.
      *
      * @return the old config
      * @throws VirtualMachineException if the virtual machine is not stopped, or the new config is
-     *         incompatible.
+     *     incompatible.
      * @hide
      */
+    @SystemApi
     @NonNull
     public VirtualMachineConfig setConfig(@NonNull VirtualMachineConfig newConfig)
             throws VirtualMachineException {
@@ -834,6 +847,10 @@
                 throw new VirtualMachineException("incompatible config");
             }
             checkStopped();
+
+            // Delete any existing file before recreating; that ensures any VirtualMachineDescriptor
+            // that refers to the old file does not see the new config.
+            mConfigFilePath.delete();
             newConfig.serialize(mConfigFilePath);
             mConfig = newConfig;
             return oldConfig;
@@ -846,13 +863,14 @@
     /**
      * Connect to a VM's binder service via vsock and return the root IBinder object. Guest VMs are
      * expected to set up vsock servers in their payload. After the host app receives the {@link
-     * VirtualMachineCallback#onPayloadReady(VirtualMachine)}, it can use this method to
-     * establish a connection to the guest VM.
+     * VirtualMachineCallback#onPayloadReady(VirtualMachine)}, it can use this method to establish a
+     * connection to the guest VM.
      *
      * @throws VirtualMachineException if the virtual machine is not running or the connection
-     *         failed.
+     *     failed.
      * @hide
      */
+    @SystemApi
     @NonNull
     public IBinder connectToVsockServer(int port) throws VirtualMachineException {
         synchronized (mLock) {
@@ -870,6 +888,7 @@
      * @throws VirtualMachineException if connecting fails.
      * @hide
      */
+    @SystemApi
     @NonNull
     public ParcelFileDescriptor connectVsock(int port) throws VirtualMachineException {
         synchronized (mLock) {
@@ -887,22 +906,27 @@
      * Captures the current state of the VM in a {@link VirtualMachineDescriptor} instance. The VM
      * needs to be stopped to avoid inconsistency in its state representation.
      *
+     * <p>The state of the VM is not actually copied until {@link
+     * VirtualMachineManager#importFromDescriptor} is called. It is recommended that the VM not be
+     * started until that operation is complete.
+     *
      * @return a {@link VirtualMachineDescriptor} instance that represents the VM's state.
      * @throws VirtualMachineException if the virtual machine is not stopped, or the state could not
      *     be captured.
      * @hide
      */
+    @SystemApi
     @NonNull
     public VirtualMachineDescriptor toDescriptor() throws VirtualMachineException {
         synchronized (mLock) {
             checkStopped();
-        }
-        try {
-            return new VirtualMachineDescriptor(
-                    ParcelFileDescriptor.open(mConfigFilePath, MODE_READ_ONLY),
-                    ParcelFileDescriptor.open(mInstanceFilePath, MODE_READ_ONLY));
-        } catch (IOException e) {
-            throw new VirtualMachineException(e);
+            try {
+                return new VirtualMachineDescriptor(
+                        ParcelFileDescriptor.open(mConfigFilePath, MODE_READ_ONLY),
+                        ParcelFileDescriptor.open(mInstanceFilePath, MODE_READ_ONLY));
+            } catch (IOException e) {
+                throw new VirtualMachineException(e);
+            }
         }
     }
 
diff --git a/javalib/src/android/system/virtualmachine/VirtualMachineCallback.java b/javalib/src/android/system/virtualmachine/VirtualMachineCallback.java
index 1f94a8b..f3c4831 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.SuppressLint;
+import android.annotation.SystemApi;
 import android.os.ParcelFileDescriptor;
 
 import java.lang.annotation.Retention;
@@ -30,7 +31,8 @@
  *
  * @hide
  */
-@SuppressLint("CallbackInterface")  // Guidance has changed, lint is out of date (b/245552641)
+@SystemApi
+@SuppressLint("CallbackInterface") // Guidance has changed, lint is out of date (b/245552641)
 public interface VirtualMachineCallback {
     /** @hide */
     @Retention(RetentionPolicy.SOURCE)
diff --git a/javalib/src/android/system/virtualmachine/VirtualMachineConfig.java b/javalib/src/android/system/virtualmachine/VirtualMachineConfig.java
index a660306..8678b99 100644
--- a/javalib/src/android/system/virtualmachine/VirtualMachineConfig.java
+++ b/javalib/src/android/system/virtualmachine/VirtualMachineConfig.java
@@ -26,6 +26,7 @@
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.annotation.RequiresPermission;
+import android.annotation.SystemApi;
 import android.content.Context;
 import android.os.ParcelFileDescriptor;
 import android.os.PersistableBundle;
@@ -51,6 +52,7 @@
  *
  * @hide
  */
+@SystemApi
 public final class VirtualMachineConfig {
     // These define the schema of the config file persisted on disk.
     private static final int VERSION = 2;
@@ -76,12 +78,12 @@
     public @interface 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.
+     * 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 static final int DEBUG_LEVEL_NONE = 0;
+    @SystemApi 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
@@ -89,7 +91,7 @@
      *
      * @hide
      */
-    public static final int DEBUG_LEVEL_APP_ONLY = 1;
+    @SystemApi public static final int DEBUG_LEVEL_APP_ONLY = 1;
 
     /**
      * Fully debuggable. All logs (both logcat and kernel message) are exported. All processes
@@ -97,7 +99,7 @@
      *
      * @hide
      */
-    public static final int DEBUG_LEVEL_FULL = 2;
+    @SystemApi public static final int DEBUG_LEVEL_FULL = 2;
 
     @DebugLevel private final int mDebugLevel;
 
@@ -270,28 +272,31 @@
      *
      * @hide
      */
+    @SystemApi
     @NonNull
     public String getApkPath() {
         return mApkPath;
     }
 
     /**
-     * Returns the path within the APK to the payload config file that defines software aspects
-     * of the VM.
+     * Returns the path within the APK to the payload config file that defines software aspects of
+     * the VM.
      *
      * @hide
      */
+    @SystemApi // TODO(b/243512115): Switch back to @TestApi
     @Nullable
     public String getPayloadConfigPath() {
         return mPayloadConfigPath;
     }
 
     /**
-     * Returns the path within the {@code lib/<ABI>} directory of the APK to the payload binary
-     * file that will be executed within the VM.
+     * Returns the path within the {@code lib/<ABI>} directory of the APK to the payload binary file
+     * that will be executed within the VM.
      *
      * @hide
      */
+    @SystemApi
     @Nullable
     public String getPayloadBinaryPath() {
         return mPayloadBinaryPath;
@@ -302,6 +307,7 @@
      *
      * @hide
      */
+    @SystemApi
     @NonNull
     @DebugLevel
     public int getDebugLevel() {
@@ -313,6 +319,7 @@
      *
      * @hide
      */
+    @SystemApi
     public boolean isProtectedVm() {
         return mProtectedVm;
     }
@@ -322,6 +329,7 @@
      *
      * @hide
      */
+    @SystemApi
     @IntRange(from = 0)
     public int getMemoryMib() {
         return mMemoryMib;
@@ -332,6 +340,7 @@
      *
      * @hide
      */
+    @SystemApi
     @IntRange(from = 1)
     public int getNumCpus() {
         return mNumCpus;
@@ -345,6 +354,7 @@
      *
      * @hide
      */
+    @SystemApi
     public boolean isCompatibleWith(@NonNull VirtualMachineConfig other) {
         return this.mDebugLevel == other.mDebugLevel
                 && this.mProtectedVm == other.mProtectedVm
@@ -397,6 +407,7 @@
      *
      * @hide
      */
+    @SystemApi
     public static final class Builder {
         private final Context mContext;
         @Nullable private String mApkPath;
@@ -413,6 +424,7 @@
          *
          * @hide
          */
+        @SystemApi
         public Builder(@NonNull Context context) {
             mContext = requireNonNull(context, "context must not be null");
             mDebugLevel = DEBUG_LEVEL_NONE;
@@ -424,6 +436,7 @@
          *
          * @hide
          */
+        @SystemApi
         @NonNull
         public VirtualMachineConfig build() {
             String apkPath = (mApkPath == null) ? mContext.getPackageCodePath() : mApkPath;
@@ -443,6 +456,7 @@
          *
          * @hide
          */
+        @SystemApi
         @NonNull
         public Builder setApkPath(@NonNull String apkPath) {
             mApkPath = requireNonNull(apkPath);
@@ -450,13 +464,14 @@
         }
 
         /**
-         * Sets the path within the APK to the payload config file that defines software aspects
-         * of the VM. The file is a JSON file; see
+         * Sets the path within the APK to the payload config file that defines software aspects of
+         * the VM. The file is a JSON file; see
          * packages/modules/Virtualization/microdroid/payload/config/src/lib.rs for the format.
          *
          * @hide
          */
         @RequiresPermission(VirtualMachine.USE_CUSTOM_VIRTUAL_MACHINE_PERMISSION)
+        @SystemApi // TODO(b/243512115): Switch to @TestApi
         @NonNull
         public Builder setPayloadConfigPath(@NonNull String payloadConfigPath) {
             mPayloadConfigPath = requireNonNull(payloadConfigPath);
@@ -469,6 +484,7 @@
          *
          * @hide
          */
+        @SystemApi
         @NonNull
         public Builder setPayloadBinaryPath(@NonNull String payloadBinaryPath) {
             mPayloadBinaryPath = requireNonNull(payloadBinaryPath);
@@ -480,6 +496,7 @@
          *
          * @hide
          */
+        @SystemApi
         @NonNull
         public Builder setDebugLevel(@DebugLevel int debugLevel) {
             mDebugLevel = debugLevel;
@@ -487,12 +504,13 @@
         }
 
         /**
-         * Sets whether to protect the VM memory from the host. No default is provided, this
-         * must be set explicitly.
+         * Sets whether to protect the VM memory from the host. No default is provided, this must be
+         * set explicitly.
          *
          * @see VirtualMachineManager#getCapabilities
          * @hide
          */
+        @SystemApi
         @NonNull
         public Builder setProtectedVm(boolean protectedVm) {
             mProtectedVm = protectedVm;
@@ -501,11 +519,12 @@
         }
 
         /**
-         * Sets the amount of RAM to give the VM, in mebibytes. If zero or not explicitly set
-         * than a default size will be used.
+         * Sets the amount of RAM to give the VM, in mebibytes. If zero or not explicitly set then a
+         * default size will be used.
          *
          * @hide
          */
+        @SystemApi
         @NonNull
         public Builder setMemoryMib(@IntRange(from = 0) int memoryMib) {
             mMemoryMib = memoryMib;
@@ -513,11 +532,12 @@
         }
 
         /**
-         * Sets the number of vCPUs in the VM. Defaults to 1. Cannot be more than the number of
-         * real CPUs (as returned by {@link Runtime#availableProcessors()}).
+         * Sets the number of vCPUs in the VM. Defaults to 1. Cannot be more than the number of real
+         * CPUs (as returned by {@link Runtime#availableProcessors()}).
          *
          * @hide
          */
+        @SystemApi
         @NonNull
         public Builder setNumCpus(@IntRange(from = 1) int num) {
             mNumCpus = num;
diff --git a/javalib/src/android/system/virtualmachine/VirtualMachineDescriptor.java b/javalib/src/android/system/virtualmachine/VirtualMachineDescriptor.java
index b51cbce..edaf5b4 100644
--- a/javalib/src/android/system/virtualmachine/VirtualMachineDescriptor.java
+++ b/javalib/src/android/system/virtualmachine/VirtualMachineDescriptor.java
@@ -19,6 +19,7 @@
 import static java.util.Objects.requireNonNull;
 
 import android.annotation.NonNull;
+import android.annotation.SystemApi;
 import android.os.Parcel;
 import android.os.ParcelFileDescriptor;
 import android.os.Parcelable;
@@ -32,6 +33,7 @@
  *
  * @hide
  */
+@SystemApi
 public final class VirtualMachineDescriptor implements Parcelable {
     @NonNull private final ParcelFileDescriptor mConfigFd;
     @NonNull private final ParcelFileDescriptor mInstanceImgFd;
diff --git a/javalib/src/android/system/virtualmachine/VirtualMachineException.java b/javalib/src/android/system/virtualmachine/VirtualMachineException.java
index 828775a..985eb70 100644
--- a/javalib/src/android/system/virtualmachine/VirtualMachineException.java
+++ b/javalib/src/android/system/virtualmachine/VirtualMachineException.java
@@ -17,12 +17,14 @@
 package android.system.virtualmachine;
 
 import android.annotation.Nullable;
+import android.annotation.SystemApi;
 
 /**
  * Exception thrown when operations on virtual machines fail.
  *
  * @hide
  */
+@SystemApi
 public class VirtualMachineException extends Exception {
     public VirtualMachineException(@Nullable String message) {
         super(message);
diff --git a/javalib/src/android/system/virtualmachine/VirtualMachineManager.java b/javalib/src/android/system/virtualmachine/VirtualMachineManager.java
index 0e96f43..a520ab4 100644
--- a/javalib/src/android/system/virtualmachine/VirtualMachineManager.java
+++ b/javalib/src/android/system/virtualmachine/VirtualMachineManager.java
@@ -23,6 +23,7 @@
 import android.annotation.Nullable;
 import android.annotation.RequiresPermission;
 import android.annotation.SuppressLint;
+import android.annotation.SystemApi;
 import android.content.Context;
 import android.sysprop.HypervisorProperties;
 import android.util.ArrayMap;
@@ -37,16 +38,17 @@
 
 /**
  * Manages {@link VirtualMachine virtual machine} instances created by an app. Each instance is
- * created from a {@link VirtualMachineConfig configuration} that defines the shape of the VM
- * (RAM, CPUs), the code to execute within it, etc.
- * <p>
- * Each virtual machine instance is named; the configuration and related state of each is
+ * created from a {@link VirtualMachineConfig configuration} that defines the shape of the VM (RAM,
+ * CPUs), the code to execute within it, etc.
+ *
+ * <p>Each virtual machine instance is named; the configuration and related state of each is
  * persisted in the app's private data directory and an instance can be retrieved given the name.
- * <p>
- * The app can then start, stop and otherwise interact with the VM.
+ *
+ * <p>The app can then start, stop and otherwise interact with the VM.
  *
  * @hide
  */
+@SystemApi
 public class VirtualMachineManager {
     /**
      * A lock used to synchronize the creation of virtual machines. It protects {@link #mVmsByName},
@@ -95,8 +97,9 @@
      *
      * @hide
      */
+    @SystemApi
     @NonNull
-    @SuppressLint("ManagerLookup") // Optional API
+    @SuppressLint("ManagerLookup") // TODO(b/249093790): remove
     public static VirtualMachineManager getInstance(@NonNull Context context) {
         requireNonNull(context, "context must not be null");
         synchronized (sInstances) {
@@ -117,6 +120,7 @@
      * @see #CAPABILITY_NON_PROTECTED_VM
      * @hide
      */
+    @SystemApi
     @Capability
     public int getCapabilities() {
         @Capability int result = 0;
@@ -134,18 +138,18 @@
      * 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.
      *
-     * 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
+     * <p>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 the VM cannot be created, or there is an existing VM with
-     *         the given name.
+     *     the given name.
      * @hide
      */
+    @SystemApi
     @NonNull
     @RequiresPermission(VirtualMachine.MANAGE_VIRTUAL_MACHINE_PERMISSION)
-    public VirtualMachine create(
-            @NonNull String name, @NonNull VirtualMachineConfig config)
+    public VirtualMachine create(@NonNull String name, @NonNull VirtualMachineConfig config)
             throws VirtualMachineException {
         synchronized (sCreateLock) {
             return createLocked(name, config);
@@ -169,6 +173,7 @@
      *     retrieved.
      * @hide
      */
+    @SystemApi
     @Nullable
     public VirtualMachine get(@NonNull String name) throws VirtualMachineException {
         synchronized (sCreateLock) {
@@ -199,6 +204,7 @@
      * @hide
      */
     @NonNull
+    @SystemApi
     public VirtualMachine importFromDescriptor(
             @NonNull String name, @NonNull VirtualMachineDescriptor vmDescriptor)
             throws VirtualMachineException {
@@ -216,9 +222,9 @@
      * @throws VirtualMachineException if the virtual machine could not be created or retrieved.
      * @hide
      */
+    @SystemApi
     @NonNull
-    public VirtualMachine getOrCreate(
-            @NonNull String name, @NonNull VirtualMachineConfig config)
+    public VirtualMachine getOrCreate(@NonNull String name, @NonNull VirtualMachineConfig config)
             throws VirtualMachineException {
         synchronized (sCreateLock) {
             VirtualMachine vm = getLocked(name);
@@ -237,10 +243,11 @@
      * with the same name is different from an already deleted virtual machine even if it has the
      * same config.
      *
-     * @throws VirtualMachineException if the virtual machine does not exist, is not stopped,
-     *                                 or cannot be deleted.
+     * @throws VirtualMachineException if the virtual machine does not exist, is not stopped, or
+     *     cannot be deleted.
      * @hide
      */
+    @SystemApi
     public void delete(@NonNull String name) throws VirtualMachineException {
         synchronized (sCreateLock) {
             VirtualMachine vm = getVmByName(name);
diff --git a/microdroid/Android.bp b/microdroid/Android.bp
index f2b223d..af6031a 100644
--- a/microdroid/Android.bp
+++ b/microdroid/Android.bp
@@ -83,7 +83,6 @@
         "microdroid_manifest",
         "microdroid_plat_sepolicy_and_mapping.sha256",
         "microdroid_property_contexts",
-        "microdroid_service_contexts",
         "mke2fs",
 
         // TODO(b/195425111) these should be added automatically
diff --git a/microdroid_manager/src/main.rs b/microdroid_manager/src/main.rs
index a53b401..a706dbe 100644
--- a/microdroid_manager/src/main.rs
+++ b/microdroid_manager/src/main.rs
@@ -25,9 +25,7 @@
 use crate::instance::{ApexData, ApkData, InstanceDisk, MicrodroidData, RootHash};
 use crate::vm_payload_service::register_vm_payload_service;
 use android_system_virtualizationcommon::aidl::android::system::virtualizationcommon::ErrorCode::ErrorCode;
-use android_system_virtualmachineservice::aidl::android::system::virtualmachineservice::IVirtualMachineService::{
-        IVirtualMachineService, VM_BINDER_SERVICE_PORT,
-};
+use android_system_virtualmachineservice::aidl::android::system::virtualmachineservice::IVirtualMachineService::IVirtualMachineService;
 use android_system_virtualization_payload::aidl::android::system::virtualization::payload::IVmPayloadService::{
     VM_APK_CONTENTS_PATH,
     VM_PAYLOAD_SERVICE_SOCKET_NAME,
@@ -160,8 +158,11 @@
 }
 
 fn get_vms_rpc_binder() -> Result<Strong<dyn IVirtualMachineService>> {
-    get_vsock_rpc_interface(VMADDR_CID_HOST, VM_BINDER_SERVICE_PORT as u32)
-        .context("Cannot connect to RPC service")
+    // The host is running a VirtualMachineService for this VM on a port equal
+    // to the CID of this VM.
+    let port = vsock::get_local_cid().context("Could not determine local CID")?;
+    get_vsock_rpc_interface(VMADDR_CID_HOST, port)
+        .context("Could not connect to IVirtualMachineService")
 }
 
 fn main() -> Result<()> {
diff --git a/tests/benchmark/Android.bp b/tests/benchmark/Android.bp
index 1747183..10cdac5 100644
--- a/tests/benchmark/Android.bp
+++ b/tests/benchmark/Android.bp
@@ -16,16 +16,13 @@
         "com.android.microdroid.testservice-java",
         "truth-prebuilt",
     ],
-    // We need to compile against the .impl library which includes the hidden
-    // APIs. Once the APIs are promoted to @SystemApi we can switch to
-    // framework-virtualization, which contains API stubs.
-    libs: ["framework-virtualization.impl"],
     jni_libs: [
         "MicrodroidBenchmarkNativeLib",
         "MicrodroidIdleNativeLib",
         "libiovsock_host_jni",
     ],
-    platform_apis: true,
+    jni_uses_platform_apis: true,
+    sdk_version: "test_current",
     use_embedded_native_libs: true,
     compile_multilib: "64",
 }
diff --git a/tests/benchmark/src/native/benchmarkbinary.cpp b/tests/benchmark/src/native/benchmarkbinary.cpp
index e43025c..66b41a1 100644
--- a/tests/benchmark/src/native/benchmarkbinary.cpp
+++ b/tests/benchmark/src/native/benchmarkbinary.cpp
@@ -160,12 +160,7 @@
 
 Result<void> run_io_benchmark_tests() {
     auto test_service = ndk::SharedRefBase::make<IOBenchmarkService>();
-    auto callback = []([[maybe_unused]] void* param) {
-        if (!AVmPayload_notifyPayloadReady()) {
-            LOG(ERROR) << "failed to notify payload ready to virtualizationservice";
-            abort();
-        }
-    };
+    auto callback = []([[maybe_unused]] void* param) { AVmPayload_notifyPayloadReady(); };
     if (!AVmPayload_runVsockRpcServer(test_service->asBinder().get(), test_service->SERVICE_PORT,
                                       callback, nullptr)) {
         return Error() << "RPC Server failed to run";
diff --git a/tests/benchmark_hostside/java/android/avf/test/AVFHostTestCase.java b/tests/benchmark_hostside/java/android/avf/test/AVFHostTestCase.java
index 1996f4b..c47e915 100644
--- a/tests/benchmark_hostside/java/android/avf/test/AVFHostTestCase.java
+++ b/tests/benchmark_hostside/java/android/avf/test/AVFHostTestCase.java
@@ -149,7 +149,9 @@
     }
 
     private void appStartupHelper(String launchIntentPackage) throws Exception {
-        assumeTrue("Skip on non-protected VMs", isProtectedVmSupported());
+        assumeTrue(
+                "Skip on non-protected VMs",
+                ((TestDevice) getDevice()).supportsMicrodroid(/*protectedVm=*/ true));
 
         StartupTimeMetricCollection mCollection =
                 new StartupTimeMetricCollection(getPackageName(launchIntentPackage), ROUND_COUNT);
diff --git a/tests/helper/Android.bp b/tests/helper/Android.bp
index 7473dab..61c5dcd 100644
--- a/tests/helper/Android.bp
+++ b/tests/helper/Android.bp
@@ -6,9 +6,7 @@
     name: "MicrodroidTestHelper",
     srcs: ["src/java/com/android/microdroid/test/common/*.java"],
     host_supported: true,
-    libs: [
-        "framework-annotations-lib",
-    ],
+    sdk_version: "system_current",
 }
 
 java_library_static {
@@ -20,8 +18,5 @@
         "MicrodroidTestHelper",
         "truth-prebuilt",
     ],
-    // We need to compile against the .impl library which includes the hidden
-    // APIs. Once the APIs are promoted to @SystemApi we can switch to
-    // framework-virtualization, which contains API stubs.
-    libs: ["framework-virtualization.impl"],
+    sdk_version: "system_current",
 }
diff --git a/tests/helper/src/java/com/android/microdroid/test/common/DeviceProperties.java b/tests/helper/src/java/com/android/microdroid/test/common/DeviceProperties.java
index 1fc163b..94f7e99 100644
--- a/tests/helper/src/java/com/android/microdroid/test/common/DeviceProperties.java
+++ b/tests/helper/src/java/com/android/microdroid/test/common/DeviceProperties.java
@@ -18,15 +18,11 @@
 
 import static java.util.Objects.requireNonNull;
 
-import android.annotation.NonNull;
-import android.annotation.Nullable;
-
 /** This class can be used in both host tests and device tests to get the device properties. */
 public final class DeviceProperties {
     /** PropertyGetter is used to get the property associated to a given key. */
     public interface PropertyGetter {
-        @Nullable
-        String getProperty(@NonNull String key) throws Exception;
+        String getProperty(String key) throws Exception;
     }
 
     private static final String KEY_VENDOR_DEVICE = "ro.product.vendor.device";
@@ -34,15 +30,14 @@
 
     private static final String CUTTLEFISH_DEVICE_PREFIX = "vsoc_";
 
-    @NonNull private final PropertyGetter mPropertyGetter;
+    private final PropertyGetter mPropertyGetter;
 
-    private DeviceProperties(@NonNull PropertyGetter propertyGetter) {
+    private DeviceProperties(PropertyGetter propertyGetter) {
         mPropertyGetter = requireNonNull(propertyGetter);
     }
 
     /** Creates a new instance of {@link DeviceProperties}. */
-    @NonNull
-    public static DeviceProperties create(@NonNull PropertyGetter propertyGetter) {
+    public static DeviceProperties create(PropertyGetter propertyGetter) {
         return new DeviceProperties(propertyGetter);
     }
 
@@ -54,7 +49,6 @@
         return vendorDeviceName != null && vendorDeviceName.startsWith(CUTTLEFISH_DEVICE_PREFIX);
     }
 
-    @Nullable
     public String getMetricsTag() {
         return getProperty(KEY_METRICS_TAG);
     }
diff --git a/tests/hostside/helper/java/com/android/microdroid/test/host/MicrodroidHostTestCaseBase.java b/tests/hostside/helper/java/com/android/microdroid/test/host/MicrodroidHostTestCaseBase.java
index ac37ee0..e5aa908 100644
--- a/tests/hostside/helper/java/com/android/microdroid/test/host/MicrodroidHostTestCaseBase.java
+++ b/tests/hostside/helper/java/com/android/microdroid/test/host/MicrodroidHostTestCaseBase.java
@@ -237,8 +237,4 @@
                 .stdoutTrimmed()
                 .isEqualTo("microdroid");
     }
-
-    public boolean isProtectedVmSupported() throws DeviceNotAvailableException {
-        return getDevice().getBooleanProperty("ro.boot.hypervisor.protected_vm.supported", false);
-    }
 }
diff --git a/tests/hostside/java/com/android/microdroid/test/MicrodroidHostTests.java b/tests/hostside/java/com/android/microdroid/test/MicrodroidHostTests.java
index cffaae1..f0c89c9 100644
--- a/tests/hostside/java/com/android/microdroid/test/MicrodroidHostTests.java
+++ b/tests/hostside/java/com/android/microdroid/test/MicrodroidHostTests.java
@@ -51,7 +51,6 @@
 import com.android.tradefed.util.xml.AbstractXmlParser;
 
 import org.json.JSONArray;
-import org.json.JSONException;
 import org.json.JSONObject;
 import org.junit.After;
 import org.junit.Before;
@@ -433,7 +432,9 @@
     @CddTest(requirements = {"9.17/C-2-1", "9.17/C-2-2", "9.17/C-2-6"})
     public void testBootFailsWhenProtectedVmStartsWithImagesSignedWithDifferentKey()
             throws Exception {
-        assumeTrue("Protected VMs are not supported", isProtectedVmSupported());
+        assumeTrue(
+                "Protected VMs are not supported",
+                getAndroidDevice().supportsMicrodroid(/*protectedVm=*/ true));
 
         File key = findTestFile("test.com.android.virt.pem");
         Map<String, File> keyOverrides = Map.of();
@@ -490,12 +491,12 @@
     private boolean isTombstoneGeneratedWithConfig(String configPath) throws Exception {
         // Note this test relies on logcat values being printed by tombstone_transmit on
         // and the reeceiver on host (virtualization_service)
-        mMicrodroidDevice = MicrodroidBuilder
-                .fromDevicePath(getPathForPackage(PACKAGE_NAME), configPath)
-                .debugLevel("full")
-                .memoryMib(minMemorySize())
-                .numCpus(NUM_VCPUS)
-                .build((TestDevice) getDevice());
+        mMicrodroidDevice =
+                MicrodroidBuilder.fromDevicePath(getPathForPackage(PACKAGE_NAME), configPath)
+                        .debugLevel("full")
+                        .memoryMib(minMemorySize())
+                        .numCpus(NUM_VCPUS)
+                        .build(getAndroidDevice());
         mMicrodroidDevice.waitForBootComplete(BOOT_COMPLETE_TIMEOUT);
         mMicrodroidDevice.enableAdbRoot();
 
@@ -546,7 +547,7 @@
         ConfigUtils.uploadConfigForPushedAtoms(getDevice(), PACKAGE_NAME, atomIds);
 
         // Create VM with microdroid
-        TestDevice device = (TestDevice) getDevice();
+        TestDevice device = getAndroidDevice();
         final String configPath = "assets/vm_config_apex.json"; // path inside the APK
         ITestDevice microdroid =
                 MicrodroidBuilder.fromDevicePath(getPathForPackage(PACKAGE_NAME), configPath)
@@ -614,7 +615,7 @@
                         .debugLevel("full")
                         .memoryMib(minMemorySize())
                         .numCpus(NUM_VCPUS)
-                        .build((TestDevice) getDevice());
+                        .build(getAndroidDevice());
         mMicrodroidDevice.waitForBootComplete(BOOT_COMPLETE_TIMEOUT);
         CommandRunner microdroid = new CommandRunner(mMicrodroidDevice);
 
@@ -676,12 +677,12 @@
     @Test
     public void testMicrodroidRamUsage() throws Exception {
         final String configPath = "assets/vm_config.json";
-        mMicrodroidDevice = MicrodroidBuilder
-                .fromDevicePath(getPathForPackage(PACKAGE_NAME), configPath)
-                .debugLevel("full")
-                .memoryMib(minMemorySize())
-                .numCpus(NUM_VCPUS)
-                .build((TestDevice) getDevice());
+        mMicrodroidDevice =
+                MicrodroidBuilder.fromDevicePath(getPathForPackage(PACKAGE_NAME), configPath)
+                        .debugLevel("full")
+                        .memoryMib(minMemorySize())
+                        .numCpus(NUM_VCPUS)
+                        .build(getAndroidDevice());
         mMicrodroidDevice.waitForBootComplete(BOOT_COMPLETE_TIMEOUT);
         mMicrodroidDevice.enableAdbRoot();
 
@@ -716,9 +717,10 @@
     }
 
     @Test
-    public void testCustomVirtualMachinePermission()
-            throws DeviceNotAvailableException, IOException, JSONException {
-        assumeTrue("Protected VMs are not supported", isProtectedVmSupported());
+    public void testCustomVirtualMachinePermission() throws Exception {
+        assumeTrue(
+                "Protected VMs are not supported",
+                getAndroidDevice().supportsMicrodroid(/*protectedVm=*/ true));
         CommandRunner android = new CommandRunner(getDevice());
 
         // Pull etc/microdroid.json
@@ -767,7 +769,7 @@
     @After
     public void shutdown() throws Exception {
         if (mMicrodroidDevice != null) {
-            ((TestDevice) getDevice()).shutdownMicrodroid(mMicrodroidDevice);
+            getAndroidDevice().shutdownMicrodroid(mMicrodroidDevice);
         }
 
         cleanUpVirtualizationTestSetup(getDevice());
@@ -785,4 +787,10 @@
                         SHELL_PACKAGE_NAME,
                         "android.permission.USE_CUSTOM_VIRTUAL_MACHINE");
     }
+
+    private TestDevice getAndroidDevice() {
+        TestDevice androidDevice = (TestDevice) getDevice();
+        assertThat(androidDevice).isNotNull();
+        return androidDevice;
+    }
 }
diff --git a/tests/testapk/Android.bp b/tests/testapk/Android.bp
index 707dca1..df7c6c0 100644
--- a/tests/testapk/Android.bp
+++ b/tests/testapk/Android.bp
@@ -19,15 +19,12 @@
         "truth-prebuilt",
         "compatibility-common-util-devicesidelib",
     ],
-    // We need to compile against the .impl library which includes the hidden
-    // APIs. Once the APIs are promoted to @SystemApi we can switch to
-    // framework-virtualization, which contains API stubs.
-    libs: ["framework-virtualization.impl"],
+    sdk_version: "test_current",
     jni_libs: [
         "MicrodroidTestNativeLib",
         "MicrodroidIdleNativeLib",
     ],
-    platform_apis: true,
+    jni_uses_platform_apis: true,
     use_embedded_native_libs: true,
     // We only support 64-bit ABI, but CTS demands all APKs to be multi-ABI.
     compile_multilib: "both",
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 17574c7..71a9e3b 100644
--- a/tests/testapk/src/java/com/android/microdroid/test/MicrodroidTests.java
+++ b/tests/testapk/src/java/com/android/microdroid/test/MicrodroidTests.java
@@ -28,7 +28,6 @@
 
 import android.content.Context;
 import android.os.Build;
-import android.os.ServiceSpecificException;
 import android.os.SystemProperties;
 import android.system.virtualmachine.VirtualMachine;
 import android.system.virtualmachine.VirtualMachineCallback;
@@ -354,7 +353,7 @@
         listener.runToFinish(TAG, vm);
         Exception e = exception.getNow(null);
         if (e != null) {
-            throw e;
+            throw new RuntimeException(e);
         }
         return vmCdis;
     }
@@ -470,7 +469,7 @@
                 .build();
         mInner.forceCreateNewVirtualMachine("test_vm", config);
 
-        assertThrows(ServiceSpecificException.class, () -> launchVmAndGetCdis("test_vm"));
+        assertThrows(Exception.class, () -> launchVmAndGetCdis("test_vm"));
     }
 
 
diff --git a/tests/testapk/src/native/testbinary.cpp b/tests/testapk/src/native/testbinary.cpp
index 694f452..5c217ff 100644
--- a/tests/testapk/src/native/testbinary.cpp
+++ b/tests/testapk/src/native/testbinary.cpp
@@ -76,40 +76,22 @@
         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 VM instance secret");
-            }
+            AVmPayload_getVmInstanceSecret(identifier, sizeof(identifier), out->data(),
+                                           out->size());
             return ndk::ScopedAStatus::ok();
         }
 
         ndk::ScopedAStatus insecurelyExposeAttestationCdi(std::vector<uint8_t>* out) override {
-            size_t cdi_size;
-            if (!AVmPayload_getDiceAttestationCdi(nullptr, 0, &cdi_size)) {
-                return ndk::ScopedAStatus::
-                        fromServiceSpecificErrorWithMessage(0, "Failed to measure attestation cdi");
-            }
+            size_t cdi_size = AVmPayload_getDiceAttestationCdi(nullptr, 0);
             out->resize(cdi_size);
-            if (!AVmPayload_getDiceAttestationCdi(out->data(), out->size(), &cdi_size)) {
-                return ndk::ScopedAStatus::
-                        fromServiceSpecificErrorWithMessage(0, "Failed to get attestation cdi");
-            }
+            AVmPayload_getDiceAttestationCdi(out->data(), out->size());
             return ndk::ScopedAStatus::ok();
         }
 
         ndk::ScopedAStatus getBcc(std::vector<uint8_t>* out) override {
-            size_t bcc_size;
-            if (!AVmPayload_getDiceAttestationChain(nullptr, 0, &bcc_size)) {
-                return ndk::ScopedAStatus::
-                        fromServiceSpecificErrorWithMessage(0,
-                                                            "Failed to measure attestation chain");
-            }
+            size_t bcc_size = AVmPayload_getDiceAttestationChain(nullptr, 0);
             out->resize(bcc_size);
-            if (!AVmPayload_getDiceAttestationChain(out->data(), out->size(), &bcc_size)) {
-                return ndk::ScopedAStatus::
-                        fromServiceSpecificErrorWithMessage(0, "Failed to get attestation chain");
-            }
+            AVmPayload_getDiceAttestationChain(out->data(), out->size());
             return ndk::ScopedAStatus::ok();
         }
 
@@ -136,12 +118,7 @@
     };
     auto testService = ndk::SharedRefBase::make<TestService>();
 
-    auto callback = []([[maybe_unused]] void* param) {
-        if (!AVmPayload_notifyPayloadReady()) {
-            std::cerr << "failed to notify payload ready to virtualizationservice" << std::endl;
-            abort();
-        }
-    };
+    auto callback = []([[maybe_unused]] void* param) { AVmPayload_notifyPayloadReady(); };
     if (!AVmPayload_runVsockRpcServer(testService->asBinder().get(), testService->SERVICE_PORT,
                                       callback, nullptr)) {
         return Error() << "RPC Server failed to run";
diff --git a/virtualizationservice/aidl/android/system/virtualmachineservice/IVirtualMachineService.aidl b/virtualizationservice/aidl/android/system/virtualmachineservice/IVirtualMachineService.aidl
index f2d92af..3fdb48a 100644
--- a/virtualizationservice/aidl/android/system/virtualmachineservice/IVirtualMachineService.aidl
+++ b/virtualizationservice/aidl/android/system/virtualmachineservice/IVirtualMachineService.aidl
@@ -21,12 +21,6 @@
 interface IVirtualMachineService {
     /**
      * Port number that VirtualMachineService listens on connections from the guest VMs for the
-     * VirtualMachineService binder service.
-     */
-    const int VM_BINDER_SERVICE_PORT = 5000;
-
-    /**
-     * Port number that VirtualMachineService listens on connections from the guest VMs for the
      * tombtones
      */
     const int VM_TOMBSTONES_SERVICE_PORT = 2000;
diff --git a/virtualizationservice/src/aidl.rs b/virtualizationservice/src/aidl.rs
index 578960c..040c0d8 100644
--- a/virtualizationservice/src/aidl.rs
+++ b/virtualizationservice/src/aidl.rs
@@ -16,7 +16,7 @@
 
 use crate::atom::{write_vm_booted_stats, write_vm_creation_stats};
 use crate::composite::make_composite_image;
-use crate::crosvm::{CrosvmConfig, DiskFile, PayloadState, VmInstance, VmState};
+use crate::crosvm::{CrosvmConfig, DiskFile, PayloadState, VmContext, VmInstance, VmState};
 use crate::payload::{add_microdroid_payload_images, add_microdroid_system_images};
 use crate::selinux::{getfilecon, SeContext};
 use android_os_permissions_aidl::aidl::android::os::IPermissionController;
@@ -41,22 +41,22 @@
     IVirtualizationServiceInternal::{BnVirtualizationServiceInternal, IVirtualizationServiceInternal},
 };
 use android_system_virtualmachineservice::aidl::android::system::virtualmachineservice::IVirtualMachineService::{
-        BnVirtualMachineService, IVirtualMachineService, VM_BINDER_SERVICE_PORT,
-        VM_TOMBSTONES_SERVICE_PORT,
+        BnVirtualMachineService, IVirtualMachineService, VM_TOMBSTONES_SERVICE_PORT,
 };
 use anyhow::{anyhow, bail, Context, Result};
 use apkverify::{HashAlgorithm, V4Signature};
 use binder::{
     self, BinderFeatures, ExceptionCode, Interface, LazyServiceGuard, ParcelFileDescriptor,
-    SpIBinder, Status, StatusCode, Strong, ThreadState,
+    Status, StatusCode, Strong, ThreadState,
 };
 use disk::QcowFile;
 use libc::VMADDR_CID_HOST;
 use log::{debug, error, info, warn};
 use microdroid_payload_config::{OsConfig, Task, TaskType, VmPayloadConfig};
-use rpcbinder::run_vsock_rpc_server_with_factory;
+use rpcbinder::RpcServer;
 use rustutils::system_properties;
 use semver::VersionReq;
+use std::collections::HashMap;
 use std::convert::TryInto;
 use std::ffi::CStr;
 use std::fs::{create_dir, File, OpenOptions};
@@ -80,7 +80,8 @@
 
 /// The first CID to assign to a guest VM managed by the VirtualizationService. CIDs lower than this
 /// are reserved for the host or other usage.
-const FIRST_GUEST_CID: Cid = 10;
+const GUEST_CID_MIN: Cid = 2048;
+const GUEST_CID_MAX: Cid = 65535;
 
 const SYSPROP_LAST_CID: &str = "virtualizationservice.state.last_cid";
 
@@ -100,6 +101,19 @@
 
 const UNFORMATTED_STORAGE_MAGIC: &str = "UNFORMATTED-STORAGE";
 
+fn is_valid_guest_cid(cid: Cid) -> bool {
+    (GUEST_CID_MIN..=GUEST_CID_MAX).contains(&cid)
+}
+
+fn next_guest_cid(cid: Cid) -> Cid {
+    assert!(is_valid_guest_cid(cid));
+    if cid == GUEST_CID_MAX {
+        GUEST_CID_MIN
+    } else {
+        cid + 1
+    }
+}
+
 /// Singleton service for allocating globally-unique VM resources, such as the CID, and running
 /// singleton servers, like tombstone receiver.
 #[derive(Debug, Default)]
@@ -136,25 +150,65 @@
 /// The mutable state of the VirtualizationServiceInternal. There should only be one instance
 /// of this struct.
 #[derive(Debug, Default)]
-struct GlobalState {}
+struct GlobalState {
+    /// CIDs currently allocated to running VMs. A CID is never recycled as long
+    /// as there is a strong reference held by a GlobalVmContext.
+    held_cids: HashMap<Cid, Weak<Cid>>,
+}
 
 impl GlobalState {
     /// Get the next available CID, or an error if we have run out. The last CID used is stored in
     /// a system property so that restart of virtualizationservice doesn't reuse CID while the host
     /// Android is up.
-    fn allocate_cid(&mut self) -> Result<Cid> {
-        let cid = match system_properties::read(SYSPROP_LAST_CID)? {
-            Some(val) => match val.parse::<Cid>() {
-                Ok(num) => num.checked_add(1).ok_or_else(|| anyhow!("ran out of CIDs"))?,
+    fn allocate_cid(&mut self) -> Result<Arc<Cid>> {
+        // Garbage collect unused CIDs.
+        self.held_cids.retain(|_, cid| cid.strong_count() > 0);
+
+        // Start trying to find a CID from the last used CID + 1. This ensures
+        // that we do not eagerly recycle CIDs. It makes debugging easier but
+        // also means that retrying to allocate a CID, eg. because it is
+        // erroneously occupied by a process, will not recycle the same CID.
+        let last_cid_prop =
+            system_properties::read(SYSPROP_LAST_CID)?.and_then(|val| match val.parse::<Cid>() {
+                Ok(num) => {
+                    if is_valid_guest_cid(num) {
+                        Some(num)
+                    } else {
+                        error!("Invalid value '{}' of property '{}'", num, SYSPROP_LAST_CID);
+                        None
+                    }
+                }
                 Err(_) => {
                     error!("Invalid value '{}' of property '{}'", val, SYSPROP_LAST_CID);
-                    FIRST_GUEST_CID
+                    None
                 }
-            },
-            None => FIRST_GUEST_CID,
+            });
+
+        let first_cid = if let Some(last_cid) = last_cid_prop {
+            next_guest_cid(last_cid)
+        } else {
+            GUEST_CID_MIN
         };
-        system_properties::write(SYSPROP_LAST_CID, &format!("{}", cid))?;
-        Ok(cid)
+
+        let cid = self
+            .find_available_cid(first_cid..=GUEST_CID_MAX)
+            .or_else(|| self.find_available_cid(GUEST_CID_MIN..first_cid));
+
+        if let Some(cid) = cid {
+            let cid_arc = Arc::new(cid);
+            self.held_cids.insert(cid, Arc::downgrade(&cid_arc));
+            system_properties::write(SYSPROP_LAST_CID, &format!("{}", cid))?;
+            Ok(cid_arc)
+        } else {
+            Err(anyhow!("Could not find an available CID."))
+        }
+    }
+
+    fn find_available_cid<I>(&self, mut range: I) -> Option<Cid>
+    where
+        I: Iterator<Item = Cid>,
+    {
+        range.find(|cid| !self.held_cids.contains_key(cid))
     }
 }
 
@@ -162,14 +216,14 @@
 #[derive(Debug, Default)]
 struct GlobalVmContext {
     /// The unique CID assigned to the VM for vsock communication.
-    cid: Cid,
-    /// Keeps our service process running as long as this VM instance exists.
+    cid: Arc<Cid>,
+    /// Keeps our service process running as long as this VM context exists.
     #[allow(dead_code)]
     lazy_service_guard: LazyServiceGuard,
 }
 
 impl GlobalVmContext {
-    fn create(cid: Cid) -> Strong<dyn IGlobalVmContext> {
+    fn create(cid: Arc<Cid>) -> Strong<dyn IGlobalVmContext> {
         let binder = GlobalVmContext { cid, ..Default::default() };
         BnGlobalVmContext::new_binder(binder, BinderFeatures::default())
     }
@@ -179,7 +233,7 @@
 
 impl IGlobalVmContext for GlobalVmContext {
     fn getCid(&self) -> binder::Result<i32> {
-        Ok(self.cid as i32)
+        Ok(*self.cid as i32)
     }
 }
 
@@ -341,8 +395,10 @@
 }
 
 fn handle_stream_connection_tombstoned() -> Result<()> {
+    // Should not listen for tombstones on a guest VM's port.
+    assert!(!is_valid_guest_cid(VM_TOMBSTONES_SERVICE_PORT as Cid));
     let listener =
-        VsockListener::bind_with_cid_port(VMADDR_CID_HOST, VM_TOMBSTONES_SERVICE_PORT as u32)?;
+        VsockListener::bind_with_cid_port(VMADDR_CID_HOST, VM_TOMBSTONES_SERVICE_PORT as Cid)?;
     for incoming_stream in listener.incoming() {
         let mut incoming_stream = match incoming_stream {
             Err(e) => {
@@ -394,22 +450,31 @@
         let global_service =
             BnVirtualizationServiceInternal::new_binder(global_service, BinderFeatures::default());
 
-        let service = VirtualizationService { global_service, state: Default::default() };
+        VirtualizationService { global_service, state: Default::default() }
+    }
 
-        // binder server for vm
-        // reference to state (not the state itself) is copied
-        let state = service.state.clone();
-        std::thread::spawn(move || {
-            debug!("VirtualMachineService is starting as an RPC service.");
-            if run_vsock_rpc_server_with_factory(VM_BINDER_SERVICE_PORT as u32, |cid| {
-                VirtualMachineService::factory(cid, &state)
-            }) {
-                debug!("RPC server has shut down gracefully");
-            } else {
-                panic!("Premature termination of RPC server");
+    fn create_vm_context(&self) -> Result<(VmContext, Cid)> {
+        const NUM_ATTEMPTS: usize = 5;
+
+        for _ in 0..NUM_ATTEMPTS {
+            let global_context = self.global_service.allocateGlobalVmContext()?;
+            let cid = global_context.getCid()? as Cid;
+            let service = VirtualMachineService::new_binder(self.state.clone(), cid).as_binder();
+
+            // Start VM service listening for connections from the new CID on port=CID.
+            // TODO(b/245727626): Only accept connections from the new VM.
+            let port = cid;
+            match RpcServer::new_vsock(service, port) {
+                Ok(vm_server) => {
+                    vm_server.start();
+                    return Ok((VmContext::new(global_context, vm_server), cid));
+                }
+                Err(err) => {
+                    warn!("Could not start RpcServer on port {}: {}", port, err);
+                }
             }
-        });
-        service
+        }
+        bail!("Too many attempts to create VM context failed.");
     }
 
     fn create_vm_internal(
@@ -435,8 +500,13 @@
             check_use_custom_virtual_machine()?;
         }
 
-        let vm_context = self.global_service.allocateGlobalVmContext()?;
-        let cid = vm_context.getCid()? as Cid;
+        let (vm_context, cid) = self.create_vm_context().map_err(|e| {
+            error!("Failed to create VmContext: {:?}", e);
+            Status::new_service_specific_error_str(
+                -1,
+                Some(format!("Failed to create VmContext: {:?}", e)),
+            )
+        })?;
 
         let state = &mut *self.state.lock().unwrap();
         let console_fd = console_fd.map(clone_file).transpose()?;
@@ -1188,17 +1258,6 @@
 }
 
 impl VirtualMachineService {
-    fn factory(cid: Cid, state: &Arc<Mutex<State>>) -> Option<SpIBinder> {
-        if let Some(vm) = state.lock().unwrap().get_vm(cid) {
-            let mut vm_service = vm.vm_service.lock().unwrap();
-            let service = vm_service.get_or_insert_with(|| Self::new_binder(state.clone(), cid));
-            Some(service.as_binder())
-        } else {
-            error!("connection from cid={} is not from a guest VM", cid);
-            None
-        }
-    }
-
     fn new_binder(state: Arc<Mutex<State>>, cid: Cid) -> Strong<dyn IVirtualMachineService> {
         BnVirtualMachineService::new_binder(
             VirtualMachineService { state, cid },
diff --git a/virtualizationservice/src/crosvm.rs b/virtualizationservice/src/crosvm.rs
index 2d31fac..85a57c9 100644
--- a/virtualizationservice/src/crosvm.rs
+++ b/virtualizationservice/src/crosvm.rs
@@ -43,6 +43,7 @@
 use binder::Strong;
 use android_system_virtualmachineservice::aidl::android::system::virtualmachineservice::IVirtualMachineService::IVirtualMachineService;
 use tombstoned_client::{TombstonedConnection, DebuggerdDumpType};
+use rpcbinder::RpcServer;
 
 /// external/crosvm
 use base::UnixSeqpacketListener;
@@ -199,14 +200,30 @@
     }
 }
 
+/// Internal struct that holds the handles to globally unique resources of a VM.
+#[derive(Debug)]
+pub struct VmContext {
+    #[allow(dead_code)] // Keeps the global context alive
+    global_context: Strong<dyn IGlobalVmContext>,
+    #[allow(dead_code)] // Keeps the server alive
+    vm_server: RpcServer,
+}
+
+impl VmContext {
+    /// Construct new VmContext.
+    pub fn new(global_context: Strong<dyn IGlobalVmContext>, vm_server: RpcServer) -> VmContext {
+        VmContext { global_context, vm_server }
+    }
+}
+
 /// Information about a particular instance of a VM which may be running.
 #[derive(Debug)]
 pub struct VmInstance {
     /// The current state of the VM.
     pub vm_state: Mutex<VmState>,
-    /// Handle to global resources allocated for this VM.
-    #[allow(dead_code)] // The handle is never read, we only need to hold it.
-    vm_context: Strong<dyn IGlobalVmContext>,
+    /// Global resources allocated for this VM.
+    #[allow(dead_code)] // Keeps the context alive
+    vm_context: VmContext,
     /// The CID assigned to the VM for vsock communication.
     pub cid: Cid,
     /// The name of the VM.
@@ -239,7 +256,7 @@
         temporary_directory: PathBuf,
         requester_uid: u32,
         requester_debug_pid: i32,
-        vm_context: Strong<dyn IGlobalVmContext>,
+        vm_context: VmContext,
     ) -> Result<VmInstance, Error> {
         validate_config(&config)?;
         let cid = config.cid;
diff --git a/microdroid/vm_payload/Android.bp b/vm_payload/Android.bp
similarity index 98%
rename from microdroid/vm_payload/Android.bp
rename to vm_payload/Android.bp
index dd2a937..6be6f22 100644
--- a/microdroid/vm_payload/Android.bp
+++ b/vm_payload/Android.bp
@@ -17,6 +17,7 @@
         "liblibc",
         "liblog_rust",
         "librpcbinder_rs",
+        "libvsock",
     ],
     apex_available: [
         "com.android.compos",
diff --git a/microdroid/vm_payload/include-restricted/vm_payload_restricted.h b/vm_payload/include-restricted/vm_payload_restricted.h
similarity index 78%
rename from microdroid/vm_payload/include-restricted/vm_payload_restricted.h
rename to vm_payload/include-restricted/vm_payload_restricted.h
index 8170a64..0b78541 100644
--- a/microdroid/vm_payload/include-restricted/vm_payload_restricted.h
+++ b/vm_payload/include-restricted/vm_payload_restricted.h
@@ -36,21 +36,19 @@
  *
  * \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.
+ * \return the total size of the chain
  */
-bool AVmPayload_getDiceAttestationChain(void *data, size_t size, size_t *total);
+size_t AVmPayload_getDiceAttestationChain(void *data, size_t size);
 
 /**
  * Get the VM's DICE attestation CDI.
  *
  * \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.
+ * \return the total size of the CDI
  */
-bool AVmPayload_getDiceAttestationCdi(void *data, size_t size, size_t *total);
+size_t AVmPayload_getDiceAttestationCdi(void *data, size_t size);
 
 __END_DECLS
diff --git a/microdroid/vm_payload/include/vm_main.h b/vm_payload/include/vm_main.h
similarity index 100%
rename from microdroid/vm_payload/include/vm_main.h
rename to vm_payload/include/vm_main.h
diff --git a/microdroid/vm_payload/include/vm_payload.h b/vm_payload/include/vm_payload.h
similarity index 85%
rename from microdroid/vm_payload/include/vm_payload.h
rename to vm_payload/include/vm_payload.h
index 48518ff..0ad4c64 100644
--- a/microdroid/vm_payload/include/vm_payload.h
+++ b/vm_payload/include/vm_payload.h
@@ -30,9 +30,13 @@
 /**
  * Notifies the host that the payload is ready.
  *
- * \return true if the notification succeeds else false.
+ * If the host app has set a `VirtualMachineCallback` for the VM, its
+ * `onPayloadReady` method will be called.
+ *
+ * Note that subsequent calls to this function after the first have no effect;
+ * `onPayloadReady` is never called more than once.
  */
-bool AVmPayload_notifyPayloadReady(void);
+void AVmPayload_notifyPayloadReady(void);
 
 /**
  * Runs a binder RPC server, serving the supplied binder service implementation on the given vsock
@@ -57,17 +61,15 @@
 
 /**
  * Get a secret that is uniquely bound to this VM instance. The secrets are
- * values up to 32 bytes long and the value associated with an identifier will
- * not change over the lifetime of the VM instance.
+ * 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.
+ * \param size number of bytes of the secret to get, <= 32.
  */
-bool AVmPayload_getVmInstanceSecret(const void *identifier, size_t identifier_size, void *secret,
+void AVmPayload_getVmInstanceSecret(const void *identifier, size_t identifier_size, void *secret,
                                     size_t size);
 
 /**
diff --git a/microdroid/vm_payload/src/vm_payload_service.rs b/vm_payload/src/api.rs
similarity index 76%
rename from microdroid/vm_payload/src/vm_payload_service.rs
rename to vm_payload/src/api.rs
index 874b7e1..febc2be 100644
--- a/microdroid/vm_payload/src/vm_payload_service.rs
+++ b/vm_payload/src/api.rs
@@ -16,15 +16,16 @@
 
 use android_system_virtualization_payload::aidl::android::system::virtualization::payload::IVmPayloadService::{
     IVmPayloadService, VM_PAYLOAD_SERVICE_SOCKET_NAME, VM_APK_CONTENTS_PATH};
-use anyhow::{Context, Result};
+use anyhow::{ensure, Context, Result};
 use binder::{Strong, unstable_api::{AIBinder, new_spibinder}};
 use lazy_static::lazy_static;
 use log::{error, info, Level};
 use rpcbinder::{get_unix_domain_rpc_interface, RpcServer};
 use std::ffi::CString;
+use std::fmt::Debug;
 use std::os::raw::{c_char, c_void};
 use std::ptr;
-use std::sync::Mutex;
+use std::sync::{Mutex, atomic::{AtomicBool, Ordering}};
 
 lazy_static! {
     static ref VM_APK_CONTENTS_PATH_C: CString =
@@ -32,6 +33,8 @@
     static ref PAYLOAD_CONNECTION: Mutex<Option<Strong<dyn IVmPayloadService>>> = Mutex::default();
 }
 
+static ALREADY_NOTIFIED: AtomicBool = AtomicBool::new(false);
+
 /// Return a connection to the payload service in Microdroid Manager. Uses the existing connection
 /// if there is one, otherwise attempts to create a new one.
 fn get_vm_payload_service() -> Result<Strong<dyn IVmPayloadService>> {
@@ -51,22 +54,32 @@
 /// Make sure our logging goes to logcat. It is harmless to call this more than once.
 fn initialize_logging() {
     android_logger::init_once(
-        android_logger::Config::default().with_tag("vm_payload").with_min_level(Level::Debug),
+        android_logger::Config::default().with_tag("vm_payload").with_min_level(Level::Info),
     );
 }
 
+/// In many cases clients can't do anything useful if API calls fail, and the failure
+/// generally indicates that the VM is exiting or otherwise doomed. So rather than
+/// returning a non-actionable error indication we just log the problem and abort
+/// the process.
+fn unwrap_or_abort<T, E: Debug>(result: Result<T, E>) -> T {
+    result.unwrap_or_else(|e| {
+        let msg = format!("{:?}", e);
+        error!("{msg}");
+        panic!("{msg}")
+    })
+}
+
 /// Notifies the host that the payload is ready.
-/// Returns true if the notification succeeds else false.
+/// Panics on failure.
 #[no_mangle]
-pub extern "C" fn AVmPayload_notifyPayloadReady() -> bool {
+pub extern "C" fn AVmPayload_notifyPayloadReady() {
     initialize_logging();
 
-    if let Err(e) = try_notify_payload_ready() {
-        error!("{:?}", e);
-        false
-    } else {
+    if !ALREADY_NOTIFIED.swap(true, Ordering::Relaxed) {
+        unwrap_or_abort(try_notify_payload_ready());
+
         info!("Notified host payload ready successfully");
-        true
     }
 }
 
@@ -125,6 +138,7 @@
 }
 
 /// Get a secret that is uniquely bound to this VM instance.
+/// Panics on failure.
 ///
 /// # Safety
 ///
@@ -133,68 +147,51 @@
 /// * `identifier` must be [valid] for reads of `identifier_size` bytes.
 /// * `secret` must be [valid] for writes of `size` bytes.
 ///
-/// [valid]: std::ptr#safety
+/// [valid]: ptr#safety
 #[no_mangle]
 pub unsafe extern "C" fn AVmPayload_getVmInstanceSecret(
     identifier: *const u8,
     identifier_size: usize,
     secret: *mut u8,
     size: usize,
-) -> bool {
+) {
     initialize_logging();
 
     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
-        }
-    }
+    let vm_secret = unwrap_or_abort(try_get_vm_instance_secret(identifier, size));
+    ptr::copy_nonoverlapping(vm_secret.as_ptr(), secret, size);
 }
 
 fn try_get_vm_instance_secret(identifier: &[u8], size: usize) -> Result<Vec<u8>> {
-    get_vm_payload_service()?
+    let vm_secret = get_vm_payload_service()?
         .getVmInstanceSecret(identifier, i32::try_from(size)?)
-        .context("Cannot get VM instance secret")
+        .context("Cannot get VM instance secret")?;
+    ensure!(
+        vm_secret.len() == size,
+        "Returned secret has {} bytes, expected {}",
+        vm_secret.len(),
+        size
+    );
+    Ok(vm_secret)
 }
 
 /// Get the VM's attestation chain.
-/// Returns true on success, else false.
+/// Panics on failure.
 ///
 /// # 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
+/// [valid]: ptr#safety
 #[no_mangle]
-pub unsafe extern "C" fn AVmPayload_getDiceAttestationChain(
-    data: *mut u8,
-    size: usize,
-    total: *mut usize,
-) -> bool {
+pub unsafe extern "C" fn AVmPayload_getDiceAttestationChain(data: *mut u8, size: usize) -> usize {
     initialize_logging();
 
-    match try_get_dice_attestation_chain() {
-        Err(e) => {
-            error!("{:?}", e);
-            false
-        }
-        Ok(chain) => {
-            total.write(chain.len());
-            std::ptr::copy_nonoverlapping(chain.as_ptr(), data, std::cmp::min(chain.len(), size));
-            true
-        }
-    }
+    let chain = unwrap_or_abort(try_get_dice_attestation_chain());
+    ptr::copy_nonoverlapping(chain.as_ptr(), data, std::cmp::min(chain.len(), size));
+    chain.len()
 }
 
 fn try_get_dice_attestation_chain() -> Result<Vec<u8>> {
@@ -202,35 +199,26 @@
 }
 
 /// Get the VM's attestation CDI.
-/// Returns true on success, else false.
+/// Panics on failure.
 ///
 /// # 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
+/// [valid]: ptr#safety
 #[no_mangle]
-pub unsafe extern "C" fn AVmPayload_getDiceAttestationCdi(
-    data: *mut u8,
-    size: usize,
-    total: *mut usize,
-) -> bool {
+pub unsafe extern "C" fn AVmPayload_getDiceAttestationCdi(data: *mut u8, size: usize) -> usize {
     initialize_logging();
 
-    match try_get_dice_attestation_cdi() {
-        Err(e) => {
-            error!("{:?}", e);
-            false
-        }
-        Ok(cdi) => {
-            total.write(cdi.len());
-            std::ptr::copy_nonoverlapping(cdi.as_ptr(), data, std::cmp::min(cdi.len(), size));
-            true
-        }
-    }
+    let cdi = unwrap_or_abort(try_get_dice_attestation_cdi());
+    ptr::copy_nonoverlapping(cdi.as_ptr(), data, std::cmp::min(cdi.len(), size));
+    cdi.len()
+}
+
+fn try_get_dice_attestation_cdi() -> Result<Vec<u8>> {
+    get_vm_payload_service()?.getDiceAttestationCdi().context("Cannot get attestation CDI")
 }
 
 /// Gets the path to the APK contents.
@@ -245,7 +233,3 @@
     // TODO(b/254454578): Return a real path if storage is present
     ptr::null()
 }
-
-fn try_get_dice_attestation_cdi() -> Result<Vec<u8>> {
-    get_vm_payload_service()?.getDiceAttestationCdi().context("Cannot get attestation CDI")
-}
diff --git a/microdroid/vm_payload/src/lib.rs b/vm_payload/src/lib.rs
similarity index 93%
rename from microdroid/vm_payload/src/lib.rs
rename to vm_payload/src/lib.rs
index be6cf93..5c3ee31 100644
--- a/microdroid/vm_payload/src/lib.rs
+++ b/vm_payload/src/lib.rs
@@ -14,9 +14,9 @@
 
 //! Library for payload to communicate with the Microdroid Manager.
 
-mod vm_payload_service;
+mod api;
 
-pub use vm_payload_service::{
+pub use api::{
     AVmPayload_getDiceAttestationCdi, AVmPayload_getDiceAttestationChain,
     AVmPayload_getVmInstanceSecret, AVmPayload_notifyPayloadReady,
 };