Merge "Set --lto-O0 to reduce binary size" into main
diff --git a/authfs/tests/common/src/java/com/android/fs/common/AuthFsTestRule.java b/authfs/tests/common/src/java/com/android/fs/common/AuthFsTestRule.java
index 9c0fd72..7c85797 100644
--- a/authfs/tests/common/src/java/com/android/fs/common/AuthFsTestRule.java
+++ b/authfs/tests/common/src/java/com/android/fs/common/AuthFsTestRule.java
@@ -52,7 +52,7 @@
     public static final String FUSE_SUPER_MAGIC_HEX = "65735546";
 
     /** VM config entry path in the test APK */
-    private static final String VM_CONFIG_PATH_IN_APK = "assets/microdroid/vm_config.json";
+    private static final String VM_CONFIG_PATH_IN_APK = "assets/vm_config.json";
 
     /** Test directory on Android where data are located */
     public static final String TEST_DIR = "/data/local/tmp/authfs";
diff --git a/compos/common/compos_client.rs b/compos/common/compos_client.rs
index ffdd0ea..d0ca026 100644
--- a/compos/common/compos_client.rs
+++ b/compos/common/compos_client.rs
@@ -24,10 +24,7 @@
 use android_system_virtualizationservice::aidl::android::system::virtualizationservice::{
     CpuTopology::CpuTopology,
     IVirtualizationService::IVirtualizationService,
-    VirtualMachineAppConfig::{
-        CustomConfig::CustomConfig, DebugLevel::DebugLevel, Payload::Payload,
-        VirtualMachineAppConfig,
-    },
+    VirtualMachineAppConfig::{DebugLevel::DebugLevel, Payload::Payload, VirtualMachineAppConfig},
     VirtualMachineConfig::VirtualMachineConfig,
 };
 use anyhow::{anyhow, bail, Context, Result};
@@ -125,14 +122,13 @@
             idsig: Some(idsig_fd),
             instanceId: instance_id,
             instanceImage: Some(instance_fd),
-            encryptedStorageImage: None,
             payload: Payload::ConfigPath(config_path),
             debugLevel: debug_level,
             extraIdsigs: extra_idsigs,
             protectedVm: protected_vm,
             memoryMib: parameters.memory_mib.unwrap_or(0), // 0 means use the default
             cpuTopology: cpu_topology,
-            customConfig: Some(CustomConfig { ..Default::default() }),
+            ..Default::default()
         });
 
         // Let logs go to logcat.
diff --git a/java/framework/api/test-current.txt b/java/framework/api/test-current.txt
index 3cd8e42..d20d543 100644
--- a/java/framework/api/test-current.txt
+++ b/java/framework/api/test-current.txt
@@ -9,9 +9,10 @@
 
   public final class VirtualMachineConfig {
     method @FlaggedApi("com.android.system.virtualmachine.flags.avf_v_test_apis") @NonNull public java.util.List<java.lang.String> getExtraApks();
-    method @FlaggedApi("com.android.system.virtualmachine.flags.avf_v_test_apis") @Nullable public String getOs();
+    method @FlaggedApi("com.android.system.virtualmachine.flags.avf_v_test_apis") @NonNull public String getOs();
     method @Nullable public String getPayloadConfigPath();
     method public boolean isVmConsoleInputSupported();
+    field @FlaggedApi("com.android.system.virtualmachine.flags.avf_v_test_apis") public static final String MICRODROID = "microdroid";
   }
 
   public static final class VirtualMachineConfig.Builder {
@@ -25,6 +26,8 @@
   public class VirtualMachineManager {
     method @FlaggedApi("com.android.system.virtualmachine.flags.avf_v_test_apis") @NonNull public java.util.List<java.lang.String> getSupportedOSList() throws android.system.virtualmachine.VirtualMachineException;
     method @FlaggedApi("com.android.system.virtualmachine.flags.avf_v_test_apis") @RequiresPermission(android.system.virtualmachine.VirtualMachine.MANAGE_VIRTUAL_MACHINE_PERMISSION) public boolean isFeatureEnabled(String) throws android.system.virtualmachine.VirtualMachineException;
+    method @FlaggedApi("com.android.system.virtualmachine.flags.avf_v_test_apis") @RequiresPermission(android.system.virtualmachine.VirtualMachine.MANAGE_VIRTUAL_MACHINE_PERMISSION) public boolean isRemoteAttestationSupported() throws android.system.virtualmachine.VirtualMachineException;
+    method @FlaggedApi("com.android.system.virtualmachine.flags.avf_v_test_apis") @RequiresPermission(android.system.virtualmachine.VirtualMachine.MANAGE_VIRTUAL_MACHINE_PERMISSION) public boolean isUpdatableVmSupported() throws android.system.virtualmachine.VirtualMachineException;
     field @FlaggedApi("com.android.system.virtualmachine.flags.avf_v_test_apis") public static final String FEATURE_DICE_CHANGES = "com.android.kvm.DICE_CHANGES";
     field @FlaggedApi("com.android.system.virtualmachine.flags.avf_v_test_apis") public static final String FEATURE_LLPVM_CHANGES = "com.android.kvm.LLPVM_CHANGES";
     field @FlaggedApi("com.android.system.virtualmachine.flags.avf_v_test_apis") public static final String FEATURE_MULTI_TENANT = "com.android.kvm.MULTI_TENANT";
diff --git a/java/framework/src/android/system/virtualmachine/VirtualMachine.java b/java/framework/src/android/system/virtualmachine/VirtualMachine.java
index a5c8062..f9ca2f2 100644
--- a/java/framework/src/android/system/virtualmachine/VirtualMachine.java
+++ b/java/framework/src/android/system/virtualmachine/VirtualMachine.java
@@ -71,6 +71,7 @@
 import android.system.virtualizationservice.MemoryTrimLevel;
 import android.system.virtualizationservice.PartitionType;
 import android.system.virtualizationservice.VirtualMachineAppConfig;
+import android.system.virtualizationservice.VirtualMachineRawConfig;
 import android.system.virtualizationservice.VirtualMachineState;
 import android.util.JsonReader;
 import android.util.Log;
@@ -810,6 +811,58 @@
         }
     }
 
+    private android.system.virtualizationservice.VirtualMachineConfig
+            createVirtualMachineConfigForRawFrom(VirtualMachineConfig vmConfig)
+                    throws IllegalStateException {
+        VirtualMachineRawConfig rawConfig = vmConfig.toVsRawConfig();
+        return android.system.virtualizationservice.VirtualMachineConfig.rawConfig(rawConfig);
+    }
+
+    private android.system.virtualizationservice.VirtualMachineConfig
+            createVirtualMachineConfigForAppFrom(
+                    VirtualMachineConfig vmConfig, IVirtualizationService service)
+                    throws RemoteException, IOException, VirtualMachineException {
+        VirtualMachineAppConfig appConfig = vmConfig.toVsConfig(mContext.getPackageManager());
+        appConfig.instanceImage = ParcelFileDescriptor.open(mInstanceFilePath, MODE_READ_WRITE);
+        appConfig.name = mName;
+        if (mInstanceIdPath != null) {
+            appConfig.instanceId = Files.readAllBytes(mInstanceIdPath.toPath());
+        } else {
+            // FEATURE_LLPVM_CHANGES is disabled, instance_id is not used.
+            appConfig.instanceId = new byte[64];
+        }
+        if (mEncryptedStoreFilePath != null) {
+            appConfig.encryptedStorageImage =
+                    ParcelFileDescriptor.open(mEncryptedStoreFilePath, MODE_READ_WRITE);
+        }
+
+        if (!vmConfig.getExtraApks().isEmpty()) {
+            // Extra APKs were specified directly, rather than via config file.
+            // We've already populated the file names for the extra APKs and IDSigs
+            // (via setupExtraApks). But we also need to open the APK files and add
+            // fds for them to the payload config.
+            // This isn't needed when the extra APKs are specified in a config file -
+            // then
+            // Virtualization Manager opens them itself.
+            List<ParcelFileDescriptor> extraApkFiles = new ArrayList<>(mExtraApks.size());
+            for (ExtraApkSpec extraApk : mExtraApks) {
+                try {
+                    extraApkFiles.add(ParcelFileDescriptor.open(extraApk.apk, MODE_READ_ONLY));
+                } catch (FileNotFoundException e) {
+                    throw new VirtualMachineException("Failed to open extra APK", e);
+                }
+            }
+            appConfig.payload.getPayloadConfig().extraApks = extraApkFiles;
+        }
+
+        try {
+            createIdSigsAndUpdateConfig(service, appConfig);
+        } catch (FileNotFoundException e) {
+            throw new VirtualMachineException("Failed to generate APK signature", e);
+        }
+        return android.system.virtualizationservice.VirtualMachineConfig.appConfig(appConfig);
+    }
+
     /**
      * 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
@@ -850,50 +903,10 @@
                 }
 
                 VirtualMachineConfig vmConfig = getConfig();
-                VirtualMachineAppConfig appConfig =
-                        vmConfig.toVsConfig(mContext.getPackageManager());
-                appConfig.instanceImage =
-                        ParcelFileDescriptor.open(mInstanceFilePath, MODE_READ_WRITE);
-                appConfig.name = mName;
-                if (mInstanceIdPath != null) {
-                    appConfig.instanceId = Files.readAllBytes(mInstanceIdPath.toPath());
-                } else {
-                    // FEATURE_LLPVM_CHANGES is disabled, instance_id is not used.
-                    appConfig.instanceId = new byte[64];
-                }
-                if (mEncryptedStoreFilePath != null) {
-                    appConfig.encryptedStorageImage =
-                            ParcelFileDescriptor.open(mEncryptedStoreFilePath, MODE_READ_WRITE);
-                }
-
-                if (!vmConfig.getExtraApks().isEmpty()) {
-                    // Extra APKs were specified directly, rather than via config file.
-                    // We've already populated the file names for the extra APKs and IDSigs
-                    // (via setupExtraApks). But we also need to open the APK files and add
-                    // fds for them to the payload config.
-                    // This isn't needed when the extra APKs are specified in a config file - then
-                    // Virtualization Manager opens them itself.
-                    List<ParcelFileDescriptor> extraApkFiles = new ArrayList<>(mExtraApks.size());
-                    for (ExtraApkSpec extraApk : mExtraApks) {
-                        try {
-                            extraApkFiles.add(
-                                    ParcelFileDescriptor.open(extraApk.apk, MODE_READ_ONLY));
-                        } catch (FileNotFoundException e) {
-                            throw new VirtualMachineException("Failed to open extra APK", e);
-                        }
-                    }
-                    appConfig.payload.getPayloadConfig().extraApks = extraApkFiles;
-                }
-
-                try {
-                    createIdSigsAndUpdateConfig(service, appConfig);
-                } catch (FileNotFoundException e) {
-                    throw new VirtualMachineException("Failed to generate APK signature", e);
-                }
-
                 android.system.virtualizationservice.VirtualMachineConfig vmConfigParcel =
-                        android.system.virtualizationservice.VirtualMachineConfig.appConfig(
-                                appConfig);
+                        vmConfig.getRawConfigPath() != null
+                                ? createVirtualMachineConfigForRawFrom(vmConfig)
+                                : createVirtualMachineConfigForAppFrom(vmConfig, service);
 
                 mVirtualMachine =
                         service.createVm(
diff --git a/java/framework/src/android/system/virtualmachine/VirtualMachineConfig.java b/java/framework/src/android/system/virtualmachine/VirtualMachineConfig.java
index 12aeac8..99c3d05 100644
--- a/java/framework/src/android/system/virtualmachine/VirtualMachineConfig.java
+++ b/java/framework/src/android/system/virtualmachine/VirtualMachineConfig.java
@@ -18,6 +18,7 @@
 
 import static android.os.ParcelFileDescriptor.AutoCloseInputStream;
 import static android.os.ParcelFileDescriptor.MODE_READ_ONLY;
+import static android.os.ParcelFileDescriptor.MODE_READ_WRITE;
 
 import static java.util.Objects.requireNonNull;
 
@@ -27,6 +28,7 @@
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.annotation.RequiresPermission;
+import android.annotation.StringDef;
 import android.annotation.SystemApi;
 import android.annotation.TestApi;
 import android.content.Context;
@@ -36,12 +38,19 @@
 import android.os.ParcelFileDescriptor;
 import android.os.PersistableBundle;
 import android.sysprop.HypervisorProperties;
+import android.system.virtualizationservice.DiskImage;
+import android.system.virtualizationservice.Partition;
 import android.system.virtualizationservice.VirtualMachineAppConfig;
 import android.system.virtualizationservice.VirtualMachinePayloadConfig;
+import android.system.virtualizationservice.VirtualMachineRawConfig;
 import android.util.Log;
 
 import com.android.system.virtualmachine.flags.Flags;
 
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+
 import java.io.File;
 import java.io.FileInputStream;
 import java.io.FileNotFoundException;
@@ -51,6 +60,8 @@
 import java.io.OutputStream;
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
+import java.nio.file.Files;
+import java.nio.file.Path;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collections;
@@ -77,6 +88,7 @@
     private static final String KEY_PACKAGENAME = "packageName";
     private static final String KEY_APKPATH = "apkPath";
     private static final String KEY_PAYLOADCONFIGPATH = "payloadConfigPath";
+    private static final String KEY_RAWCONFIGPATH = "rawConfigPath";
     private static final String KEY_PAYLOADBINARYNAME = "payloadBinaryPath";
     private static final String KEY_DEBUGLEVEL = "debugLevel";
     private static final String KEY_PROTECTED_VM = "protectedVm";
@@ -172,6 +184,9 @@
     /** Name of the payload binary file within the APK that will be executed within the VM. */
     @Nullable private final String mPayloadBinaryName;
 
+    /** Path within the raw config file to launch the VM. */
+    @Nullable private final String mRawConfigPath;
+
     /** The size of storage in bytes. 0 indicates that encryptedStorage is not required */
     private final long mEncryptedStorageBytes;
 
@@ -183,8 +198,25 @@
 
     @Nullable private final File mVendorDiskImage;
 
-    /** OS name of the VM using payload binaries. null if the VM uses a payload config file. */
-    @Nullable private final String mOs;
+    /** OS name of the VM using payload binaries. */
+    @NonNull @OsName private final String mOs;
+
+    @Retention(RetentionPolicy.SOURCE)
+    @StringDef(
+            prefix = "MICRODROID",
+            value = {MICRODROID})
+    private @interface OsName {}
+
+    /**
+     * OS name of microdroid using microdroid kernel.
+     *
+     * @see Builder#setOs
+     * @hide
+     */
+    @TestApi
+    @FlaggedApi(Flags.FLAG_AVF_V_TEST_APIS)
+    @OsName
+    public static final String MICRODROID = "microdroid";
 
     private VirtualMachineConfig(
             @Nullable String packageName,
@@ -192,6 +224,7 @@
             List<String> extraApks,
             @Nullable String payloadConfigPath,
             @Nullable String payloadBinaryName,
+            @Nullable String rawConfigPath,
             @DebugLevel int debugLevel,
             boolean protectedVm,
             long memoryBytes,
@@ -200,7 +233,7 @@
             boolean vmOutputCaptured,
             boolean vmConsoleInputSupported,
             @Nullable File vendorDiskImage,
-            @Nullable String os) {
+            @NonNull @OsName String os) {
         // This is only called from Builder.build(); the builder handles parameter validation.
         mPackageName = packageName;
         mApkPath = apkPath;
@@ -211,6 +244,7 @@
                                 Arrays.asList(extraApks.toArray(new String[0])));
         mPayloadConfigPath = payloadConfigPath;
         mPayloadBinaryName = payloadBinaryName;
+        mRawConfigPath = rawConfigPath;
         mDebugLevel = debugLevel;
         mProtectedVm = protectedVm;
         mMemoryBytes = memoryBytes;
@@ -272,10 +306,14 @@
         }
 
         String payloadConfigPath = b.getString(KEY_PAYLOADCONFIGPATH);
-        if (payloadConfigPath == null) {
-            builder.setPayloadBinaryName(b.getString(KEY_PAYLOADBINARYNAME));
-        } else {
+        String payloadBinaryName = b.getString(KEY_PAYLOADBINARYNAME);
+        String rawConfigPath = b.getString(KEY_RAWCONFIGPATH);
+        if (rawConfigPath != null) {
+            builder.setRawConfigPath(rawConfigPath);
+        } else if (payloadConfigPath != null) {
             builder.setPayloadConfigPath(payloadConfigPath);
+        } else {
+            builder.setPayloadBinaryName(payloadBinaryName);
         }
 
         @DebugLevel int debugLevel = b.getInt(KEY_DEBUGLEVEL);
@@ -301,10 +339,7 @@
             builder.setVendorDiskImage(new File(vendorDiskImagePath));
         }
 
-        String os = b.getString(KEY_OS);
-        if (os != null) {
-            builder.setOs(os);
-        }
+        builder.setOs(b.getString(KEY_OS));
 
         String[] extraApks = b.getStringArray(KEY_EXTRA_APKS);
         if (extraApks != null) {
@@ -337,6 +372,7 @@
         }
         b.putString(KEY_PAYLOADCONFIGPATH, mPayloadConfigPath);
         b.putString(KEY_PAYLOADBINARYNAME, mPayloadBinaryName);
+        b.putString(KEY_RAWCONFIGPATH, mRawConfigPath);
         b.putInt(KEY_DEBUGLEVEL, mDebugLevel);
         b.putBoolean(KEY_PROTECTED_VM, mProtectedVm);
         b.putInt(KEY_CPU_TOPOLOGY, mCpuTopology);
@@ -397,6 +433,16 @@
     }
 
     /**
+     * Returns the path within the raw config file to launch the VM.
+     *
+     * @hide
+     */
+    @Nullable
+    public String getRawConfigPath() {
+        return mRawConfigPath;
+    }
+
+    /**
      * Returns the name of the payload binary file, in the {@code lib/<ABI>} directory of the APK,
      * that will be executed within the VM.
      *
@@ -498,14 +544,15 @@
     }
 
     /**
-     * Returns the OS of the VM using a payload binary. Returns null if the VM uses payload config.
+     * Returns the OS of the VM.
      *
      * @see Builder#setOs
      * @hide
      */
     @TestApi
     @FlaggedApi(Flags.FLAG_AVF_V_TEST_APIS)
-    @Nullable
+    @NonNull
+    @OsName
     public String getOs() {
         return mOs;
     }
@@ -538,6 +585,99 @@
                 && Objects.equals(this.mExtraApks, other.mExtraApks);
     }
 
+    // Needs to sync with packages/modules/Virtualization/libs/vmconfig/src/lib.rs
+    VirtualMachineRawConfig toVsRawConfig() throws IllegalStateException {
+        VirtualMachineRawConfig config = new VirtualMachineRawConfig();
+
+        try {
+            String rawJson = new String(Files.readAllBytes(Path.of(mRawConfigPath)));
+            JSONObject json = new JSONObject(rawJson);
+            config.name = json.optString("name", "");
+            config.instanceId = new byte[64];
+            if (json.has("kernel")) {
+                config.kernel =
+                        ParcelFileDescriptor.open(
+                                new File(json.getString("kernel")), MODE_READ_ONLY);
+            }
+            if (json.has("initrd")) {
+                config.initrd =
+                        ParcelFileDescriptor.open(
+                                new File(json.getString("initrd")), MODE_READ_ONLY);
+            }
+            if (json.has("params")) {
+                config.params = json.getString("params");
+            }
+            if (json.has("bootloader")) {
+                config.bootloader =
+                        ParcelFileDescriptor.open(
+                                new File(json.getString("bootloader")), MODE_READ_ONLY);
+            }
+            if (json.has("disks")) {
+                JSONArray diskArr = json.getJSONArray("disks");
+                config.disks = new DiskImage[diskArr.length()];
+                for (int i = 0; i < diskArr.length(); i++) {
+                    config.disks[i] = new DiskImage();
+                    JSONObject item = diskArr.getJSONObject(i);
+                    config.disks[i].writable = item.optBoolean("writable", false);
+                    if (item.has("image")) {
+                        config.disks[i].image =
+                                ParcelFileDescriptor.open(
+                                        new File(item.getString("image")),
+                                        config.disks[i].writable
+                                                ? MODE_READ_WRITE
+                                                : MODE_READ_ONLY);
+                    }
+                    if (item.has("partition")) {
+                        JSONArray partitionArr = json.getJSONArray("partition");
+                        config.disks[i].partitions = new Partition[partitionArr.length()];
+                        for (int j = 0; j < partitionArr.length(); j++) {
+                            config.disks[i].partitions[j] = new Partition();
+                            JSONObject partitionItem = partitionArr.getJSONObject(j);
+                            config.disks[i].partitions[j].writable =
+                                    partitionItem.optBoolean("writable", false);
+                            config.disks[i].partitions[j].label = partitionItem.getString("label");
+                            config.disks[i].partitions[j].image =
+                                    ParcelFileDescriptor.open(
+                                            new File(partitionItem.getString("image")),
+                                            config.disks[i].partitions[j].writable
+                                                    ? MODE_READ_WRITE
+                                                    : MODE_READ_ONLY);
+                        }
+                    } else {
+                        config.disks[i].partitions = new Partition[0];
+                    }
+                }
+            } else {
+                config.disks = new DiskImage[0];
+            }
+            // The value which is set by setProtectedVm is used.
+            if (json.has("protected")) {
+                Log.d(TAG, "'protected' field is ignored, the value from setProtectedVm is used");
+            }
+            config.protectedVm = this.mProtectedVm;
+            config.memoryMib = json.optInt("memory_mib", 0);
+            if (json.optString("cpu_topology", "one_cpu").equals("match_host")) {
+                config.cpuTopology = CPU_TOPOLOGY_MATCH_HOST;
+            } else {
+                config.cpuTopology = CPU_TOPOLOGY_ONE_CPU;
+            }
+            config.platformVersion = json.getString("platform_version");
+            if (json.has("devices")) {
+                JSONArray arr = json.getJSONArray("devices");
+                config.devices = new String[arr.length()];
+                for (int i = 0; i < arr.length(); i++) {
+                    config.devices[i] = arr.getString(i);
+                }
+            } else {
+                config.devices = new String[0];
+            }
+
+        } catch (JSONException | IOException e) {
+            throw new IllegalStateException("malformed input", e);
+        }
+        return config;
+    }
+
     /**
      * Converts this config object into the parcelable type used when creating a VM via the
      * virtualization service. Notice that the files are not passed as paths, but as file
@@ -559,7 +699,6 @@
         if (mPayloadBinaryName != null) {
             VirtualMachinePayloadConfig payloadConfig = new VirtualMachinePayloadConfig();
             payloadConfig.payloadBinaryName = mPayloadBinaryName;
-            payloadConfig.osName = mOs;
             payloadConfig.extraApks = Collections.emptyList();
             vsConfig.payload =
                     VirtualMachineAppConfig.Payload.payloadConfig(payloadConfig);
@@ -567,6 +706,7 @@
             vsConfig.payload =
                     VirtualMachineAppConfig.Payload.configPath(mPayloadConfigPath);
         }
+        vsConfig.osName = mOs;
         switch (mDebugLevel) {
             case DEBUG_LEVEL_FULL:
                 vsConfig.debugLevel = VirtualMachineAppConfig.DebugLevel.FULL;
@@ -658,12 +798,13 @@
      */
     @SystemApi
     public static final class Builder {
-        private final String DEFAULT_OS = "microdroid";
+        @OsName private final String DEFAULT_OS = MICRODROID;
 
         @Nullable private final String mPackageName;
         @Nullable private String mApkPath;
         private final List<String> mExtraApks = new ArrayList<>();
         @Nullable private String mPayloadConfigPath;
+        @Nullable private String mRawConfigPath;
         @Nullable private String mPayloadBinaryName;
         @DebugLevel private int mDebugLevel = DEBUG_LEVEL_NONE;
         private boolean mProtectedVm;
@@ -674,7 +815,7 @@
         private boolean mVmOutputCaptured = false;
         private boolean mVmConsoleInputSupported = false;
         @Nullable private File mVendorDiskImage;
-        @Nullable private String mOs;
+        @NonNull @OsName private String mOs = DEFAULT_OS;
 
         /**
          * Creates a builder for the given context.
@@ -713,16 +854,16 @@
                 // This should never happen, unless we're deserializing a bad config
                 throw new IllegalStateException("apkPath or packageName must be specified");
             }
-
-            String os = null;
-            if (mPayloadBinaryName == null) {
+            if (mRawConfigPath != null) {
+                if (mPayloadBinaryName != null || mPayloadConfigPath != null) {
+                    throw new IllegalStateException(
+                            "setRawConfigPath and (setPayloadBinaryName or setPayloadConfigPath)"
+                                    + " may not both be called");
+                }
+            } else if (mPayloadBinaryName == null) {
                 if (mPayloadConfigPath == null) {
                     throw new IllegalStateException("setPayloadBinaryName must be called");
                 }
-                if (mOs != null) {
-                    throw new IllegalStateException(
-                            "setPayloadConfigPath and setOs may not both be called");
-                }
                 if (!mExtraApks.isEmpty()) {
                     throw new IllegalStateException(
                             "setPayloadConfigPath and addExtraApk may not both be called");
@@ -732,11 +873,6 @@
                     throw new IllegalStateException(
                             "setPayloadBinaryName and setPayloadConfigPath may not both be called");
                 }
-                if (mOs != null) {
-                    os = mOs;
-                } else {
-                    os = DEFAULT_OS;
-                }
             }
 
             if (!mProtectedVmSet) {
@@ -757,6 +893,7 @@
                     mExtraApks,
                     mPayloadConfigPath,
                     mPayloadBinaryName,
+                    mRawConfigPath,
                     mDebugLevel,
                     mProtectedVm,
                     mMemoryBytes,
@@ -765,7 +902,7 @@
                     mVmOutputCaptured,
                     mVmConsoleInputSupported,
                     mVendorDiskImage,
-                    os);
+                    mOs);
         }
 
         /**
@@ -818,6 +955,29 @@
         }
 
         /**
+         * Sets the path within the raw config file to launch the VM. The file is a JSON file; see
+         * packages/modules/Virtualization/libs/vmconfig/src/lib.rs for the format.
+         *
+         * @hide
+         */
+        @RequiresPermission(VirtualMachine.USE_CUSTOM_VIRTUAL_MACHINE_PERMISSION)
+        @NonNull
+        public Builder setRawConfigPath(@NonNull String rawConfigPath) {
+            // TODO: This method will be removed when the builder support more structured methods
+            // for that like below.
+            //    builder
+            //      .setLinuxKernelConfig(new LinuxKernelConfig()
+            //        .setKernelPath(...)
+            //        .setInitRamdiskPath(...)
+            //        .setKernelCommandLine(...))
+            //      .setDiskConfig(new DiskConfig()
+            //        .addDisk(...)
+            //        .addDisk(...))
+            mRawConfigPath = requireNonNull(rawConfigPath, "rawConfigPath must not be null");
+            return this;
+        }
+
+        /**
          * Sets the name of the payload binary file that will be executed within the VM, e.g.
          * "payload.so". The file must reside in the {@code lib/<ABI>} directory of the APK.
          *
@@ -1025,7 +1185,7 @@
         @FlaggedApi(Flags.FLAG_AVF_V_TEST_APIS)
         @RequiresPermission(VirtualMachine.USE_CUSTOM_VIRTUAL_MACHINE_PERMISSION)
         @NonNull
-        public Builder setOs(@NonNull String os) {
+        public Builder setOs(@NonNull @OsName String os) {
             mOs = requireNonNull(os, "os must not be null");
             return this;
         }
diff --git a/java/framework/src/android/system/virtualmachine/VirtualMachineManager.java b/java/framework/src/android/system/virtualmachine/VirtualMachineManager.java
index 5020ff0..091d317 100644
--- a/java/framework/src/android/system/virtualmachine/VirtualMachineManager.java
+++ b/java/framework/src/android/system/virtualmachine/VirtualMachineManager.java
@@ -393,4 +393,45 @@
             }
         }
     }
+
+    /**
+     * Returns {@code true} if the pVM remote attestation feature is supported. Remote attestation
+     * allows a protected VM to attest its authenticity to a remote server.
+     *
+     * @hide
+     */
+    @TestApi
+    @FlaggedApi(Flags.FLAG_AVF_V_TEST_APIS)
+    @RequiresPermission(VirtualMachine.MANAGE_VIRTUAL_MACHINE_PERMISSION)
+    public boolean isRemoteAttestationSupported() throws VirtualMachineException {
+        synchronized (sCreateLock) {
+            VirtualizationService service = VirtualizationService.getInstance();
+            try {
+                return service.getBinder().isRemoteAttestationSupported();
+            } catch (RemoteException e) {
+                throw e.rethrowAsRuntimeException();
+            }
+        }
+    }
+
+    /**
+     * Returns {@code true} if Updatable VM feature is supported by AVF. Updatable VM allow secrets
+     * and data to be accessible even after updates of boot images and apks. For more info see
+     * packages/modules/Virtualization/docs/updatable_vm.md
+     *
+     * @hide
+     */
+    @TestApi
+    @FlaggedApi(Flags.FLAG_AVF_V_TEST_APIS)
+    @RequiresPermission(VirtualMachine.MANAGE_VIRTUAL_MACHINE_PERMISSION)
+    public boolean isUpdatableVmSupported() throws VirtualMachineException {
+        synchronized (sCreateLock) {
+            VirtualizationService service = VirtualizationService.getInstance();
+            try {
+                return service.getBinder().isUpdatableVmSupported();
+            } catch (RemoteException e) {
+                throw e.rethrowAsRuntimeException();
+            }
+        }
+    }
 }
diff --git a/libs/bssl/src/lib.rs b/libs/bssl/src/lib.rs
index ad51b61..686abf9 100644
--- a/libs/bssl/src/lib.rs
+++ b/libs/bssl/src/lib.rs
@@ -15,6 +15,7 @@
 //! Safe wrappers around the BoringSSL API.
 
 #![cfg_attr(not(feature = "std"), no_std)]
+#![warn(clippy::or_fun_call)]
 
 extern crate alloc;
 
diff --git a/libs/vmconfig/src/lib.rs b/libs/vmconfig/src/lib.rs
index 50f3c8e..907e0d3 100644
--- a/libs/vmconfig/src/lib.rs
+++ b/libs/vmconfig/src/lib.rs
@@ -15,6 +15,7 @@
 //! Struct for VM configuration with JSON (de)serialization and AIDL parcelables
 
 use android_system_virtualizationservice::{
+    aidl::android::system::virtualizationservice::CpuTopology::CpuTopology,
     aidl::android::system::virtualizationservice::DiskImage::DiskImage as AidlDiskImage,
     aidl::android::system::virtualizationservice::Partition::Partition as AidlPartition,
     aidl::android::system::virtualizationservice::VirtualMachineRawConfig::VirtualMachineRawConfig,
@@ -54,6 +55,8 @@
     /// The amount of RAM to give the VM, in MiB.
     #[serde(default)]
     pub memory_mib: Option<NonZeroU32>,
+    /// The CPU topology: either "one_cpu"(default) or "match_host"
+    pub cpu_topology: Option<String>,
     /// Version or range of versions of the virtual platform that this config is compatible with.
     /// The format follows SemVer (https://semver.org).
     pub platform_version: VersionReq,
@@ -96,7 +99,12 @@
         } else {
             0
         };
-
+        let cpu_topology = match self.cpu_topology.as_deref() {
+            None => CpuTopology::ONE_CPU,
+            Some("one_cpu") => CpuTopology::ONE_CPU,
+            Some("match_host") => CpuTopology::MATCH_HOST,
+            Some(cpu_topology) => bail!("Invalid cpu topology {}", cpu_topology),
+        };
         Ok(VirtualMachineRawConfig {
             kernel: maybe_open_parcel_file(&self.kernel, false)?,
             initrd: maybe_open_parcel_file(&self.initrd, false)?,
@@ -105,6 +113,7 @@
             disks: self.disks.iter().map(DiskImage::to_parcelable).collect::<Result<_, Error>>()?,
             protectedVm: self.protected,
             memoryMib: memory_mib,
+            cpuTopology: cpu_topology,
             platformVersion: self.platform_version.to_string(),
             devices: self
                 .devices
diff --git a/microdroid/payload/config/src/lib.rs b/microdroid/payload/config/src/lib.rs
index cdef3e4..d6f65bd 100644
--- a/microdroid/payload/config/src/lib.rs
+++ b/microdroid/payload/config/src/lib.rs
@@ -17,10 +17,12 @@
 use serde::{Deserialize, Serialize};
 
 /// VM payload config
-#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
+#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
 pub struct VmPayloadConfig {
-    /// OS config. Default: "microdroid"
+    /// OS config.
+    /// Deprecated: don't use. Error if not "" or "microdroid".
     #[serde(default)]
+    #[deprecated]
     pub os: OsConfig,
 
     /// Task to run in a VM
@@ -58,7 +60,7 @@
 
 impl Default for OsConfig {
     fn default() -> Self {
-        Self { name: "microdroid".to_owned() }
+        Self { name: "".to_owned() }
     }
 }
 
diff --git a/microdroid_manager/src/vm_secret.rs b/microdroid_manager/src/vm_secret.rs
index ed8ab1d..ec40b45 100644
--- a/microdroid_manager/src/vm_secret.rs
+++ b/microdroid_manager/src/vm_secret.rs
@@ -109,15 +109,14 @@
                 .ok_or(anyhow!("Missing explicit dice chain, this is unusual"))?;
             let policy = sealing_policy(explicit_dice_chain)
                 .map_err(|e| anyhow!("Failed to build a sealing_policy: {e}"))?;
-            if super::is_new_instance() {
-                // New instance -> create a secret & store in Secretkeeper.
-                *skp_secret = rand::random();
-                store_secret(&mut session, id, skp_secret.clone(), policy)
-                    .context("Failed to store secret in Secretkeeper")?;
+            if let Some(secret) = get_secret(&mut session, id, Some(policy.clone()))? {
+                *skp_secret = secret;
             } else {
-                // Subsequent run of the pVM -> get the secret stored in Secretkeeper.
-                *skp_secret = get_secret(&mut session, id, Some(policy))
-                    .context("Failed to get secret from Secretkeeper")?;
+                log::warn!(
+                    "No entry found in Secretkeeper for this VM instance, creating new secret."
+                );
+                *skp_secret = rand::random();
+                store_secret(&mut session, id, skp_secret.clone(), policy)?;
             }
         }
         Ok(Self::V2 {
@@ -248,21 +247,24 @@
     session: &mut SkSession,
     id: [u8; ID_SIZE],
     updated_sealing_policy: Option<Vec<u8>>,
-) -> Result<[u8; SECRET_SIZE]> {
+) -> Result<Option<[u8; SECRET_SIZE]>> {
     let get_request = GetSecretRequest { id: Id(id), updated_sealing_policy };
     log::info!("Secretkeeper operation: {:?}", get_request);
     let get_request = get_request.serialize_to_packet().to_vec().map_err(anyhow_err)?;
     let get_response = session.secret_management_request(&get_request)?;
     let get_response = ResponsePacket::from_slice(&get_response).map_err(anyhow_err)?;
     let response_type = get_response.response_type().map_err(anyhow_err)?;
-    ensure!(
-        response_type == ResponseType::Success,
-        "Secretkeeper get failed with error: {:?}",
-        *SecretkeeperError::deserialize_from_packet(get_response).map_err(anyhow_err)?
-    );
-    let get_response =
-        *GetSecretResponse::deserialize_from_packet(get_response).map_err(anyhow_err)?;
-    Ok(get_response.secret.0)
+    if response_type == ResponseType::Success {
+        let get_response =
+            *GetSecretResponse::deserialize_from_packet(get_response).map_err(anyhow_err)?;
+        Ok(Some(get_response.secret.0))
+    } else {
+        let error = SecretkeeperError::deserialize_from_packet(get_response).map_err(anyhow_err)?;
+        if *error == SecretkeeperError::EntryNotFound {
+            return Ok(None);
+        }
+        Err(anyhow!("Secretkeeper get failed: {error:?}"))
+    }
 }
 
 #[inline]
diff --git a/pvmfw/src/dice.rs b/pvmfw/src/dice.rs
index 540fd03..67865e5 100644
--- a/pvmfw/src/dice.rs
+++ b/pvmfw/src/dice.rs
@@ -93,7 +93,8 @@
             rkp_vm_marker: bool,
             salt: [u8; HIDDEN_SIZE],
         }
-
+        // TODO(b/291213394): Include `defer_rollback_protection` flag in the Hidden Input to
+        // differentiate the secrets in both cases.
         hash(HiddenInput { rkp_vm_marker: self.rkp_vm_marker, salt: *salt }.as_bytes())
     }
 
diff --git a/pvmfw/src/main.rs b/pvmfw/src/main.rs
index 12d63d5..2af19c4 100644
--- a/pvmfw/src/main.rs
+++ b/pvmfw/src/main.rs
@@ -42,10 +42,12 @@
 use crate::instance::{get_recorded_entry, record_instance_entry};
 use alloc::borrow::Cow;
 use alloc::boxed::Box;
+use bssl_avf::Digester;
 use core::ops::Range;
-use diced_open_dice::{bcc_handover_parse, DiceArtifacts};
+use cstr::cstr;
+use diced_open_dice::{bcc_handover_parse, DiceArtifacts, Hidden};
 use fdtpci::{PciError, PciInfo};
-use libfdt::Fdt;
+use libfdt::{Fdt, FdtNode};
 use log::{debug, error, info, trace, warn};
 use pvmfw_avb::verify_payload;
 use pvmfw_avb::Capability;
@@ -129,18 +131,6 @@
         }
     }
 
-    if verified_boot_data.has_capability(Capability::SecretkeeperProtection) {
-        info!("Guest OS is capable of Secretkeeper protection");
-        // For Secretkeeper based Antirollback protection, rollback_index of the image > 0
-        if verified_boot_data.rollback_index == 0 {
-            error!(
-                "Expected positive rollback_index, found {:?}",
-                verified_boot_data.rollback_index
-            );
-            return Err(RebootReason::InvalidPayload);
-        };
-    }
-
     let next_bcc = heap::aligned_boxed_slice(NEXT_BCC_SIZE, GUEST_PAGE_SIZE).ok_or_else(|| {
         error!("Failed to allocate the next-stage BCC");
         RebootReason::InternalError
@@ -153,43 +143,51 @@
         RebootReason::InternalError
     })?;
 
-    let (recorded_entry, mut instance_img, header_index) =
-        get_recorded_entry(&mut pci_root, cdi_seal).map_err(|e| {
-            error!("Failed to get entry from instance.img: {e}");
-            RebootReason::InternalError
-        })?;
-    let (new_instance, salt) = if let Some(entry) = recorded_entry {
-        // The RKP VM is allowed to run if it has passed the verified boot check and
-        // contains the expected version in its AVB footer.
-        // The comparison below with the previous boot information is skipped to enable the
-        // simultaneous update of the pvmfw and RKP VM.
-        // For instance, when both the pvmfw and RKP VM are updated, the code hash of the
-        // RKP VM will differ from the one stored in the instance image. In this case, the
-        // RKP VM is still allowed to run.
-        // This ensures that the updated RKP VM will retain the same CDIs in the next stage.
-        if !dice_inputs.rkp_vm_marker {
-            ensure_dice_measurements_match_entry(&dice_inputs, &entry).map_err(|e| {
-                error!(
-                    "Dice measurements do not match recorded entry.
-                This may be because of update: {e}"
-                );
+    let (new_instance, salt) = if cfg!(llpvm_changes)
+        && should_defer_rollback_protection(fdt)?
+        && verified_boot_data.has_capability(Capability::SecretkeeperProtection)
+    {
+        info!("Guest OS is capable of Secretkeeper protection, deferring rollback protection");
+        // rollback_index of the image is used as security_version and is expected to be > 0 to
+        // discourage implicit allocation.
+        if verified_boot_data.rollback_index == 0 {
+            error!("Expected positive rollback_index, found 0");
+            return Err(RebootReason::InvalidPayload);
+        };
+        // `new_instance` cannot be known to pvmfw
+        (false, salt_from_instance_id(fdt)?)
+    } else {
+        let (recorded_entry, mut instance_img, header_index) =
+            get_recorded_entry(&mut pci_root, cdi_seal).map_err(|e| {
+                error!("Failed to get entry from instance.img: {e}");
                 RebootReason::InternalError
             })?;
-        }
-        (false, entry.salt)
-    } else {
-        let salt = rand::random_array().map_err(|e| {
-            error!("Failed to generated instance.img salt: {e}");
-            RebootReason::InternalError
-        })?;
-        let entry = EntryBody::new(&dice_inputs, &salt);
-        record_instance_entry(&entry, cdi_seal, &mut instance_img, header_index).map_err(|e| {
-            error!("Failed to get recorded entry in instance.img: {e}");
-            RebootReason::InternalError
-        })?;
-        (true, salt)
+        let (new_instance, salt) = if let Some(entry) = recorded_entry {
+            maybe_check_dice_measurements_match_entry(&dice_inputs, &entry)?;
+            let salt = if cfg!(llpvm_changes) { salt_from_instance_id(fdt)? } else { entry.salt };
+            (false, salt)
+        } else {
+            // New instance!
+            let salt = if cfg!(llpvm_changes) {
+                salt_from_instance_id(fdt)?
+            } else {
+                rand::random_array().map_err(|e| {
+                    error!("Failed to generated instance.img salt: {e}");
+                    RebootReason::InternalError
+                })?
+            };
+            let entry = EntryBody::new(&dice_inputs, &salt);
+            record_instance_entry(&entry, cdi_seal, &mut instance_img, header_index).map_err(
+                |e| {
+                    error!("Failed to get recorded entry in instance.img: {e}");
+                    RebootReason::InternalError
+                },
+            )?;
+            (true, salt)
+        };
+        (new_instance, salt)
     };
-    trace!("Got salt from instance.img: {salt:x?}");
+    trace!("Got salt for instance: {salt:x?}");
 
     let new_bcc_handover = if cfg!(dice_changes) {
         Cow::Borrowed(current_bcc_handover)
@@ -241,6 +239,32 @@
     Ok(bcc_range)
 }
 
+fn maybe_check_dice_measurements_match_entry(
+    dice_inputs: &PartialInputs,
+    entry: &EntryBody,
+) -> Result<(), RebootReason> {
+    // The RKP VM is allowed to run if it has passed the verified boot check and
+    // contains the expected version in its AVB footer.
+    // The comparison below with the previous boot information is skipped to enable the
+    // simultaneous update of the pvmfw and RKP VM.
+    // For instance, when both the pvmfw and RKP VM are updated, the code hash of the
+    // RKP VM will differ from the one stored in the instance image. In this case, the
+    // RKP VM is still allowed to run.
+    // This ensures that the updated RKP VM will retain the same CDIs in the next stage.
+    if dice_inputs.rkp_vm_marker {
+        return Ok(());
+    }
+    ensure_dice_measurements_match_entry(dice_inputs, entry).map_err(|e| {
+        error!(
+            "Dice measurements do not match recorded entry. \
+        This may be because of update: {e}"
+        );
+        RebootReason::InternalError
+    })?;
+
+    Ok(())
+}
+
 fn ensure_dice_measurements_match_entry(
     dice_inputs: &PartialInputs,
     entry: &EntryBody,
@@ -256,6 +280,56 @@
     }
 }
 
+// Get the "salt" which is one of the input for DICE derivation.
+// This provides differentiation of secrets for different VM instances with same payloads.
+fn salt_from_instance_id(fdt: &Fdt) -> Result<Hidden, RebootReason> {
+    let id = instance_id(fdt)?;
+    let salt = Digester::sha512()
+        .digest(&[&b"InstanceId:"[..], id].concat())
+        .map_err(|e| {
+            error!("Failed to get digest of instance-id: {e}");
+            RebootReason::InternalError
+        })?
+        .try_into()
+        .map_err(|_| RebootReason::InternalError)?;
+    Ok(salt)
+}
+
+fn instance_id(fdt: &Fdt) -> Result<&[u8], RebootReason> {
+    let node = avf_untrusted_node(fdt)?;
+    let id = node.getprop(cstr!("instance-id")).map_err(|e| {
+        error!("Failed to get instance-id in DT: {e}");
+        RebootReason::InvalidFdt
+    })?;
+    id.ok_or_else(|| {
+        error!("Missing instance-id");
+        RebootReason::InvalidFdt
+    })
+}
+
+fn should_defer_rollback_protection(fdt: &Fdt) -> Result<bool, RebootReason> {
+    let node = avf_untrusted_node(fdt)?;
+    let defer_rbp = node
+        .getprop(cstr!("defer-rollback-protection"))
+        .map_err(|e| {
+            error!("Failed to get defer-rollback-protection property in DT: {e}");
+            RebootReason::InvalidFdt
+        })?
+        .is_some();
+    Ok(defer_rbp)
+}
+
+fn avf_untrusted_node(fdt: &Fdt) -> Result<FdtNode, RebootReason> {
+    let node = fdt.node(cstr!("/avf/untrusted")).map_err(|e| {
+        error!("Failed to get /avf/untrusted node: {e}");
+        RebootReason::InvalidFdt
+    })?;
+    node.ok_or_else(|| {
+        error!("/avf/untrusted node is missing in DT");
+        RebootReason::InvalidFdt
+    })
+}
+
 /// Logs the given PCI error and returns the appropriate `RebootReason`.
 fn handle_pci_error(e: PciError) -> RebootReason {
     error!("{}", e);
diff --git a/rialto/tests/test.rs b/rialto/tests/test.rs
index 8899875..9151ce1 100644
--- a/rialto/tests/test.rs
+++ b/rialto/tests/test.rs
@@ -22,7 +22,7 @@
     binder::{ParcelFileDescriptor, ProcessState},
 };
 use anyhow::{bail, Context, Result};
-use bssl_avf::{sha256, EcKey, PKey};
+use bssl_avf::{rand_bytes, sha256, EcKey, PKey};
 use client_vm_csr::generate_attestation_key_and_csr;
 use coset::{CborSerializable, CoseMac0, CoseSign};
 use hwtrust::{rkp, session::Session};
@@ -52,8 +52,12 @@
 const INSTANCE_IMG_PATH: &str = "/data/local/tmp/rialto_test/arm64/instance.img";
 const TEST_CERT_CHAIN_PATH: &str = "testdata/rkp_cert_chain.der";
 
+#[cfg(dice_changes)]
 #[test]
 fn process_requests_in_protected_vm() -> Result<()> {
+    // The test is skipped if the feature flag |dice_changes| is not enabled, because when
+    // the flag is off, the DICE chain is truncated in the pvmfw, and the service VM cannot
+    // verify the chain due to the missing entries in the chain.
     check_processing_requests(VmType::ProtectedVm)
 }
 
@@ -302,12 +306,18 @@
 
 fn nonprotected_vm_instance() -> Result<VmInstance> {
     let rialto = File::open(UNSIGNED_RIALTO_PATH).context("Failed to open Rialto kernel binary")?;
+    // Do not use `#allocateInstanceId` to generate the instance ID because the method
+    // also adds an instance ID to the database it manages.
+    // This is not necessary for this test.
+    let mut instance_id = [0u8; 64];
+    rand_bytes(&mut instance_id).unwrap();
     let config = VirtualMachineConfig::RawConfig(VirtualMachineRawConfig {
         name: String::from("Non protected rialto"),
         bootloader: Some(ParcelFileDescriptor::new(rialto)),
         protectedVm: false,
         memoryMib: 300,
         platformVersion: "~1.0".to_string(),
+        instanceId: instance_id,
         ..Default::default()
     });
     let console = Some(service_vm_manager::android_log_fd()?);
diff --git a/service_vm/manager/src/lib.rs b/service_vm/manager/src/lib.rs
index 8dedec5..c50f0b3 100644
--- a/service_vm/manager/src/lib.rs
+++ b/service_vm/manager/src/lib.rs
@@ -28,11 +28,11 @@
 use lazy_static::lazy_static;
 use log::{info, warn};
 use service_vm_comm::{Request, Response, ServiceVmRequest, VmType};
-use std::fs::{File, OpenOptions};
-use std::io::{self, BufRead, BufReader, BufWriter, Write};
+use std::fs::{self, File, OpenOptions};
+use std::io::{self, BufRead, BufReader, BufWriter, Read, Write};
 use std::os::unix::io::FromRawFd;
 use std::path::{Path, PathBuf};
-use std::sync::{Condvar, Mutex, MutexGuard};
+use std::sync::{Condvar, Mutex};
 use std::thread;
 use std::time::Duration;
 use vmclient::{DeathReason, VmInstance};
@@ -41,47 +41,85 @@
 const VIRT_DATA_DIR: &str = "/data/misc/apexdata/com.android.virt";
 const RIALTO_PATH: &str = "/apex/com.android.virt/etc/rialto.bin";
 const INSTANCE_IMG_NAME: &str = "service_vm_instance.img";
+const INSTANCE_ID_FILENAME: &str = "service_vm_instance_id";
 const INSTANCE_IMG_SIZE_BYTES: i64 = 1 << 20; // 1MB
 const MEMORY_MB: i32 = 300;
 const WRITE_BUFFER_CAPACITY: usize = 512;
 const READ_TIMEOUT: Duration = Duration::from_secs(10);
 const WRITE_TIMEOUT: Duration = Duration::from_secs(10);
-
 lazy_static! {
-    static ref SERVICE_VM_STATE: State = State::default();
+    static ref PENDING_REQUESTS: AtomicCounter = AtomicCounter::default();
+    static ref SERVICE_VM: Mutex<Option<ServiceVm>> = Mutex::new(None);
+    static ref SERVICE_VM_SHUTDOWN: Condvar = Condvar::new();
 }
 
-/// The running state of the Service VM.
+/// Atomic counter with a condition variable that is used to wait for the counter
+/// to become positive within a timeout.
 #[derive(Debug, Default)]
-struct State {
-    is_running: Mutex<bool>,
-    stopped: Condvar,
+struct AtomicCounter {
+    num: Mutex<usize>,
+    num_increased: Condvar,
 }
 
-impl State {
-    fn wait_until_no_service_vm_running(&self) -> Result<MutexGuard<'_, bool>> {
-        // The real timeout can be longer than 10 seconds since the time to acquire
-        // is_running mutex is not counted in the 10 seconds.
-        let (guard, wait_result) = self
-            .stopped
-            .wait_timeout_while(
-                self.is_running.lock().unwrap(),
-                Duration::from_secs(10),
-                |&mut is_running| is_running,
-            )
+impl AtomicCounter {
+    /// Checks if the counter becomes positive within the given timeout.
+    fn is_positive_within_timeout(&self, timeout: Duration) -> bool {
+        let (guard, _wait_result) = self
+            .num_increased
+            .wait_timeout_while(self.num.lock().unwrap(), timeout, |&mut x| x == 0)
             .unwrap();
-        ensure!(
-            !wait_result.timed_out(),
-            "Timed out while waiting for the running service VM to stop."
-        );
-        Ok(guard)
+        *guard > 0
     }
 
-    fn notify_service_vm_shutdown(&self) {
-        let mut is_running_guard = self.is_running.lock().unwrap();
-        *is_running_guard = false;
-        self.stopped.notify_one();
+    fn increment(&self) {
+        let mut num = self.num.lock().unwrap();
+        *num = num.checked_add(1).unwrap();
+        self.num_increased.notify_all();
     }
+
+    fn decrement(&self) {
+        let mut num = self.num.lock().unwrap();
+        *num = num.checked_sub(1).unwrap();
+    }
+}
+
+/// Processes the request in the service VM.
+pub fn process_request(request: Request) -> Result<Response> {
+    PENDING_REQUESTS.increment();
+    let result = process_request_in_service_vm(request);
+    PENDING_REQUESTS.decrement();
+    thread::spawn(stop_service_vm_if_idle);
+    result
+}
+
+fn process_request_in_service_vm(request: Request) -> Result<Response> {
+    let mut service_vm = SERVICE_VM.lock().unwrap();
+    if service_vm.is_none() {
+        *service_vm = Some(ServiceVm::start()?);
+    }
+    service_vm.as_mut().unwrap().process_request(request)
+}
+
+fn stop_service_vm_if_idle() {
+    if PENDING_REQUESTS.is_positive_within_timeout(Duration::from_secs(1)) {
+        info!("Service VM has pending requests, keeping it running.");
+    } else {
+        info!("Service VM is idle, shutting it down.");
+        *SERVICE_VM.lock().unwrap() = None;
+        SERVICE_VM_SHUTDOWN.notify_all();
+    }
+}
+
+/// Waits until the service VM shuts down.
+/// This function is only used in tests.
+pub fn wait_until_service_vm_shuts_down() -> Result<()> {
+    const WAIT_FOR_SHUTDOWN_TIMEOUT: Duration = Duration::from_secs(5);
+
+    let (_guard, wait_result) = SERVICE_VM_SHUTDOWN
+        .wait_timeout_while(SERVICE_VM.lock().unwrap(), WAIT_FOR_SHUTDOWN_TIMEOUT, |x| x.is_some())
+        .unwrap();
+    ensure!(!wait_result.timed_out(), "Service VM didn't shut down within the timeout");
+    Ok(())
 }
 
 /// Service VM.
@@ -94,17 +132,12 @@
 impl ServiceVm {
     /// Starts the service VM and returns its instance.
     /// The same instance image is used for different VMs.
-    /// At any given time,  only one service should be running. If a service VM is
-    /// already running, this function will start the service VM once the running one
-    /// shuts down.
+    /// TODO(b/27593612): Remove instance image usage for Service VM.
     pub fn start() -> Result<Self> {
-        let mut is_running_guard = SERVICE_VM_STATE.wait_until_no_service_vm_running()?;
-
         let instance_img_path = Path::new(VIRT_DATA_DIR).join(INSTANCE_IMG_NAME);
         let vm = protected_vm_instance(instance_img_path)?;
 
         let vm = Self::start_vm(vm, VmType::ProtectedVm)?;
-        *is_running_guard = true;
         Ok(vm)
     }
 
@@ -174,7 +207,6 @@
             Ok(reason) => info!("Exit the service VM successfully: {reason:?}"),
             Err(e) => warn!("Service VM shutdown request failed '{e:?}', killing it."),
         }
-        SERVICE_VM_STATE.notify_service_vm_shutdown();
     }
 }
 
@@ -191,10 +223,13 @@
         writable: true,
     }];
     let rialto = File::open(RIALTO_PATH).context("Failed to open Rialto kernel binary")?;
+    let instance_id_file = Path::new(VIRT_DATA_DIR).join(INSTANCE_ID_FILENAME);
+    let instance_id = get_or_allocate_instance_id(service.as_ref(), instance_id_file)?;
     let config = VirtualMachineConfig::RawConfig(VirtualMachineRawConfig {
         name: String::from("Service VM"),
         bootloader: Some(ParcelFileDescriptor::new(rialto)),
         disks: vec![DiskImage { image: None, partitions: writable_partitions, writable: true }],
+        instanceId: instance_id,
         protectedVm: true,
         memoryMib: MEMORY_MB,
         cpuTopology: CpuTopology::ONE_CPU,
@@ -210,6 +245,23 @@
         .context("Failed to create service VM")
 }
 
+/// TODO(b/291213394): Reuse this method in other places such as vm and compos.
+fn get_or_allocate_instance_id(
+    service: &dyn IVirtualizationService,
+    instance_id_file: PathBuf,
+) -> Result<[u8; 64]> {
+    let mut instance_id = [0; 64];
+    if instance_id_file.exists() {
+        let mut file = File::open(&instance_id_file)?;
+        file.read_exact(&mut instance_id)?;
+    } else {
+        info!("Allocating a new instance ID for the Service VM");
+        instance_id = service.allocateInstanceId()?;
+        fs::write(instance_id_file, instance_id)?;
+    }
+    Ok(instance_id)
+}
+
 /// Returns the file descriptor of the instance image at the given path.
 fn instance_img(
     service: &dyn IVirtualizationService,
diff --git a/service_vm/test_apk/Android.bp b/service_vm/test_apk/Android.bp
index cd992db..450d475 100644
--- a/service_vm/test_apk/Android.bp
+++ b/service_vm/test_apk/Android.bp
@@ -45,7 +45,10 @@
 rust_ffi {
     name: "libvm_attestation_test_payload",
     defaults: ["vm_attestation_test_payload_defaults"],
-    visibility: [":__subpackages__"],
+    visibility: [
+        ":__subpackages__",
+        "//packages/modules/Virtualization/tests/testapk",
+    ],
 }
 
 android_test {
@@ -56,11 +59,20 @@
     test_config: "AndroidTest.rkpd.xml",
     static_libs: [
         "RkpdAppTestUtil",
+        "VmAttestationTestUtil",
         "androidx.work_work-testing",
-        "bouncycastle-unbundled",
     ],
     instrumentation_for: "rkpdapp",
     // This app is a variation of rkpdapp, with additional permissions to run
     // a VM. It is defined in packages/modules/RemoteKeyProvisioning.
     data: [":avf-rkpdapp"],
 }
+
+java_library {
+    name: "VmAttestationTestUtil",
+    srcs: ["src/java/com/android/virt/vm_attestation/util/*.java"],
+    static_libs: [
+        "bouncycastle-unbundled",
+        "truth",
+    ],
+}
diff --git a/service_vm/test_apk/aidl/com/android/virt/vm_attestation/testservice/IAttestationService.aidl b/service_vm/test_apk/aidl/com/android/virt/vm_attestation/testservice/IAttestationService.aidl
index 34c8549..18df572 100644
--- a/service_vm/test_apk/aidl/com/android/virt/vm_attestation/testservice/IAttestationService.aidl
+++ b/service_vm/test_apk/aidl/com/android/virt/vm_attestation/testservice/IAttestationService.aidl
@@ -21,6 +21,27 @@
     const int PORT = 5679;
 
     /**
+     * The status of the attestation.
+     *
+     * The status here maps to the status defined in
+     * vm_payload/include/vm_payload.h
+     */
+    @Backing(type="int")
+    enum AttestationStatus {
+        /** The remote attestation completes successfully. */
+        ATTESTATION_OK = 0,
+
+        /** The challenge size is not between 0 and 64. */
+        ATTESTATION_ERROR_INVALID_CHALLENGE = 1,
+
+        /** Failed to attest the VM. Please retry at a later time. */
+        ATTESTATION_ERROR_ATTESTATION_FAILED = 2,
+
+        /** Remote attestation is not supported in the current environment. */
+        ATTESTATION_ERROR_UNSUPPORTED = 3,
+    }
+
+    /**
      * The result of signing a message with the attested key.
      */
     parcelable SigningResult {
@@ -29,6 +50,9 @@
 
         /** The DER-encoded attestation X509 certificate chain. */
         byte[] certificateChain;
+
+        /** The status of the attestation. */
+        AttestationStatus status;
     }
 
     /**
diff --git a/service_vm/test_apk/src/java/com/android/virt/rkpd/vm_attestation/testapp/RkpdVmAttestationTest.java b/service_vm/test_apk/src/java/com/android/virt/rkpd/vm_attestation/testapp/RkpdVmAttestationTest.java
index 2a771f3..ce7fc45 100644
--- a/service_vm/test_apk/src/java/com/android/virt/rkpd/vm_attestation/testapp/RkpdVmAttestationTest.java
+++ b/service_vm/test_apk/src/java/com/android/virt/rkpd/vm_attestation/testapp/RkpdVmAttestationTest.java
@@ -19,7 +19,6 @@
 import static android.system.virtualmachine.VirtualMachineConfig.DEBUG_LEVEL_FULL;
 
 import static com.google.common.truth.Truth.assertThat;
-import static com.google.common.truth.Truth.assertWithMessage;
 import static com.google.common.truth.TruthJUnit.assume;
 
 import android.content.Context;
@@ -40,29 +39,20 @@
 import com.android.rkpdapp.provisioner.PeriodicProvisioner;
 import com.android.rkpdapp.testutil.SystemInterfaceSelector;
 import com.android.rkpdapp.utils.Settings;
-import com.android.rkpdapp.utils.X509Utils;
-import com.android.virt.vm_attestation.testservice.IAttestationService;
 import com.android.virt.vm_attestation.testservice.IAttestationService.SigningResult;
+import com.android.virt.vm_attestation.util.X509Utils;
 
-import org.bouncycastle.asn1.ASN1Boolean;
-import org.bouncycastle.asn1.ASN1Encodable;
-import org.bouncycastle.asn1.ASN1OctetString;
-import org.bouncycastle.asn1.ASN1Sequence;
-import org.bouncycastle.asn1.DEROctetString;
-import org.bouncycastle.asn1.DERUTF8String;
 import org.junit.After;
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.Parameterized;
 
-import java.security.Signature;
 import java.security.cert.X509Certificate;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collection;
 import java.util.List;
-import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.Executors;
 
 /**
@@ -88,7 +78,7 @@
 @RunWith(Parameterized.class)
 public class RkpdVmAttestationTest extends MicrodroidDeviceTestBase {
     private static final String TAG = "RkpdVmAttestationTest";
-    private static final String AVF_ATTESTATION_EXTENSION_OID = "1.3.6.1.4.1.11129.2.1.29.1";
+
     private static final String SERVICE_NAME = IRemotelyProvisionedComponent.DESCRIPTOR + "/avf";
     private static final String VM_PAYLOAD_PATH = "libvm_attestation_test_payload.so";
     private static final String MESSAGE = "Hello RKP from AVF!";
@@ -171,96 +161,13 @@
         Arrays.fill(challenge, (byte) 0xab);
 
         // Act.
-        CompletableFuture<Exception> exception = new CompletableFuture<>();
-        CompletableFuture<Boolean> payloadReady = new CompletableFuture<>();
-        CompletableFuture<SigningResult> signingResultFuture = new CompletableFuture<>();
-        VmEventListener listener =
-                new VmEventListener() {
-                    @Override
-                    public void onPayloadReady(VirtualMachine vm) {
-                        payloadReady.complete(true);
-                        try {
-                            IAttestationService service =
-                                    IAttestationService.Stub.asInterface(
-                                            vm.connectToVsockServer(IAttestationService.PORT));
-                            signingResultFuture.complete(
-                                    service.signWithAttestationKey(challenge, MESSAGE.getBytes()));
-                        } catch (Exception e) {
-                            exception.complete(e);
-                        } finally {
-                            forceStop(vm);
-                        }
-                    }
-                };
-        listener.runToFinish(TAG, vm);
+        SigningResult signingResult =
+                runVmAttestationService(TAG, vm, challenge, MESSAGE.getBytes());
 
         // Assert.
-        assertThat(payloadReady.getNow(false)).isTrue();
-        assertThat(exception.getNow(null)).isNull();
-        SigningResult signingResult = signingResultFuture.getNow(null);
-        assertThat(signingResult).isNotNull();
-
-        // Parsing the certificate chain successfully indicates that the certificate
-        // chain is valid, that each certificate is signed by the next one and the last
-        // one is self-signed.
-        X509Certificate[] certs = X509Utils.formatX509Certs(signingResult.certificateChain);
-        assertThat(certs.length).isGreaterThan(2);
-        assertWithMessage("The first certificate should be generated in the RKP VM")
-                .that(certs[0].getSubjectX500Principal().getName())
-                .isEqualTo("CN=Android Protected Virtual Machine Key");
-        checkAvfAttestationExtension(certs[0], challenge);
-        assertWithMessage("The second certificate should contain AVF in the subject")
-                .that(certs[1].getSubjectX500Principal().getName())
-                .contains("O=AVF");
-
-        // Verify the signature using the public key from the leaf certificate generated
-        // in the RKP VM.
-        Signature sig = Signature.getInstance("SHA256withECDSA");
-        sig.initVerify(certs[0].getPublicKey());
-        sig.update(MESSAGE.getBytes());
-        assertThat(sig.verify(signingResult.signature)).isTrue();
-    }
-
-    private void checkAvfAttestationExtension(X509Certificate cert, byte[] challenge)
-            throws Exception {
-        byte[] extensionValue = cert.getExtensionValue(AVF_ATTESTATION_EXTENSION_OID);
-        ASN1OctetString extString = ASN1OctetString.getInstance(extensionValue);
-        ASN1Sequence seq = ASN1Sequence.getInstance(extString.getOctets());
-        // AVF attestation extension should contain 3 elements in the following format:
-        //
-        //  AttestationExtension ::= SEQUENCE {
-        //     attestationChallenge       OCTET_STRING,
-        //     isVmSecure                 BOOLEAN,
-        //     vmComponents               SEQUENCE OF VmComponent,
-        //  }
-        //   VmComponent ::= SEQUENCE {
-        //     name               UTF8String,
-        //     securityVersion    INTEGER,
-        //     codeHash           OCTET STRING,
-        //     authorityHash      OCTET STRING,
-        //  }
-        assertThat(seq).hasSize(3);
-
-        ASN1OctetString expectedChallenge = new DEROctetString(challenge);
-        assertThat(seq.getObjectAt(0)).isEqualTo(expectedChallenge);
-        assertWithMessage("The VM should be unsecure as it is debuggable.")
-                .that(seq.getObjectAt(1))
-                .isEqualTo(ASN1Boolean.FALSE);
-        ASN1Sequence vmComponents = ASN1Sequence.getInstance(seq.getObjectAt(2));
-        assertExtensionContainsPayloadApk(vmComponents);
-    }
-
-    private void assertExtensionContainsPayloadApk(ASN1Sequence vmComponents) throws Exception {
-        DERUTF8String payloadApkName = new DERUTF8String("apk:" + TEST_APP_PACKAGE_NAME);
-        boolean found = false;
-        for (ASN1Encodable encodable : vmComponents) {
-            ASN1Sequence vmComponent = ASN1Sequence.getInstance(encodable);
-            assertThat(vmComponent).hasSize(4);
-            if (payloadApkName.equals(vmComponent.getObjectAt(0))) {
-                assertWithMessage("Payload APK should not be found twice.").that(found).isFalse();
-                found = true;
-            }
-        }
-        assertWithMessage("vmComponents should contain the payload APK.").that(found).isTrue();
+        X509Certificate[] certs =
+                X509Utils.validateAndParseX509CertChain(signingResult.certificateChain);
+        X509Utils.verifyAvfRelatedCerts(certs, challenge, TEST_APP_PACKAGE_NAME);
+        X509Utils.verifySignature(certs[0], MESSAGE.getBytes(), signingResult.signature);
     }
 }
diff --git a/service_vm/test_apk/src/java/com/android/virt/vm_attestation/util/X509Utils.java b/service_vm/test_apk/src/java/com/android/virt/vm_attestation/util/X509Utils.java
new file mode 100644
index 0000000..cfa3663
--- /dev/null
+++ b/service_vm/test_apk/src/java/com/android/virt/vm_attestation/util/X509Utils.java
@@ -0,0 +1,143 @@
+/**
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * <p>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
+ *
+ * <p>http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * <p>Unless required by applicable law or agreed to in writing, software distributed under the
+ * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
+ * express or implied. See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.virt.vm_attestation.util;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assertWithMessage;
+
+import org.bouncycastle.asn1.ASN1Boolean;
+import org.bouncycastle.asn1.ASN1Encodable;
+import org.bouncycastle.asn1.ASN1OctetString;
+import org.bouncycastle.asn1.ASN1Sequence;
+import org.bouncycastle.asn1.DEROctetString;
+import org.bouncycastle.asn1.DERUTF8String;
+
+import java.io.ByteArrayInputStream;
+import java.security.Signature;
+import java.security.cert.CertPathValidator;
+import java.security.cert.Certificate;
+import java.security.cert.CertificateFactory;
+import java.security.cert.PKIXParameters;
+import java.security.cert.TrustAnchor;
+import java.security.cert.X509Certificate;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Set;
+
+/**
+ * Provides utility methods for parsing and verifying X.509 certificate chain issued from pVM remote
+ * attestation.
+ */
+public class X509Utils {
+    private static final String AVF_ATTESTATION_EXTENSION_OID = "1.3.6.1.4.1.11129.2.1.29.1";
+
+    /** Validates and parses the given DER-encoded X.509 certificate chain. */
+    public static X509Certificate[] validateAndParseX509CertChain(byte[] x509CertChain)
+            throws Exception {
+        CertificateFactory factory = CertificateFactory.getInstance("X.509");
+        ByteArrayInputStream in = new ByteArrayInputStream(x509CertChain);
+        ArrayList<Certificate> certs = new ArrayList<>(factory.generateCertificates(in));
+        X509Certificate[] certChain = certs.toArray(new X509Certificate[0]);
+        validateCertChain(certChain);
+        return certChain;
+    }
+
+    private static void validateCertChain(X509Certificate[] certChain) throws Exception {
+        X509Certificate rootCert = certChain[certChain.length - 1];
+        // The root certificate should be self-signed.
+        rootCert.verify(rootCert.getPublicKey());
+
+        // Only add the self-signed root certificate as trust anchor.
+        // All the other certificates in the chain should be signed by the previous cert's key.
+        Set<TrustAnchor> trustAnchors =
+                Set.of(new TrustAnchor(rootCert, /* nameConstraints= */ null));
+
+        CertificateFactory factory = CertificateFactory.getInstance("X.509");
+        CertPathValidator validator = CertPathValidator.getInstance("PKIX");
+        PKIXParameters parameters = new PKIXParameters(trustAnchors);
+        parameters.setRevocationEnabled(false);
+        validator.validate(factory.generateCertPath(Arrays.asList(certChain)), parameters);
+    }
+
+    /**
+     * Verifies the AVF related certificates in the given certificate chain. The AVF Attestation
+     * extension should be found in the leaf certificate.
+     */
+    public static void verifyAvfRelatedCerts(
+            X509Certificate[] certChain, byte[] challenge, String payloadApk) throws Exception {
+        assertThat(certChain.length).isGreaterThan(2);
+        assertWithMessage("The first certificate should be generated in the RKP VM")
+                .that(certChain[0].getSubjectX500Principal().getName())
+                .isEqualTo("CN=Android Protected Virtual Machine Key");
+        verifyAvfAttestationExtension(certChain[0], challenge, payloadApk);
+
+        assertWithMessage("The second certificate should contain AVF in the subject")
+                .that(certChain[1].getSubjectX500Principal().getName())
+                .contains("O=AVF");
+    }
+
+    private static void verifyAvfAttestationExtension(
+            X509Certificate cert, byte[] challenge, String payloadApk) throws Exception {
+        byte[] extensionValue = cert.getExtensionValue(AVF_ATTESTATION_EXTENSION_OID);
+        ASN1OctetString extString = ASN1OctetString.getInstance(extensionValue);
+        ASN1Sequence seq = ASN1Sequence.getInstance(extString.getOctets());
+        // AVF attestation extension should contain 3 elements in the following format:
+        //
+        //  AttestationExtension ::= SEQUENCE {
+        //     attestationChallenge       OCTET_STRING,
+        //     isVmSecure                 BOOLEAN,
+        //     vmComponents               SEQUENCE OF VmComponent,
+        //  }
+        //   VmComponent ::= SEQUENCE {
+        //     name               UTF8String,
+        //     securityVersion    INTEGER,
+        //     codeHash           OCTET STRING,
+        //     authorityHash      OCTET STRING,
+        //  }
+        assertThat(seq).hasSize(3);
+
+        ASN1OctetString expectedChallenge = new DEROctetString(challenge);
+        assertThat(seq.getObjectAt(0)).isEqualTo(expectedChallenge);
+        assertWithMessage("The VM should be unsecure as it is debuggable.")
+                .that(seq.getObjectAt(1))
+                .isEqualTo(ASN1Boolean.FALSE);
+        ASN1Sequence vmComponents = ASN1Sequence.getInstance(seq.getObjectAt(2));
+        assertExtensionContainsPayloadApk(vmComponents, payloadApk);
+    }
+
+    private static void assertExtensionContainsPayloadApk(
+            ASN1Sequence vmComponents, String payloadApk) throws Exception {
+        DERUTF8String payloadApkName = new DERUTF8String("apk:" + payloadApk);
+        boolean found = false;
+        for (ASN1Encodable encodable : vmComponents) {
+            ASN1Sequence vmComponent = ASN1Sequence.getInstance(encodable);
+            assertThat(vmComponent).hasSize(4);
+            if (payloadApkName.equals(vmComponent.getObjectAt(0))) {
+                assertWithMessage("Payload APK should not be found twice.").that(found).isFalse();
+                found = true;
+            }
+        }
+        assertWithMessage("vmComponents should contain the payload APK.").that(found).isTrue();
+    }
+
+    /** Verifies the given signature using the public key from the given certificate. */
+    public static void verifySignature(
+            X509Certificate publicKeyCert, byte[] messageToSign, byte[] signature)
+            throws Exception {
+        Signature sig = Signature.getInstance("SHA256withECDSA");
+        sig.initVerify(publicKeyCert.getPublicKey());
+        sig.update(messageToSign);
+        assertThat(sig.verify(signature)).isTrue();
+    }
+}
diff --git a/service_vm/test_apk/src/native/main.rs b/service_vm/test_apk/src/native/main.rs
index a04fb1f..ff21bd8 100644
--- a/service_vm/test_apk/src/native/main.rs
+++ b/service_vm/test_apk/src/native/main.rs
@@ -18,7 +18,8 @@
 use avflog::LogResult;
 use com_android_virt_vm_attestation_testservice::{
     aidl::com::android::virt::vm_attestation::testservice::IAttestationService::{
-        BnAttestationService, IAttestationService, SigningResult::SigningResult, PORT,
+        AttestationStatus::AttestationStatus, BnAttestationService, IAttestationService,
+        SigningResult::SigningResult, PORT,
     },
     binder::{self, unstable_api::AsNative, BinderFeatures, Interface, IntoBinderResult, Strong},
 };
@@ -103,14 +104,18 @@
         challenge: &[u8],
         message: &[u8],
     ) -> binder::Result<SigningResult> {
-        let res = AttestationResult::request_attestation(challenge)
-            .map_err(|e| anyhow!("Unexpected status: {:?}", status_to_cstr(e)))
-            .with_log()
-            .or_service_specific_exception(-1)?;
+        let res = match AttestationResult::request_attestation(challenge) {
+            Ok(res) => res,
+            Err(status) => {
+                let status = to_attestation_status(status);
+                return Ok(SigningResult { certificateChain: vec![], signature: vec![], status });
+            }
+        };
         let certificate_chain =
             res.certificate_chain().with_log().or_service_specific_exception(-1)?;
+        let status = AttestationStatus::ATTESTATION_OK;
         let signature = res.sign(message).with_log().or_service_specific_exception(-1)?;
-        Ok(SigningResult { certificateChain: certificate_chain, signature })
+        Ok(SigningResult { certificateChain: certificate_chain, signature, status })
     }
 
     fn validateAttestationResult(&self) -> binder::Result<()> {
@@ -119,6 +124,21 @@
     }
 }
 
+fn to_attestation_status(status: AVmAttestationStatus) -> AttestationStatus {
+    match status {
+        AVmAttestationStatus::ATTESTATION_OK => AttestationStatus::ATTESTATION_OK,
+        AVmAttestationStatus::ATTESTATION_ERROR_INVALID_CHALLENGE => {
+            AttestationStatus::ATTESTATION_ERROR_INVALID_CHALLENGE
+        }
+        AVmAttestationStatus::ATTESTATION_ERROR_ATTESTATION_FAILED => {
+            AttestationStatus::ATTESTATION_ERROR_ATTESTATION_FAILED
+        }
+        AVmAttestationStatus::ATTESTATION_ERROR_UNSUPPORTED => {
+            AttestationStatus::ATTESTATION_ERROR_UNSUPPORTED
+        }
+    }
+}
+
 #[derive(Debug)]
 struct AttestationResult(NonNull<AVmAttestationResult>);
 
diff --git a/tests/benchmark/assets/microdroid_gki-android14-6.1/vm_config.json b/tests/benchmark/assets/microdroid_gki-android14-6.1/vm_config.json
deleted file mode 100644
index c4fdc6e..0000000
--- a/tests/benchmark/assets/microdroid_gki-android14-6.1/vm_config.json
+++ /dev/null
@@ -1,10 +0,0 @@
-{
-  "os": {
-    "name": "microdroid_gki-android14-6.1"
-  },
-  "task": {
-    "type": "microdroid_launcher",
-    "command": "MicrodroidIdleNativeLib.so"
-  },
-  "export_tombstones": true
-}
diff --git a/tests/benchmark/assets/microdroid_gki-android14-6.1/vm_config_io.json b/tests/benchmark/assets/microdroid_gki-android14-6.1/vm_config_io.json
deleted file mode 100644
index 34c204e..0000000
--- a/tests/benchmark/assets/microdroid_gki-android14-6.1/vm_config_io.json
+++ /dev/null
@@ -1,15 +0,0 @@
-{
-  "os": {
-    "name": "microdroid_gki-android14-6.1"
-  },
-  "task": {
-    "type": "microdroid_launcher",
-    "command": "MicrodroidBenchmarkNativeLib.so"
-  },
-  "apexes": [
-    {
-      "name": "com.android.virt"
-    }
-  ],
-  "export_tombstones": true
-}
diff --git a/tests/benchmark/assets/microdroid/vm_config.json b/tests/benchmark/assets/vm_config.json
similarity index 76%
rename from tests/benchmark/assets/microdroid/vm_config.json
rename to tests/benchmark/assets/vm_config.json
index 5a604a9..d4c66d7 100644
--- a/tests/benchmark/assets/microdroid/vm_config.json
+++ b/tests/benchmark/assets/vm_config.json
@@ -1,7 +1,4 @@
 {
-  "os": {
-    "name": "microdroid"
-  },
   "task": {
     "type": "microdroid_launcher",
     "command": "MicrodroidIdleNativeLib.so"
diff --git a/tests/benchmark/assets/microdroid/vm_config_io.json b/tests/benchmark/assets/vm_config_io.json
similarity index 82%
rename from tests/benchmark/assets/microdroid/vm_config_io.json
rename to tests/benchmark/assets/vm_config_io.json
index 66046ba..f982d41 100644
--- a/tests/benchmark/assets/microdroid/vm_config_io.json
+++ b/tests/benchmark/assets/vm_config_io.json
@@ -1,7 +1,4 @@
 {
-  "os": {
-    "name": "microdroid"
-  },
   "task": {
     "type": "microdroid_launcher",
     "command": "MicrodroidBenchmarkNativeLib.so"
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 acd6f2c..9cc1b7b 100644
--- a/tests/benchmark/src/java/com/android/microdroid/benchmark/MicrodroidBenchmarks.java
+++ b/tests/benchmark/src/java/com/android/microdroid/benchmark/MicrodroidBenchmarks.java
@@ -222,7 +222,6 @@
 
     private void runBootTimeTest(
             String name,
-            String payloadConfig,
             boolean fullDebug,
             Function<VirtualMachineConfig.Builder, VirtualMachineConfig.Builder> fnConfig)
             throws VirtualMachineException, InterruptedException, IOException {
@@ -261,7 +260,6 @@
             throws VirtualMachineException, InterruptedException, IOException {
         runBootTimeTest(
                 "test_vm_boot_time",
-                "assets/" + os() + "/vm_config.json",
                 /* fullDebug */ false,
                 (builder) -> builder.setCpuTopology(CPU_TOPOLOGY_ONE_CPU));
     }
@@ -270,7 +268,6 @@
             throws VirtualMachineException, InterruptedException, IOException {
         runBootTimeTest(
                 "test_vm_boot_time_host_topology",
-                "assets/" + os() + "/vm_config.json",
                 /* fullDebug */ false,
                 (builder) -> builder.setCpuTopology(CPU_TOPOLOGY_MATCH_HOST));
     }
@@ -280,7 +277,6 @@
             throws VirtualMachineException, InterruptedException, IOException {
         runBootTimeTest(
                 "test_vm_boot_time_debug",
-                "assets/" + os() + "/vm_config.json",
                 /* fullDebug */ true,
                 (builder) -> builder);
     }
@@ -298,7 +294,6 @@
         assumeFeatureEnabled(VirtualMachineManager.FEATURE_VENDOR_MODULES);
         runBootTimeTest(
                 "test_vm_boot_time_debug_with_vendor_partition",
-                "assets/" + os() + "/vm_config.json",
                 /* fullDebug */ true,
                 (builder) -> builder.setVendorDiskImage(vendorDiskImage));
     }
@@ -348,7 +343,7 @@
     @Test
     public void testVsockTransferFromHostToVM() throws Exception {
         VirtualMachineConfig config =
-                newVmConfigBuilderWithPayloadConfig("assets/" + os() + "/vm_config_io.json")
+                newVmConfigBuilderWithPayloadConfig("assets/vm_config_io.json")
                         .setDebugLevel(DEBUG_LEVEL_NONE)
                         .build();
         List<Double> transferRates = new ArrayList<>(IO_TEST_TRIAL_COUNT);
@@ -374,7 +369,7 @@
 
     private void testVirtioBlkReadRate(boolean isRand) throws Exception {
         VirtualMachineConfig config =
-                newVmConfigBuilderWithPayloadConfig("assets/" + os() + "/vm_config_io.json")
+                newVmConfigBuilderWithPayloadConfig("assets/vm_config_io.json")
                         .setDebugLevel(DEBUG_LEVEL_NONE)
                         .build();
         List<Double> readRates = new ArrayList<>(IO_TEST_TRIAL_COUNT);
@@ -525,7 +520,7 @@
     public void testMemoryUsage() throws Exception {
         final String vmName = "test_vm_mem_usage";
         VirtualMachineConfig config =
-                newVmConfigBuilderWithPayloadConfig("assets/" + os() + "/vm_config_io.json")
+                newVmConfigBuilderWithPayloadConfig("assets/vm_config_io.json")
                         .setDebugLevel(DEBUG_LEVEL_NONE)
                         .setMemoryBytes(256 * ONE_MEBI)
                         .build();
@@ -611,7 +606,7 @@
     public void testMemoryReclaim() throws Exception {
         final String vmName = "test_vm_mem_reclaim";
         VirtualMachineConfig config =
-                newVmConfigBuilderWithPayloadConfig("assets/" + os() + "/vm_config_io.json")
+                newVmConfigBuilderWithPayloadConfig("assets/vm_config_io.json")
                         .setDebugLevel(DEBUG_LEVEL_NONE)
                         .setMemoryBytes(256 * ONE_MEBI)
                         .build();
@@ -836,7 +831,7 @@
     @Test
     public void testVmKillTime() throws Exception {
         VirtualMachineConfig config =
-                newVmConfigBuilderWithPayloadConfig("assets/" + os() + "/vm_config_io.json")
+                newVmConfigBuilderWithPayloadConfig("assets/vm_config_io.json")
                         .setDebugLevel(DEBUG_LEVEL_NONE)
                         .build();
         List<Double> vmKillTime = new ArrayList<>(TEST_TRIAL_COUNT);
diff --git a/tests/benchmark_hostside/java/android/avf/test/AVFHostTestCase.java b/tests/benchmark_hostside/java/android/avf/test/AVFHostTestCase.java
index f01a76b..b176cfc 100644
--- a/tests/benchmark_hostside/java/android/avf/test/AVFHostTestCase.java
+++ b/tests/benchmark_hostside/java/android/avf/test/AVFHostTestCase.java
@@ -231,7 +231,7 @@
         android.tryRun("rm", "-rf", MicrodroidHostTestCaseBase.TEST_ROOT);
 
         // Donate 80% of the available device memory to the VM
-        final String configPath = "assets/microdroid/vm_config.json";
+        final String configPath = "assets/vm_config.json";
         final int vm_mem_mb = getFreeMemoryInfoMb(android) * 80 / 100;
         ITestDevice microdroidDevice =
                 MicrodroidBuilder.fromDevicePath(getPathForPackage(PACKAGE_NAME), configPath)
diff --git a/tests/helper/Android.bp b/tests/helper/Android.bp
index 614c70c..41d1ba2 100644
--- a/tests/helper/Android.bp
+++ b/tests/helper/Android.bp
@@ -16,6 +16,7 @@
         "androidx.test.runner",
         "androidx.test.ext.junit",
         "com.android.microdroid.testservice-java",
+        "com.android.virt.vm_attestation.testservice-java",
         "MicrodroidTestHelper",
         "truth",
     ],
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 2c92f04..6c82de8 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
@@ -43,6 +43,8 @@
 import com.android.microdroid.test.common.DeviceProperties;
 import com.android.microdroid.test.common.MetricsProcessor;
 import com.android.microdroid.testservice.ITestService;
+import com.android.virt.vm_attestation.testservice.IAttestationService;
+import com.android.virt.vm_attestation.testservice.IAttestationService.SigningResult;
 
 import java.io.BufferedReader;
 import java.io.ByteArrayOutputStream;
@@ -127,6 +129,7 @@
     public VirtualMachineConfig.Builder newVmConfigBuilderWithPayloadConfig(String configPath) {
         return new VirtualMachineConfig.Builder(mCtx)
                 .setProtectedVm(mProtectedVm)
+                .setOs(os())
                 .setPayloadConfigPath(configPath);
     }
 
@@ -205,6 +208,12 @@
                 .isNotEqualTo("5.4");
     }
 
+    protected void assumeNoUpdatableVmSupport() throws VirtualMachineException {
+        assume().withMessage("Secretkeeper not supported")
+                .that(getVirtualMachineManager().isUpdatableVmSupported())
+                .isFalse();
+    }
+
     public abstract static class VmEventListener implements VirtualMachineCallback {
         private ExecutorService mExecutorService = Executors.newSingleThreadExecutor();
         private OptionalLong mVcpuStartedNanoTime = OptionalLong.empty();
@@ -523,6 +532,40 @@
         }
     }
 
+    protected SigningResult runVmAttestationService(
+            String logTag, VirtualMachine vm, byte[] challenge, byte[] messageToSign)
+            throws Exception {
+
+        CompletableFuture<Exception> exception = new CompletableFuture<>();
+        CompletableFuture<Boolean> payloadReady = new CompletableFuture<>();
+        CompletableFuture<SigningResult> signingResultFuture = new CompletableFuture<>();
+        VmEventListener listener =
+                new VmEventListener() {
+                    @Override
+                    public void onPayloadReady(VirtualMachine vm) {
+                        payloadReady.complete(true);
+                        try {
+                            IAttestationService service =
+                                    IAttestationService.Stub.asInterface(
+                                            vm.connectToVsockServer(IAttestationService.PORT));
+                            signingResultFuture.complete(
+                                    service.signWithAttestationKey(challenge, messageToSign));
+                        } catch (Exception e) {
+                            exception.complete(e);
+                        } finally {
+                            forceStop(vm);
+                        }
+                    }
+                };
+        listener.runToFinish(TAG, vm);
+
+        assertThat(payloadReady.getNow(false)).isTrue();
+        assertThat(exception.getNow(null)).isNull();
+        SigningResult signingResult = signingResultFuture.getNow(null);
+        assertThat(signingResult).isNotNull();
+        return signingResult;
+    }
+
     protected TestResults runVmTestService(
             String logTag, VirtualMachine vm, RunTestsAgainstTestService testsToRun)
             throws Exception {
diff --git a/tests/hostside/Android.bp b/tests/hostside/Android.bp
index 41d244d..068d8f9 100644
--- a/tests/hostside/Android.bp
+++ b/tests/hostside/Android.bp
@@ -38,8 +38,6 @@
         "lz4",
         "sign_virt_apex",
         "simg2img",
-        "dtdiff",
-        "dtc", // for dtdiff
     ],
     // java_test_host doesn't have data_native_libs but jni_libs can be used to put
     // native modules under ./lib directory.
@@ -54,6 +52,5 @@
         "liblp",
         "libsparse",
         "libz",
-        "libfdt", // for dtc
     ],
 }
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 14cc0ae..007f38c 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
@@ -35,6 +35,7 @@
 import com.android.tradefed.util.RunUtil;
 
 import org.json.JSONArray;
+import org.json.JSONObject;
 
 import java.io.File;
 import java.io.IOException;
@@ -65,6 +66,17 @@
     protected static final Set<String> SUPPORTED_GKI_VERSIONS =
             Collections.unmodifiableSet(new HashSet(Arrays.asList("android14-6.1")));
 
+    /* Keep this sync with AssignableDevice.aidl */
+    public static final class AssignableDevice {
+        public final String node;
+        public final String dtbo_label;
+
+        public AssignableDevice(String node, String dtbo_label) {
+            this.node = node;
+            this.dtbo_label = dtbo_label;
+        }
+    }
+
     public static void prepareVirtualizationTestSetup(ITestDevice androidDevice)
             throws DeviceNotAvailableException {
         CommandRunner android = new CommandRunner(androidDevice);
@@ -186,18 +198,26 @@
         return pathLine.substring("package:".length());
     }
 
-    public List<String> parseStringArrayFieldsFromVmInfo(String header) throws Exception {
+    public String parseFieldFromVmInfo(String header) throws Exception {
         CommandRunner android = new CommandRunner(getDevice());
         String result = android.run("/apex/com.android.virt/bin/vm", "info");
-        List<String> ret = new ArrayList<>();
         for (String line : result.split("\n")) {
             if (!line.startsWith(header)) continue;
 
-            JSONArray jsonArray = new JSONArray(line.substring(header.length()));
+            return line.substring(header.length());
+        }
+        return "";
+    }
+
+    public List<String> parseStringArrayFieldsFromVmInfo(String header) throws Exception {
+        String field = parseFieldFromVmInfo(header);
+
+        List<String> ret = new ArrayList<>();
+        if (!field.isEmpty()) {
+            JSONArray jsonArray = new JSONArray(field);
             for (int i = 0; i < jsonArray.length(); i++) {
                 ret.add(jsonArray.getString(i));
             }
-            break;
         }
         return ret;
     }
@@ -208,8 +228,20 @@
         return result.contains("enabled");
     }
 
-    public List<String> getAssignableDevices() throws Exception {
-        return parseStringArrayFieldsFromVmInfo("Assignable devices: ");
+    public List<AssignableDevice> getAssignableDevices() throws Exception {
+        String field = parseFieldFromVmInfo("Assignable devices: ");
+
+        List<AssignableDevice> ret = new ArrayList<>();
+        if (!field.isEmpty()) {
+            JSONArray jsonArray = new JSONArray(field);
+            for (int i = 0; i < jsonArray.length(); i++) {
+                JSONObject jsonObject = jsonArray.getJSONObject(i);
+                ret.add(
+                        new AssignableDevice(
+                                jsonObject.getString("node"), jsonObject.getString("dtbo_label")));
+            }
+        }
+        return ret;
     }
 
     public List<String> getSupportedOSList() throws Exception {
diff --git a/tests/hostside/java/com/android/microdroid/test/MicrodroidHostTests.java b/tests/hostside/java/com/android/microdroid/test/MicrodroidHostTests.java
index 6dd3afe..06806ec 100644
--- a/tests/hostside/java/com/android/microdroid/test/MicrodroidHostTests.java
+++ b/tests/hostside/java/com/android/microdroid/test/MicrodroidHostTests.java
@@ -82,6 +82,7 @@
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 import java.util.stream.Collectors;
+import java.util.Objects;
 
 @RunWith(DeviceJUnit4Parameterized.class)
 @UseParametersRunnerFactory(DeviceJUnit4ClassRunnerWithParameters.RunnerFactory.class)
@@ -153,7 +154,7 @@
             throws Exception {
         PayloadMetadata.write(
                 PayloadMetadata.metadata(
-                        "/mnt/apk/assets/" + mOs + "/vm_config.json",
+                        "/mnt/apk/assets/vm_config.json",
                         PayloadMetadata.apk("microdroid-apk"),
                         apexes.stream()
                                 .map(apex -> PayloadMetadata.apex(apex.name))
@@ -412,7 +413,7 @@
     public void protectedVmRunsPvmfw() throws Exception {
         // Arrange
         assumeProtectedVm();
-        final String configPath = "assets/" + mOs + "/vm_config_apex.json";
+        final String configPath = "assets/vm_config_apex.json";
 
         // Act
         mMicrodroidDevice =
@@ -421,6 +422,7 @@
                         .memoryMib(minMemorySize())
                         .cpuTopology("match_host")
                         .protectedVm(true)
+                        .gki(mGki)
                         .build(getAndroidDevice());
 
         // Assert
@@ -548,6 +550,7 @@
                         .memoryMib(minMemorySize())
                         .cpuTopology("match_host")
                         .protectedVm(protectedVm)
+                        .gki(mGki)
                         .build(getAndroidDevice());
         mMicrodroidDevice.waitForBootComplete(BOOT_COMPLETE_TIMEOUT);
         mMicrodroidDevice.enableAdbRoot();
@@ -569,7 +572,7 @@
         assertThat(
                         isTombstoneGeneratedWithCmd(
                                 mProtectedVm,
-                                "assets/" + mOs + "/vm_config.json",
+                                "assets/vm_config.json",
                                 "kill",
                                 "-SIGSEGV",
                                 "$(pidof microdroid_launcher)"))
@@ -583,7 +586,7 @@
         assertThat(
                         isTombstoneGeneratedWithCmd(
                                 mProtectedVm,
-                                "assets/" + mOs + "/vm_config_no_tombstone.json",
+                                "assets/vm_config_no_tombstone.json",
                                 "kill",
                                 "-SIGSEGV",
                                 "$(pidof microdroid_launcher)"))
@@ -597,7 +600,7 @@
         assertThat(
                         isTombstoneGeneratedWithCmd(
                                 mProtectedVm,
-                                "assets/" + mOs + "/vm_config.json",
+                                "assets/vm_config.json",
                                 "echo",
                                 "c",
                                 ">",
@@ -636,6 +639,10 @@
         if (protectedVm) {
             cmd.add("--protected");
         }
+        if (mGki != null) {
+            cmd.add("--gki");
+            cmd.add(mGki);
+        }
         Collections.addAll(cmd, additionalArgs);
 
         android.run(cmd.toArray(new String[0]));
@@ -667,10 +674,7 @@
     private boolean isTombstoneGeneratedWithCrashConfig(boolean protectedVm, boolean debuggable)
             throws Exception {
         return isTombstoneGeneratedWithVmRunApp(
-                protectedVm,
-                debuggable,
-                "--config-path",
-                "assets/" + mOs + "/vm_config_crash.json");
+                protectedVm, debuggable, "--config-path", "assets/vm_config_crash.json");
     }
 
     @Test
@@ -705,13 +709,14 @@
 
         // Create VM with microdroid
         TestDevice device = getAndroidDevice();
-        final String configPath = "assets/" + mOs + "/vm_config_apex.json"; // path inside the APK
+        final String configPath = "assets/vm_config_apex.json"; // path inside the APK
         ITestDevice microdroid =
                 MicrodroidBuilder.fromDevicePath(getPathForPackage(PACKAGE_NAME), configPath)
                         .debugLevel("full")
                         .memoryMib(minMemorySize())
                         .cpuTopology("match_host")
                         .protectedVm(mProtectedVm)
+                        .gki(mGki)
                         .build(device);
         microdroid.waitForBootComplete(BOOT_COMPLETE_TIMEOUT);
         device.shutdownMicrodroid(microdroid);
@@ -797,7 +802,7 @@
                 getDevice().pullFileContents(CONSOLE_PATH) + getDevice().pullFileContents(LOG_PATH);
         assertWithMessage("Unexpected denials during VM boot")
                 .that(logText)
-                .doesNotContainMatch("avc:\s+denied");
+                .doesNotContainMatch("avc:\\s+denied");
 
         assertThat(getDeviceNumCpus(microdroid)).isEqualTo(getDeviceNumCpus(android));
 
@@ -833,24 +838,26 @@
     @Test
     @CddTest(requirements = {"9.17/C-1-1", "9.17/C-1-2", "9.17/C/1-3"})
     public void testMicrodroidBoots() throws Exception {
-        final String configPath = "assets/" + mOs + "/vm_config.json"; // path inside the APK
+        final String configPath = "assets/vm_config.json"; // path inside the APK
         testMicrodroidBootsWithBuilder(
                 MicrodroidBuilder.fromDevicePath(getPathForPackage(PACKAGE_NAME), configPath)
                         .debugLevel("full")
                         .memoryMib(minMemorySize())
                         .cpuTopology("match_host")
-                        .protectedVm(mProtectedVm));
+                        .protectedVm(mProtectedVm)
+                        .gki(mGki));
     }
 
     @Test
     public void testMicrodroidRamUsage() throws Exception {
-        final String configPath = "assets/" + mOs + "/vm_config.json";
+        final String configPath = "assets/vm_config.json";
         mMicrodroidDevice =
                 MicrodroidBuilder.fromDevicePath(getPathForPackage(PACKAGE_NAME), configPath)
                         .debugLevel("full")
                         .memoryMib(minMemorySize())
                         .cpuTopology("match_host")
                         .protectedVm(mProtectedVm)
+                        .gki(mGki)
                         .build(getAndroidDevice());
         mMicrodroidDevice.waitForBootComplete(BOOT_COMPLETE_TIMEOUT);
         mMicrodroidDevice.enableAdbRoot();
@@ -1035,65 +1042,48 @@
         // Check for preconditions
         assumeVfioPlatformSupported();
 
-        List<String> devices = getAssignableDevices();
+        List<AssignableDevice> devices = getAssignableDevices();
         assumeFalse("no assignable devices", devices.isEmpty());
 
-        String vmFdtPath = "/sys/firmware/fdt";
-        File testDir = FileUtil.createTempDir("device_assignment_test");
-        File baseFdtFile = new File(testDir, "base_fdt.dtb");
-        File fdtFile = new File(testDir, "fdt.dtb");
-
-        // Generates baseline DT
-        launchWithDeviceAssignment(/* device= */ null);
-        assertThat(mMicrodroidDevice.pullFile(vmFdtPath, baseFdtFile)).isTrue();
-        getAndroidDevice().shutdownMicrodroid(mMicrodroidDevice);
-
-        // Prepares to run dtdiff. It requires dtc.
-        File dtdiff = findTestFile("dtdiff");
-        RunUtil runner = new RunUtil();
-        String separator = System.getProperty("path.separator");
-        String path = dtdiff.getParent() + separator + System.getenv("PATH");
-        runner.setEnvVariable("PATH", path);
+        String dtSysfsPath = "/proc/device-tree/";
 
         // Try assign devices one by one
-        for (String device : devices) {
-            assertThat(device).isNotNull();
-            launchWithDeviceAssignment(device);
-            assertThat(mMicrodroidDevice.pullFile(vmFdtPath, fdtFile)).isTrue();
+        for (AssignableDevice device : devices) {
+            launchWithDeviceAssignment(device.node);
+
+            String dtPath =
+                    new CommandRunner(mMicrodroidDevice)
+                            .run("cat", dtSysfsPath + "__symbols__/" + device.dtbo_label);
+            assertThat(dtPath).isNotEmpty();
+
+            String resolvedDtPath =
+                    new CommandRunner(mMicrodroidDevice)
+                            .run("readlink", "-e", dtSysfsPath + dtPath);
+            assertThat(resolvedDtPath).isNotEmpty();
+
+            String allDevices =
+                    new CommandRunner(mMicrodroidDevice)
+                            .run("readlink", "-e", "/sys/bus/platform/devices/*/of_node");
+            assertThat(allDevices.split("\n")).asList().contains(resolvedDtPath);
+
             getAndroidDevice().shutdownMicrodroid(mMicrodroidDevice);
-
-            CommandResult result =
-                    runner.runTimedCmd(
-                            500,
-                            dtdiff.getAbsolutePath(),
-                            baseFdtFile.getPath(),
-                            fdtFile.getPath());
-
-            assertWithMessage(
-                            "VM's device tree hasn't changed when assigning "
-                                    + device
-                                    + ", result="
-                                    + result)
-                    .that(result.getStatus())
-                    .isNotEqualTo(CommandStatus.SUCCESS);
+            mMicrodroidDevice = null;
         }
-
-        mMicrodroidDevice = null;
     }
 
     private void launchWithDeviceAssignment(String device) throws Exception {
-        final String configPath = "assets/" + mOs + "/vm_config.json";
+        Objects.requireNonNull(device);
+        final String configPath = "assets/vm_config.json";
 
-        MicrodroidBuilder builder =
+        mMicrodroidDevice =
                 MicrodroidBuilder.fromDevicePath(getPathForPackage(PACKAGE_NAME), configPath)
                         .debugLevel("full")
                         .memoryMib(minMemorySize())
                         .cpuTopology("match_host")
-                        .protectedVm(mProtectedVm);
-        if (device != null) {
-            builder.addAssignableDevice(device);
-        }
-        mMicrodroidDevice = builder.build(getAndroidDevice());
+                        .protectedVm(mProtectedVm)
+                        .gki(mGki)
+                        .addAssignableDevice(device)
+                        .build(getAndroidDevice());
 
         assertThat(mMicrodroidDevice.waitForBootComplete(BOOT_COMPLETE_TIMEOUT)).isTrue();
         assertThat(mMicrodroidDevice.enableAdbRoot()).isTrue();
diff --git a/tests/pvmfw/java/com/android/pvmfw/test/CustomPvmfwHostTestCaseBase.java b/tests/pvmfw/java/com/android/pvmfw/test/CustomPvmfwHostTestCaseBase.java
index a3216c2..541f5ec 100644
--- a/tests/pvmfw/java/com/android/pvmfw/test/CustomPvmfwHostTestCaseBase.java
+++ b/tests/pvmfw/java/com/android/pvmfw/test/CustomPvmfwHostTestCaseBase.java
@@ -50,8 +50,7 @@
     @NonNull public static final String MICRODROID_DEBUG_FULL = "full";
     @NonNull public static final String MICRODROID_DEBUG_NONE = "none";
 
-    @NonNull
-    public static final String MICRODROID_CONFIG_PATH = "assets/microdroid/vm_config_apex.json";
+    @NonNull public static final String MICRODROID_CONFIG_PATH = "assets/vm_config_apex.json";
 
     @NonNull
     public static final String VM_REFERENCE_DT_PATH = "/data/local/tmp/pvmfw/reference_dt.dtb";
diff --git a/tests/testapk/Android.bp b/tests/testapk/Android.bp
index 732be94..84bf098 100644
--- a/tests/testapk/Android.bp
+++ b/tests/testapk/Android.bp
@@ -23,6 +23,7 @@
     static_libs: [
         "com.android.microdroid.testservice-java",
         "com.android.microdroid.test.vmshare_service-java",
+        "com.android.virt.vm_attestation.testservice-java",
     ],
     certificate: ":MicrodroidTestAppCert",
     sdk_version: "test_current",
@@ -38,6 +39,7 @@
     srcs: ["src/java/**/*.java"],
     static_libs: [
         "MicrodroidDeviceTestHelper",
+        "VmAttestationTestUtil",
         "androidx.test.runner",
         "androidx.test.ext.junit",
         "authfs_test_apk_assets",
@@ -53,6 +55,7 @@
         "MicrodroidExitNativeLib",
         "MicrodroidPrivateLinkingNativeLib",
         "MicrodroidCrashNativeLib",
+        "libvm_attestation_test_payload",
     ],
     min_sdk_version: "33",
     // Defined in ../vmshareapp/Android.bp
diff --git a/tests/testapk/assets/microdroid/vm_config_no_task.json b/tests/testapk/assets/microdroid/vm_config_no_task.json
deleted file mode 100644
index 3162bd0..0000000
--- a/tests/testapk/assets/microdroid/vm_config_no_task.json
+++ /dev/null
@@ -1,6 +0,0 @@
-{
-  "os": {
-    "name": "microdroid"
-  },
-  "export_tombstones": true
-}
diff --git a/tests/testapk/assets/microdroid_gki-android14-6.1/vm_config.json b/tests/testapk/assets/microdroid_gki-android14-6.1/vm_config.json
deleted file mode 100644
index 2022127..0000000
--- a/tests/testapk/assets/microdroid_gki-android14-6.1/vm_config.json
+++ /dev/null
@@ -1,10 +0,0 @@
-{
-  "os": {
-    "name": "microdroid_gki-android14-6.1"
-  },
-  "task": {
-    "type": "microdroid_launcher",
-    "command": "MicrodroidTestNativeLib.so"
-  },
-  "export_tombstones": true
-}
diff --git a/tests/testapk/assets/microdroid_gki-android14-6.1/vm_config_apex.json b/tests/testapk/assets/microdroid_gki-android14-6.1/vm_config_apex.json
deleted file mode 100644
index bd3998d..0000000
--- a/tests/testapk/assets/microdroid_gki-android14-6.1/vm_config_apex.json
+++ /dev/null
@@ -1,21 +0,0 @@
-{
-  "os": {
-    "name": "microdroid_gki-android14-6.1"
-  },
-  "task": {
-    "type": "microdroid_launcher",
-    "command": "MicrodroidTestNativeLib.so"
-  },
-  "apexes": [
-    {
-      "name": "com.android.art"
-    },
-    {
-      "name": "com.android.compos"
-    },
-    {
-      "name": "com.android.sdkext"
-    }
-  ],
-  "export_tombstones": true
-}
diff --git a/tests/testapk/assets/microdroid_gki-android14-6.1/vm_config_crash.json b/tests/testapk/assets/microdroid_gki-android14-6.1/vm_config_crash.json
deleted file mode 100644
index 4692258..0000000
--- a/tests/testapk/assets/microdroid_gki-android14-6.1/vm_config_crash.json
+++ /dev/null
@@ -1,9 +0,0 @@
-{
-  "os": {
-    "name": "microdroid_gki-android14-6.1"
-  },
-  "task": {
-    "type": "microdroid_launcher",
-    "command": "MicrodroidCrashNativeLib.so"
-  }
-}
diff --git a/tests/testapk/assets/microdroid_gki-android14-6.1/vm_config_extra_apk.json b/tests/testapk/assets/microdroid_gki-android14-6.1/vm_config_extra_apk.json
deleted file mode 100644
index 1602294..0000000
--- a/tests/testapk/assets/microdroid_gki-android14-6.1/vm_config_extra_apk.json
+++ /dev/null
@@ -1,15 +0,0 @@
-{
-  "os": {
-    "name": "microdroid_gki-android14-6.1"
-  },
-  "task": {
-    "type": "microdroid_launcher",
-    "command": "MicrodroidTestNativeLib.so"
-  },
-  "extra_apks": [
-    {
-      "path": "/system/etc/security/fsverity/BuildManifest.apk"
-    }
-  ],
-  "export_tombstones": true
-}
diff --git a/tests/testapk/assets/microdroid_gki-android14-6.1/vm_config_no_task.json b/tests/testapk/assets/microdroid_gki-android14-6.1/vm_config_no_task.json
deleted file mode 100644
index 8282f99..0000000
--- a/tests/testapk/assets/microdroid_gki-android14-6.1/vm_config_no_task.json
+++ /dev/null
@@ -1,6 +0,0 @@
-{
-  "os": {
-    "name": "microdroid_gki-android14-6.1"
-  },
-  "export_tombstones": true
-}
diff --git a/tests/testapk/assets/microdroid_gki-android14-6.1/vm_config_no_tombstone.json b/tests/testapk/assets/microdroid_gki-android14-6.1/vm_config_no_tombstone.json
deleted file mode 100644
index 6e8a136..0000000
--- a/tests/testapk/assets/microdroid_gki-android14-6.1/vm_config_no_tombstone.json
+++ /dev/null
@@ -1,10 +0,0 @@
-{
-  "os": {
-    "name": "microdroid_gki-android14-6.1"
-  },
-  "task": {
-    "type": "microdroid_launcher",
-    "command": "MicrodroidTestNativeLib.so"
-  },
-  "export_tombstones": false
-}
diff --git a/tests/testapk/assets/microdroid/vm_config.json b/tests/testapk/assets/vm_config.json
similarity index 76%
rename from tests/testapk/assets/microdroid/vm_config.json
rename to tests/testapk/assets/vm_config.json
index d12eb5c..90945a5 100644
--- a/tests/testapk/assets/microdroid/vm_config.json
+++ b/tests/testapk/assets/vm_config.json
@@ -1,7 +1,4 @@
 {
-  "os": {
-    "name": "microdroid"
-  },
   "task": {
     "type": "microdroid_launcher",
     "command": "MicrodroidTestNativeLib.so"
diff --git a/tests/testapk/assets/microdroid/vm_config_apex.json b/tests/testapk/assets/vm_config_apex.json
similarity index 87%
rename from tests/testapk/assets/microdroid/vm_config_apex.json
rename to tests/testapk/assets/vm_config_apex.json
index c00787f..591bfea 100644
--- a/tests/testapk/assets/microdroid/vm_config_apex.json
+++ b/tests/testapk/assets/vm_config_apex.json
@@ -1,7 +1,4 @@
 {
-  "os": {
-    "name": "microdroid"
-  },
   "task": {
     "type": "microdroid_launcher",
     "command": "MicrodroidTestNativeLib.so"
diff --git a/tests/testapk/assets/microdroid/vm_config_crash.json b/tests/testapk/assets/vm_config_crash.json
similarity index 71%
rename from tests/testapk/assets/microdroid/vm_config_crash.json
rename to tests/testapk/assets/vm_config_crash.json
index ce6af80..ef2a383 100644
--- a/tests/testapk/assets/microdroid/vm_config_crash.json
+++ b/tests/testapk/assets/vm_config_crash.json
@@ -1,7 +1,4 @@
 {
-  "os": {
-    "name": "microdroid"
-  },
   "task": {
     "type": "microdroid_launcher",
     "command": "MicrodroidCrashNativeLib.so"
diff --git a/tests/testapk/assets/microdroid/vm_config_extra_apk.json b/tests/testapk/assets/vm_config_extra_apk.json
similarity index 84%
rename from tests/testapk/assets/microdroid/vm_config_extra_apk.json
rename to tests/testapk/assets/vm_config_extra_apk.json
index b45e57d..3e4bf2d 100644
--- a/tests/testapk/assets/microdroid/vm_config_extra_apk.json
+++ b/tests/testapk/assets/vm_config_extra_apk.json
@@ -1,7 +1,4 @@
 {
-  "os": {
-    "name": "microdroid"
-  },
   "task": {
     "type": "microdroid_launcher",
     "command": "MicrodroidTestNativeLib.so"
diff --git a/tests/testapk/assets/vm_config_no_task.json b/tests/testapk/assets/vm_config_no_task.json
new file mode 100644
index 0000000..b71140e
--- /dev/null
+++ b/tests/testapk/assets/vm_config_no_task.json
@@ -0,0 +1,3 @@
+{
+  "export_tombstones": true
+}
diff --git a/tests/testapk/assets/microdroid/vm_config_no_tombstone.json b/tests/testapk/assets/vm_config_no_tombstone.json
similarity index 76%
rename from tests/testapk/assets/microdroid/vm_config_no_tombstone.json
rename to tests/testapk/assets/vm_config_no_tombstone.json
index 97e764d..a07ec89 100644
--- a/tests/testapk/assets/microdroid/vm_config_no_tombstone.json
+++ b/tests/testapk/assets/vm_config_no_tombstone.json
@@ -1,7 +1,4 @@
 {
-  "os": {
-    "name": "microdroid"
-  },
   "task": {
     "type": "microdroid_launcher",
     "command": "MicrodroidTestNativeLib.so"
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 51aace4..1195cd3 100644
--- a/tests/testapk/src/java/com/android/microdroid/test/MicrodroidTests.java
+++ b/tests/testapk/src/java/com/android/microdroid/test/MicrodroidTests.java
@@ -28,6 +28,8 @@
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.common.truth.Truth.assertWithMessage;
 import static com.google.common.truth.TruthJUnit.assume;
+import com.android.virt.vm_attestation.testservice.IAttestationService.AttestationStatus;
+import com.android.virt.vm_attestation.testservice.IAttestationService.SigningResult;
 
 import static org.junit.Assert.assertThrows;
 import static org.junit.Assert.assertTrue;
@@ -67,6 +69,7 @@
 import com.android.microdroid.testservice.IAppCallback;
 import com.android.microdroid.testservice.ITestService;
 import com.android.microdroid.testservice.IVmCallback;
+import com.android.virt.vm_attestation.util.X509Utils;
 
 import com.google.common.base.Strings;
 import com.google.common.truth.BooleanSubject;
@@ -94,6 +97,7 @@
 import java.nio.file.Files;
 import java.nio.file.Path;
 import java.nio.file.Paths;
+import java.security.cert.X509Certificate;
 import java.time.LocalDateTime;
 import java.time.format.DateTimeFormatter;
 import java.util.ArrayList;
@@ -115,6 +119,9 @@
 @RunWith(Parameterized.class)
 public class MicrodroidTests extends MicrodroidDeviceTestBase {
     private static final String TAG = "MicrodroidTests";
+    private static final String TEST_APP_PACKAGE_NAME = "com.android.microdroid.test";
+    private static final String VM_ATTESTATION_PAYLOAD_PATH = "libvm_attestation_test_payload.so";
+    private static final String VM_ATTESTATION_MESSAGE = "Hello RKP from AVF!";
 
     @Rule public Timeout globalTimeout = Timeout.seconds(300);
 
@@ -210,6 +217,80 @@
 
     @Test
     @CddTest(requirements = {"9.17/C-1-1", "9.17/C-2-1"})
+    public void vmAttestationWhenRemoteAttestationIsNotSupported() throws Exception {
+        // pVM remote attestation is only supported on protected VMs.
+        assumeProtectedVM();
+        assumeFeatureEnabled(VirtualMachineManager.FEATURE_REMOTE_ATTESTATION);
+        assume().withMessage(
+                        "This test does not apply to a device that supports Remote Attestation")
+                .that(getVirtualMachineManager().isRemoteAttestationSupported())
+                .isFalse();
+        VirtualMachineConfig config =
+                newVmConfigBuilderWithPayloadBinary(VM_ATTESTATION_PAYLOAD_PATH)
+                        .setProtectedVm(mProtectedVm)
+                        .setDebugLevel(DEBUG_LEVEL_FULL)
+                        .build();
+        VirtualMachine vm =
+                forceCreateNewVirtualMachine("cts_attestation_with_rkpd_unsupported", config);
+        byte[] challenge = new byte[32];
+        Arrays.fill(challenge, (byte) 0xcc);
+
+        // Act.
+        SigningResult signingResult =
+                runVmAttestationService(TAG, vm, challenge, VM_ATTESTATION_MESSAGE.getBytes());
+
+        // Assert.
+        assertThat(signingResult.status).isEqualTo(AttestationStatus.ATTESTATION_ERROR_UNSUPPORTED);
+    }
+
+    @Test
+    @CddTest(requirements = {"9.17/C-1-1", "9.17/C-2-1"})
+    public void vmAttestationWhenRemoteAttestationIsSupported() throws Exception {
+        // pVM remote attestation is only supported on protected VMs.
+        assumeProtectedVM();
+        assumeFeatureEnabled(VirtualMachineManager.FEATURE_REMOTE_ATTESTATION);
+        assume().withMessage("Test needs Remote Attestation support")
+                .that(getVirtualMachineManager().isRemoteAttestationSupported())
+                .isTrue();
+        VirtualMachineConfig config =
+                newVmConfigBuilderWithPayloadBinary(VM_ATTESTATION_PAYLOAD_PATH)
+                        .setProtectedVm(mProtectedVm)
+                        .setDebugLevel(DEBUG_LEVEL_FULL)
+                        .build();
+        VirtualMachine vm =
+                forceCreateNewVirtualMachine("cts_attestation_with_rkpd_supported", config);
+
+        // Check with an invalid challenge.
+        byte[] invalidChallenge = new byte[65];
+        Arrays.fill(invalidChallenge, (byte) 0xbb);
+        SigningResult signingResultInvalidChallenge =
+                runVmAttestationService(
+                        TAG, vm, invalidChallenge, VM_ATTESTATION_MESSAGE.getBytes());
+        assertThat(signingResultInvalidChallenge.status)
+                .isEqualTo(AttestationStatus.ATTESTATION_ERROR_INVALID_CHALLENGE);
+
+        // Check with a valid challenge.
+        byte[] challenge = new byte[32];
+        Arrays.fill(challenge, (byte) 0xac);
+        SigningResult signingResult =
+                runVmAttestationService(TAG, vm, challenge, VM_ATTESTATION_MESSAGE.getBytes());
+        assertWithMessage(
+                        "VM attestation should either succeed or fail when the network is unstable")
+                .that(signingResult.status)
+                .isAnyOf(
+                        AttestationStatus.ATTESTATION_OK,
+                        AttestationStatus.ATTESTATION_ERROR_ATTESTATION_FAILED);
+        if (signingResult.status == AttestationStatus.ATTESTATION_OK) {
+            X509Certificate[] certs =
+                    X509Utils.validateAndParseX509CertChain(signingResult.certificateChain);
+            X509Utils.verifyAvfRelatedCerts(certs, challenge, TEST_APP_PACKAGE_NAME);
+            X509Utils.verifySignature(
+                    certs[0], VM_ATTESTATION_MESSAGE.getBytes(), signingResult.signature);
+        }
+    }
+
+    @Test
+    @CddTest(requirements = {"9.17/C-1-1", "9.17/C-2-1"})
     public void createAndRunNoDebugVm() throws Exception {
         assumeSupportedDevice();
 
@@ -469,7 +550,7 @@
         assertThat(minimal.isEncryptedStorageEnabled()).isFalse();
         assertThat(minimal.getEncryptedStorageBytes()).isEqualTo(0);
         assertThat(minimal.isVmOutputCaptured()).isEqualTo(false);
-        assertThat(minimal.getOs()).isNull();
+        assertThat(minimal.getOs()).isEqualTo("microdroid");
 
         // Maximal has everything that can be set to some non-default value. (And has different
         // values than minimal for the required fields.)
@@ -484,7 +565,8 @@
                         .setMemoryBytes(42)
                         .setCpuTopology(CPU_TOPOLOGY_MATCH_HOST)
                         .setEncryptedStorageBytes(1_000_000)
-                        .setVmOutputCaptured(true);
+                        .setVmOutputCaptured(true)
+                        .setOs("microdroid_gki-android14-6.1");
         VirtualMachineConfig maximal = maximalBuilder.build();
 
         assertThat(maximal.getApkPath()).isEqualTo("/apk/path");
@@ -500,16 +582,11 @@
         assertThat(maximal.isEncryptedStorageEnabled()).isTrue();
         assertThat(maximal.getEncryptedStorageBytes()).isEqualTo(1_000_000);
         assertThat(maximal.isVmOutputCaptured()).isEqualTo(true);
-        assertThat(maximal.getOs()).isEqualTo("microdroid");
+        assertThat(maximal.getOs()).isEqualTo("microdroid_gki-android14-6.1");
 
         assertThat(minimal.isCompatibleWith(maximal)).isFalse();
         assertThat(minimal.isCompatibleWith(minimal)).isTrue();
         assertThat(maximal.isCompatibleWith(maximal)).isTrue();
-
-        VirtualMachineConfig os = maximalBuilder.setOs("microdroid_gki-android14-6.1").build();
-        assertThat(os.getPayloadBinaryName()).isEqualTo("binary.so");
-        assertThat(os.getOs()).isEqualTo("microdroid_gki-android14-6.1");
-        assertThat(os.isCompatibleWith(maximal)).isFalse();
     }
 
     @Test
@@ -560,16 +637,6 @@
                         .setVmConsoleInputSupported(true);
         e = assertThrows(IllegalStateException.class, () -> captureInputOnNonDebuggable.build());
         assertThat(e).hasMessageThat().contains("debug level must be FULL to use console input");
-
-        VirtualMachineConfig.Builder specifyBothOsAndConfig =
-                new VirtualMachineConfig.Builder(getContext())
-                        .setPayloadConfigPath("config/path")
-                        .setProtectedVm(mProtectedVm)
-                        .setOs("microdroid");
-        e = assertThrows(IllegalStateException.class, () -> specifyBothOsAndConfig.build());
-        assertThat(e)
-                .hasMessageThat()
-                .contains("setPayloadConfigPath and setOs may not both be called");
     }
 
     @Test
@@ -787,7 +854,7 @@
         revokePermission(VirtualMachine.USE_CUSTOM_VIRTUAL_MACHINE_PERMISSION);
 
         VirtualMachineConfig config =
-                newVmConfigBuilderWithPayloadConfig("assets/" + os() + "/vm_config.json")
+                newVmConfigBuilderWithPayloadConfig("assets/vm_config.json")
                         .setMemoryBytes(minMemoryRequired())
                         .build();
 
@@ -905,7 +972,7 @@
 
         grantPermission(VirtualMachine.USE_CUSTOM_VIRTUAL_MACHINE_PERMISSION);
         VirtualMachineConfig config =
-                newVmConfigBuilderWithPayloadConfig("assets/" + os() + "/vm_config_extra_apk.json")
+                newVmConfigBuilderWithPayloadConfig("assets/vm_config_extra_apk.json")
                         .setMemoryBytes(minMemoryRequired())
                         .setDebugLevel(DEBUG_LEVEL_FULL)
                         .build();
@@ -1009,23 +1076,43 @@
         VirtualMachineConfig normalConfig = builder.build();
         assertThat(tryBootVmWithConfig(normalConfig, "test_vm").payloadStarted).isTrue();
 
-        // Try to run the VM again with the previous instance.img
+        // Try to run the VM again with the previous instance
         // We need to make sure that no changes on config don't invalidate the identity, to compare
         // the result with the below "different debug level" test.
+        File vmInstanceBackup = null, vmIdBackup = null;
         File vmInstance = getVmFile("test_vm", "instance.img");
-        File vmInstanceBackup = File.createTempFile("instance", ".img");
-        Files.copy(vmInstance.toPath(), vmInstanceBackup.toPath(), REPLACE_EXISTING);
+        File vmId = getVmFile("test_vm", "instance_id");
+        if (vmInstance.exists()) {
+            vmInstanceBackup = File.createTempFile("instance", ".img");
+            Files.copy(vmInstance.toPath(), vmInstanceBackup.toPath(), REPLACE_EXISTING);
+        }
+        if (vmId.exists()) {
+            vmIdBackup = File.createTempFile("instance_id", "backup");
+            Files.copy(vmId.toPath(), vmIdBackup.toPath(), REPLACE_EXISTING);
+        }
+
         forceCreateNewVirtualMachine("test_vm", normalConfig);
-        Files.copy(vmInstanceBackup.toPath(), vmInstance.toPath(), REPLACE_EXISTING);
+
+        if (vmInstanceBackup != null) {
+            Files.copy(vmInstanceBackup.toPath(), vmInstance.toPath(), REPLACE_EXISTING);
+        }
+        if (vmIdBackup != null) {
+            Files.copy(vmIdBackup.toPath(), vmId.toPath(), REPLACE_EXISTING);
+        }
         assertThat(tryBootVm(TAG, "test_vm").payloadStarted).isTrue();
 
         // Launch the same VM with a different debug level. The Java API prohibits this
         // (thankfully).
-        // For testing, we do that by creating a new VM with debug level, and copy the old instance
-        // image to the new VM instance image.
+        // For testing, we do that by creating a new VM with debug level, and overwriting the old
+        // instance data to the new VM instance data.
         VirtualMachineConfig debugConfig = builder.setDebugLevel(toLevel).build();
         forceCreateNewVirtualMachine("test_vm", debugConfig);
-        Files.copy(vmInstanceBackup.toPath(), vmInstance.toPath(), REPLACE_EXISTING);
+        if (vmInstanceBackup != null) {
+            Files.copy(vmInstanceBackup.toPath(), vmInstance.toPath(), REPLACE_EXISTING);
+        }
+        if (vmIdBackup != null) {
+            Files.copy(vmIdBackup.toPath(), vmId.toPath(), REPLACE_EXISTING);
+        }
         assertThat(tryBootVm(TAG, "test_vm").payloadStarted).isFalse();
     }
 
@@ -1077,7 +1164,7 @@
 
         grantPermission(VirtualMachine.USE_CUSTOM_VIRTUAL_MACHINE_PERMISSION);
         VirtualMachineConfig normalConfig =
-                newVmConfigBuilderWithPayloadConfig("assets/" + os() + "/vm_config.json")
+                newVmConfigBuilderWithPayloadConfig("assets/vm_config.json")
                         .setDebugLevel(DEBUG_LEVEL_FULL)
                         .build();
         forceCreateNewVirtualMachine("test_vm_a", normalConfig);
@@ -1103,7 +1190,7 @@
 
         grantPermission(VirtualMachine.USE_CUSTOM_VIRTUAL_MACHINE_PERMISSION);
         VirtualMachineConfig normalConfig =
-                newVmConfigBuilderWithPayloadConfig("assets/" + os() + "/vm_config.json")
+                newVmConfigBuilderWithPayloadConfig("assets/vm_config.json")
                         .setDebugLevel(DEBUG_LEVEL_FULL)
                         .build();
         forceCreateNewVirtualMachine("test_vm", normalConfig);
@@ -1123,7 +1210,7 @@
 
         grantPermission(VirtualMachine.USE_CUSTOM_VIRTUAL_MACHINE_PERMISSION);
         VirtualMachineConfig normalConfig =
-                newVmConfigBuilderWithPayloadConfig("assets/" + os() + "/vm_config.json")
+                newVmConfigBuilderWithPayloadConfig("assets/vm_config.json")
                         .setDebugLevel(DEBUG_LEVEL_FULL)
                         .build();
         VirtualMachine vm = forceCreateNewVirtualMachine("bcc_vm", normalConfig);
@@ -1245,6 +1332,8 @@
             "9.17/C-2-7"
     })
     public void bootFailsWhenMicrodroidDataIsCompromised() throws Exception {
+        // If Updatable VM is supported => No instance.img required
+        assumeNoUpdatableVmSupport();
         assertThatBootFailsAfterCompromisingPartition(MICRODROID_PARTITION_UUID);
     }
 
@@ -1254,6 +1343,8 @@
             "9.17/C-2-7"
     })
     public void bootFailsWhenPvmFwDataIsCompromised() throws Exception {
+        // If Updatable VM is supported => No instance.img required
+        assumeNoUpdatableVmSupport();
         if (mProtectedVm) {
             assertThatBootFailsAfterCompromisingPartition(PVM_FW_PARTITION_UUID);
         } else {
@@ -1266,7 +1357,7 @@
     public void bootFailsWhenConfigIsInvalid() throws Exception {
         grantPermission(VirtualMachine.USE_CUSTOM_VIRTUAL_MACHINE_PERMISSION);
         VirtualMachineConfig config =
-                newVmConfigBuilderWithPayloadConfig("assets/" + os() + "/vm_config_no_task.json")
+                newVmConfigBuilderWithPayloadConfig("assets/vm_config_no_task.json")
                         .setDebugLevel(DEBUG_LEVEL_FULL)
                         .build();
 
@@ -1391,7 +1482,7 @@
         // Arrange
         grantPermission(VirtualMachine.USE_CUSTOM_VIRTUAL_MACHINE_PERMISSION);
         VirtualMachineConfig config =
-                newVmConfigBuilderWithPayloadConfig("assets/" + os() + "/vm_config.json")
+                newVmConfigBuilderWithPayloadConfig("assets/vm_config.json")
                         .setDebugLevel(DEBUG_LEVEL_FULL)
                         .build();
         String vmNameOrig = "test_vm_orig";
@@ -1680,9 +1771,7 @@
         assumeSupportedDevice();
 
         final VirtualMachineConfig vmConfig =
-                new VirtualMachineConfig.Builder(getContext())
-                        .setProtectedVm(mProtectedVm)
-                        .setPayloadBinaryName("MicrodroidTestNativeLib.so")
+                newVmConfigBuilderWithPayloadBinary("MicrodroidTestNativeLib.so")
                         .setDebugLevel(DEBUG_LEVEL_FULL)
                         .setVmConsoleInputSupported(true) // even if console input is supported
                         .build();
@@ -1704,9 +1793,7 @@
         assumeSupportedDevice();
 
         final VirtualMachineConfig vmConfig =
-                new VirtualMachineConfig.Builder(getContext())
-                        .setProtectedVm(mProtectedVm)
-                        .setPayloadBinaryName("MicrodroidTestNativeLib.so")
+                newVmConfigBuilderWithPayloadBinary("MicrodroidTestNativeLib.so")
                         .setDebugLevel(DEBUG_LEVEL_FULL)
                         .setVmOutputCaptured(true) // even if output is captured
                         .build();
@@ -1730,6 +1817,7 @@
                         .setPayloadBinaryName("MicrodroidTestNativeLib.so")
                         .setDebugLevel(debuggable ? DEBUG_LEVEL_FULL : DEBUG_LEVEL_NONE)
                         .setVmOutputCaptured(false)
+                        .setOs(os())
                         .build();
         final VirtualMachine vm = forceCreateNewVirtualMachine("test_vm_logcat", vmConfig);
 
@@ -1824,6 +1912,7 @@
                         .setDebugLevel(DEBUG_LEVEL_FULL)
                         .setProtectedVm(isProtectedVm())
                         .setPayloadBinaryName("MicrodroidPayloadInOtherAppNativeLib.so")
+                        .setOs(os())
                         .build();
 
         try (VirtualMachine vm = forceCreateNewVirtualMachine("vm_from_another_app", config)) {
@@ -1938,6 +2027,7 @@
                         .setDebugLevel(DEBUG_LEVEL_FULL)
                         .setProtectedVm(isProtectedVm())
                         .setPayloadBinaryName("MicrodroidPayloadInOtherAppNativeLib.so")
+                        .setOs(os())
                         .build();
 
         VirtualMachine vm = forceCreateNewVirtualMachine("vm_to_share", config);
@@ -1987,6 +2077,7 @@
                         .setProtectedVm(isProtectedVm())
                         .setEncryptedStorageBytes(3_000_000)
                         .setPayloadBinaryName("MicrodroidPayloadInOtherAppNativeLib.so")
+                        .setOs(os())
                         .build();
 
         VirtualMachine vm = forceCreateNewVirtualMachine("vm_to_share", config);
diff --git a/tests/vmshareapp/AndroidManifest.xml b/tests/vmshareapp/AndroidManifest.xml
index b623f7f..9626599 100644
--- a/tests/vmshareapp/AndroidManifest.xml
+++ b/tests/vmshareapp/AndroidManifest.xml
@@ -16,6 +16,7 @@
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
       package="com.android.microdroid.vmshare_app">
     <uses-permission android:name="android.permission.MANAGE_VIRTUAL_MACHINE" />
+    <uses-permission android:name="android.permission.USE_CUSTOM_VIRTUAL_MACHINE" />
 
     <uses-feature android:name="android.software.virtualization_framework"
                   android:required="false" />
diff --git a/virtualizationmanager/src/aidl.rs b/virtualizationmanager/src/aidl.rs
index 6be219e..b8c6315 100644
--- a/virtualizationmanager/src/aidl.rs
+++ b/virtualizationmanager/src/aidl.rs
@@ -70,7 +70,7 @@
 use glob::glob;
 use lazy_static::lazy_static;
 use log::{debug, error, info, warn};
-use microdroid_payload_config::{ApkConfig, OsConfig, Task, TaskType, VmPayloadConfig};
+use microdroid_payload_config::{ApkConfig, Task, TaskType, VmPayloadConfig};
 use nix::unistd::pipe;
 use rpcbinder::RpcServer;
 use rustutils::system_properties;
@@ -313,6 +313,19 @@
     fn enableTestAttestation(&self) -> binder::Result<()> {
         GLOBAL_SERVICE.enableTestAttestation()
     }
+
+    fn isRemoteAttestationSupported(&self) -> binder::Result<bool> {
+        check_manage_access()?;
+        GLOBAL_SERVICE.isRemoteAttestationSupported()
+    }
+
+    fn isUpdatableVmSupported(&self) -> binder::Result<bool> {
+        // The response is specific to Microdroid. Updatable VMs are only possible if device
+        // supports Secretkeeper. Guest OS needs to use Secretkeeper based secrets. Microdroid does
+        // this, however other guest OSes may do things differently.
+        check_manage_access()?;
+        Ok(is_secretkeeper_supported())
+    }
 }
 
 impl VirtualizationService {
@@ -622,13 +635,10 @@
                 // - specifying a config file;
                 // - specifying extra APKs;
                 // - specifying an OS other than Microdroid.
-                match &config.payload {
+                (match &config.payload {
                     Payload::ConfigPath(_) => true,
-                    Payload::PayloadConfig(payload_config) => {
-                        !payload_config.extraApks.is_empty()
-                            || payload_config.osName != MICRODROID_OS_NAME
-                    }
-                }
+                    Payload::PayloadConfig(payload_config) => !payload_config.extraApks.is_empty(),
+                }) || config.osName != MICRODROID_OS_NAME
             }
         }
     }
@@ -813,8 +823,13 @@
         }
     };
 
+    let payload_config_os = vm_payload_config.os.name.as_str();
+    if !payload_config_os.is_empty() && payload_config_os != "microdroid" {
+        bail!("'os' in payload config is deprecated");
+    }
+
     // For now, the only supported OS is Microdroid and Microdroid GKI
-    let os_name = vm_payload_config.os.name.as_str();
+    let os_name = config.osName.as_str();
     if !is_valid_os(os_name) {
         bail!("Unknown OS \"{}\"", os_name);
     }
@@ -916,22 +931,13 @@
     }
 
     let task = Task { type_: TaskType::MicrodroidLauncher, command: payload_binary_name.clone() };
-    let name = payload_config.osName.clone();
 
     // The VM only cares about how many there are, these names are actually ignored.
     let extra_apk_count = payload_config.extraApks.len();
     let extra_apks =
         (0..extra_apk_count).map(|i| ApkConfig { path: format!("extra-apk-{i}") }).collect();
 
-    Ok(VmPayloadConfig {
-        os: OsConfig { name },
-        task: Some(task),
-        apexes: vec![],
-        extra_apks,
-        prefer_staged: false,
-        export_tombstones: None,
-        enable_authfs: false,
-    })
+    Ok(VmPayloadConfig { task: Some(task), extra_apks, ..Default::default() })
 }
 
 /// Generates a unique filename to use for a composite disk image.
@@ -1515,11 +1521,8 @@
 }
 
 fn is_secretkeeper_supported() -> bool {
-    // TODO(b/327526008): Session establishment wth secretkeeper is failing.
-    // Re-enable this when fixed.
-    let _sk_supported = binder::is_declared(SECRETKEEPER_IDENTIFIER)
-        .expect("Could not check for declared Secretkeeper interface");
-    false
+    binder::is_declared(SECRETKEEPER_IDENTIFIER)
+        .expect("Could not check for declared Secretkeeper interface")
 }
 
 impl VirtualMachineService {
diff --git a/virtualizationservice/aidl/android/system/virtualizationservice/AssignableDevice.aidl b/virtualizationservice/aidl/android/system/virtualizationservice/AssignableDevice.aidl
index 014d78c..20114d7 100644
--- a/virtualizationservice/aidl/android/system/virtualizationservice/AssignableDevice.aidl
+++ b/virtualizationservice/aidl/android/system/virtualizationservice/AssignableDevice.aidl
@@ -21,6 +21,6 @@
     /** Path to SysFS node of the device. */
     String node;
 
-    /** Kind of the device. */
-    String kind;
+    /** DTBO label. */
+    String dtbo_label;
 }
diff --git a/virtualizationservice/aidl/android/system/virtualizationservice/IVirtualizationService.aidl b/virtualizationservice/aidl/android/system/virtualizationservice/IVirtualizationService.aidl
index e11d8b8..462932c 100644
--- a/virtualizationservice/aidl/android/system/virtualizationservice/IVirtualizationService.aidl
+++ b/virtualizationservice/aidl/android/system/virtualizationservice/IVirtualizationService.aidl
@@ -86,4 +86,16 @@
      * associated to the fake key pair when the VM requests attestation in testing mode.
      */
     void enableTestAttestation();
+
+    /**
+     * Returns {@code true} if the pVM remote attestation feature is supported
+     */
+    boolean isRemoteAttestationSupported();
+
+    /**
+     * Check if Updatable VM feature is supported by AVF. Updatable VM allows secrets and data of
+     * a VM instance to be accessible even after updates of boot images and apks.
+     * For more info see packages/modules/Virtualization/docs/updatable_vm.md
+     */
+    boolean isUpdatableVmSupported();
 }
diff --git a/virtualizationservice/aidl/android/system/virtualizationservice/VirtualMachineAppConfig.aidl b/virtualizationservice/aidl/android/system/virtualizationservice/VirtualMachineAppConfig.aidl
index 29232ff..890535b 100644
--- a/virtualizationservice/aidl/android/system/virtualizationservice/VirtualMachineAppConfig.aidl
+++ b/virtualizationservice/aidl/android/system/virtualizationservice/VirtualMachineAppConfig.aidl
@@ -62,6 +62,15 @@
     /** Detailed configuration for the VM, specifying how the payload will be run. */
     Payload payload;
 
+    /**
+     * Name of the OS to run the payload. Currently "microdroid" and
+     * "microdroid_gki-android14-6.1" is supported.
+     *
+     * <p>Setting this field to a value other than "microdroid" requires
+     * android.permission.USE_CUSTOM_VIRTUAL_MACHINE
+     */
+    @utf8InCpp String osName = "microdroid";
+
     enum DebugLevel {
         /** Not debuggable at all */
         NONE,
diff --git a/virtualizationservice/aidl/android/system/virtualizationservice/VirtualMachinePayloadConfig.aidl b/virtualizationservice/aidl/android/system/virtualizationservice/VirtualMachinePayloadConfig.aidl
index 7ca5b62..efd363c 100644
--- a/virtualizationservice/aidl/android/system/virtualizationservice/VirtualMachinePayloadConfig.aidl
+++ b/virtualizationservice/aidl/android/system/virtualizationservice/VirtualMachinePayloadConfig.aidl
@@ -24,11 +24,6 @@
      */
     @utf8InCpp String payloadBinaryName;
 
-    /**
-     * Name of the OS to run the payload. Currently "microdroid" and "microdroid_gki" is supported.
-     */
-    @utf8InCpp String osName = "microdroid";
-
     /** Any extra APKs. */
     List<ParcelFileDescriptor> extraApks;
 }
diff --git a/virtualizationservice/aidl/android/system/virtualizationservice_internal/IVirtualizationServiceInternal.aidl b/virtualizationservice/aidl/android/system/virtualizationservice_internal/IVirtualizationServiceInternal.aidl
index 8af881b..16975ee 100644
--- a/virtualizationservice/aidl/android/system/virtualizationservice_internal/IVirtualizationServiceInternal.aidl
+++ b/virtualizationservice/aidl/android/system/virtualizationservice_internal/IVirtualizationServiceInternal.aidl
@@ -77,6 +77,11 @@
     void enableTestAttestation();
 
     /**
+     * Returns {@code true} if the pVM remote attestation feature is supported
+     */
+    boolean isRemoteAttestationSupported();
+
+    /**
      * Get a list of assignable devices.
      */
     AssignableDevice[] getAssignableDevices();
@@ -103,4 +108,12 @@
      * @param instanceId The ID for the VM.
      */
     void removeVmInstance(in byte[64] instanceId);
+
+    /**
+     * Notification that ownership of a VM has been claimed by the caller.  Note that no permission
+     * checks (with respect to the previous owner) are performed.
+     *
+     * @param instanceId The ID for the VM.
+     */
+    void claimVmInstance(in byte[64] instanceId);
 }
diff --git a/virtualizationservice/src/aidl.rs b/virtualizationservice/src/aidl.rs
index 2fe14c0..c6150b2 100644
--- a/virtualizationservice/src/aidl.rs
+++ b/virtualizationservice/src/aidl.rs
@@ -269,6 +269,13 @@
             .context("Failed to generate ECDSA P-256 key pair for testing")
             .with_log()
             .or_service_specific_exception(-1)?;
+        // Wait until the service VM shuts down, so that the Service VM will be restarted when
+        // the key generated in the current session will be used for attestation.
+        // This ensures that different Service VM sessions have the same KEK for the key blob.
+        service_vm_manager::wait_until_service_vm_shuts_down()
+            .context("Failed to wait until the service VM shuts down")
+            .with_log()
+            .or_service_specific_exception(-1)?;
         match res {
             Response::GenerateEcdsaP256KeyPair(key_pair) => {
                 FAKE_PROVISIONED_KEY_BLOB_FOR_TESTING
@@ -299,6 +306,13 @@
             ))
             .with_log();
         }
+        if !remotely_provisioned_component_service_exists()? {
+            return Err(Status::new_exception_str(
+                ExceptionCode::UNSUPPORTED_OPERATION,
+                Some("AVF remotely provisioned component service is not declared"),
+            ))
+            .with_log();
+        }
         info!("Received csr. Requestting attestation...");
         let (key_blob, certificate_chain) = if test_mode {
             check_use_custom_virtual_machine()?;
@@ -348,13 +362,17 @@
         Ok(certificate_chain)
     }
 
+    fn isRemoteAttestationSupported(&self) -> binder::Result<bool> {
+        remotely_provisioned_component_service_exists()
+    }
+
     fn getAssignableDevices(&self) -> binder::Result<Vec<AssignableDevice>> {
         check_use_custom_virtual_machine()?;
 
         Ok(get_assignable_devices()?
             .device
             .into_iter()
-            .map(|x| AssignableDevice { node: x.sysfs_path, kind: x.kind })
+            .map(|x| AssignableDevice { node: x.sysfs_path, dtbo_label: x.dtbo_label })
             .collect::<Vec<_>>())
     }
 
@@ -399,7 +417,7 @@
         if let Some(sk_state) = &mut state.sk_state {
             let user_id = multiuser_get_user_id(uid);
             let app_id = multiuser_get_app_id(uid);
-            info!("Recording potential existence of state for (user_id={user_id}, app_id={app_id}");
+            info!("Recording possible existence of state for (user_id={user_id}, app_id={app_id})");
             if let Err(e) = sk_state.add_id(&id, user_id, app_id) {
                 error!("Failed to record the instance_id: {e:?}");
             }
@@ -418,6 +436,28 @@
         }
         Ok(())
     }
+
+    fn claimVmInstance(&self, instance_id: &[u8; 64]) -> binder::Result<()> {
+        let state = &mut *self.state.lock().unwrap();
+        if let Some(sk_state) = &mut state.sk_state {
+            let uid = get_calling_uid();
+            info!(
+                "Claiming a VM's instance_id: {:?}, for uid: {:?}",
+                hex::encode(instance_id),
+                uid
+            );
+
+            let user_id = multiuser_get_user_id(uid);
+            let app_id = multiuser_get_app_id(uid);
+            info!("Recording possible new owner of state for (user_id={user_id}, app_id={app_id})");
+            if let Err(e) = sk_state.add_id(instance_id, user_id, app_id) {
+                error!("Failed to update the instance_id owner: {e:?}");
+            }
+        } else {
+            info!("ignoring claimVmInstance() as no ISecretkeeper");
+        }
+        Ok(())
+    }
 }
 
 impl IVirtualizationMaintenance for VirtualizationServiceInternal {
@@ -445,17 +485,21 @@
 
     fn performReconciliation(
         &self,
-        _callback: &Strong<dyn IVirtualizationReconciliationCallback>,
+        callback: &Strong<dyn IVirtualizationReconciliationCallback>,
     ) -> binder::Result<()> {
-        Err(anyhow!("performReconciliation not supported"))
-            .or_binder_exception(ExceptionCode::UNSUPPORTED_OPERATION)
+        let state = &mut *self.state.lock().unwrap();
+        if let Some(sk_state) = &mut state.sk_state {
+            info!("performReconciliation()");
+            sk_state.reconcile(callback).or_service_specific_exception(-1)?;
+        } else {
+            info!("ignoring performReconciliation()");
+        }
+        Ok(())
     }
 }
 
-// KEEP IN SYNC WITH assignable_devices.xsd
 #[derive(Debug, Deserialize)]
 struct Device {
-    kind: String,
     dtbo_label: String,
     sysfs_path: String,
 }
@@ -763,6 +807,10 @@
     Ok(())
 }
 
+fn remotely_provisioned_component_service_exists() -> binder::Result<bool> {
+    Ok(binder::is_declared(REMOTELY_PROVISIONED_COMPONENT_SERVICE_NAME)?)
+}
+
 /// Checks whether the caller has a specific permission
 fn check_permission(perm: &str) -> binder::Result<()> {
     let calling_pid = get_calling_pid();
diff --git a/virtualizationservice/src/maintenance.rs b/virtualizationservice/src/maintenance.rs
index 0a367c5..f950db9 100644
--- a/virtualizationservice/src/maintenance.rs
+++ b/virtualizationservice/src/maintenance.rs
@@ -15,12 +15,20 @@
 use android_hardware_security_secretkeeper::aidl::android::hardware::security::secretkeeper::{
     ISecretkeeper::ISecretkeeper, SecretId::SecretId,
 };
-use anyhow::{Context, Result};
+use android_system_virtualizationmaintenance::aidl::android::system::virtualizationmaintenance;
+use anyhow::{anyhow, Context, Result};
+use binder::Strong;
 use log::{error, info, warn};
+use virtualizationmaintenance::IVirtualizationReconciliationCallback::IVirtualizationReconciliationCallback;
 
 mod vmdb;
 use vmdb::{VmId, VmIdDb};
 
+/// Indicate whether an app ID belongs to a system core component.
+fn core_app_id(app_id: i32) -> bool {
+    app_id < 10000
+}
+
 /// Interface name for the Secretkeeper HAL.
 const SECRETKEEPER_SERVICE: &str = "android.hardware.security.secretkeeper.ISecretkeeper/default";
 
@@ -88,7 +96,8 @@
         }
     }
 
-    /// Record a new VM ID.
+    /// Record a new VM ID.  If there is an existing owner (user_id, app_id) for the VM ID,
+    /// it will be replaced.
     pub fn add_id(&mut self, vm_id: &VmId, user_id: u32, app_id: u32) -> Result<()> {
         let user_id: i32 = user_id.try_into().context(format!("user_id {user_id} out of range"))?;
         let app_id: i32 = app_id.try_into().context(format!("app_id {app_id} out of range"))?;
@@ -140,6 +149,79 @@
             error!("failed to remove secret IDs from database: {e:?}");
         }
     }
+
+    /// Perform reconciliation to allow for possibly missed notifications of user or app removal.
+    pub fn reconcile(
+        &mut self,
+        callback: &Strong<dyn IVirtualizationReconciliationCallback>,
+    ) -> Result<()> {
+        // First, retrieve all (user_id, app_id) pairs that own a VM.
+        let owners = self.vm_id_db.get_all_owners().context("failed to retrieve owners from DB")?;
+        if owners.is_empty() {
+            info!("no VM owners, nothing to do");
+            return Ok(());
+        }
+
+        // Look for absent users.
+        let mut users: Vec<i32> = owners.iter().map(|(u, _a)| *u).collect();
+        users.sort();
+        users.dedup();
+        let users_exist = callback
+            .doUsersExist(&users)
+            .context(format!("failed to determine if {} users exist", users.len()))?;
+        if users_exist.len() != users.len() {
+            error!("callback returned {} bools for {} inputs!", users_exist.len(), users.len());
+            return Err(anyhow!("unexpected number of results from callback"));
+        }
+
+        for (user_id, present) in users.into_iter().zip(users_exist.into_iter()) {
+            if present {
+                // User is still present, but are all of the associated apps?
+                let mut apps: Vec<i32> = owners
+                    .iter()
+                    .filter_map(|(u, a)| if *u == user_id { Some(*a) } else { None })
+                    .collect();
+                apps.sort();
+                apps.dedup();
+
+                let apps_exist = callback
+                    .doAppsExist(user_id, &apps)
+                    .context(format!("failed to check apps for user {user_id}"))?;
+                if apps_exist.len() != apps.len() {
+                    error!(
+                        "callback returned {} bools for {} inputs!",
+                        apps_exist.len(),
+                        apps.len()
+                    );
+                    return Err(anyhow!("unexpected number of results from callback"));
+                }
+
+                let missing_apps: Vec<i32> = apps
+                    .iter()
+                    .zip(apps_exist.into_iter())
+                    .filter_map(|(app_id, present)| if present { None } else { Some(*app_id) })
+                    .collect();
+
+                for app_id in missing_apps {
+                    if core_app_id(app_id) {
+                        info!("Skipping deletion for core app {app_id} for user {user_id}");
+                        continue;
+                    }
+                    info!("App {app_id} for user {user_id} absent, deleting associated VM IDs");
+                    if let Err(err) = self.delete_ids_for_app(user_id, app_id) {
+                        error!("Failed to delete VM ID for user {user_id} app {app_id}: {err:?}");
+                    }
+                }
+            } else {
+                info!("user {user_id} no longer present, deleting associated VM IDs");
+                if let Err(err) = self.delete_ids_for_user(user_id) {
+                    error!("Failed to delete VM IDs for user {user_id} : {err:?}");
+                }
+            }
+        }
+
+        Ok(())
+    }
 }
 
 #[cfg(test)]
@@ -152,6 +234,9 @@
     use android_hardware_security_secretkeeper::aidl::android::hardware::security::secretkeeper::{
         ISecretkeeper::BnSecretkeeper
     };
+    use virtualizationmaintenance::IVirtualizationReconciliationCallback::{
+        BnVirtualizationReconciliationCallback
+    };
 
     /// Fake implementation of Secretkeeper that keeps a history of what operations were invoked.
     #[derive(Default)]
@@ -195,12 +280,35 @@
         State { sk, vm_id_db, batch_size }
     }
 
+    struct Reconciliation {
+        gone_users: Vec<i32>,
+        gone_apps: Vec<i32>,
+    }
+
+    impl IVirtualizationReconciliationCallback for Reconciliation {
+        fn doUsersExist(&self, user_ids: &[i32]) -> binder::Result<Vec<bool>> {
+            Ok(user_ids.iter().map(|user_id| !self.gone_users.contains(user_id)).collect())
+        }
+        fn doAppsExist(&self, _user_id: i32, app_ids: &[i32]) -> binder::Result<Vec<bool>> {
+            Ok(app_ids.iter().map(|app_id| !self.gone_apps.contains(app_id)).collect())
+        }
+    }
+    impl binder::Interface for Reconciliation {}
+
     const VM_ID1: VmId = [1u8; 64];
     const VM_ID2: VmId = [2u8; 64];
     const VM_ID3: VmId = [3u8; 64];
     const VM_ID4: VmId = [4u8; 64];
     const VM_ID5: VmId = [5u8; 64];
 
+    const USER1: i32 = 1;
+    const USER2: i32 = 2;
+    const USER3: i32 = 3;
+    const APP_A: i32 = 10050;
+    const APP_B: i32 = 10060;
+    const APP_C: i32 = 10070;
+    const CORE_APP_A: i32 = 45;
+
     #[test]
     fn test_sk_state_batching() {
         let history = Arc::new(Mutex::new(Vec::new()));
@@ -228,13 +336,6 @@
 
     #[test]
     fn test_sk_state() {
-        const USER1: i32 = 1;
-        const USER2: i32 = 2;
-        const USER3: i32 = 3;
-        const APP_A: i32 = 50;
-        const APP_B: i32 = 60;
-        const APP_C: i32 = 70;
-
         let history = Arc::new(Mutex::new(Vec::new()));
         let mut sk_state = new_test_state(history.clone(), 2);
 
@@ -242,7 +343,7 @@
         sk_state.vm_id_db.add_vm_id(&VM_ID2, USER1, APP_A).unwrap();
         sk_state.vm_id_db.add_vm_id(&VM_ID3, USER2, APP_B).unwrap();
         sk_state.vm_id_db.add_vm_id(&VM_ID4, USER3, APP_A).unwrap();
-        sk_state.vm_id_db.add_vm_id(&VM_ID5, USER3, APP_C).unwrap();
+        sk_state.vm_id_db.add_vm_id(&VM_ID5, USER3, APP_C).unwrap(); // Overwrites APP_A
         assert_eq!((*history.lock().unwrap()).clone(), vec![]);
 
         sk_state.delete_ids_for_app(USER2, APP_B).unwrap();
@@ -260,4 +361,71 @@
         assert_eq!(empty, sk_state.vm_id_db.vm_ids_for_app(USER2, APP_B).unwrap());
         assert_eq!(empty, sk_state.vm_id_db.vm_ids_for_user(USER3).unwrap());
     }
+
+    #[test]
+    fn test_sk_state_reconcile() {
+        let history = Arc::new(Mutex::new(Vec::new()));
+        let mut sk_state = new_test_state(history.clone(), 20);
+
+        sk_state.vm_id_db.add_vm_id(&VM_ID1, USER1, APP_A).unwrap();
+        sk_state.vm_id_db.add_vm_id(&VM_ID2, USER1, APP_A).unwrap();
+        sk_state.vm_id_db.add_vm_id(&VM_ID3, USER2, APP_B).unwrap();
+        sk_state.vm_id_db.add_vm_id(&VM_ID4, USER2, CORE_APP_A).unwrap();
+        sk_state.vm_id_db.add_vm_id(&VM_ID5, USER3, APP_C).unwrap();
+
+        assert_eq!(vec![VM_ID1, VM_ID2], sk_state.vm_id_db.vm_ids_for_user(USER1).unwrap());
+        assert_eq!(vec![VM_ID1, VM_ID2], sk_state.vm_id_db.vm_ids_for_app(USER1, APP_A).unwrap());
+        assert_eq!(vec![VM_ID3], sk_state.vm_id_db.vm_ids_for_app(USER2, APP_B).unwrap());
+        assert_eq!(vec![VM_ID5], sk_state.vm_id_db.vm_ids_for_user(USER3).unwrap());
+
+        // Perform a reconciliation and pretend that USER1 and [CORE_APP_A, APP_B] are gone.
+        let reconciliation =
+            Reconciliation { gone_users: vec![USER1], gone_apps: vec![CORE_APP_A, APP_B] };
+        let callback = BnVirtualizationReconciliationCallback::new_binder(
+            reconciliation,
+            binder::BinderFeatures::default(),
+        );
+        sk_state.reconcile(&callback).unwrap();
+
+        let empty: Vec<VmId> = Vec::new();
+        assert_eq!(empty, sk_state.vm_id_db.vm_ids_for_user(USER1).unwrap());
+        assert_eq!(empty, sk_state.vm_id_db.vm_ids_for_app(USER1, APP_A).unwrap());
+        // VM for core app stays even though it's reported as absent.
+        assert_eq!(vec![VM_ID4], sk_state.vm_id_db.vm_ids_for_user(USER2).unwrap());
+        assert_eq!(empty, sk_state.vm_id_db.vm_ids_for_app(USER2, APP_B).unwrap());
+        assert_eq!(vec![VM_ID5], sk_state.vm_id_db.vm_ids_for_user(USER3).unwrap());
+    }
+
+    struct Irreconcilable;
+
+    impl IVirtualizationReconciliationCallback for Irreconcilable {
+        fn doUsersExist(&self, user_ids: &[i32]) -> binder::Result<Vec<bool>> {
+            panic!("doUsersExist called with {user_ids:?}");
+        }
+        fn doAppsExist(&self, user_id: i32, app_ids: &[i32]) -> binder::Result<Vec<bool>> {
+            panic!("doAppsExist called with {user_id:?}, {app_ids:?}");
+        }
+    }
+    impl binder::Interface for Irreconcilable {}
+
+    #[test]
+    fn test_sk_state_reconcile_not_needed() {
+        let history = Arc::new(Mutex::new(Vec::new()));
+        let mut sk_state = new_test_state(history.clone(), 20);
+
+        sk_state.vm_id_db.add_vm_id(&VM_ID1, USER1, APP_A).unwrap();
+        sk_state.vm_id_db.add_vm_id(&VM_ID2, USER1, APP_A).unwrap();
+        sk_state.vm_id_db.add_vm_id(&VM_ID3, USER2, APP_B).unwrap();
+        sk_state.vm_id_db.add_vm_id(&VM_ID5, USER3, APP_C).unwrap();
+        sk_state.delete_ids_for_user(USER1).unwrap();
+        sk_state.delete_ids_for_user(USER2).unwrap();
+        sk_state.delete_ids_for_user(USER3).unwrap();
+
+        // No extant secrets, so reconciliation should not trigger the callback.
+        let callback = BnVirtualizationReconciliationCallback::new_binder(
+            Irreconcilable,
+            binder::BinderFeatures::default(),
+        );
+        sk_state.reconcile(&callback).unwrap();
+    }
 }
diff --git a/virtualizationservice/src/maintenance/vmdb.rs b/virtualizationservice/src/maintenance/vmdb.rs
index ce1e1e7..47704bc 100644
--- a/virtualizationservice/src/maintenance/vmdb.rs
+++ b/virtualizationservice/src/maintenance/vmdb.rs
@@ -265,12 +265,41 @@
         while let Some(row) = rows.next().context("failed row unpack")? {
             match row.get(0) {
                 Ok(vm_id) => vm_ids.push(vm_id),
-                Err(e) => log::error!("failed to parse row: {e:?}"),
+                Err(e) => error!("failed to parse row: {e:?}"),
             }
         }
 
         Ok(vm_ids)
     }
+
+    /// Return all of the `(user_id, app_id)` pairs present in the database.
+    pub fn get_all_owners(&mut self) -> Result<Vec<(i32, i32)>> {
+        let mut stmt = self
+            .conn
+            .prepare("SELECT DISTINCT user_id, app_id FROM main.vmids;")
+            .context("failed to prepare SELECT stmt")?;
+        let mut rows = stmt.query(()).context("query failed")?;
+        let mut owners: Vec<(i32, i32)> = Vec::new();
+        while let Some(row) = rows.next().context("failed row unpack")? {
+            let user_id = match row.get(0) {
+                Ok(v) => v,
+                Err(e) => {
+                    error!("failed to parse row: {e:?}");
+                    continue;
+                }
+            };
+            let app_id = match row.get(1) {
+                Ok(v) => v,
+                Err(e) => {
+                    error!("failed to parse row: {e:?}");
+                    continue;
+                }
+            };
+            owners.push((user_id, app_id));
+        }
+
+        Ok(owners)
+    }
 }
 
 /// Current schema version.
@@ -417,7 +446,13 @@
         db.add_vm_id(&VM_ID3, USER1, APP_A).unwrap();
         db.add_vm_id(&VM_ID4, USER2, APP_B).unwrap();
         db.add_vm_id(&VM_ID5, USER3, APP_A).unwrap();
-        db.add_vm_id(&VM_ID5, USER3, APP_C).unwrap();
+        db.add_vm_id(&VM_ID5, USER3, APP_C).unwrap(); // Overwrites APP_A
+
+        assert_eq!(
+            vec![(USER1, APP_A), (USER2, APP_B), (USER3, APP_C)],
+            db.get_all_owners().unwrap()
+        );
+
         let empty: Vec<VmId> = Vec::new();
 
         assert_eq!(vec![VM_ID1, VM_ID2, VM_ID3], db.vm_ids_for_user(USER1).unwrap());
@@ -447,6 +482,12 @@
         assert_eq!(vec![VM_ID5], db.vm_ids_for_user(USER3).unwrap());
         assert_eq!(empty, db.vm_ids_for_user(USER_UNKNOWN).unwrap());
         assert_eq!(empty, db.vm_ids_for_app(USER1, APP_UNKNOWN).unwrap());
+
+        assert_eq!(
+            vec![(USER1, APP_A), (USER2, APP_B), (USER3, APP_C)],
+            db.get_all_owners().unwrap()
+        );
+
         show_contents(&db);
     }
 
diff --git a/virtualizationservice/src/rkpvm.rs b/virtualizationservice/src/rkpvm.rs
index 67ba740..6898921 100644
--- a/virtualizationservice/src/rkpvm.rs
+++ b/virtualizationservice/src/rkpvm.rs
@@ -21,28 +21,25 @@
 use service_vm_comm::{
     ClientVmAttestationParams, GenerateCertificateRequestParams, Request, Response,
 };
-use service_vm_manager::ServiceVm;
+use service_vm_manager::process_request;
 
 pub(crate) fn request_attestation(
     csr: Vec<u8>,
     remotely_provisioned_key_blob: Vec<u8>,
     remotely_provisioned_cert: Vec<u8>,
 ) -> Result<Vec<u8>> {
-    let mut vm = ServiceVm::start()?;
-
     let params =
         ClientVmAttestationParams { csr, remotely_provisioned_key_blob, remotely_provisioned_cert };
     let request = Request::RequestClientVmAttestation(params);
-    match vm.process_request(request).context("Failed to process request")? {
+    match process_request(request).context("Failed to process request")? {
         Response::RequestClientVmAttestation(cert) => Ok(cert),
         other => bail!("Incorrect response type {other:?}"),
     }
 }
 
 pub(crate) fn generate_ecdsa_p256_key_pair() -> Result<Response> {
-    let mut vm = ServiceVm::start()?;
     let request = Request::GenerateEcdsaP256KeyPair;
-    vm.process_request(request).context("Failed to process request")
+    process_request(request).context("Failed to process request")
 }
 
 pub(crate) fn generate_certificate_request(
@@ -55,6 +52,5 @@
     };
     let request = Request::GenerateCertificateRequest(params);
 
-    let mut vm = ServiceVm::start()?;
-    vm.process_request(request).context("Failed to process request")
+    process_request(request).context("Failed to process request")
 }
diff --git a/vm/src/main.rs b/vm/src/main.rs
index 063f992..b60f2db 100644
--- a/vm/src/main.rs
+++ b/vm/src/main.rs
@@ -30,12 +30,10 @@
 use create_idsig::command_create_idsig;
 use create_partition::command_create_partition;
 use run::{command_run, command_run_app, command_run_microdroid};
+use serde::Serialize;
 use std::num::NonZeroU16;
 use std::path::{Path, PathBuf};
 
-#[derive(Debug)]
-struct Idsigs(Vec<PathBuf>);
-
 #[derive(Args, Default)]
 /// Collection of flags that are at VM level and therefore applicable to all subcommands
 pub struct CommonConfig {
@@ -405,8 +403,17 @@
         println!("VFIO-platform is not supported.");
     }
 
+    #[derive(Serialize)]
+    struct AssignableDevice {
+        node: String,
+        dtbo_label: String,
+    }
+
     let devices = get_service()?.getAssignableDevices()?;
-    let devices = devices.into_iter().map(|x| x.node).collect::<Vec<_>>();
+    let devices: Vec<_> = devices
+        .into_iter()
+        .map(|device| AssignableDevice { node: device.node, dtbo_label: device.dtbo_label })
+        .collect();
     println!("Assignable devices: {}", serde_json::to_string(&devices)?);
 
     let os_list = get_service()?.getSupportedOSList()?;
diff --git a/vm/src/run.rs b/vm/src/run.rs
index 57b7641..07e0276 100644
--- a/vm/src/run.rs
+++ b/vm/src/run.rs
@@ -127,29 +127,25 @@
         if config.payload_binary_name.is_some() {
             bail!("Only one of --config-path or --payload-binary-name can be defined")
         }
-        if config.microdroid.gki().is_some() {
-            bail!("--gki cannot be defined with --config-path. Use 'os' field in the config file")
-        }
         Payload::ConfigPath(config_path)
     } else if let Some(payload_binary_name) = config.payload_binary_name {
-        let os_name = if let Some(ver) = config.microdroid.gki() {
-            format!("microdroid_gki-{ver}")
-        } else {
-            "microdroid".to_owned()
-        };
-
         let extra_apk_files: Result<Vec<_>, _> = extra_apks.iter().map(File::open).collect();
         let extra_apk_fds = extra_apk_files?.into_iter().map(ParcelFileDescriptor::new).collect();
 
         Payload::PayloadConfig(VirtualMachinePayloadConfig {
             payloadBinaryName: payload_binary_name,
-            osName: os_name,
             extraApks: extra_apk_fds,
         })
     } else {
         bail!("Either --config-path or --payload-binary-name must be defined")
     };
 
+    let os_name = if let Some(ver) = config.microdroid.gki() {
+        format!("microdroid_gki-{ver}")
+    } else {
+        "microdroid".to_owned()
+    };
+
     let payload_config_str = format!("{:?}!{:?}", config.apk, payload);
 
     let custom_config = CustomConfig {
@@ -180,6 +176,7 @@
         memoryMib: config.common.mem.unwrap_or(0) as i32, // 0 means use the VM default
         cpuTopology: config.common.cpu_topology,
         customConfig: Some(custom_config),
+        osName: os_name,
     });
     run(
         service.as_ref(),
diff --git a/vmlauncher_app/Android.bp b/vmlauncher_app/Android.bp
new file mode 100644
index 0000000..cd40448
--- /dev/null
+++ b/vmlauncher_app/Android.bp
@@ -0,0 +1,20 @@
+package {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+android_app {
+    name: "VmLauncherApp",
+    srcs: ["java/**/*.java"],
+    resource_dirs: ["res"],
+    static_libs: [
+        "androidx-constraintlayout_constraintlayout",
+        "androidx.appcompat_appcompat",
+        "com.google.android.material_material",
+    ],
+    libs: [
+        "framework-virtualization.impl",
+        "framework-annotations-lib",
+    ],
+    platform_apis: true,
+    privileged: true,
+}
diff --git a/vmlauncher_app/AndroidManifest.xml b/vmlauncher_app/AndroidManifest.xml
new file mode 100644
index 0000000..de9d094
--- /dev/null
+++ b/vmlauncher_app/AndroidManifest.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="com.android.virtualization.vmlauncher" >
+
+    <uses-permission android:name="android.permission.MANAGE_VIRTUAL_MACHINE" />
+    <uses-permission android:name="android.permission.USE_CUSTOM_VIRTUAL_MACHINE" />
+    <uses-feature android:name="android.software.virtualization_framework" android:required="true" />
+    <application
+        android:label="VmLauncherApp">
+        <activity android:name=".MainActivity" android:exported="true">
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN" />
+                <category android:name="android.intent.category.LAUNCHER" />
+            </intent-filter>
+        </activity>
+    </application>
+
+</manifest>
diff --git a/vmlauncher_app/README.md b/vmlauncher_app/README.md
new file mode 100644
index 0000000..9175e57
--- /dev/null
+++ b/vmlauncher_app/README.md
@@ -0,0 +1,16 @@
+# VM launcher app
+
+## Building & Installing
+
+Add `VmLauncherApp` into `PRODUCT_PACKAGES` and then `m`
+
+You can also explicitly grant or revoke the permission, e.g.
+```
+adb shell pm grant com.android.virtualization.vmlauncher android.permission.USE_CUSTOM_VIRTUAL_MACHINE
+adb shell pm grant com.android.virtualization.vmlauncher android.permission.MANAGE_VIRTUAL_MACHINE
+```
+
+## Running
+
+Copy vm config json file to /data/local/tmp/vm_config.json.
+And then, run the app, check log meesage.
diff --git a/vmlauncher_app/java/com/android/virtualization/vmlauncher/MainActivity.java b/vmlauncher_app/java/com/android/virtualization/vmlauncher/MainActivity.java
new file mode 100644
index 0000000..90d7fcc
--- /dev/null
+++ b/vmlauncher_app/java/com/android/virtualization/vmlauncher/MainActivity.java
@@ -0,0 +1,153 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.virtualization.vmlauncher;
+
+import android.app.Activity;
+import android.os.Bundle;
+import android.util.Log;
+import android.system.virtualmachine.VirtualMachine;
+import android.system.virtualmachine.VirtualMachineCallback;
+import android.system.virtualmachine.VirtualMachineConfig;
+import android.system.virtualmachine.VirtualMachineException;
+import android.system.virtualmachine.VirtualMachineManager;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+
+public class MainActivity extends Activity {
+    private static final String TAG = "VmLauncherApp";
+    private static final String VM_NAME = "my_custom_vm";
+    private static final boolean DEBUG = true;
+    private final ExecutorService mExecutorService = Executors.newFixedThreadPool(4);
+    private VirtualMachine mVirtualMachine;
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        setContentView(R.layout.activity_main);
+        VirtualMachineCallback callback =
+                new VirtualMachineCallback() {
+                    // store reference to ExecutorService to avoid race condition
+                    private final ExecutorService mService = mExecutorService;
+
+                    @Override
+                    public void onPayloadStarted(VirtualMachine vm) {
+                        Log.e(TAG, "payload start");
+                    }
+
+                    @Override
+                    public void onPayloadReady(VirtualMachine vm) {
+                        // This check doesn't 100% prevent race condition or UI hang.
+                        // However, it's fine for demo.
+                        if (mService.isShutdown()) {
+                            return;
+                        }
+                        Log.d(TAG, "(Payload is ready. Testing VM service...)");
+                    }
+
+                    @Override
+                    public void onPayloadFinished(VirtualMachine vm, int exitCode) {
+                        // This check doesn't 100% prevent race condition, but is fine for demo.
+                        if (!mService.isShutdown()) {
+                            Log.d(
+                                    TAG,
+                                    String.format("(Payload finished. exit code: %d)", exitCode));
+                        }
+                    }
+
+                    @Override
+                    public void onError(VirtualMachine vm, int errorCode, String message) {
+                        Log.d(
+                                TAG,
+                                String.format(
+                                        "(Error occurred. code: %d, message: %s)",
+                                        errorCode, message));
+                    }
+
+                    @Override
+                    public void onStopped(VirtualMachine vm, int reason) {
+                        Log.e(TAG, "vm stop");
+                    }
+                };
+
+        try {
+            VirtualMachineConfig.Builder builder =
+                    new VirtualMachineConfig.Builder(getApplication());
+            builder.setRawConfigPath("/data/local/tmp/vm_config.json");
+            builder.setProtectedVm(false);
+            if (DEBUG) {
+                builder.setDebugLevel(VirtualMachineConfig.DEBUG_LEVEL_FULL);
+                builder.setVmOutputCaptured(true);
+            }
+            VirtualMachineConfig config = builder.build();
+            VirtualMachineManager vmm =
+                    getApplication().getSystemService(VirtualMachineManager.class);
+            if (vmm == null) {
+                Log.e(TAG, "vmm is null");
+                return;
+            }
+            mVirtualMachine = vmm.getOrCreate(VM_NAME, config);
+            try {
+                mVirtualMachine.setConfig(config);
+            } catch (VirtualMachineException e) {
+                vmm.delete(VM_NAME);
+                mVirtualMachine = vmm.create(VM_NAME, config);
+                Log.e(TAG, "error" + e);
+            }
+
+            Log.d(TAG, "vm start");
+            mVirtualMachine.run();
+            mVirtualMachine.setCallback(Executors.newSingleThreadExecutor(), callback);
+            if (DEBUG) {
+                InputStream console = mVirtualMachine.getConsoleOutput();
+                InputStream log = mVirtualMachine.getLogOutput();
+                mExecutorService.execute(new Reader("console", console));
+                mExecutorService.execute(new Reader("log", log));
+            }
+        } catch (VirtualMachineException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    /** Reads data from an input stream and posts it to the output data */
+    static class Reader implements Runnable {
+        private final String mName;
+        private final InputStream mStream;
+
+        Reader(String name, InputStream stream) {
+            mName = name;
+            mStream = stream;
+        }
+
+        @Override
+        public void run() {
+            try {
+                BufferedReader reader = new BufferedReader(new InputStreamReader(mStream));
+                String line;
+                while ((line = reader.readLine()) != null && !Thread.interrupted()) {
+                    Log.d(TAG, mName + ": " + line);
+                }
+            } catch (IOException e) {
+                Log.e(TAG, "Exception while posting " + mName + " output: " + e.getMessage());
+            }
+        }
+    }
+}
diff --git a/vmlauncher_app/res/layout/activity_main.xml b/vmlauncher_app/res/layout/activity_main.xml
new file mode 100644
index 0000000..5cbda78
--- /dev/null
+++ b/vmlauncher_app/res/layout/activity_main.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:scrollbars="horizontal|vertical"
+    android:textAlignment="textStart"
+    tools:context=".MainActivity">
+
+    <LinearLayout
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:orientation="vertical">
+
+    </LinearLayout>
+
+</androidx.constraintlayout.widget.ConstraintLayout>