Merge "pvmfw: Add DtPathTokens" into main
diff --git a/TEST_MAPPING b/TEST_MAPPING
index f146b4e..ec9042c 100644
--- a/TEST_MAPPING
+++ b/TEST_MAPPING
@@ -12,6 +12,9 @@
       "name": "MicrodroidTestApp"
     },
     {
+      "name": "MicrodroidTestAppNoPerm"
+    },
+    {
       "name": "VmAttestationTestApp"
     },
     {
@@ -54,6 +57,11 @@
     },
     {
       "name": "AVFHostTestCases"
+    },
+    {
+      // TODO(b/325610326): Add this target to presubmit once there is enough
+      // SLO data for it.
+      "name": "AvfRkpdAppIntegrationTests"
     }
   ],
   "postsubmit": [
diff --git a/apex/Android.bp b/apex/Android.bp
index 399ad97..3b5141e 100644
--- a/apex/Android.bp
+++ b/apex/Android.bp
@@ -254,6 +254,7 @@
         "initrd_bootconfig",
         "lpmake",
         "lpunpack",
+        "lz4",
         "simg2img",
     ],
 }
@@ -274,6 +275,7 @@
         "initrd_bootconfig",
         "lpmake",
         "lpunpack",
+        "lz4",
         "sign_virt_apex",
         "simg2img",
     ],
diff --git a/apex/sign_virt_apex.py b/apex/sign_virt_apex.py
index 0b6137b..7c59b54 100644
--- a/apex/sign_virt_apex.py
+++ b/apex/sign_virt_apex.py
@@ -153,12 +153,18 @@
                '--key', key, '--output', output])
 
 
+def is_lz4(args, path):
+    # error 44: Unrecognized header
+    result = RunCommand(args, ['lz4', '-t', path], expected_return_values={0, 44})
+    return result[1] == 0
+
+
 def AvbInfo(args, image_path):
     """Parses avbtool --info image output
 
     Args:
       args: program arguments.
-      image_path: The path to the image.
+      image_path: The path to the image, either raw or lz4 compressed
       descriptor_name: Descriptor name of interest.
 
     Returns:
@@ -169,6 +175,11 @@
     if not os.path.exists(image_path):
         raise ValueError(f'Failed to find image: {image_path}')
 
+    if is_lz4(args, image_path):
+        with tempfile.NamedTemporaryFile() as decompressed_image:
+            RunCommand(args, ['lz4', '-d', '-f', image_path, decompressed_image.name])
+            return AvbInfo(args, decompressed_image.name)
+
     output, ret_code = RunCommand(
         args, ['avbtool', 'info_image', '--image', image_path], expected_return_values={0, 1})
     if ret_code == 1:
@@ -560,11 +571,7 @@
                             wait=[vbmeta_f])
 
     # Re-sign kernel. Note kernel's vbmeta contain addition descriptor from ramdisk(s)
-    def resign_kernel(kernel, initrd_normal, initrd_debug):
-        kernel_file = files[kernel]
-        initrd_normal_file = files[initrd_normal]
-        initrd_debug_file = files[initrd_debug]
-
+    def resign_decompressed_kernel(kernel_file, initrd_normal_file, initrd_debug_file):
         _, kernel_image_descriptors = AvbInfo(args, kernel_file)
         salts = extract_hash_descriptors(
             kernel_image_descriptors, lambda descriptor: descriptor['Salt'])
@@ -580,21 +587,47 @@
               additional_images=[initrd_normal_hashdesc, initrd_debug_hashdesc],
               wait=[initrd_n_f, initrd_d_f])
 
+    def resign_compressed_kernel(kernel_file, initrd_normal_file, initrd_debug_file):
+        # decompress, re-sign, compress again
+        with tempfile.TemporaryDirectory() as work_dir:
+            decompressed_kernel_file = os.path.join(work_dir, os.path.basename(kernel_file))
+            RunCommand(args, ['lz4', '-d', kernel_file, decompressed_kernel_file])
+            resign_decompressed_kernel(decompressed_kernel_file, initrd_normal_file,
+                                       initrd_debug_file).result()
+            RunCommand(args, ['lz4', '-9', '-f', decompressed_kernel_file, kernel_file])
+
+    def resign_kernel(kernel, initrd_normal, initrd_debug):
+        kernel_file = files[kernel]
+        initrd_normal_file = files[initrd_normal]
+        initrd_debug_file = files[initrd_debug]
+
+        # kernel may be compressed with lz4.
+        if is_lz4(args, kernel_file):
+            return Async(resign_compressed_kernel, kernel_file, initrd_normal_file,
+                         initrd_debug_file)
+        else:
+            return resign_decompressed_kernel(kernel_file, initrd_normal_file, initrd_debug_file)
+
     _, original_kernel_descriptors = AvbInfo(args, files['kernel'])
-    resign_kernel_task = resign_kernel('kernel', 'initrd_normal.img', 'initrd_debuggable.img')
+    resign_kernel_tasks = [resign_kernel('kernel', 'initrd_normal.img', 'initrd_debuggable.img')]
+    original_kernels = {"kernel" : original_kernel_descriptors}
 
     for ver in gki_versions:
         if f'gki-{ver}_kernel' in files:
-            resign_kernel(
-                f'gki-{ver}_kernel',
+            kernel_name = f'gki-{ver}_kernel'
+            _, original_kernel_descriptors = AvbInfo(args, files[kernel_name])
+            task = resign_kernel(
+                kernel_name,
                 f'gki-{ver}_initrd_normal.img',
                 f'gki-{ver}_initrd_debuggable.img')
+            resign_kernel_tasks.append(task)
+            original_kernels[kernel_name] = original_kernel_descriptors
 
     # Re-sign rialto if it exists. Rialto only exists in arm64 environment.
     if os.path.exists(files['rialto']):
         update_initrd_digests_task = Async(
-            update_initrd_digests_in_rialto, original_kernel_descriptors, args,
-            files, wait=[resign_kernel_task])
+            update_initrd_digests_of_kernels_in_rialto, original_kernels, args, files,
+            wait=resign_kernel_tasks)
         Async(resign_rialto, args, key, files['rialto'], wait=[update_initrd_digests_task])
 
 def resign_rialto(args, key, rialto_path):
@@ -628,18 +661,7 @@
         f"Value of '{key}' should change for '{context}'" \
         f"Original value: {original[key]}, updated value: {updated[key]}"
 
-def update_initrd_digests_in_rialto(original_descriptors, args, files):
-    _, updated_descriptors = AvbInfo(args, files['kernel'])
-
-    original_digests = extract_hash_descriptors(
-        original_descriptors, lambda x: binascii.unhexlify(x['Digest']))
-    updated_digests = extract_hash_descriptors(
-        updated_descriptors, lambda x: binascii.unhexlify(x['Digest']))
-    assert original_digests.pop("boot") == updated_digests.pop("boot"), \
-        "Hash descriptor of boot should not change for kernel. " \
-        f"Original descriptors: {original_descriptors}, " \
-        f"updated descriptors: {updated_descriptors}"
-
+def update_initrd_digests_of_kernels_in_rialto(original_kernels, args, files):
     # Update the hashes of initrd_normal and initrd_debug in rialto if the
     # bootconfigs in them are updated.
     if args.do_not_update_bootconfigs:
@@ -648,6 +670,26 @@
     with open(files['rialto'], "rb") as file:
         content = file.read()
 
+    for kernel_name, descriptors in original_kernels.items():
+        content = update_initrd_digests_in_rialto(
+            descriptors, args, files, kernel_name, content)
+
+    with open(files['rialto'], "wb") as file:
+        file.write(content)
+
+def update_initrd_digests_in_rialto(
+        original_descriptors, args, files, kernel_name, content):
+    _, updated_descriptors = AvbInfo(args, files[kernel_name])
+
+    original_digests = extract_hash_descriptors(
+        original_descriptors, lambda x: binascii.unhexlify(x['Digest']))
+    updated_digests = extract_hash_descriptors(
+        updated_descriptors, lambda x: binascii.unhexlify(x['Digest']))
+    assert original_digests.pop("boot") == updated_digests.pop("boot"), \
+        "Hash descriptor of boot should not change for " + kernel_name + \
+        f"\nOriginal descriptors: {original_descriptors}, " \
+        f"\nUpdated descriptors: {updated_descriptors}"
+
     # Check that the original and updated digests are different before updating rialto.
     partition_names = {'initrd_normal', 'initrd_debug'}
     assert set(original_digests.keys()) == set(updated_digests.keys()) == partition_names, \
@@ -671,8 +713,7 @@
             f"original digest of the partition {partition_name} not found."
         content = new_content
 
-    with open(files['rialto'], "wb") as file:
-        file.write(content)
+    return content
 
 def extract_hash_descriptors(descriptors, f=lambda x: x):
     return {desc["Partition Name"]: f(desc) for desc in
diff --git a/compos/common/compos_client.rs b/compos/common/compos_client.rs
index abaa74c..077a0ef 100644
--- a/compos/common/compos_client.rs
+++ b/compos/common/compos_client.rs
@@ -72,6 +72,7 @@
     /// Start a new CompOS VM instance using the specified instance image file and parameters.
     pub fn start(
         service: &dyn IVirtualizationService,
+        instance_id: [u8; 64],
         instance_image: File,
         idsig: &Path,
         idsig_manifest_apk: &Path,
@@ -121,6 +122,7 @@
             name: parameters.name.clone(),
             apk: Some(apk_fd),
             idsig: Some(idsig_fd),
+            instanceId: instance_id,
             instanceImage: Some(instance_fd),
             encryptedStorageImage: None,
             payload: Payload::ConfigPath(config_path),
diff --git a/compos/common/lib.rs b/compos/common/lib.rs
index 1f937c9..9d90717 100644
--- a/compos/common/lib.rs
+++ b/compos/common/lib.rs
@@ -39,6 +39,9 @@
 /// tests.
 pub const TEST_INSTANCE_DIR: &str = "test";
 
+/// The file that holds the instance_id of CompOS instance.
+pub const INSTANCE_ID_FILE: &str = "instance_id";
+
 /// The file that holds the instance image for a CompOS instance.
 pub const INSTANCE_IMAGE_FILE: &str = "instance.img";
 
diff --git a/compos/composd/src/instance_starter.rs b/compos/composd/src/instance_starter.rs
index 457520f..76001a4 100644
--- a/compos/composd/src/instance_starter.rs
+++ b/compos/composd/src/instance_starter.rs
@@ -20,13 +20,13 @@
 use android_system_virtualizationservice::aidl::android::system::virtualizationservice::{
     IVirtualizationService::IVirtualizationService, PartitionType::PartitionType,
 };
-use anyhow::{Context, Result};
+use anyhow::{anyhow, Context, Result};
 use binder::{LazyServiceGuard, ParcelFileDescriptor, Strong};
 use compos_aidl_interface::aidl::com::android::compos::ICompOsService::ICompOsService;
 use compos_common::compos_client::{ComposClient, VmParameters};
 use compos_common::{
     COMPOS_DATA_ROOT, IDSIG_FILE, IDSIG_MANIFEST_APK_FILE, IDSIG_MANIFEST_EXT_APK_FILE,
-    INSTANCE_IMAGE_FILE,
+    INSTANCE_ID_FILE, INSTANCE_IMAGE_FILE,
 };
 use log::info;
 use std::fs;
@@ -66,6 +66,7 @@
 pub struct InstanceStarter {
     instance_name: String,
     instance_root: PathBuf,
+    instance_id_file: PathBuf,
     instance_image: PathBuf,
     idsig: PathBuf,
     idsig_manifest_apk: PathBuf,
@@ -77,6 +78,7 @@
     pub fn new(instance_name: &str, vm_parameters: VmParameters) -> Self {
         let instance_root = Path::new(COMPOS_DATA_ROOT).join(instance_name);
         let instance_root_path = instance_root.as_path();
+        let instance_id_file = instance_root_path.join(INSTANCE_ID_FILE);
         let instance_image = instance_root_path.join(INSTANCE_IMAGE_FILE);
         let idsig = instance_root_path.join(IDSIG_FILE);
         let idsig_manifest_apk = instance_root_path.join(IDSIG_MANIFEST_APK_FILE);
@@ -84,6 +86,7 @@
         Self {
             instance_name: instance_name.to_owned(),
             instance_root,
+            instance_id_file,
             instance_image,
             idsig,
             idsig_manifest_apk,
@@ -103,7 +106,10 @@
         // Overwrite any existing instance - it's unlikely to be valid with the current set
         // of APEXes, and finding out it isn't is much more expensive than creating a new one.
         self.create_instance_image(virtualization_service)?;
-
+        // TODO(b/294177871): Ping VS to delete the old instance's secret.
+        if cfg!(llpvm_changes) {
+            self.allocate_instance_id(virtualization_service)?;
+        }
         // Delete existing idsig files. Ignore error in case idsig doesn't exist.
         let _ignored1 = fs::remove_file(&self.idsig);
         let _ignored2 = fs::remove_file(&self.idsig_manifest_apk);
@@ -122,6 +128,14 @@
         &self,
         virtualization_service: &dyn IVirtualizationService,
     ) -> Result<CompOsInstance> {
+        let instance_id: [u8; 64] = if cfg!(llpvm_changes) {
+            fs::read(&self.instance_id_file)?
+                .try_into()
+                .map_err(|_| anyhow!("Failed to get instance_id"))?
+        } else {
+            [0u8; 64]
+        };
+
         let instance_image = fs::OpenOptions::new()
             .read(true)
             .write(true)
@@ -129,6 +143,7 @@
             .context("Failed to open instance image")?;
         let vm_instance = ComposClient::start(
             virtualization_service,
+            instance_id,
             instance_image,
             &self.idsig,
             &self.idsig_manifest_apk,
@@ -164,4 +179,13 @@
             .context("Writing instance image file")?;
         Ok(())
     }
+
+    fn allocate_instance_id(
+        &self,
+        virtualization_service: &dyn IVirtualizationService,
+    ) -> Result<()> {
+        let id = virtualization_service.allocateInstanceId().context("Allocating Instance Id")?;
+        fs::write(&self.instance_id_file, id)?;
+        Ok(())
+    }
 }
diff --git a/compos/verify/verify.rs b/compos/verify/verify.rs
index 567083d..a3f18d5 100644
--- a/compos/verify/verify.rs
+++ b/compos/verify/verify.rs
@@ -18,7 +18,7 @@
 //!  public key. The tool is intended to be run by odsign during boot.
 
 use android_logger::LogId;
-use anyhow::{bail, Context, Result};
+use anyhow::{anyhow, bail, Context, Result};
 use binder::ProcessState;
 use clap::{Parser, ValueEnum};
 use compos_common::compos_client::{ComposClient, VmCpuTopology, VmParameters};
@@ -28,9 +28,10 @@
 };
 use compos_common::{
     COMPOS_DATA_ROOT, CURRENT_INSTANCE_DIR, IDSIG_FILE, IDSIG_MANIFEST_APK_FILE,
-    IDSIG_MANIFEST_EXT_APK_FILE, INSTANCE_IMAGE_FILE, TEST_INSTANCE_DIR,
+    IDSIG_MANIFEST_EXT_APK_FILE, INSTANCE_ID_FILE, INSTANCE_IMAGE_FILE, TEST_INSTANCE_DIR,
 };
 use log::error;
+use std::fs;
 use std::fs::File;
 use std::io::Read;
 use std::panic;
@@ -90,11 +91,17 @@
         bail!("{:?} is not a directory", instance_dir);
     }
 
+    let instance_id_file = instance_dir.join(INSTANCE_ID_FILE);
     let instance_image = instance_dir.join(INSTANCE_IMAGE_FILE);
     let idsig = instance_dir.join(IDSIG_FILE);
     let idsig_manifest_apk = instance_dir.join(IDSIG_MANIFEST_APK_FILE);
     let idsig_manifest_ext_apk = instance_dir.join(IDSIG_MANIFEST_EXT_APK_FILE);
 
+    let instance_id: [u8; 64] = if cfg!(llpvm_changes) {
+        fs::read(instance_id_file)?.try_into().map_err(|_| anyhow!("Failed to get instance_id"))?
+    } else {
+        [0u8; 64]
+    };
     let instance_image = File::open(instance_image).context("Failed to open instance image")?;
 
     let info = artifacts_dir.join("compos.info");
@@ -110,6 +117,7 @@
     let virtualization_service = virtmgr.connect()?;
     let vm_instance = ComposClient::start(
         &*virtualization_service,
+        instance_id,
         instance_image,
         &idsig,
         &idsig_manifest_apk,
diff --git a/java/framework/src/android/system/virtualmachine/VirtualMachine.java b/java/framework/src/android/system/virtualmachine/VirtualMachine.java
index 6b03cfe..a5c8062 100644
--- a/java/framework/src/android/system/virtualmachine/VirtualMachine.java
+++ b/java/framework/src/android/system/virtualmachine/VirtualMachine.java
@@ -190,6 +190,9 @@
     /** Name of the instance image file for a VM. (Not implemented) */
     private static final String INSTANCE_IMAGE_FILE = "instance.img";
 
+    /** Name of the file for a VM containing Id. */
+    private static final String INSTANCE_ID_FILE = "instance_id";
+
     /** Name of the idsig file for a VM */
     private static final String IDSIG_FILE = "idsig";
 
@@ -227,6 +230,9 @@
     /** File that backs the encrypted storage - Will be null if not enabled. */
     @Nullable private final File mEncryptedStoreFilePath;
 
+    /** File that contains the Id. This is NULL iff FEATURE_LLPVM is disabled */
+    @Nullable private final File mInstanceIdPath;
+
     /**
      * Unmodifiable list of extra apks. Apks are specified by the vm config, and corresponding
      * idsigs are to be generated.
@@ -376,6 +382,16 @@
         File thisVmDir = getVmDir(context, mName);
         mVmRootPath = thisVmDir;
         mConfigFilePath = new File(thisVmDir, CONFIG_FILE);
+        try {
+            mInstanceIdPath =
+                    (mVirtualizationService
+                                    .getBinder()
+                                    .isFeatureEnabled(IVirtualizationService.FEATURE_LLPVM_CHANGES))
+                            ? new File(thisVmDir, INSTANCE_ID_FILE)
+                            : null;
+        } catch (RemoteException e) {
+            throw e.rethrowAsRuntimeException();
+        }
         mInstanceFilePath = new File(thisVmDir, INSTANCE_IMAGE_FILE);
         mIdsigFilePath = new File(thisVmDir, IDSIG_FILE);
         mExtraApks = setupExtraApks(context, config, thisVmDir);
@@ -414,6 +430,10 @@
                 VirtualMachineConfig config = VirtualMachineConfig.from(vmDescriptor.getConfigFd());
                 vm = new VirtualMachine(context, name, config, VirtualizationService.getInstance());
                 config.serialize(vm.mConfigFilePath);
+                if (vm.mInstanceIdPath != null) {
+                    vm.importInstanceIdFrom(vmDescriptor.getInstanceIdFd());
+                }
+
                 try {
                     vm.mInstanceFilePath.createNewFile();
                 } catch (IOException e) {
@@ -475,6 +495,21 @@
 
             IVirtualizationService service = vm.mVirtualizationService.getBinder();
 
+            if (vm.mInstanceIdPath != null) {
+                try (FileOutputStream stream = new FileOutputStream(vm.mInstanceIdPath)) {
+                    byte[] id = service.allocateInstanceId();
+                    stream.write(id);
+                } catch (FileNotFoundException e) {
+                    throw new VirtualMachineException("instance_id file missing", e);
+                } catch (IOException e) {
+                    throw new VirtualMachineException("failed to persist instance_id", e);
+                } catch (RemoteException e) {
+                    throw e.rethrowAsRuntimeException();
+                } catch (ServiceSpecificException | IllegalArgumentException e) {
+                    throw new VirtualMachineException("failed to create instance_id", e);
+                }
+            }
+
             try {
                 service.initializeWritablePartition(
                         ParcelFileDescriptor.open(vm.mInstanceFilePath, MODE_READ_WRITE),
@@ -530,6 +565,9 @@
         VirtualMachine vm =
                 new VirtualMachine(context, name, config, VirtualizationService.getInstance());
 
+        if (vm.mInstanceIdPath != null && !vm.mInstanceIdPath.exists()) {
+            throw new VirtualMachineException("instance_id file missing");
+        }
         if (!vm.mInstanceFilePath.exists()) {
             throw new VirtualMachineException("instance image missing");
         }
@@ -547,6 +585,7 @@
             // if a new VM is created with the same name (and files) that's unrelated.
             mWasDeleted = true;
         }
+        // TODO(b/294177871): Request deletion of VM secrets.
         deleteVmDirectory(context, name);
     }
 
@@ -813,7 +852,19 @@
                 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.
@@ -835,7 +886,7 @@
                 }
 
                 try {
-                    createIdSigs(service, appConfig);
+                    createIdSigsAndUpdateConfig(service, appConfig);
                 } catch (FileNotFoundException e) {
                     throw new VirtualMachineException("Failed to generate APK signature", e);
                 }
@@ -850,6 +901,8 @@
                 mVirtualMachine.registerCallback(new CallbackTranslator(service));
                 mContext.registerComponentCallbacks(mMemoryManagementCallbacks);
                 mVirtualMachine.start();
+            } catch (IOException e) {
+                throw new VirtualMachineException("failed to persist files", e);
             } catch (IllegalStateException | ServiceSpecificException e) {
                 throw new VirtualMachineException(e);
             } catch (RemoteException e) {
@@ -858,7 +911,8 @@
         }
     }
 
-    private void createIdSigs(IVirtualizationService service, VirtualMachineAppConfig appConfig)
+    private void createIdSigsAndUpdateConfig(
+            IVirtualizationService service, VirtualMachineAppConfig appConfig)
             throws RemoteException, FileNotFoundException {
         // Fill the idsig file by hashing the apk
         service.createOrUpdateIdsigFile(
@@ -872,11 +926,6 @@
 
         // Re-open idsig files in read-only mode
         appConfig.idsig = ParcelFileDescriptor.open(mIdsigFilePath, MODE_READ_ONLY);
-        appConfig.instanceImage = ParcelFileDescriptor.open(mInstanceFilePath, MODE_READ_WRITE);
-        if (mEncryptedStoreFilePath != null) {
-            appConfig.encryptedStorageImage =
-                    ParcelFileDescriptor.open(mEncryptedStoreFilePath, MODE_READ_WRITE);
-        }
         List<ParcelFileDescriptor> extraIdsigs = new ArrayList<>();
         for (ExtraApkSpec extraApk : mExtraApks) {
             extraIdsigs.add(ParcelFileDescriptor.open(extraApk.idsig, MODE_READ_ONLY));
@@ -1249,6 +1298,9 @@
             try {
                 return new VirtualMachineDescriptor(
                         ParcelFileDescriptor.open(mConfigFilePath, MODE_READ_ONLY),
+                        mInstanceIdPath != null
+                                ? ParcelFileDescriptor.open(mInstanceIdPath, MODE_READ_ONLY)
+                                : null,
                         ParcelFileDescriptor.open(mInstanceFilePath, MODE_READ_ONLY),
                         mEncryptedStoreFilePath != null
                                 ? ParcelFileDescriptor.open(mEncryptedStoreFilePath, MODE_READ_ONLY)
@@ -1384,6 +1436,16 @@
         return extraApks;
     }
 
+    private void importInstanceIdFrom(@NonNull ParcelFileDescriptor instanceIdFd)
+            throws VirtualMachineException {
+        try (FileChannel idOutput = new FileOutputStream(mInstanceIdPath).getChannel();
+                FileChannel idInput = new AutoCloseInputStream(instanceIdFd).getChannel()) {
+            idOutput.transferFrom(idInput, /*position=*/ 0, idInput.size());
+        } catch (IOException e) {
+            throw new VirtualMachineException("failed to copy instance_id", e);
+        }
+    }
+
     private void importInstanceFrom(@NonNull ParcelFileDescriptor instanceFd)
             throws VirtualMachineException {
         try (FileChannel instance = new FileOutputStream(mInstanceFilePath).getChannel();
diff --git a/java/framework/src/android/system/virtualmachine/VirtualMachineDescriptor.java b/java/framework/src/android/system/virtualmachine/VirtualMachineDescriptor.java
index 710925d..ee79256 100644
--- a/java/framework/src/android/system/virtualmachine/VirtualMachineDescriptor.java
+++ b/java/framework/src/android/system/virtualmachine/VirtualMachineDescriptor.java
@@ -40,6 +40,9 @@
 public final class VirtualMachineDescriptor implements Parcelable, AutoCloseable {
     private volatile boolean mClosed = false;
     @NonNull private final ParcelFileDescriptor mConfigFd;
+    // File descriptor of the file containing the instance id - will be null iff
+    // FEATURE_LLPVM_CHANGES is disabled.
+    @Nullable private final ParcelFileDescriptor mInstanceIdFd;
     @NonNull private final ParcelFileDescriptor mInstanceImgFd;
     // File descriptor of the image backing the encrypted storage - Will be null if encrypted
     // storage is not enabled. */
@@ -54,6 +57,7 @@
     public void writeToParcel(@NonNull Parcel out, int flags) {
         checkNotClosed();
         out.writeParcelable(mConfigFd, flags);
+        out.writeParcelable(mInstanceIdFd, flags);
         out.writeParcelable(mInstanceImgFd, flags);
         out.writeParcelable(mEncryptedStoreFd, flags);
     }
@@ -80,6 +84,15 @@
     }
 
     /**
+     * @return File descriptor of the file containing instance_id of the VM.
+     */
+    @Nullable
+    ParcelFileDescriptor getInstanceIdFd() {
+        checkNotClosed();
+        return mInstanceIdFd;
+    }
+
+    /**
      * @return File descriptor of the instance.img of the VM.
      */
     @NonNull
@@ -100,15 +113,18 @@
 
     VirtualMachineDescriptor(
             @NonNull ParcelFileDescriptor configFd,
+            @Nullable ParcelFileDescriptor instanceIdFd,
             @NonNull ParcelFileDescriptor instanceImgFd,
             @Nullable ParcelFileDescriptor encryptedStoreFd) {
         mConfigFd = requireNonNull(configFd);
+        mInstanceIdFd = instanceIdFd;
         mInstanceImgFd = requireNonNull(instanceImgFd);
         mEncryptedStoreFd = encryptedStoreFd;
     }
 
     private VirtualMachineDescriptor(Parcel in) {
         mConfigFd = requireNonNull(readParcelFileDescriptor(in));
+        mInstanceIdFd = readParcelFileDescriptor(in);
         mInstanceImgFd = requireNonNull(readParcelFileDescriptor(in));
         mEncryptedStoreFd = readParcelFileDescriptor(in);
     }
@@ -127,6 +143,7 @@
         mClosed = true;
         // Let the compiler do the work: close everything, throw if any of them fail, skipping null.
         try (mConfigFd;
+                mInstanceIdFd;
                 mInstanceImgFd;
                 mEncryptedStoreFd) {
         } catch (IOException ignored) {
diff --git a/java/service/Android.bp b/java/service/Android.bp
index fdfb203..8bac7be 100644
--- a/java/service/Android.bp
+++ b/java/service/Android.bp
@@ -29,6 +29,9 @@
         "framework",
         "services.core",
     ],
+    static_libs: [
+        "android.system.virtualizationmaintenance-java",
+    ],
     sdk_version: "core_platform",
     apex_available: ["com.android.virt"],
     installable: true,
diff --git a/java/service/src/com/android/system/virtualmachine/SecretkeeperJobService.java b/java/service/src/com/android/system/virtualmachine/SecretkeeperJobService.java
new file mode 100644
index 0000000..473fbfb
--- /dev/null
+++ b/java/service/src/com/android/system/virtualmachine/SecretkeeperJobService.java
@@ -0,0 +1,184 @@
+/*
+ * 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.system.virtualmachine;
+
+import static android.content.pm.PackageManager.MATCH_UNINSTALLED_PACKAGES;
+
+import android.app.job.JobInfo;
+import android.app.job.JobParameters;
+import android.app.job.JobScheduler;
+import android.app.job.JobService;
+import android.content.ComponentName;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.PackageManager.ApplicationInfoFlags;
+import android.os.RemoteException;
+import android.os.ServiceSpecificException;
+import android.os.UserHandle;
+import android.system.virtualizationmaintenance.IVirtualizationMaintenance;
+import android.system.virtualizationmaintenance.IVirtualizationReconciliationCallback;
+import android.util.Log;
+
+import com.android.server.LocalServices;
+import com.android.server.pm.UserManagerInternal;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicReference;
+
+/**
+ * A job scheduler service responsible for triggering the Virtualization Service reconciliation
+ * process when scheduled. The job is scheduled to run once per day while idle and charging.
+ *
+ * <p>The reconciliation process ensures that Secretkeeper secrets belonging to apps or users that
+ * have been removed get deleted.
+ *
+ * @hide
+ */
+public class SecretkeeperJobService extends JobService {
+    private static final String TAG = SecretkeeperJobService.class.getName();
+    private static final String JOBSCHEDULER_NAMESPACE = "VirtualizationSystemService";
+    private static final int JOB_ID = 1;
+    private static final AtomicReference<SecretkeeperJob> sJob = new AtomicReference<>();
+
+    static void scheduleJob(JobScheduler scheduler) {
+        try {
+            ComponentName serviceName =
+                    new ComponentName("android", SecretkeeperJobService.class.getName());
+            scheduler = scheduler.forNamespace(JOBSCHEDULER_NAMESPACE);
+            if (scheduler.schedule(
+                            new JobInfo.Builder(JOB_ID, serviceName)
+                                    // We consume CPU and power
+                                    .setRequiresDeviceIdle(true)
+                                    .setRequiresCharging(true)
+                                    .setPeriodic(24 * 60 * 60 * 1000L)
+                                    .build())
+                    != JobScheduler.RESULT_SUCCESS) {
+                Log.e(TAG, "Unable to schedule job");
+                return;
+            }
+            Log.i(TAG, "Scheduled job");
+        } catch (Exception e) {
+            Log.e(TAG, "Failed to schedule job", e);
+        }
+    }
+
+    @Override
+    public boolean onStartJob(JobParameters params) {
+        Log.i(TAG, "Starting job");
+
+        SecretkeeperJob job = new SecretkeeperJob(getPackageManager());
+        sJob.set(job);
+
+        new Thread("SecretkeeperJob") {
+            @Override
+            public void run() {
+                try {
+                    job.run();
+                    Log.i(TAG, "Job finished");
+                } catch (Exception e) {
+                    Log.e(TAG, "Job failed", e);
+                }
+                sJob.set(null);
+                // We don't reschedule on error, we will try again the next day anyway.
+                jobFinished(params, /*wantReschedule=*/ false);
+            }
+        }.start();
+
+        return true; // Job is running in the background
+    }
+
+    @Override
+    public boolean onStopJob(JobParameters params) {
+        Log.i(TAG, "Stopping job");
+        SecretkeeperJob job = sJob.getAndSet(null);
+        if (job != null) {
+            job.stop();
+        }
+        return false; // Idle jobs get rescheduled anyway
+    }
+
+    private static class SecretkeeperJob {
+        private final UserManagerInternal mUserManager =
+                LocalServices.getService(UserManagerInternal.class);
+        private volatile boolean mStopRequested = false;
+        private PackageManager mPackageManager;
+
+        public SecretkeeperJob(PackageManager packageManager) {
+            mPackageManager = packageManager;
+        }
+
+        public void run() throws RemoteException {
+            IVirtualizationMaintenance maintenance =
+                    VirtualizationSystemService.connectToMaintenanceService();
+            maintenance.performReconciliation(new Callback());
+        }
+
+        public void stop() {
+            mStopRequested = true;
+        }
+
+        class Callback extends IVirtualizationReconciliationCallback.Stub {
+            @Override
+            public boolean[] doUsersExist(int[] userIds) {
+                checkForStop();
+                int[] currentUsers = mUserManager.getUserIds();
+                boolean[] results = new boolean[userIds.length];
+                for (int i = 0; i < userIds.length; i++) {
+                    // The total number of users is likely to be small, so no need to make this
+                    // better than O(N).
+                    for (int user : currentUsers) {
+                        if (user == userIds[i]) {
+                            results[i] = true;
+                            break;
+                        }
+                    }
+                }
+                return results;
+            }
+
+            @Override
+            public boolean[] doAppsExist(int userId, int[] appIds) {
+                checkForStop();
+
+                // If an app has been uninstalled but its data is still present we want to include
+                // it, since that might include a VM which will be used in the future.
+                ApplicationInfoFlags flags = ApplicationInfoFlags.of(MATCH_UNINSTALLED_PACKAGES);
+                List<ApplicationInfo> appInfos =
+                        mPackageManager.getInstalledApplicationsAsUser(flags, userId);
+                int[] currentAppIds = new int[appInfos.size()];
+                for (int i = 0; i < appInfos.size(); i++) {
+                    currentAppIds[i] = UserHandle.getAppId(appInfos.get(i).uid);
+                }
+                Arrays.sort(currentAppIds);
+
+                boolean[] results = new boolean[appIds.length];
+                for (int i = 0; i < appIds.length; i++) {
+                    results[i] = Arrays.binarySearch(currentAppIds, appIds[i]) >= 0;
+                }
+
+                return results;
+            }
+
+            private void checkForStop() {
+                if (mStopRequested) {
+                    throw new ServiceSpecificException(ERROR_STOP_REQUESTED, "Stop requested");
+                }
+            }
+        }
+    }
+}
diff --git a/java/service/src/com/android/system/virtualmachine/VirtualizationSystemService.java b/java/service/src/com/android/system/virtualmachine/VirtualizationSystemService.java
index 2905acd..2461755 100644
--- a/java/service/src/com/android/system/virtualmachine/VirtualizationSystemService.java
+++ b/java/service/src/com/android/system/virtualmachine/VirtualizationSystemService.java
@@ -16,16 +16,124 @@
 
 package com.android.system.virtualmachine;
 
+import android.app.job.JobScheduler;
+import android.content.BroadcastReceiver;
 import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.ServiceManager;
+import android.os.UserHandle;
+import android.system.virtualizationmaintenance.IVirtualizationMaintenance;
+import android.util.Log;
+
+import com.android.internal.os.BackgroundThread;
 import com.android.server.SystemService;
 
-/** TODO */
+/**
+ * This class exists to notify virtualization service of relevant things happening in the Android
+ * framework.
+ *
+ * <p>It currently is responsible for Secretkeeper-related maintenance - ensuring that we are not
+ * storing secrets for apps or users that no longer exist.
+ */
 public class VirtualizationSystemService extends SystemService {
+    private static final String TAG = VirtualizationSystemService.class.getName();
+    private static final String SERVICE_NAME = "android.system.virtualizationmaintenance";
+    private Handler mHandler;
 
     public VirtualizationSystemService(Context context) {
         super(context);
     }
 
     @Override
-    public void onStart() {}
+    public void onStart() {
+        // Nothing needed here - we don't expose any binder service. The binder service we use is
+        // exposed as a lazy service by the virtualizationservice native binary.
+    }
+
+    @Override
+    public void onBootPhase(int phase) {
+        if (phase != PHASE_BOOT_COMPLETED) return;
+
+        mHandler = BackgroundThread.getHandler();
+        new Receiver().registerForBroadcasts();
+
+        SecretkeeperJobService.scheduleJob(getContext().getSystemService(JobScheduler.class));
+    }
+
+    private void notifyAppRemoved(int uid) {
+        try {
+            IVirtualizationMaintenance maintenance = connectToMaintenanceService();
+            maintenance.appRemoved(UserHandle.getUserId(uid), UserHandle.getAppId(uid));
+        } catch (Exception e) {
+            Log.e(TAG, "notifyAppRemoved failed", e);
+        }
+    }
+
+    private void notifyUserRemoved(int userId) {
+        try {
+            IVirtualizationMaintenance maintenance = connectToMaintenanceService();
+            maintenance.userRemoved(userId);
+        } catch (Exception e) {
+            Log.e(TAG, "notifyUserRemoved failed", e);
+        }
+    }
+
+    static IVirtualizationMaintenance connectToMaintenanceService() {
+        IBinder binder = ServiceManager.waitForService(SERVICE_NAME);
+        IVirtualizationMaintenance maintenance =
+                IVirtualizationMaintenance.Stub.asInterface(binder);
+        if (maintenance == null) {
+            throw new IllegalStateException("Failed to connect to " + SERVICE_NAME);
+        }
+        return maintenance;
+    }
+
+    private class Receiver extends BroadcastReceiver {
+        public void registerForBroadcasts() {
+            Context allUsers = getContext().createContextAsUser(UserHandle.ALL, 0 /* flags */);
+
+            allUsers.registerReceiver(this, new IntentFilter(Intent.ACTION_USER_REMOVED));
+
+            IntentFilter packageFilter = new IntentFilter(Intent.ACTION_PACKAGE_REMOVED);
+            packageFilter.addDataScheme("package");
+            allUsers.registerReceiver(this, packageFilter);
+        }
+
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            switch (intent.getAction()) {
+                case Intent.ACTION_USER_REMOVED:
+                    onUserRemoved(intent);
+                    break;
+                case Intent.ACTION_PACKAGE_REMOVED:
+                    onPackageRemoved(intent);
+                    break;
+                default:
+                    Log.e(TAG, "received unexpected intent: " + intent.getAction());
+                    break;
+            }
+        }
+
+        private void onUserRemoved(Intent intent) {
+            int userId = intent.getIntExtra(Intent.EXTRA_USER_HANDLE, UserHandle.USER_NULL);
+            if (userId != UserHandle.USER_NULL) {
+                mHandler.post(() -> notifyUserRemoved(userId));
+            }
+        }
+
+        private void onPackageRemoved(Intent intent) {
+            if (intent.getBooleanExtra(Intent.EXTRA_REPLACING, false)
+                    || !intent.getBooleanExtra(Intent.EXTRA_DATA_REMOVED, false)) {
+                // Package is being updated rather than uninstalled.
+                return;
+            }
+            int uid = intent.getIntExtra(Intent.EXTRA_UID, -1);
+            if (uid != -1) {
+                mHandler.post(() -> notifyAppRemoved(uid));
+            }
+        }
+    }
 }
diff --git a/microdroid/Android.bp b/microdroid/Android.bp
index 36688fc..999dc52 100644
--- a/microdroid/Android.bp
+++ b/microdroid/Android.bp
@@ -559,7 +559,7 @@
 avb_add_hash_footer {
     name: "microdroid_gki-android14-6.1_kernel_signed",
     defaults: ["microdroid_kernel_signed_defaults"],
-    filename: "microdroid_gki-android14-6.1_kernel",
+    filename: "microdroid_gki-android14-6.1_kernel_signed",
     arch: {
         arm64: {
             src: ":microdroid_gki_kernel_prebuilts-6.1-arm64",
@@ -574,13 +574,29 @@
     ],
 }
 
+// HACK: use cc_genrule for arch-specific properties
+cc_genrule {
+    name: "microdroid_gki-android14-6.1_kernel_signed-lz4",
+    out: ["microdroid_gki-android14-6.1_kernel_signed-lz4"],
+    srcs: [":empty_file"],
+    arch: {
+        arm64: {
+            srcs: [":microdroid_gki-android14-6.1_kernel_signed"],
+            exclude_srcs: [":empty_file"],
+        },
+    },
+    tools: ["lz4"],
+    cmd: "$(location lz4) -9 $(in) $(out)",
+}
+
 prebuilt_etc {
     name: "microdroid_gki-android14-6.1_kernel",
+    filename: "microdroid_gki-android14-6.1_kernel",
     src: ":empty_file",
     relative_install_path: "fs",
     arch: {
         arm64: {
-            src: ":microdroid_gki-android14-6.1_kernel_signed",
+            src: ":microdroid_gki-android14-6.1_kernel_signed-lz4",
         },
         x86_64: {
             src: ":microdroid_gki-android14-6.1_kernel_signed",
@@ -605,21 +621,25 @@
     srcs: ["extract_microdroid_kernel_hashes.py"],
 }
 
-genrule {
+// HACK: use cc_genrule for arch-specific properties
+cc_genrule {
     name: "microdroid_kernel_hashes_rs",
-    srcs: [
-        ":microdroid_kernel",
-        ":microdroid_gki-android14-6.1_kernel",
-    ],
+    srcs: [":microdroid_kernel"],
+    arch: {
+        arm64: {
+            srcs: [":microdroid_gki-android14-6.1_kernel_signed"],
+        },
+        x86_64: {
+            srcs: [":microdroid_gki-android14-6.1_kernel_signed"],
+        },
+    },
     out: ["lib.rs"],
     tools: [
         "extract_microdroid_kernel_hashes",
         "avbtool",
     ],
     cmd: "$(location extract_microdroid_kernel_hashes) --avbtool $(location avbtool) " +
-        "--kernel $(location :microdroid_kernel) " +
-        "$(location :microdroid_gki-android14-6.1_kernel) " +
-        "> $(out)",
+        "--kernel $(in) > $(out)",
 }
 
 rust_library_rlib {
diff --git a/microdroid/README.md b/microdroid/README.md
index c82ef0b..6e7b20c 100644
--- a/microdroid/README.md
+++ b/microdroid/README.md
@@ -107,6 +107,7 @@
 PATH_TO_YOUR_APP \
 $TEST_ROOT/MyApp.apk.idsig \
 $TEST_ROOT/instance.img \
+--instance-id-file $TEST_ROOT/instance_id \
 --payload-binary-name MyMicrodroidPayload.so
 ```
 
diff --git a/microdroid_manager/src/main.rs b/microdroid_manager/src/main.rs
index 86284a5..0d67632 100644
--- a/microdroid_manager/src/main.rs
+++ b/microdroid_manager/src/main.rs
@@ -45,8 +45,6 @@
 use microdroid_metadata::PayloadMetadata;
 use microdroid_payload_config::{ApkConfig, OsConfig, Task, TaskType, VmPayloadConfig};
 use nix::sys::signal::Signal;
-use openssl::hkdf::hkdf;
-use openssl::md::Md;
 use payload::load_metadata;
 use rpcbinder::RpcSession;
 use rustutils::sockets::android_get_control_socket;
@@ -73,6 +71,8 @@
 const AVF_DEBUG_POLICY_RAMDUMP: &str = "/proc/device-tree/avf/guest/common/ramdump";
 const DEBUG_MICRODROID_NO_VERIFIED_BOOT: &str =
     "/proc/device-tree/virtualization/guest/debug-microdroid,no-verified-boot";
+const SECRETKEEPER_KEY: &str = "/proc/device-tree/avf/secretkeeper_public_key";
+const INSTANCE_ID_PATH: &str = "/proc/device-tree/avf/untrusted/instance-id";
 
 const ENCRYPTEDSTORE_BIN: &str = "/system/bin/encryptedstore";
 const ZIPFUSE_BIN: &str = "/system/bin/zipfuse";
@@ -145,6 +145,23 @@
     Ok(())
 }
 
+/// The (host allocated) instance_id can be found at node /avf/untrusted/ in the device tree.
+fn get_instance_id() -> Result<Option<[u8; ID_SIZE]>> {
+    let path = Path::new(INSTANCE_ID_PATH);
+    let instance_id = if path.exists() {
+        Some(
+            fs::read(path)?
+                .try_into()
+                .map_err(|x: Vec<_>| anyhow!("Expected {ID_SIZE} bytes, found {:?}", x.len()))?,
+        )
+    } else {
+        // TODO(b/325094712): x86 support for Device tree in nested guest is limited/broken/
+        // untested. So instance_id will not be present in cuttlefish.
+        None
+    };
+    Ok(instance_id)
+}
+
 fn main() -> Result<()> {
     // If debuggable, print full backtrace to console log with stdio_to_kmsg
     if is_debuggable()? {
@@ -284,19 +301,8 @@
     // To minimize the exposure to untrusted data, derive dice profile as soon as possible.
     info!("DICE derivation for payload");
     let dice_artifacts = dice_derivation(dice, &instance_data, &payload_metadata)?;
-    // TODO(b/291213394): This will be the Id for non-pVM only, instance_data.salt is all 0
-    // for protected VM, implement a mechanism for pVM!
-    let mut vm_id = [0u8; ID_SIZE];
-    hkdf(
-        &mut vm_id,
-        Md::sha256(),
-        &instance_data.salt,
-        /* salt */ b"",
-        /* info */ b"VM_ID",
-    )
-    .context("hkdf failed")?;
     let vm_secret =
-        VmSecret::new(vm_id, dice_artifacts, service).context("Failed to create VM secrets")?;
+        VmSecret::new(dice_artifacts, service).context("Failed to create VM secrets")?;
 
     if cfg!(dice_changes) {
         // Now that the DICE derivation is done, it's ok to allow payload code to run.
diff --git a/microdroid_manager/src/vm_secret.rs b/microdroid_manager/src/vm_secret.rs
index 0e1ec71..5ceedea 100644
--- a/microdroid_manager/src/vm_secret.rs
+++ b/microdroid_manager/src/vm_secret.rs
@@ -14,12 +14,12 @@
 
 //! Class for encapsulating & managing represent VM secrets.
 
-use anyhow::{anyhow, ensure, Result};
+use anyhow::{anyhow, ensure, Context, Result};
 use android_system_virtualmachineservice::aidl::android::system::virtualmachineservice::IVirtualMachineService::IVirtualMachineService;
 use android_hardware_security_secretkeeper::aidl::android::hardware::security::secretkeeper::ISecretkeeper::ISecretkeeper;
 use secretkeeper_comm::data_types::request::Request;
 use binder::{Strong};
-use coset::CborSerializable;
+use coset::{CoseKey, CborSerializable, CborOrdering};
 use dice_policy_builder::{CertIndex, ConstraintSpec, ConstraintType, policy_for_dice_chain, MissingAction, WILDCARD_FULL_ARRAY};
 use diced_open_dice::{DiceArtifacts, OwnedDiceArtifacts};
 use keystore2_crypto::ZVec;
@@ -34,6 +34,7 @@
 use secretkeeper_comm::data_types::request_response_impl::{
     StoreSecretRequest, GetSecretResponse, GetSecretRequest};
 use secretkeeper_comm::data_types::error::SecretkeeperError;
+use std::fs;
 use zeroize::Zeroizing;
 
 const ENCRYPTEDSTORE_KEY_IDENTIFIER: &str = "encryptedstore_key";
@@ -58,11 +59,6 @@
     0x55, 0xF8, 0x08, 0x23, 0x81, 0x5F, 0xF5, 0x16, 0x20, 0x3E, 0xBE, 0xBA, 0xB7, 0xA8, 0x43, 0x92,
 ];
 
-const SKP_SECRET_NP_VM: [u8; SECRET_SIZE] = [
-    0xA9, 0x89, 0x97, 0xFE, 0xAE, 0x97, 0x55, 0x4B, 0x32, 0x35, 0xF0, 0xE8, 0x93, 0xDA, 0xEA, 0x24,
-    0x06, 0xAC, 0x36, 0x8B, 0x3C, 0x95, 0x50, 0x16, 0x67, 0x71, 0x65, 0x26, 0xEB, 0xD0, 0xC3, 0x98,
-];
-
 pub enum VmSecret {
     // V2 secrets are derived from 2 independently secured secrets:
     //      1. Secretkeeper protected secrets (skp secret).
@@ -81,41 +77,57 @@
     V1 { dice_artifacts: OwnedDiceArtifacts },
 }
 
+// For supporting V2 secrets, guest expects the public key to be present in the Linux device tree.
+fn get_secretkeeper_identity() -> Result<CoseKey> {
+    let key = fs::read(super::SECRETKEEPER_KEY)?;
+    let mut key = CoseKey::from_slice(&key)?;
+    key.canonicalize(CborOrdering::Lexicographic);
+    Ok(key)
+}
+
 impl VmSecret {
     pub fn new(
-        id: [u8; ID_SIZE],
         dice_artifacts: OwnedDiceArtifacts,
         vm_service: &Strong<dyn IVirtualMachineService>,
     ) -> Result<Self> {
         ensure!(dice_artifacts.bcc().is_some(), "Dice chain missing");
 
-        let Some(sk_service) = is_sk_supported(vm_service)? else {
+        let Some(sk_service) =
+            is_sk_supported(vm_service).context("Failed to check if Secretkeeper is supported")?
+        else {
             // Use V1 secrets if Secretkeeper is not supported.
             return Ok(Self::V1 { dice_artifacts });
         };
-        let explicit_dice =
-            OwnedDiceArtifactsWithExplicitKey::from_owned_artifacts(dice_artifacts)?;
-        let explicit_dice_chain = explicit_dice
-            .explicit_key_dice_chain()
-            .ok_or(anyhow!("Missing explicit dice chain, this is unusual"))?;
-        let policy = sealing_policy(explicit_dice_chain).map_err(anyhow_err)?;
 
-        // Start a new session with Secretkeeper!
-        let mut session = SkSession::new(sk_service, &explicit_dice)?;
+        let explicit_dice = OwnedDiceArtifactsWithExplicitKey::from_owned_artifacts(dice_artifacts)
+            .context("Failed to get Dice artifacts in explicit key format")?;
+        // For pVM, skp_secret are stored in Secretkeeper. For non-protected it is all 0s.
         let mut skp_secret = Zeroizing::new([0u8; SECRET_SIZE]);
         if super::is_strict_boot() {
+            let mut session = SkSession::new(
+                sk_service,
+                &explicit_dice,
+                Some(get_secretkeeper_identity().context("Failed to get secretkeeper identity")?),
+            )
+            .context("Failed to setup a Secretkeeper session")?;
+            let id = super::get_instance_id()
+                .context("Failed to get instance-id")?
+                .ok_or(anyhow!("Missing instance-id"))?;
+            let explicit_dice_chain = explicit_dice
+                .explicit_key_dice_chain()
+                .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)?;
+                store_secret(&mut session, id, skp_secret.clone(), policy)
+                    .context("Failed to store secret in Secretkeeper")?;
             } else {
                 // Subsequent run of the pVM -> get the secret stored in Secretkeeper.
-                *skp_secret = get_secret(&mut session, id, Some(policy))?;
+                *skp_secret = get_secret(&mut session, id, Some(policy))
+                    .context("Failed to get secret from Secretkeeper")?;
             }
-        } else {
-            // TODO(b/291213394): Non protected VM don't need to use Secretkeeper, remove this
-            // once we have sufficient testing on protected VM.
-            store_secret(&mut session, id, SKP_SECRET_NP_VM.into(), policy)?;
-            *skp_secret = get_secret(&mut session, id, None)?;
         }
         Ok(Self::V2 {
             dice_artifacts: explicit_dice,
@@ -273,17 +285,13 @@
     host: &Strong<dyn IVirtualMachineService>,
 ) -> Result<Option<Strong<dyn ISecretkeeper>>> {
     let sk = if cfg!(llpvm_changes) {
-        if super::is_strict_boot() {
-            // TODO: For protected VM check for Secretkeeper authentication data in device tree.
-            None
-        } else {
-            // For non-protected VM, believe what host claims.
-            host.getSecretkeeper()
-                // TODO rename this error!
-                .map_err(|e| {
-                    super::MicrodroidError::FailedToConnectToVirtualizationService(e.to_string())
-                })?
-        }
+        host.getSecretkeeper()
+            // TODO rename this error!
+            .map_err(|e| {
+                super::MicrodroidError::FailedToConnectToVirtualizationService(format!(
+                    "Failed to get Secretkeeper: {e:?}"
+                ))
+            })?
     } else {
         // LLPVM flag is disabled
         None
diff --git a/service_vm/demo_apk/README.md b/service_vm/demo_apk/README.md
index 551d47b..8f67f6e 100644
--- a/service_vm/demo_apk/README.md
+++ b/service_vm/demo_apk/README.md
@@ -42,7 +42,9 @@
   --config-path assets/config.json --debug full \
   $(adb shell pm path com.android.virt.vm_attestation.demo | cut -c 9-) \
   $TEST_ROOT/VmAttestationDemoApp.apk.idsig \
-  $TEST_ROOT/instance.vm_attestation.debug.img --protected
+  $TEST_ROOT/instance.vm_attestation.debug.img \
+  --instance-id-file $TEST_ROOT/instance_id \
+  --protected
 ```
 
 Please note that remote attestation is only available for protected VMs.
diff --git a/service_vm/demo_apk/src/main.rs b/service_vm/demo_apk/src/main.rs
index 0d1efb0..8ea4e65 100644
--- a/service_vm/demo_apk/src/main.rs
+++ b/service_vm/demo_apk/src/main.rs
@@ -23,10 +23,10 @@
     result,
 };
 use vm_payload_bindgen::{
-    attestation_status_t, AVmAttestationResult, AVmAttestationResult_free,
-    AVmAttestationResult_getCertificateAt, AVmAttestationResult_getCertificateCount,
-    AVmAttestationResult_getPrivateKey, AVmAttestationResult_resultToString,
-    AVmAttestationResult_sign, AVmPayload_requestAttestation,
+    AVmAttestationResult, AVmAttestationResult_free, AVmAttestationResult_getCertificateAt,
+    AVmAttestationResult_getCertificateCount, AVmAttestationResult_getPrivateKey,
+    AVmAttestationResult_sign, AVmAttestationStatus, AVmAttestationStatus_toString,
+    AVmPayload_requestAttestation,
 };
 
 /// Entry point of the Service VM client.
@@ -56,7 +56,7 @@
     ensure!(res.is_err());
     let status = res.unwrap_err();
     ensure!(
-        status == attestation_status_t::ATTESTATION_ERROR_INVALID_CHALLENGE,
+        status == AVmAttestationStatus::ATTESTATION_ERROR_INVALID_CHALLENGE,
         "Unexpected status: {:?}",
         status
     );
@@ -89,7 +89,7 @@
 struct AttestationResult(NonNull<AVmAttestationResult>);
 
 impl AttestationResult {
-    fn request_attestation(challenge: &[u8]) -> result::Result<Self, attestation_status_t> {
+    fn request_attestation(challenge: &[u8]) -> result::Result<Self, AVmAttestationStatus> {
         let mut res: *mut AVmAttestationResult = ptr::null_mut();
         // SAFETY: It is safe as we only read the challenge within its bounds and the
         // function does not retain any reference to it.
@@ -100,7 +100,7 @@
                 &mut res,
             )
         };
-        if status == attestation_status_t::ATTESTATION_OK {
+        if status == AVmAttestationStatus::ATTESTATION_OK {
             info!("Attestation succeeds. Status: {:?}", status_to_cstr(status));
             let res = NonNull::new(res).expect("The attestation result is null");
             Ok(Self(res))
@@ -219,11 +219,11 @@
     Ok(signature.into_boxed_slice())
 }
 
-fn status_to_cstr(status: attestation_status_t) -> &'static CStr {
+fn status_to_cstr(status: AVmAttestationStatus) -> &'static CStr {
     // SAFETY: The function only reads the given enum status and returns a pointer to a
     // static string.
-    let message = unsafe { AVmAttestationResult_resultToString(status) };
-    // SAFETY: The pointer returned by `AVmAttestationResult_resultToString` is guaranteed to
+    let message = unsafe { AVmAttestationStatus_toString(status) };
+    // SAFETY: The pointer returned by `AVmAttestationStatus_toString` is guaranteed to
     // point to a valid C String that lives forever.
     unsafe { CStr::from_ptr(message) }
 }
diff --git a/service_vm/test_apk/src/native/main.rs b/service_vm/test_apk/src/native/main.rs
index d5d599d..199b45c 100644
--- a/service_vm/test_apk/src/native/main.rs
+++ b/service_vm/test_apk/src/native/main.rs
@@ -31,10 +31,10 @@
     sync::{Arc, Mutex},
 };
 use vm_payload_bindgen::{
-    attestation_status_t, AIBinder, AVmAttestationResult, AVmAttestationResult_free,
+    AIBinder, AVmAttestationResult, AVmAttestationResult_free,
     AVmAttestationResult_getCertificateAt, AVmAttestationResult_getCertificateCount,
-    AVmAttestationResult_getPrivateKey, AVmAttestationResult_resultToString,
-    AVmAttestationResult_sign, AVmPayload_notifyPayloadReady,
+    AVmAttestationResult_getPrivateKey, AVmAttestationResult_sign, AVmAttestationStatus,
+    AVmAttestationStatus_toString, AVmPayload_notifyPayloadReady,
     AVmPayload_requestAttestationForTesting, AVmPayload_runVsockRpcServer,
 };
 
@@ -116,7 +116,7 @@
 unsafe impl Send for AttestationResult {}
 
 impl AttestationResult {
-    fn request_attestation(challenge: &[u8]) -> result::Result<Self, attestation_status_t> {
+    fn request_attestation(challenge: &[u8]) -> result::Result<Self, AVmAttestationStatus> {
         let mut res: *mut AVmAttestationResult = ptr::null_mut();
         // SAFETY: It is safe as we only read the challenge within its bounds and the
         // function does not retain any reference to it.
@@ -127,7 +127,7 @@
                 &mut res,
             )
         };
-        if status == attestation_status_t::ATTESTATION_OK {
+        if status == AVmAttestationStatus::ATTESTATION_OK {
             info!("Attestation succeeds. Status: {:?}", status_to_cstr(status));
             let res = NonNull::new(res).expect("The attestation result is null");
             Ok(Self(res))
@@ -261,11 +261,11 @@
     Ok(signature.into_boxed_slice())
 }
 
-fn status_to_cstr(status: attestation_status_t) -> &'static CStr {
+fn status_to_cstr(status: AVmAttestationStatus) -> &'static CStr {
     // SAFETY: The function only reads the given enum status and returns a pointer to a
     // static string.
-    let message = unsafe { AVmAttestationResult_resultToString(status) };
-    // SAFETY: The pointer returned by `AVmAttestationResult_resultToString` is guaranteed to
+    let message = unsafe { AVmAttestationStatus_toString(status) };
+    // SAFETY: The pointer returned by `AVmAttestationStatus_toString` is guaranteed to
     // point to a valid C String that lives forever.
     unsafe { CStr::from_ptr(message) }
 }
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 ba02067..acd6f2c 100644
--- a/tests/benchmark/src/java/com/android/microdroid/benchmark/MicrodroidBenchmarks.java
+++ b/tests/benchmark/src/java/com/android/microdroid/benchmark/MicrodroidBenchmarks.java
@@ -127,12 +127,21 @@
         }
     }
 
+    public MicrodroidBenchmarks() throws IOException {
+        // See b/325745564#comment28. Calling this method here ensures that threads spawned for
+        // @Test methods are with the desired task profile. If this is called in @Before, the task
+        // profile may not be set to the test threads because they may be spanwed prior to the
+        // execution of the @Before method (though the test methods will be executed after the
+        // @Before method).  With this, children of this benchmark process (virtmgr and crosvm) also
+        // run in the desired task profile.
+        setMaxPerformanceTaskProfile();
+    }
+
     @Before
     public void setup() throws IOException {
         grantPermission(VirtualMachine.MANAGE_VIRTUAL_MACHINE_PERMISSION);
         grantPermission(VirtualMachine.USE_CUSTOM_VIRTUAL_MACHINE_PERMISSION);
         prepareTestSetup(mProtectedVm, mGki);
-        setMaxPerformanceTaskProfile();
         mInstrumentation = getInstrumentation();
     }
 
diff --git a/tests/hostside/Android.bp b/tests/hostside/Android.bp
index 13a9925..41d244d 100644
--- a/tests/hostside/Android.bp
+++ b/tests/hostside/Android.bp
@@ -35,6 +35,7 @@
         "initrd_bootconfig",
         "lpmake",
         "lpunpack",
+        "lz4",
         "sign_virt_apex",
         "simg2img",
         "dtdiff",
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 9e19b7d..2abf110 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
@@ -52,6 +52,7 @@
     private static final int TEST_VM_ADB_PORT = 8000;
     private static final String MICRODROID_SERIAL = "localhost:" + TEST_VM_ADB_PORT;
     private static final String INSTANCE_IMG = "instance.img";
+    protected static final String VIRT_APEX = "/apex/com.android.virt/";
 
     private static final long MICRODROID_ADB_CONNECT_TIMEOUT_MINUTES = 5;
     protected static final long MICRODROID_COMMAND_TIMEOUT_MILLIS = 30000;
@@ -186,6 +187,12 @@
         return ret;
     }
 
+    public boolean isFeatureEnabled(String feature) throws Exception {
+        CommandRunner android = new CommandRunner(getDevice());
+        String result = android.run(VIRT_APEX + "bin/vm", "check-feature-enabled", feature);
+        return result.contains("enabled");
+    }
+
     public List<String> getAssignableDevices() throws Exception {
         return parseStringArrayFieldsFromVmInfo("Assignable devices: ");
     }
diff --git a/tests/hostside/java/com/android/microdroid/test/MicrodroidHostTests.java b/tests/hostside/java/com/android/microdroid/test/MicrodroidHostTests.java
index 0901fd4..4f502ab 100644
--- a/tests/hostside/java/com/android/microdroid/test/MicrodroidHostTests.java
+++ b/tests/hostside/java/com/android/microdroid/test/MicrodroidHostTests.java
@@ -617,6 +617,7 @@
         final String apkPath = getPathForPackage(PACKAGE_NAME);
         final String idsigPath = TEST_ROOT + "idsig";
         final String instanceImgPath = TEST_ROOT + "instance.img";
+        final String instanceIdPath = TEST_ROOT + "instance_id";
         List<String> cmd =
                 new ArrayList<>(
                         Arrays.asList(
@@ -627,6 +628,11 @@
                                 apkPath,
                                 idsigPath,
                                 instanceImgPath));
+        if (isFeatureEnabled("com.android.kvm.LLPVM_CHANGES")) {
+            cmd.add("--instance-id-file");
+            cmd.add(instanceIdPath);
+        }
+        ;
         if (protectedVm) {
             cmd.add("--protected");
         }
@@ -887,7 +893,6 @@
         final String apkPath = getPathForPackage(PACKAGE_NAME);
         final String idSigPath = TEST_ROOT + "idsig";
         android.run(VIRT_APEX + "bin/vm", "create-idsig", apkPath, idSigPath);
-
         // Create the instance image for the VM
         final String instanceImgPath = TEST_ROOT + "instance.img";
         android.run(
@@ -897,17 +902,22 @@
                 instanceImgPath,
                 Integer.toString(10 * 1024 * 1024));
 
-        final String ret =
-                android.runForResult(
+        List<String> cmd =
+                new ArrayList<>(
+                        Arrays.asList(
                                 VIRT_APEX + "bin/vm",
                                 "run-app",
                                 "--payload-binary-name",
                                 "./MicrodroidTestNativeLib.so",
                                 apkPath,
                                 idSigPath,
-                                instanceImgPath)
-                        .getStderr()
-                        .trim();
+                                instanceImgPath));
+        if (isFeatureEnabled("com.android.kvm.LLPVM_CHANGES")) {
+            cmd.add("--instance-id-file");
+            cmd.add(TEST_ROOT + "instance_id");
+        }
+
+        final String ret = android.runForResult(String.join(" ", cmd)).getStderr().trim();
 
         assertThat(ret).contains("Payload binary name must not specify a path");
     }
@@ -944,7 +954,43 @@
         assertThat(hasDebugPolicy).isFalse();
     }
 
+    private boolean isLz4(String path) throws Exception {
+        File lz4tool = findTestFile("lz4");
+        CommandResult result =
+                new RunUtil().runTimedCmd(5000, lz4tool.getAbsolutePath(), "-t", path);
+        return result.getStatus() == CommandStatus.SUCCESS;
+    }
+
+    private void decompressLz4(String inputPath, String outputPath) throws Exception {
+        File lz4tool = findTestFile("lz4");
+        CommandResult result =
+                new RunUtil()
+                        .runTimedCmd(
+                                5000, lz4tool.getAbsolutePath(), "-d", "-f", inputPath, outputPath);
+        String out = result.getStdout();
+        String err = result.getStderr();
+        assertWithMessage(
+                        "lz4 image "
+                                + inputPath
+                                + " decompression failed."
+                                + "\n\tout: "
+                                + out
+                                + "\n\terr: "
+                                + err
+                                + "\n")
+                .about(command_results())
+                .that(result)
+                .isSuccess();
+    }
+
     private String avbInfo(String image_path) throws Exception {
+        if (isLz4(image_path)) {
+            File decompressedImage = FileUtil.createTempFile("decompressed", ".img");
+            decompressedImage.deleteOnExit();
+            decompressLz4(image_path, decompressedImage.getAbsolutePath());
+            image_path = decompressedImage.getAbsolutePath();
+        }
+
         File avbtool = findTestFile("avbtool");
         List<String> command =
                 Arrays.asList(avbtool.getAbsolutePath(), "info_image", "--image", image_path);
diff --git a/tests/pvmfw/java/com/android/pvmfw/test/DebugPolicyHostTests.java b/tests/pvmfw/java/com/android/pvmfw/test/DebugPolicyHostTests.java
index b84d7dc..26f5993 100644
--- a/tests/pvmfw/java/com/android/pvmfw/test/DebugPolicyHostTests.java
+++ b/tests/pvmfw/java/com/android/pvmfw/test/DebugPolicyHostTests.java
@@ -301,7 +301,7 @@
 
     // Try to launch protected non-debuggable VM for a while and quit.
     // Non-debuggable VM might not enable adb, so there's no ITestDevice instance of it.
-    private CommandResult tryLaunchProtectedNonDebuggableVm() throws DeviceNotAvailableException {
+    private CommandResult tryLaunchProtectedNonDebuggableVm() throws Exception {
         // Can't use MicrodroidBuilder because it expects adb connection
         // but non-debuggable VM may not enable adb.
         CommandRunner runner = new CommandRunner(mAndroidDevice);
@@ -314,7 +314,7 @@
         String command =
                 String.join(
                         " ",
-                        "/apex/com.android.virt/bin/vm",
+                        VIRT_APEX + "bin/vm",
                         "run-app",
                         "--log",
                         MICRODROID_LOG_PATH,
@@ -324,6 +324,9 @@
                         TEST_ROOT + "instance.img",
                         "--config-path",
                         MICRODROID_CONFIG_PATH);
+        if (isFeatureEnabled("com.android.kvm.LLPVM_CHANGES")) {
+            command = String.join(" ", command, "--instance-id-file", TEST_ROOT + "instance_id");
+        }
         return mAndroidDevice.executeShellV2Command(
                 command, CONSOLE_OUTPUT_WAIT_MS, TimeUnit.MILLISECONDS, /* retryAttempts= */ 0);
     }
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 3b8b4ac..51aace4 100644
--- a/tests/testapk/src/java/com/android/microdroid/test/MicrodroidTests.java
+++ b/tests/testapk/src/java/com/android/microdroid/test/MicrodroidTests.java
@@ -1070,6 +1070,10 @@
     })
     public void instancesOfSameVmHaveDifferentCdis() throws Exception {
         assumeSupportedDevice();
+        // TODO(b/325094712): VMs on CF with same payload have the same secret. This is because
+        // `instance-id` which is input to DICE is contained in DT which is missing in CF.
+        assumeFalse(
+                "Cuttlefish doesn't support device tree under /proc/device-tree", isCuttlefish());
 
         grantPermission(VirtualMachine.USE_CUSTOM_VIRTUAL_MACHINE_PERMISSION);
         VirtualMachineConfig normalConfig =
@@ -1504,6 +1508,10 @@
     @CddTest(requirements = {"9.17/C-1-1"})
     public void encryptedStorageIsInaccessibleToDifferentVm() throws Exception {
         assumeSupportedDevice();
+        // TODO(b/325094712): VMs on CF with same payload have the same secret. This is because
+        // `instance-id` which is input to DICE is contained in DT which is missing in CF.
+        assumeFalse(
+                "Cuttlefish doesn't support device tree under /proc/device-tree", isCuttlefish());
 
         VirtualMachineConfig config =
                 newVmConfigBuilderWithPayloadBinary("MicrodroidTestNativeLib.so")
diff --git a/tests/testapk_no_perm/Android.bp b/tests/testapk_no_perm/Android.bp
new file mode 100644
index 0000000..22616de
--- /dev/null
+++ b/tests/testapk_no_perm/Android.bp
@@ -0,0 +1,26 @@
+package {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+android_test {
+    name: "MicrodroidTestAppNoPerm",
+    static_libs: [
+        "MicrodroidDeviceTestHelper",
+        "MicrodroidTestHelper",
+        "androidx.test.runner",
+        "androidx.test.ext.junit",
+        "com.android.microdroid.testservice-java",
+        "truth",
+        "compatibility-common-util-devicesidelib",
+    ],
+    jni_libs: [
+        "MicrodroidTestNativeLib",
+    ],
+    test_suites: [
+        "general-tests",
+        "cts",
+    ],
+    srcs: ["src/java/**/*.java"],
+    defaults: ["MicrodroidTestAppsDefaults"],
+    min_sdk_version: "33",
+}
diff --git a/tests/testapk_no_perm/AndroidManifest.xml b/tests/testapk_no_perm/AndroidManifest.xml
new file mode 100644
index 0000000..44aa92a
--- /dev/null
+++ b/tests/testapk_no_perm/AndroidManifest.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2024 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+      package="com.android.microdroid.test_no_perm">
+    <uses-sdk android:minSdkVersion="33" android:targetSdkVersion="33" />
+    <uses-feature android:name="android.software.virtualization_framework" android:required="false" />
+    <application />
+    <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
+        android:targetPackage="com.android.microdroid.test_no_perm"
+        android:label="No Permission Microdroid Test" />
+</manifest>
diff --git a/tests/testapk_no_perm/AndroidTest.xml b/tests/testapk_no_perm/AndroidTest.xml
new file mode 100644
index 0000000..d4a818f
--- /dev/null
+++ b/tests/testapk_no_perm/AndroidTest.xml
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2024 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+<configuration description="Runs Microdroid Tests with no permission">
+    <option name="test-suite-tag" value="cts" />
+    <option name="config-descriptor:metadata" key="component" value="security" />
+    <option name="config-descriptor:metadata" key="parameter" value="not_instant_app" />
+    <option name="config-descriptor:metadata" key="parameter" value="not_multi_abi" />
+    <option name="config-descriptor:metadata" key="parameter" value="secondary_user" />
+    <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
+        <option name="test-file-name" value="MicrodroidTestAppNoPerm.apk" />
+    </target_preparer>
+    <test class="com.android.tradefed.testtype.AndroidJUnitTest" >
+        <option name="package" value="com.android.microdroid.test_no_perm" />
+        <option name="runner" value="androidx.test.runner.AndroidJUnitRunner" />
+        <option name="shell-timeout" value="300000" />
+        <option name="test-timeout" value="300000" />
+    </test>
+</configuration>
diff --git a/tests/testapk_no_perm/src/java/com/android/microdroid/test/MicrodroidTestAppNoPerm.java b/tests/testapk_no_perm/src/java/com/android/microdroid/test/MicrodroidTestAppNoPerm.java
new file mode 100644
index 0000000..1772e6b
--- /dev/null
+++ b/tests/testapk_no_perm/src/java/com/android/microdroid/test/MicrodroidTestAppNoPerm.java
@@ -0,0 +1,72 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.microdroid.test;
+
+import android.system.virtualmachine.VirtualMachineConfig;
+
+import com.android.compatibility.common.util.CddTest;
+import com.android.microdroid.test.device.MicrodroidDeviceTestBase;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.assertThrows;
+
+import org.junit.Before;
+import org.junit.runners.Parameterized;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/**
+ * Test that the android.permission.MANAGE_VIRTUAL_MACHINE is enforced and that an app cannot launch
+ * a VM without said permission.
+ */
+@RunWith(Parameterized.class)
+public class MicrodroidTestAppNoPerm extends MicrodroidDeviceTestBase {
+
+    @Parameterized.Parameters(name = "protectedVm={0}")
+    public static Object[] protectedVmConfigs() {
+        return new Object[] {false, true};
+    }
+
+    @Parameterized.Parameter public boolean mProtectedVm;
+
+    @Before
+    public void setup() {
+        prepareTestSetup(mProtectedVm, null);
+    }
+
+    @Test
+    @CddTest(
+            requirements = {
+                "9.17/C-1-1",
+                "9.17/C-1-2",
+                "9.17/C-1-4",
+            })
+    public void createVmRequiresPermission() {
+        assumeSupportedDevice();
+
+        VirtualMachineConfig config =
+                newVmConfigBuilderWithPayloadBinary("MicrodroidTestNativeLib.so").build();
+
+        SecurityException e =
+                assertThrows(
+                        SecurityException.class,
+                        () -> forceCreateNewVirtualMachine("test_vm_requires_permission", config));
+        assertThat(e)
+                .hasMessageThat()
+                .contains("android.permission.MANAGE_VIRTUAL_MACHINE permission");
+    }
+}
diff --git a/virtualizationmanager/src/aidl.rs b/virtualizationmanager/src/aidl.rs
index 0655e5f..ea3a481 100644
--- a/virtualizationmanager/src/aidl.rs
+++ b/virtualizationmanager/src/aidl.rs
@@ -83,7 +83,7 @@
 use std::io::{BufRead, BufReader, Error, ErrorKind, Seek, SeekFrom, Write};
 use std::iter;
 use std::num::{NonZeroU16, NonZeroU32};
-use std::os::unix::io::{FromRawFd, IntoRawFd};
+use std::os::unix::io::{AsRawFd, FromRawFd, IntoRawFd};
 use std::os::unix::raw::pid_t;
 use std::path::{Path, PathBuf};
 use std::sync::{Arc, Mutex, Weak};
@@ -109,9 +109,8 @@
 
 const MICRODROID_OS_NAME: &str = "microdroid";
 
-// TODO(b/291213394): Use 'default' instance for secretkeeper instead of 'nonsecure'
 const SECRETKEEPER_IDENTIFIER: &str =
-    "android.hardware.security.secretkeeper.ISecretkeeper/nonsecure";
+    "android.hardware.security.secretkeeper.ISecretkeeper/default";
 
 const UNFORMATTED_STORAGE_MAGIC: &str = "UNFORMATTED-STORAGE";
 
@@ -230,6 +229,11 @@
         ret
     }
 
+    /// Allocate a new instance_id to the VM
+    fn allocateInstanceId(&self) -> binder::Result<[u8; 64]> {
+        GLOBAL_SERVICE.allocateInstanceId()
+    }
+
     /// Initialise an empty partition image of the given size to be used as a writable partition.
     fn initializeWritablePartition(
         &self,
@@ -371,7 +375,8 @@
             check_gdb_allowed(config)?;
         }
 
-        // Currently, VirtMgr adds the host copy of reference DT & an untrusted prop (instance-id)
+        // Currently, VirtMgr adds the host copy of reference DT & untrusted properties
+        // (e.g. instance-id)
         let host_ref_dt = Path::new(VM_REFERENCE_DT_ON_HOST_PATH);
         let host_ref_dt = if host_ref_dt.exists()
             && read_dir(host_ref_dt).or_service_specific_exception(-1)?.next().is_some()
@@ -399,13 +404,17 @@
             vec![]
         };
 
-        let untrusted_props = if cfg!(llpvm_changes) {
-            // TODO(b/291213394): Replace this with a per-VM instance Id.
-            let instance_id = b"sixtyfourbyteslonghardcoded_indeed_sixtyfourbyteslonghardcoded_h";
-            vec![(cstr!("instance-id"), &instance_id[..])]
-        } else {
-            vec![]
-        };
+        let instance_id;
+        let mut untrusted_props = Vec::with_capacity(2);
+        if cfg!(llpvm_changes) {
+            instance_id = extract_instance_id(config);
+            untrusted_props.push((cstr!("instance-id"), &instance_id[..]));
+            if is_secretkeeper_supported() {
+                // Let guest know that it can defer rollback protection to Secretkeeper by setting
+                // an empty property in untrusted node in DT. This enables Updatable VMs.
+                untrusted_props.push((cstr!("defer-rollback-protection"), &[]))
+            }
+        }
 
         let device_tree_overlay =
             if host_ref_dt.is_some() || !untrusted_props.is_empty() || !trusted_props.is_empty() {
@@ -484,6 +493,11 @@
             .try_for_each(check_label_for_partition)
             .or_service_specific_exception(-1)?;
 
+        // Check if files for payloads and bases are NOT coming from /vendor and /odm, as they may
+        // have unstable interfaces.
+        // TODO(b/316431494): remove once Treble interfaces are stabilized.
+        check_partitions_for_files(config).or_service_specific_exception(-1)?;
+
         let kernel = maybe_clone_file(&config.kernel)?;
         let initrd = maybe_clone_file(&config.initrd)?;
 
@@ -855,6 +869,38 @@
     Ok(vm_config)
 }
 
+fn check_partition_for_file(fd: &ParcelFileDescriptor) -> Result<()> {
+    let path = format!("/proc/self/fd/{}", fd.as_raw_fd());
+    let link = fs::read_link(&path).context(format!("can't read_link {path}"))?;
+
+    // microdroid vendor image is OK
+    if cfg!(vendor_modules) && link == Path::new("/vendor/etc/avf/microdroid/microdroid_vendor.img")
+    {
+        return Ok(());
+    }
+
+    if link.starts_with("/vendor") || link.starts_with("/odm") {
+        bail!("vendor or odm file {} can't be used for VM", link.display());
+    }
+
+    Ok(())
+}
+
+fn check_partitions_for_files(config: &VirtualMachineRawConfig) -> Result<()> {
+    config
+        .disks
+        .iter()
+        .flat_map(|disk| disk.partitions.iter())
+        .filter_map(|partition| partition.image.as_ref())
+        .try_for_each(check_partition_for_file)?;
+
+    config.kernel.as_ref().map_or(Ok(()), check_partition_for_file)?;
+    config.initrd.as_ref().map_or(Ok(()), check_partition_for_file)?;
+    config.bootloader.as_ref().map_or(Ok(()), check_partition_for_file)?;
+
+    Ok(())
+}
+
 fn load_vm_payload_config_from_file(apk_file: &File, config_path: &str) -> Result<VmPayloadConfig> {
     let mut apk_zip = ZipArchive::new(apk_file)?;
     let config_file = apk_zip.by_name(config_path)?;
@@ -1269,6 +1315,13 @@
     }
 }
 
+fn extract_instance_id(config: &VirtualMachineConfig) -> [u8; 64] {
+    match config {
+        VirtualMachineConfig::RawConfig(config) => config.instanceId,
+        VirtualMachineConfig::AppConfig(config) => config.instanceId,
+    }
+}
+
 fn extract_gdb_port(config: &VirtualMachineConfig) -> Option<NonZeroU16> {
     match config {
         VirtualMachineConfig::RawConfig(config) => NonZeroU16::new(config.gdbPort as u16),
@@ -1453,7 +1506,7 @@
     }
 
     fn getSecretkeeper(&self) -> binder::Result<Option<Strong<dyn ISecretkeeper>>> {
-        let sk = if is_secretkeeper_present() {
+        let sk = if is_secretkeeper_supported() {
             Some(binder::wait_for_interface(SECRETKEEPER_IDENTIFIER)?)
         } else {
             None
@@ -1466,7 +1519,7 @@
     }
 }
 
-fn is_secretkeeper_present() -> bool {
+fn is_secretkeeper_supported() -> bool {
     binder::is_declared(SECRETKEEPER_IDENTIFIER)
         .expect("Could not check for declared Secretkeeper interface")
 }
diff --git a/virtualizationservice/Android.bp b/virtualizationservice/Android.bp
index 38a9ec5..fc7fcd2 100644
--- a/virtualizationservice/Android.bp
+++ b/virtualizationservice/Android.bp
@@ -5,7 +5,10 @@
 rust_defaults {
     name: "virtualizationservice_defaults",
     crate_name: "virtualizationservice",
-    defaults: ["avf_build_flags_rust"],
+    defaults: [
+        "avf_build_flags_rust",
+        "secretkeeper_use_latest_hal_aidl_rust",
+    ],
     edition: "2021",
     srcs: ["src/main.rs"],
     // Only build on targets which crosvm builds on.
@@ -32,13 +35,17 @@
         "libanyhow",
         "libavflog",
         "libbinder_rs",
+        "libhex",
         "libhypervisor_props",
         "liblazy_static",
         "liblibc",
+        "liblibsqlite3_sys",
         "liblog_rust",
         "libnix",
         "libopenssl",
+        "librand",
         "librkpd_client",
+        "librusqlite",
         "librustutils",
         "libstatslog_virtualization_rust",
         "libtombstoned_client_rust",
@@ -65,7 +72,10 @@
 
 rust_test {
     name: "virtualizationservice_test",
-    defaults: ["virtualizationservice_defaults"],
+    defaults: [
+        "authgraph_use_latest_hal_aidl_rust",
+        "virtualizationservice_defaults",
+    ],
     test_suites: ["general-tests"],
     data: [
         ":test_rkp_cert_chain",
diff --git a/virtualizationservice/aidl/Android.bp b/virtualizationservice/aidl/Android.bp
index 66de092..112e1cc 100644
--- a/virtualizationservice/aidl/Android.bp
+++ b/virtualizationservice/aidl/Android.bp
@@ -61,6 +61,9 @@
     backend: {
         java: {
             sdk_version: "module_current",
+            apex_available: [
+                "com.android.virt",
+            ],
         },
         rust: {
             enabled: true,
diff --git a/virtualizationservice/aidl/android/system/virtualizationmaintenance/IVirtualizationMaintenance.aidl b/virtualizationservice/aidl/android/system/virtualizationmaintenance/IVirtualizationMaintenance.aidl
index 161673a..08d61c1 100644
--- a/virtualizationservice/aidl/android/system/virtualizationmaintenance/IVirtualizationMaintenance.aidl
+++ b/virtualizationservice/aidl/android/system/virtualizationmaintenance/IVirtualizationMaintenance.aidl
@@ -16,10 +16,31 @@
 
 package android.system.virtualizationmaintenance;
 
+import android.system.virtualizationmaintenance.IVirtualizationReconciliationCallback;
+
 interface IVirtualizationMaintenance {
+    /**
+     * Notification that an app has been permanently removed, to allow related global state to
+     * be removed.
+     *
+     * @param userId The Android user ID for whom the notification applies.
+     */
     void appRemoved(int userId, int appId);
 
+    /**
+     * Notification that a user has been removed, to allow related global state to be removed.
+     *
+     * @param userId The Android user ID of the user.
+     */
     void userRemoved(int userId);
 
-    // TODO: Something for daily reconciliation
+    /*
+     * Requests virtualization service to perform reconciliation of Secretkeeper secrets.
+     * Secrets belonging to apps or users that no longer exist should be deleted.
+     * The supplied callback allows for querying of existence.
+     * This method should return on successful completion of the reconciliation process.
+     * It should throw an exception if there is any failure, or if any of the callback
+     * functions return {@code ERROR_STOP_REQUESTED}.
+     */
+    void performReconciliation(IVirtualizationReconciliationCallback callback);
 }
diff --git a/virtualizationservice/aidl/android/system/virtualizationmaintenance/IVirtualizationReconciliationCallback.aidl b/virtualizationservice/aidl/android/system/virtualizationmaintenance/IVirtualizationReconciliationCallback.aidl
new file mode 100644
index 0000000..6466aa2
--- /dev/null
+++ b/virtualizationservice/aidl/android/system/virtualizationmaintenance/IVirtualizationReconciliationCallback.aidl
@@ -0,0 +1,43 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.system.virtualizationmaintenance;
+
+/*
+ * Callback interface provided when reconciliation is performed to allow verifying whether users
+ * and apps currently exist.
+ */
+interface IVirtualizationReconciliationCallback {
+    /*
+     * Service-specific error code indicating that the job scheduler has requested that we
+     * stop
+     */
+    const int ERROR_STOP_REQUESTED = 1;
+
+    /*
+     * Determine whether users with selected IDs currently exist. The result is an array of booleans
+     * which indicate whether the corresponding entry in the {@code userIds} array is a valid
+     * user ID.
+     */
+    boolean[] doUsersExist(in int[] userIds);
+
+    /*
+     * Determine whether apps with selected app IDs currently exist for a specific user.
+     * The result is an array of booleans which indicate whether the corresponding entry in the
+     * {@code appIds} array is a current app ID for the user.
+     */
+    boolean[] doAppsExist(int userId, in int[] appIds);
+}
diff --git a/virtualizationservice/aidl/android/system/virtualizationservice/IVirtualizationService.aidl b/virtualizationservice/aidl/android/system/virtualizationservice/IVirtualizationService.aidl
index f7bbf55..e11d8b8 100644
--- a/virtualizationservice/aidl/android/system/virtualizationservice/IVirtualizationService.aidl
+++ b/virtualizationservice/aidl/android/system/virtualizationservice/IVirtualizationService.aidl
@@ -41,6 +41,11 @@
             in @nullable ParcelFileDescriptor osLogFd);
 
     /**
+     * Allocate an instance_id to the (newly created) VM.
+     */
+    byte[64] allocateInstanceId();
+
+    /**
      * Initialise an empty partition image of the given size to be used as a writable partition.
      *
      * The file must be open with both read and write permissions, and should be a new empty file.
diff --git a/virtualizationservice/aidl/android/system/virtualizationservice/VirtualMachineAppConfig.aidl b/virtualizationservice/aidl/android/system/virtualizationservice/VirtualMachineAppConfig.aidl
index 8302a2f..29232ff 100644
--- a/virtualizationservice/aidl/android/system/virtualizationservice/VirtualMachineAppConfig.aidl
+++ b/virtualizationservice/aidl/android/system/virtualizationservice/VirtualMachineAppConfig.aidl
@@ -23,6 +23,9 @@
     /** Name of VM */
     String name;
 
+    /** Id of the VM instance */
+    byte[64] instanceId;
+
     /** Main APK */
     ParcelFileDescriptor apk;
 
diff --git a/virtualizationservice/aidl/android/system/virtualizationservice/VirtualMachineRawConfig.aidl b/virtualizationservice/aidl/android/system/virtualizationservice/VirtualMachineRawConfig.aidl
index 6be2833..b2116c4 100644
--- a/virtualizationservice/aidl/android/system/virtualizationservice/VirtualMachineRawConfig.aidl
+++ b/virtualizationservice/aidl/android/system/virtualizationservice/VirtualMachineRawConfig.aidl
@@ -23,6 +23,9 @@
     /** Name of VM */
     String name;
 
+    /** Id of the VM instance */
+    byte[64] instanceId;
+
     /** The kernel image, if any. */
     @nullable ParcelFileDescriptor kernel;
 
diff --git a/virtualizationservice/aidl/android/system/virtualizationservice_internal/IVirtualizationServiceInternal.aidl b/virtualizationservice/aidl/android/system/virtualizationservice_internal/IVirtualizationServiceInternal.aidl
index abfc45a..8af881b 100644
--- a/virtualizationservice/aidl/android/system/virtualizationservice_internal/IVirtualizationServiceInternal.aidl
+++ b/virtualizationservice/aidl/android/system/virtualizationservice_internal/IVirtualizationServiceInternal.aidl
@@ -91,4 +91,16 @@
 
     /** Returns a read-only file descriptor of the VM DTBO file. */
     ParcelFileDescriptor getDtboFile();
+
+    /**
+     * Allocate an instance_id to the (newly created) VM.
+     */
+    byte[64] allocateInstanceId();
+
+    /**
+     * Notification that state associated with a VM should be removed.
+     *
+     * @param instanceId The ID for the VM.
+     */
+    void removeVmInstance(in byte[64] instanceId);
 }
diff --git a/virtualizationservice/src/aidl.rs b/virtualizationservice/src/aidl.rs
index c7a33ba..bbfb220 100644
--- a/virtualizationservice/src/aidl.rs
+++ b/virtualizationservice/src/aidl.rs
@@ -15,11 +15,13 @@
 //! Implementation of the AIDL interface of the VirtualizationService.
 
 use crate::atom::{forward_vm_booted_atom, forward_vm_creation_atom, forward_vm_exited_atom};
+use crate::maintenance;
 use crate::remote_provisioning;
 use crate::rkpvm::{generate_ecdsa_p256_key_pair, request_attestation};
 use crate::{get_calling_pid, get_calling_uid, REMOTELY_PROVISIONED_COMPONENT_SERVICE_NAME};
 use android_os_permissions_aidl::aidl::android::os::IPermissionController;
 use android_system_virtualizationcommon::aidl::android::system::virtualizationcommon;
+use android_system_virtualizationmaintenance::aidl::android::system::virtualizationmaintenance;
 use android_system_virtualizationservice::aidl::android::system::virtualizationservice;
 use android_system_virtualizationservice_internal as android_vs_internal;
 use android_system_virtualmachineservice::aidl::android::system::virtualmachineservice;
@@ -35,6 +37,7 @@
 use log::{error, info, warn};
 use nix::unistd::{chown, Uid};
 use openssl::x509::X509;
+use rand::Fill;
 use rkpd_client::get_rkpd_attestation_key;
 use rustutils::system_properties;
 use serde::Deserialize;
@@ -48,6 +51,10 @@
 use std::sync::{Arc, Mutex, Weak};
 use tombstoned_client::{DebuggerdDumpType, TombstonedConnection};
 use virtualizationcommon::Certificate::Certificate;
+use virtualizationmaintenance::{
+    IVirtualizationMaintenance::IVirtualizationMaintenance,
+    IVirtualizationReconciliationCallback::IVirtualizationReconciliationCallback,
+};
 use virtualizationservice::{
     AssignableDevice::AssignableDevice, VirtualMachineDebugInfo::VirtualMachineDebugInfo,
 };
@@ -59,9 +66,7 @@
     IGlobalVmContext::{BnGlobalVmContext, IGlobalVmContext},
     IVfioHandler::VfioDev::VfioDev,
     IVfioHandler::{BpVfioHandler, IVfioHandler},
-    IVirtualizationServiceInternal::{
-        BnVirtualizationServiceInternal, IVirtualizationServiceInternal,
-    },
+    IVirtualizationServiceInternal::IVirtualizationServiceInternal,
 };
 use virtualmachineservice::IVirtualMachineService::VM_TOMBSTONES_SERVICE_PORT;
 use vsock::{VsockListener, VsockStream};
@@ -159,14 +164,15 @@
 
 /// Singleton service for allocating globally-unique VM resources, such as the CID, and running
 /// singleton servers, like tombstone receiver.
-#[derive(Debug, Default)]
+#[derive(Clone)]
 pub struct VirtualizationServiceInternal {
     state: Arc<Mutex<GlobalState>>,
 }
 
 impl VirtualizationServiceInternal {
-    pub fn init() -> Strong<dyn IVirtualizationServiceInternal> {
-        let service = VirtualizationServiceInternal::default();
+    pub fn init() -> VirtualizationServiceInternal {
+        let service =
+            VirtualizationServiceInternal { state: Arc::new(Mutex::new(GlobalState::new())) };
 
         std::thread::spawn(|| {
             if let Err(e) = handle_stream_connection_tombstoned() {
@@ -174,7 +180,7 @@
             }
         });
 
-        BnVirtualizationServiceInternal::new_binder(service, BinderFeatures::default())
+        service
     }
 }
 
@@ -378,6 +384,60 @@
         let file = state.get_dtbo_file().or_service_specific_exception(-1)?;
         Ok(ParcelFileDescriptor::new(file))
     }
+
+    // TODO(b/294177871) Persist this Id, along with client uuid.
+    fn allocateInstanceId(&self) -> binder::Result<[u8; 64]> {
+        let mut id = [0u8; 64];
+        id.try_fill(&mut rand::thread_rng())
+            .context("Failed to allocate instance_id")
+            .or_service_specific_exception(-1)?;
+        let uid = get_calling_uid();
+        info!("Allocated a VM's instance_id: {:?}, for uid: {:?}", hex::encode(id), uid);
+        Ok(id)
+    }
+
+    fn removeVmInstance(&self, instance_id: &[u8; 64]) -> binder::Result<()> {
+        let state = &mut *self.state.lock().unwrap();
+        if let Some(sk_state) = &mut state.sk_state {
+            info!("removeVmInstance(): delete secret");
+            sk_state.delete_ids(&[*instance_id]);
+        } else {
+            info!("ignoring removeVmInstance() as no ISecretkeeper");
+        }
+        Ok(())
+    }
+}
+
+impl IVirtualizationMaintenance for VirtualizationServiceInternal {
+    fn appRemoved(&self, user_id: i32, app_id: i32) -> binder::Result<()> {
+        let state = &mut *self.state.lock().unwrap();
+        if let Some(sk_state) = &mut state.sk_state {
+            info!("packageRemoved(user_id={user_id}, app_id={app_id})");
+            sk_state.delete_ids_for_app(user_id, app_id).or_service_specific_exception(-1)?;
+        } else {
+            info!("ignoring packageRemoved(user_id={user_id}, app_id={app_id})");
+        }
+        Ok(())
+    }
+
+    fn userRemoved(&self, user_id: i32) -> binder::Result<()> {
+        let state = &mut *self.state.lock().unwrap();
+        if let Some(sk_state) = &mut state.sk_state {
+            info!("userRemoved({user_id})");
+            sk_state.delete_ids_for_user(user_id).or_service_specific_exception(-1)?;
+        } else {
+            info!("ignoring userRemoved(user_id={user_id})");
+        }
+        Ok(())
+    }
+
+    fn performReconciliation(
+        &self,
+        _callback: &Strong<dyn IVirtualizationReconciliationCallback>,
+    ) -> binder::Result<()> {
+        Err(anyhow!("performReconciliation not supported"))
+            .or_binder_exception(ExceptionCode::UNSUPPORTED_OPERATION)
+    }
 }
 
 // KEEP IN SYNC WITH assignable_devices.xsd
@@ -462,7 +522,6 @@
 
 /// The mutable state of the VirtualizationServiceInternal. There should only be one instance
 /// of this struct.
-#[derive(Debug, Default)]
 struct GlobalState {
     /// VM contexts currently allocated to running VMs. A CID is never recycled as long
     /// as there is a strong reference held by a GlobalVmContext.
@@ -470,9 +529,20 @@
 
     /// Cached read-only FD of VM DTBO file. Also serves as a lock for creating the file.
     dtbo_file: Mutex<Option<File>>,
+
+    /// State relating to secrets held by (optional) Secretkeeper instance on behalf of VMs.
+    sk_state: Option<maintenance::State>,
 }
 
 impl GlobalState {
+    fn new() -> Self {
+        Self {
+            held_contexts: HashMap::new(),
+            dtbo_file: Mutex::new(None),
+            sk_state: maintenance::State::new(),
+        }
+    }
+
     /// Get the next available CID, or an error if we have run out. The last CID used is stored in
     /// a system property so that restart of virtualizationservice doesn't reuse CID while the host
     /// Android is up.
@@ -717,7 +787,6 @@
 #[cfg(test)]
 mod tests {
     use super::*;
-    use std::fs;
 
     const TEST_RKP_CERT_CHAIN_PATH: &str = "testdata/rkp_cert_chain.der";
 
diff --git a/virtualizationservice/src/main.rs b/virtualizationservice/src/main.rs
index 97bb38f..bcea1bc 100644
--- a/virtualizationservice/src/main.rs
+++ b/virtualizationservice/src/main.rs
@@ -20,10 +20,16 @@
 mod remote_provisioning;
 mod rkpvm;
 
-use crate::aidl::{remove_temporary_dir, VirtualizationServiceInternal, TEMPORARY_DIRECTORY};
+use crate::aidl::{remove_temporary_dir, TEMPORARY_DIRECTORY, VirtualizationServiceInternal};
 use android_logger::{Config, FilterBuilder};
+use android_system_virtualizationservice_internal::aidl::android::system::{
+    virtualizationservice_internal::IVirtualizationServiceInternal::BnVirtualizationServiceInternal
+};
+use android_system_virtualizationmaintenance::aidl::android::system::virtualizationmaintenance::{
+    IVirtualizationMaintenance::BnVirtualizationMaintenance
+};
 use anyhow::{bail, Context, Error, Result};
-use binder::{register_lazy_service, ProcessState, ThreadState};
+use binder::{register_lazy_service, BinderFeatures, ProcessState, ThreadState};
 use log::{error, info, LevelFilter};
 use std::fs::{create_dir, read_dir};
 use std::os::unix::raw::{pid_t, uid_t};
@@ -69,16 +75,25 @@
     create_dir(common_dir_path).context("Failed to create common directory")?;
 
     ProcessState::start_thread_pool();
-    register(INTERNAL_SERVICE_NAME, VirtualizationServiceInternal::init())?;
+
+    // One instance of `VirtualizationServiceInternal` implements both the internal interface
+    // and (optionally) the maintenance interface.
+    let service = VirtualizationServiceInternal::init();
+    let internal_service =
+        BnVirtualizationServiceInternal::new_binder(service.clone(), BinderFeatures::default());
+    register(INTERNAL_SERVICE_NAME, internal_service)?;
 
     if cfg!(remote_attestation) {
         // The IRemotelyProvisionedComponent service is only supposed to be triggered by rkpd for
         // RKP VM attestation.
-        register(REMOTELY_PROVISIONED_COMPONENT_SERVICE_NAME, remote_provisioning::new_binder())?;
+        let remote_provisioning_service = remote_provisioning::new_binder();
+        register(REMOTELY_PROVISIONED_COMPONENT_SERVICE_NAME, remote_provisioning_service)?;
     }
 
     if cfg!(llpvm_changes) {
-        register(MAINTENANCE_SERVICE_NAME, maintenance::new_binder())?;
+        let maintenance_service =
+            BnVirtualizationMaintenance::new_binder(service.clone(), BinderFeatures::default());
+        register(MAINTENANCE_SERVICE_NAME, maintenance_service)?;
     }
 
     ProcessState::join_thread_pool();
diff --git a/virtualizationservice/src/maintenance.rs b/virtualizationservice/src/maintenance.rs
index 191d39a..7fc2f37 100644
--- a/virtualizationservice/src/maintenance.rs
+++ b/virtualizationservice/src/maintenance.rs
@@ -12,33 +12,245 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-use android_system_virtualizationmaintenance::aidl::android::system::virtualizationmaintenance;
-use anyhow::anyhow;
-use binder::{BinderFeatures, ExceptionCode, Interface, IntoBinderResult, Strong};
-use virtualizationmaintenance::IVirtualizationMaintenance::{
-    BnVirtualizationMaintenance, IVirtualizationMaintenance,
+use android_hardware_security_secretkeeper::aidl::android::hardware::security::secretkeeper::{
+    ISecretkeeper::ISecretkeeper, SecretId::SecretId,
 };
+use anyhow::Result;
+use log::{error, info, warn};
 
-pub(crate) fn new_binder() -> Strong<dyn IVirtualizationMaintenance> {
-    BnVirtualizationMaintenance::new_binder(
-        VirtualizationMaintenanceService {},
-        BinderFeatures::default(),
-    )
+mod vmdb;
+use vmdb::{VmId, VmIdDb};
+
+/// Interface name for the Secretkeeper HAL.
+const SECRETKEEPER_SERVICE: &str = "android.hardware.security.secretkeeper.ISecretkeeper/default";
+
+/// Directory in which to write persistent state.
+const PERSISTENT_DIRECTORY: &str = "/data/misc/apexdata/com.android.virt";
+
+/// Maximum number of VM IDs to delete at once.  Needs to be smaller than both the maximum
+/// number of SQLite parameters (999) and also small enough that an ISecretkeeper::deleteIds
+/// parcel fits within max AIDL message size.
+const DELETE_MAX_BATCH_SIZE: usize = 100;
+
+/// State related to VM secrets.
+pub struct State {
+    sk: binder::Strong<dyn ISecretkeeper>,
+    /// Database of VM IDs,
+    vm_id_db: VmIdDb,
+    batch_size: usize,
 }
 
-pub struct VirtualizationMaintenanceService;
-
-impl Interface for VirtualizationMaintenanceService {}
-
-#[allow(non_snake_case)]
-impl IVirtualizationMaintenance for VirtualizationMaintenanceService {
-    fn appRemoved(&self, _user_id: i32, _app_id: i32) -> binder::Result<()> {
-        Err(anyhow!("appRemoved not supported"))
-            .or_binder_exception(ExceptionCode::UNSUPPORTED_OPERATION)
+impl State {
+    pub fn new() -> Option<Self> {
+        let sk = match Self::find_sk() {
+            Some(sk) => sk,
+            None => {
+                warn!("failed to find a Secretkeeper instance; skipping secret management");
+                return None;
+            }
+        };
+        let (vm_id_db, created) = match VmIdDb::new(PERSISTENT_DIRECTORY) {
+            Ok(v) => v,
+            Err(e) => {
+                error!("skipping secret management, failed to connect to database: {e:?}");
+                return None;
+            }
+        };
+        if created {
+            // If the database did not previously exist, then this appears to be the first run of
+            // `virtualizationservice` since device setup or factory reset.  In case of the latter,
+            // delete any secrets that may be left over from before reset, thus ensuring that the
+            // local database state matches that of the TA (i.e. empty).
+            warn!("no existing VM ID DB; clearing any previous secrets to match fresh DB");
+            if let Err(e) = sk.deleteAll() {
+                error!("failed to delete previous secrets, dropping database: {e:?}");
+                vm_id_db.delete_db_file(PERSISTENT_DIRECTORY);
+                return None;
+            }
+        } else {
+            info!("re-using existing VM ID DB");
+        }
+        Some(Self { sk, vm_id_db, batch_size: DELETE_MAX_BATCH_SIZE })
     }
 
-    fn userRemoved(&self, _user_id: i32) -> binder::Result<()> {
-        Err(anyhow!("userRemoved not supported"))
-            .or_binder_exception(ExceptionCode::UNSUPPORTED_OPERATION)
+    fn find_sk() -> Option<binder::Strong<dyn ISecretkeeper>> {
+        if let Ok(true) = binder::is_declared(SECRETKEEPER_SERVICE) {
+            match binder::get_interface(SECRETKEEPER_SERVICE) {
+                Ok(sk) => Some(sk),
+                Err(e) => {
+                    error!("failed to connect to {SECRETKEEPER_SERVICE}: {e:?}");
+                    None
+                }
+            }
+        } else {
+            info!("instance {SECRETKEEPER_SERVICE} not declared");
+            None
+        }
+    }
+
+    /// Delete the VM IDs associated with Android user ID `user_id`.
+    pub fn delete_ids_for_user(&mut self, user_id: i32) -> Result<()> {
+        let vm_ids = self.vm_id_db.vm_ids_for_user(user_id)?;
+        info!(
+            "delete_ids_for_user(user_id={user_id}) triggers deletion of {} secrets",
+            vm_ids.len()
+        );
+        self.delete_ids(&vm_ids);
+        Ok(())
+    }
+
+    /// Delete the VM IDs associated with `(user_id, app_id)`.
+    pub fn delete_ids_for_app(&mut self, user_id: i32, app_id: i32) -> Result<()> {
+        let vm_ids = self.vm_id_db.vm_ids_for_app(user_id, app_id)?;
+        info!(
+            "delete_ids_for_app(user_id={user_id}, app_id={app_id}) removes {} secrets",
+            vm_ids.len()
+        );
+        self.delete_ids(&vm_ids);
+        Ok(())
+    }
+
+    /// Delete the provided VM IDs from both Secretkeeper and the database.
+    pub fn delete_ids(&mut self, mut vm_ids: &[VmId]) {
+        while !vm_ids.is_empty() {
+            let len = std::cmp::min(vm_ids.len(), self.batch_size);
+            let batch = &vm_ids[..len];
+            self.delete_ids_batch(batch);
+            vm_ids = &vm_ids[len..];
+        }
+    }
+
+    /// Delete a batch of VM IDs from both Secretkeeper and the database. The batch is assumed
+    /// to be smaller than both:
+    /// - the corresponding limit for number of database parameters
+    /// - the corresponding limit for maximum size of a single AIDL message for `ISecretkeeper`.
+    fn delete_ids_batch(&mut self, vm_ids: &[VmId]) {
+        let secret_ids: Vec<SecretId> = vm_ids.iter().map(|id| SecretId { id: *id }).collect();
+        if let Err(e) = self.sk.deleteIds(&secret_ids) {
+            error!("failed to delete all secrets from Secretkeeper: {e:?}");
+        }
+        if let Err(e) = self.vm_id_db.delete_vm_ids(vm_ids) {
+            error!("failed to remove secret IDs from database: {e:?}");
+        }
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use std::sync::{Arc, Mutex};
+    use android_hardware_security_authgraph::aidl::android::hardware::security::authgraph::{
+        IAuthGraphKeyExchange::IAuthGraphKeyExchange,
+    };
+    use android_hardware_security_secretkeeper::aidl::android::hardware::security::secretkeeper::{
+        ISecretkeeper::BnSecretkeeper
+    };
+
+    /// Fake implementation of Secretkeeper that keeps a history of what operations were invoked.
+    #[derive(Default)]
+    struct FakeSk {
+        history: Arc<Mutex<Vec<SkOp>>>,
+    }
+
+    #[derive(Clone, PartialEq, Eq, Debug)]
+    enum SkOp {
+        Management,
+        DeleteIds(Vec<VmId>),
+        DeleteAll,
+    }
+
+    impl ISecretkeeper for FakeSk {
+        fn processSecretManagementRequest(&self, _req: &[u8]) -> binder::Result<Vec<u8>> {
+            self.history.lock().unwrap().push(SkOp::Management);
+            Ok(vec![])
+        }
+
+        fn getAuthGraphKe(&self) -> binder::Result<binder::Strong<dyn IAuthGraphKeyExchange>> {
+            unimplemented!()
+        }
+
+        fn deleteIds(&self, ids: &[SecretId]) -> binder::Result<()> {
+            self.history.lock().unwrap().push(SkOp::DeleteIds(ids.iter().map(|s| s.id).collect()));
+            Ok(())
+        }
+
+        fn deleteAll(&self) -> binder::Result<()> {
+            self.history.lock().unwrap().push(SkOp::DeleteAll);
+            Ok(())
+        }
+    }
+    impl binder::Interface for FakeSk {}
+
+    fn new_test_state(history: Arc<Mutex<Vec<SkOp>>>, batch_size: usize) -> State {
+        let vm_id_db = vmdb::new_test_db();
+        let sk = FakeSk { history };
+        let sk = BnSecretkeeper::new_binder(sk, binder::BinderFeatures::default());
+        State { sk, vm_id_db, batch_size }
+    }
+
+    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];
+
+    #[test]
+    fn test_sk_state_batching() {
+        let history = Arc::new(Mutex::new(Vec::new()));
+        let mut sk_state = new_test_state(history.clone(), 2);
+        sk_state.delete_ids(&[VM_ID1, VM_ID2, VM_ID3, VM_ID4, VM_ID5]);
+        let got = (*history.lock().unwrap()).clone();
+        assert_eq!(
+            got,
+            vec![
+                SkOp::DeleteIds(vec![VM_ID1, VM_ID2]),
+                SkOp::DeleteIds(vec![VM_ID3, VM_ID4]),
+                SkOp::DeleteIds(vec![VM_ID5]),
+            ]
+        );
+    }
+
+    #[test]
+    fn test_sk_state_no_batching() {
+        let history = Arc::new(Mutex::new(Vec::new()));
+        let mut sk_state = new_test_state(history.clone(), 6);
+        sk_state.delete_ids(&[VM_ID1, VM_ID2, VM_ID3, VM_ID4, VM_ID5]);
+        let got = (*history.lock().unwrap()).clone();
+        assert_eq!(got, vec![SkOp::DeleteIds(vec![VM_ID1, VM_ID2, VM_ID3, VM_ID4, VM_ID5])]);
+    }
+
+    #[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);
+
+        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, USER3, APP_A).unwrap();
+        sk_state.vm_id_db.add_vm_id(&VM_ID5, USER3, APP_C).unwrap();
+        assert_eq!((*history.lock().unwrap()).clone(), vec![]);
+
+        sk_state.delete_ids_for_app(USER2, APP_B).unwrap();
+        assert_eq!((*history.lock().unwrap()).clone(), vec![SkOp::DeleteIds(vec![VM_ID3])]);
+
+        sk_state.delete_ids_for_user(USER3).unwrap();
+        assert_eq!(
+            (*history.lock().unwrap()).clone(),
+            vec![SkOp::DeleteIds(vec![VM_ID3]), SkOp::DeleteIds(vec![VM_ID4, VM_ID5]),]
+        );
+
+        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());
+        let empty: Vec<VmId> = Vec::new();
+        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());
     }
 }
diff --git a/virtualizationservice/src/maintenance/vmdb.rs b/virtualizationservice/src/maintenance/vmdb.rs
new file mode 100644
index 0000000..bdff034
--- /dev/null
+++ b/virtualizationservice/src/maintenance/vmdb.rs
@@ -0,0 +1,265 @@
+// Copyright 2024, The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+//! Database of VM IDs.
+
+use anyhow::{Context, Result};
+use log::{debug, error, info, warn};
+use rusqlite::{params, params_from_iter, Connection, OpenFlags, Rows};
+use std::path::PathBuf;
+
+/// Subdirectory to hold the database.
+const DB_DIR: &str = "vmdb";
+
+/// Name of the file that holds the database.
+const DB_FILENAME: &str = "vmids.sqlite";
+
+/// Maximum number of host parameters in a single SQL statement.
+/// (Default value of `SQLITE_LIMIT_VARIABLE_NUMBER` for <= 3.32.0)
+const MAX_VARIABLES: usize = 999;
+
+/// Identifier for a VM and its corresponding secret.
+pub type VmId = [u8; 64];
+
+/// Representation of an on-disk database of VM IDs.
+pub struct VmIdDb {
+    conn: Connection,
+}
+
+impl VmIdDb {
+    /// Connect to the VM ID database file held in the given directory, creating it if necessary.
+    /// The second return value indicates whether a new database file was created.
+    ///
+    /// This function assumes no other threads/processes are attempting to connect concurrently.
+    pub fn new(db_dir: &str) -> Result<(Self, bool)> {
+        let mut db_path = PathBuf::from(db_dir);
+        db_path.push(DB_DIR);
+        if !db_path.exists() {
+            std::fs::create_dir(&db_path).context("failed to create {db_path:?}")?;
+            info!("created persistent db dir {db_path:?}");
+        }
+
+        db_path.push(DB_FILENAME);
+        let (flags, created) = if db_path.exists() {
+            debug!("connecting to existing database {db_path:?}");
+            (
+                OpenFlags::SQLITE_OPEN_READ_WRITE
+                    | OpenFlags::SQLITE_OPEN_URI
+                    | OpenFlags::SQLITE_OPEN_NO_MUTEX,
+                false,
+            )
+        } else {
+            info!("creating fresh database {db_path:?}");
+            (
+                OpenFlags::SQLITE_OPEN_READ_WRITE
+                    | OpenFlags::SQLITE_OPEN_CREATE
+                    | OpenFlags::SQLITE_OPEN_URI
+                    | OpenFlags::SQLITE_OPEN_NO_MUTEX,
+                true,
+            )
+        };
+        let mut result = Self {
+            conn: Connection::open_with_flags(db_path, flags)
+                .context(format!("failed to open/create DB with {flags:?}"))?,
+        };
+
+        if created {
+            result.init_tables().context("failed to create tables")?;
+        }
+        Ok((result, created))
+    }
+
+    /// Delete the associated database file.
+    pub fn delete_db_file(self, db_dir: &str) {
+        let mut db_path = PathBuf::from(db_dir);
+        db_path.push(DB_DIR);
+        db_path.push(DB_FILENAME);
+
+        // Drop the connection before removing the backing file.
+        drop(self);
+        warn!("removing database file {db_path:?}");
+        if let Err(e) = std::fs::remove_file(&db_path) {
+            error!("failed to remove database file {db_path:?}: {e:?}");
+        }
+    }
+
+    /// Create the database table and indices.
+    fn init_tables(&mut self) -> Result<()> {
+        self.conn
+            .execute(
+                "CREATE TABLE IF NOT EXISTS main.vmids (
+                     vm_id BLOB PRIMARY KEY,
+                     user_id INTEGER,
+                     app_id INTEGER
+                 ) WITHOUT ROWID;",
+                (),
+            )
+            .context("failed to create table")?;
+        self.conn
+            .execute("CREATE INDEX IF NOT EXISTS main.vmids_user_index ON vmids(user_id);", [])
+            .context("Failed to create user index")?;
+        self.conn
+            .execute(
+                "CREATE INDEX IF NOT EXISTS main.vmids_app_index ON vmids(user_id, app_id);",
+                [],
+            )
+            .context("Failed to create app index")?;
+        Ok(())
+    }
+
+    /// Add the given VM ID into the database.
+    #[allow(dead_code)] // TODO(b/294177871): connect this up
+    pub fn add_vm_id(&mut self, vm_id: &VmId, user_id: i32, app_id: i32) -> Result<()> {
+        let _rows = self
+            .conn
+            .execute(
+                "REPLACE INTO main.vmids (vm_id, user_id, app_id) VALUES (?1, ?2, ?3);",
+                params![vm_id, &user_id, &app_id],
+            )
+            .context("failed to add VM ID")?;
+        Ok(())
+    }
+
+    /// Remove the given VM IDs from the database.  The collection of IDs is assumed to be smaller
+    /// than the maximum number of SQLite parameters.
+    pub fn delete_vm_ids(&mut self, vm_ids: &[VmId]) -> Result<()> {
+        assert!(vm_ids.len() < MAX_VARIABLES);
+        let mut vars = "?,".repeat(vm_ids.len());
+        vars.pop(); // remove trailing comma
+        let sql = format!("DELETE FROM main.vmids WHERE vm_id IN ({});", vars);
+        let mut stmt = self.conn.prepare(&sql).context("failed to prepare DELETE stmt")?;
+        let _rows = stmt.execute(params_from_iter(vm_ids)).context("failed to delete VM IDs")?;
+        Ok(())
+    }
+
+    /// Return the VM IDs associated with Android user ID `user_id`.
+    pub fn vm_ids_for_user(&mut self, user_id: i32) -> Result<Vec<VmId>> {
+        let mut stmt = self
+            .conn
+            .prepare("SELECT vm_id FROM main.vmids WHERE user_id = ?;")
+            .context("failed to prepare SELECT stmt")?;
+        let rows = stmt.query(params![user_id]).context("query failed")?;
+        Self::vm_ids_from_rows(rows)
+    }
+
+    /// Return the VM IDs associated with `(user_id, app_id)`.
+    pub fn vm_ids_for_app(&mut self, user_id: i32, app_id: i32) -> Result<Vec<VmId>> {
+        let mut stmt = self
+            .conn
+            .prepare("SELECT vm_id FROM main.vmids WHERE user_id = ? AND app_id = ?;")
+            .context("failed to prepare SELECT stmt")?;
+        let rows = stmt.query(params![user_id, app_id]).context("query failed")?;
+        Self::vm_ids_from_rows(rows)
+    }
+
+    /// Retrieve a collection of VM IDs from database rows.
+    fn vm_ids_from_rows(mut rows: Rows) -> Result<Vec<VmId>> {
+        let mut vm_ids: Vec<VmId> = Vec::new();
+        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:?}"),
+            }
+        }
+
+        Ok(vm_ids)
+    }
+}
+
+#[cfg(test)]
+pub fn new_test_db() -> VmIdDb {
+    let mut db = VmIdDb { conn: Connection::open_in_memory().unwrap() };
+    db.init_tables().unwrap();
+    db
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    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 USER_UNKNOWN: i32 = 4;
+    const APP_A: i32 = 50;
+    const APP_B: i32 = 60;
+    const APP_C: i32 = 70;
+    const APP_UNKNOWN: i32 = 99;
+
+    #[test]
+    fn test_add_remove() {
+        let mut db = new_test_db();
+        db.add_vm_id(&VM_ID1, USER1, APP_A).unwrap();
+        db.add_vm_id(&VM_ID2, USER1, APP_A).unwrap();
+        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();
+        let empty: Vec<VmId> = Vec::new();
+
+        assert_eq!(vec![VM_ID1, VM_ID2, VM_ID3], db.vm_ids_for_user(USER1).unwrap());
+        assert_eq!(vec![VM_ID1, VM_ID2, VM_ID3], db.vm_ids_for_app(USER1, APP_A).unwrap());
+        assert_eq!(vec![VM_ID4], db.vm_ids_for_app(USER2, APP_B).unwrap());
+        assert_eq!(vec![VM_ID5], db.vm_ids_for_user(USER3).unwrap());
+        assert_eq!(empty, db.vm_ids_for_user(USER_UNKNOWN).unwrap());
+        assert_eq!(empty, db.vm_ids_for_app(USER1, APP_UNKNOWN).unwrap());
+
+        db.delete_vm_ids(&[VM_ID2, VM_ID3]).unwrap();
+
+        assert_eq!(vec![VM_ID1], db.vm_ids_for_user(USER1).unwrap());
+        assert_eq!(vec![VM_ID1], db.vm_ids_for_app(USER1, APP_A).unwrap());
+
+        // OK to delete things that don't exist.
+        db.delete_vm_ids(&[VM_ID2, VM_ID3]).unwrap();
+
+        assert_eq!(vec![VM_ID1], db.vm_ids_for_user(USER1).unwrap());
+        assert_eq!(vec![VM_ID1], db.vm_ids_for_app(USER1, APP_A).unwrap());
+
+        db.add_vm_id(&VM_ID2, USER1, APP_A).unwrap();
+        db.add_vm_id(&VM_ID3, USER1, APP_A).unwrap();
+
+        assert_eq!(vec![VM_ID1, VM_ID2, VM_ID3], db.vm_ids_for_user(USER1).unwrap());
+        assert_eq!(vec![VM_ID1, VM_ID2, VM_ID3], db.vm_ids_for_app(USER1, APP_A).unwrap());
+        assert_eq!(vec![VM_ID4], db.vm_ids_for_app(USER2, APP_B).unwrap());
+        assert_eq!(vec![VM_ID5], db.vm_ids_for_user(USER3).unwrap());
+        assert_eq!(empty, db.vm_ids_for_user(USER_UNKNOWN).unwrap());
+        assert_eq!(empty, db.vm_ids_for_app(USER1, APP_UNKNOWN).unwrap());
+    }
+
+    #[test]
+    fn test_invalid_vm_id() {
+        let mut db = new_test_db();
+        db.add_vm_id(&VM_ID3, USER1, APP_A).unwrap();
+        db.add_vm_id(&VM_ID2, USER1, APP_A).unwrap();
+        db.add_vm_id(&VM_ID1, USER1, APP_A).unwrap();
+
+        // Note that results are returned in `vm_id` order, because the table is `WITHOUT ROWID`.
+        assert_eq!(vec![VM_ID1, VM_ID2, VM_ID3], db.vm_ids_for_user(USER1).unwrap());
+
+        // Manually insert a row with a VM ID that's the wrong size.
+        db.conn
+            .execute(
+                "REPLACE INTO main.vmids (vm_id, user_id, app_id) VALUES (?1, ?2, ?3);",
+                params![&[99u8; 60], &USER1, APP_A],
+            )
+            .unwrap();
+
+        // Invalid row is skipped and remainder returned.
+        assert_eq!(vec![VM_ID1, VM_ID2, VM_ID3], db.vm_ids_for_user(USER1).unwrap());
+    }
+}
diff --git a/vm/src/main.rs b/vm/src/main.rs
index 355e193..063f992 100644
--- a/vm/src/main.rs
+++ b/vm/src/main.rs
@@ -22,6 +22,8 @@
     CpuTopology::CpuTopology, IVirtualizationService::IVirtualizationService,
     PartitionType::PartitionType, VirtualMachineAppConfig::DebugLevel::DebugLevel,
 };
+#[cfg(not(llpvm_changes))]
+use anyhow::anyhow;
 use anyhow::{Context, Error};
 use binder::{ProcessState, Strong};
 use clap::{Args, Parser};
@@ -162,6 +164,11 @@
     /// Path to the instance image. Created if not exists.
     instance: PathBuf,
 
+    /// Path to file containing instance_id. Required iff llpvm feature is enabled.
+    #[cfg(llpvm_changes)]
+    #[arg(long = "instance-id-file")]
+    instance_id: PathBuf,
+
     /// Path to VM config JSON within APK (e.g. assets/vm_config.json)
     #[arg(long)]
     config_path: Option<String>,
@@ -192,6 +199,27 @@
     fn extra_apks(&self) -> &[PathBuf] {
         &[]
     }
+
+    #[cfg(llpvm_changes)]
+    fn instance_id(&self) -> Result<PathBuf, Error> {
+        Ok(self.instance_id.clone())
+    }
+
+    #[cfg(not(llpvm_changes))]
+    fn instance_id(&self) -> Result<PathBuf, Error> {
+        Err(anyhow!("LLPVM feature is disabled, --instance_id flag not supported"))
+    }
+
+    #[cfg(llpvm_changes)]
+    fn set_instance_id(&mut self, instance_id_file: PathBuf) -> Result<(), Error> {
+        self.instance_id = instance_id_file;
+        Ok(())
+    }
+
+    #[cfg(not(llpvm_changes))]
+    fn set_instance_id(&mut self, _: PathBuf) -> Result<(), Error> {
+        Err(anyhow!("LLPVM feature is disabled, --instance_id flag not supported"))
+    }
 }
 
 #[derive(Args, Default)]
diff --git a/vm/src/run.rs b/vm/src/run.rs
index 5a4a459..57b7641 100644
--- a/vm/src/run.rs
+++ b/vm/src/run.rs
@@ -35,6 +35,7 @@
 use std::fs;
 use std::fs::File;
 use std::io;
+use std::io::{Read, Write};
 use std::os::unix::io::{AsRawFd, FromRawFd};
 use std::path::{Path, PathBuf};
 use vmclient::{ErrorCode, VmInstance};
@@ -84,6 +85,24 @@
         )?;
     }
 
+    let instance_id = if cfg!(llpvm_changes) {
+        let id_file = config.instance_id()?;
+        if id_file.exists() {
+            let mut id = [0u8; 64];
+            let mut instance_id_file = File::open(id_file)?;
+            instance_id_file.read_exact(&mut id)?;
+            id
+        } else {
+            let id = service.allocateInstanceId().context("Failed to allocate instance_id")?;
+            let mut instance_id_file = File::create(id_file)?;
+            instance_id_file.write_all(&id)?;
+            id
+        }
+    } else {
+        // if llpvm feature flag is disabled, instance_id is not used.
+        [0u8; 64]
+    };
+
     let storage = if let Some(ref path) = config.microdroid.storage {
         if !path.exists() {
             command_create_partition(
@@ -153,6 +172,7 @@
         idsig: idsig_fd.into(),
         extraIdsigs: extra_idsig_fds,
         instanceImage: open_parcel_file(&config.instance, true /* writable */)?.into(),
+        instanceId: instance_id,
         encryptedStorageImage: storage,
         payload,
         debugLevel: config.debug.debug,
@@ -204,7 +224,7 @@
     let instance_img = work_dir.join("instance.img");
     println!("instance.img path: {}", instance_img.display());
 
-    let app_config = RunAppConfig {
+    let mut app_config = RunAppConfig {
         common: config.common,
         debug: config.debug,
         microdroid: config.microdroid,
@@ -214,6 +234,12 @@
         payload_binary_name: Some("MicrodroidEmptyPayloadJniLib.so".to_owned()),
         ..Default::default()
     };
+
+    if cfg!(llpvm_changes) {
+        app_config.set_instance_id(work_dir.join("instance_id"))?;
+        println!("instance_id file path: {}", app_config.instance_id()?.display());
+    }
+
     command_run_app(app_config)
 }
 
diff --git a/vm_payload/Android.bp b/vm_payload/Android.bp
index a745fd6..80d289b 100644
--- a/vm_payload/Android.bp
+++ b/vm_payload/Android.bp
@@ -34,7 +34,7 @@
     source_stem: "bindings",
     bindgen_flags: [
         "--default-enum-style rust",
-        "--allowlist-type=attestation_status_t",
+        "--allowlist-type=AVmAttestationStatus",
     ],
     visibility: [":__subpackages__"],
 }
diff --git a/vm_payload/include-restricted/vm_payload_restricted.h b/vm_payload/include-restricted/vm_payload_restricted.h
index d7324a8..5dd12ad 100644
--- a/vm_payload/include-restricted/vm_payload_restricted.h
+++ b/vm_payload/include-restricted/vm_payload_restricted.h
@@ -72,7 +72,7 @@
  *               succeeds. The result remains valid until it is freed with
  *              `AVmPayload_freeAttestationResult`.
  */
-attestation_status_t AVmPayload_requestAttestationForTesting(
+AVmAttestationStatus AVmPayload_requestAttestationForTesting(
         const void* _Nonnull challenge, size_t challenge_size,
         struct AVmAttestationResult* _Nullable* _Nonnull result) __INTRODUCED_IN(__ANDROID_API_V__);
 
diff --git a/vm_payload/include/vm_payload.h b/vm_payload/include/vm_payload.h
index af755c9..5e15607 100644
--- a/vm_payload/include/vm_payload.h
+++ b/vm_payload/include/vm_payload.h
@@ -25,20 +25,19 @@
 
 __BEGIN_DECLS
 
-struct AIBinder;
 typedef struct AIBinder AIBinder;
 
 /**
  * Introduced in API 35.
  * Remote attestation result if the attestation succeeds.
  */
-struct AVmAttestationResult;
+typedef struct AVmAttestationResult AVmAttestationResult;
 
 /**
  * Introduced in API 35.
  * Remote attestation status types returned from remote attestation functions.
  */
-typedef enum attestation_status_t : int32_t {
+typedef enum AVmAttestationStatus : int32_t {
     /** The remote attestation completes successfully. */
     ATTESTATION_OK = 0,
 
@@ -50,7 +49,7 @@
 
     /** Remote attestation is not supported in the current environment. */
     ATTESTATION_ERROR_UNSUPPORTED = -10003,
-} attestation_status_t;
+} AVmAttestationStatus;
 
 /**
  * Notifies the host that the payload is ready.
@@ -151,9 +150,10 @@
  *
  * \return ATTESTATION_OK upon successful attestation.
  */
-attestation_status_t AVmPayload_requestAttestation(
-        const void* _Nonnull challenge, size_t challenge_size,
-        struct AVmAttestationResult* _Nullable* _Nonnull result) __INTRODUCED_IN(__ANDROID_API_V__);
+AVmAttestationStatus AVmPayload_requestAttestation(const void* _Nonnull challenge,
+                                                   size_t challenge_size,
+                                                   AVmAttestationResult* _Nullable* _Nonnull result)
+        __INTRODUCED_IN(__ANDROID_API_V__);
 
 /**
  * Converts the return value from `AVmPayload_requestAttestation` to a text string
@@ -162,7 +162,7 @@
  * \return a constant string value representing the status code. The string should not
  * be deleted or freed by the application and remains valid for the lifetime of the VM.
  */
-const char* _Nonnull AVmAttestationResult_resultToString(attestation_status_t status)
+const char* _Nonnull AVmAttestationStatus_toString(AVmAttestationStatus status)
         __INTRODUCED_IN(__ANDROID_API_V__);
 
 /**
@@ -173,7 +173,7 @@
  *
  * \param result A pointer to the attestation result.
  */
-void AVmAttestationResult_free(struct AVmAttestationResult* _Nullable result)
+void AVmAttestationResult_free(AVmAttestationResult* _Nullable result)
         __INTRODUCED_IN(__ANDROID_API_V__);
 
 /**
@@ -192,7 +192,7 @@
  *
  * [RFC 5915 s3]: https://datatracker.ietf.org/doc/html/rfc5915#section-3
  */
-size_t AVmAttestationResult_getPrivateKey(const struct AVmAttestationResult* _Nonnull result,
+size_t AVmAttestationResult_getPrivateKey(const AVmAttestationResult* _Nonnull result,
                                           void* _Nullable data, size_t size)
         __INTRODUCED_IN(__ANDROID_API_V__);
 
@@ -215,7 +215,7 @@
  *
  * [RFC 6979]: https://datatracker.ietf.org/doc/html/rfc6979
  */
-size_t AVmAttestationResult_sign(const struct AVmAttestationResult* _Nonnull result,
+size_t AVmAttestationResult_sign(const AVmAttestationResult* _Nonnull result,
                                  const void* _Nonnull message, size_t message_size,
                                  void* _Nullable data, size_t size)
         __INTRODUCED_IN(__ANDROID_API_V__);
@@ -232,7 +232,7 @@
  *
  * \return The number of certificates in the certificate chain.
  */
-size_t AVmAttestationResult_getCertificateCount(const struct AVmAttestationResult* _Nonnull result)
+size_t AVmAttestationResult_getCertificateCount(const AVmAttestationResult* _Nonnull result)
         __INTRODUCED_IN(__ANDROID_API_V__);
 
 /**
@@ -256,7 +256,7 @@
  *
  * \return The total size of the certificate at the given `index`.
  */
-size_t AVmAttestationResult_getCertificateAt(const struct AVmAttestationResult* _Nonnull result,
+size_t AVmAttestationResult_getCertificateAt(const AVmAttestationResult* _Nonnull result,
                                              size_t index, void* _Nullable data, size_t size)
         __INTRODUCED_IN(__ANDROID_API_V__);
 
diff --git a/vm_payload/libvm_payload.map.txt b/vm_payload/libvm_payload.map.txt
index caf8f84..3daad00 100644
--- a/vm_payload/libvm_payload.map.txt
+++ b/vm_payload/libvm_payload.map.txt
@@ -12,7 +12,7 @@
     AVmAttestationResult_getPrivateKey;  # systemapi introduced=VanillaIceCream
     AVmAttestationResult_sign;           # systemapi introduced=VanillaIceCream
     AVmAttestationResult_free;           # systemapi introduced=VanillaIceCream
-    AVmAttestationResult_resultToString; # systemapi introduced=VanillaIceCream
+    AVmAttestationStatus_toString;       # systemapi introduced=VanillaIceCream
     AVmAttestationResult_getCertificateCount; # systemapi introduced=VanillaIceCream
     AVmAttestationResult_getCertificateAt; # systemapi introduced=VanillaIceCream
   local:
diff --git a/vm_payload/src/lib.rs b/vm_payload/src/lib.rs
index 6188b21..5cc4431 100644
--- a/vm_payload/src/lib.rs
+++ b/vm_payload/src/lib.rs
@@ -37,7 +37,7 @@
     atomic::{AtomicBool, Ordering},
     Mutex,
 };
-use vm_payload_status_bindgen::attestation_status_t;
+use vm_payload_status_bindgen::AVmAttestationStatus;
 
 /// Maximum size of an ECDSA signature for EC P-256 key is 72 bytes.
 const MAX_ECDSA_P256_SIGNATURE_SIZE: usize = 72;
@@ -283,7 +283,7 @@
     challenge: *const u8,
     challenge_size: usize,
     res: &mut *mut AttestationResult,
-) -> attestation_status_t {
+) -> AVmAttestationStatus {
     // SAFETY: The caller guarantees that `challenge` is valid for reads and `res` is valid
     // for writes.
     unsafe {
@@ -310,7 +310,7 @@
     challenge: *const u8,
     challenge_size: usize,
     res: &mut *mut AttestationResult,
-) -> attestation_status_t {
+) -> AVmAttestationStatus {
     // SAFETY: The caller guarantees that `challenge` is valid for reads and `res` is valid
     // for writes.
     unsafe {
@@ -337,11 +337,11 @@
     challenge_size: usize,
     test_mode: bool,
     res: &mut *mut AttestationResult,
-) -> attestation_status_t {
+) -> AVmAttestationStatus {
     initialize_logging();
     const MAX_CHALLENGE_SIZE: usize = 64;
     if challenge_size > MAX_CHALLENGE_SIZE {
-        return attestation_status_t::ATTESTATION_ERROR_INVALID_CHALLENGE;
+        return AVmAttestationStatus::ATTESTATION_ERROR_INVALID_CHALLENGE;
     }
     let challenge = if challenge_size == 0 {
         &[]
@@ -354,7 +354,7 @@
     match service.requestAttestation(challenge, test_mode) {
         Ok(attestation_res) => {
             *res = Box::into_raw(Box::new(attestation_res));
-            attestation_status_t::ATTESTATION_OK
+            AVmAttestationStatus::ATTESTATION_OK
         }
         Err(e) => {
             error!("Remote attestation failed: {e:?}");
@@ -363,31 +363,29 @@
     }
 }
 
-fn binder_status_to_attestation_status(status: binder::Status) -> attestation_status_t {
+fn binder_status_to_attestation_status(status: binder::Status) -> AVmAttestationStatus {
     match status.exception_code() {
-        ExceptionCode::UNSUPPORTED_OPERATION => attestation_status_t::ATTESTATION_ERROR_UNSUPPORTED,
-        _ => attestation_status_t::ATTESTATION_ERROR_ATTESTATION_FAILED,
+        ExceptionCode::UNSUPPORTED_OPERATION => AVmAttestationStatus::ATTESTATION_ERROR_UNSUPPORTED,
+        _ => AVmAttestationStatus::ATTESTATION_ERROR_ATTESTATION_FAILED,
     }
 }
 
 /// Converts the return value from `AVmPayload_requestAttestation` to a text string
 /// representing the error code.
 #[no_mangle]
-pub extern "C" fn AVmAttestationResult_resultToString(
-    status: attestation_status_t,
-) -> *const c_char {
+pub extern "C" fn AVmAttestationStatus_toString(status: AVmAttestationStatus) -> *const c_char {
     let message = match status {
-        attestation_status_t::ATTESTATION_OK => {
+        AVmAttestationStatus::ATTESTATION_OK => {
             CStr::from_bytes_with_nul(b"The remote attestation completes successfully.\0").unwrap()
         }
-        attestation_status_t::ATTESTATION_ERROR_INVALID_CHALLENGE => {
+        AVmAttestationStatus::ATTESTATION_ERROR_INVALID_CHALLENGE => {
             CStr::from_bytes_with_nul(b"The challenge size is not between 0 and 64.\0").unwrap()
         }
-        attestation_status_t::ATTESTATION_ERROR_ATTESTATION_FAILED => {
+        AVmAttestationStatus::ATTESTATION_ERROR_ATTESTATION_FAILED => {
             CStr::from_bytes_with_nul(b"Failed to attest the VM. Please retry at a later time.\0")
                 .unwrap()
         }
-        attestation_status_t::ATTESTATION_ERROR_UNSUPPORTED => CStr::from_bytes_with_nul(
+        AVmAttestationStatus::ATTESTATION_ERROR_UNSUPPORTED => CStr::from_bytes_with_nul(
             b"Remote attestation is not supported in the current environment.\0",
         )
         .unwrap(),