Merge "Disallow vendor and odm files for a VM" into main
diff --git a/TEST_MAPPING b/TEST_MAPPING
index f146b4e..8fe6948 100644
--- a/TEST_MAPPING
+++ b/TEST_MAPPING
@@ -12,6 +12,9 @@
       "name": "MicrodroidTestApp"
     },
     {
+      "name": "MicrodroidTestAppNoPerm"
+    },
+    {
       "name": "VmAttestationTestApp"
     },
     {
@@ -44,9 +47,6 @@
   ],
   "avf-postsubmit": [
     {
-      "name": "odsign_e2e_tests_full"
-    },
-    {
       "name": "MicrodroidBenchmarkApp"
     },
     {
diff --git a/apex/Android.bp b/apex/Android.bp
index f2badfa..399ad97 100644
--- a/apex/Android.bp
+++ b/apex/Android.bp
@@ -81,7 +81,6 @@
     // TODO(b/295593640) Unfortunately these are added to the apex even though they are unused.
     // Once the build system is fixed, remove this.
     unwanted_transitive_deps: [
-        "libdrm",
         "libsso",
         "libutils",
     ],
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..9a38acf 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);
     }
 
@@ -814,6 +853,18 @@
                 VirtualMachineAppConfig appConfig =
                         vmConfig.toVsConfig(mContext.getPackageManager());
                 appConfig.name = mName;
+                if (mInstanceIdPath != null) {
+                    appConfig.instanceId = Files.readAllBytes(mInstanceIdPath.toPath());
+                    appConfig.instanceImage =
+                            ParcelFileDescriptor.open(mInstanceFilePath, MODE_READ_WRITE);
+                } 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,7 @@
             try {
                 return new VirtualMachineDescriptor(
                         ParcelFileDescriptor.open(mConfigFilePath, MODE_READ_ONLY),
+                        ParcelFileDescriptor.open(mInstanceIdPath, MODE_READ_ONLY),
                         ParcelFileDescriptor.open(mInstanceFilePath, MODE_READ_ONLY),
                         mEncryptedStoreFilePath != null
                                 ? ParcelFileDescriptor.open(mEncryptedStoreFilePath, MODE_READ_ONLY)
@@ -1384,6 +1434,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/VirtualizationSystemService.java b/java/service/src/com/android/system/virtualmachine/VirtualizationSystemService.java
index 2905acd..3f973b4 100644
--- a/java/service/src/com/android/system/virtualmachine/VirtualizationSystemService.java
+++ b/java/service/src/com/android/system/virtualmachine/VirtualizationSystemService.java
@@ -16,16 +16,121 @@
 
 package com.android.system.virtualmachine;
 
+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();
+    }
+
+    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);
+        }
+    }
+
+    private 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/libs/libfdt/src/lib.rs b/libs/libfdt/src/lib.rs
index 2ec097a..8ea9cd9 100644
--- a/libs/libfdt/src/lib.rs
+++ b/libs/libfdt/src/lib.rs
@@ -478,14 +478,38 @@
         self.delete_and_next(next_offset)
     }
 
-    /// Returns the next node
+    /// Returns the next node. Use this API to travel descendant of a node.
+    ///
+    /// Returned depth is relative to the initial node that had called with any of next node APIs.
+    /// Returns None if end of FDT reached or depth becomes negative.
+    ///
+    /// See also: [`next_node_skip_subnodes`], and [`delete_and_next_node`]
     pub fn next_node(self, depth: usize) -> Result<Option<(Self, usize)>> {
         let next = self.fdt.next_node(self.offset, depth)?;
 
         Ok(next.map(|(offset, depth)| (Self { fdt: self.fdt, offset }, depth)))
     }
 
-    /// Deletes this and returns the next node
+    /// Returns the next node skipping subnodes. Use this API to travel descendants of a node while
+    /// ignoring certain node.
+    ///
+    /// Returned depth is relative to the initial node that had called with any of next node APIs.
+    /// Returns None if end of FDT reached or depth becomes negative.
+    ///
+    /// See also: [`next_node`], and [`delete_and_next_node`]
+    pub fn next_node_skip_subnodes(self, depth: usize) -> Result<Option<(Self, usize)>> {
+        let next = self.fdt.next_node_skip_subnodes(self.offset, depth)?;
+
+        Ok(next.map(|(offset, depth)| (Self { fdt: self.fdt, offset }, depth)))
+    }
+
+    /// Deletes this and returns the next node. Use this API to travel descendants of a node while
+    /// removing certain node.
+    ///
+    /// Returned depth is relative to the initial node that had called with any of next node APIs.
+    /// Returns None if end of FDT reached or depth becomes negative.
+    ///
+    /// See also: [`next_node`], and [`next_node_skip_subnodes`]
     pub fn delete_and_next_node(self, depth: usize) -> Result<Option<(Self, usize)>> {
         let next_node = self.fdt.next_node_skip_subnodes(self.offset, depth)?;
         if let Some((offset, depth)) = next_node {
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/vm_secret.rs b/microdroid_manager/src/vm_secret.rs
index 0e1ec71..7df7cc9 100644
--- a/microdroid_manager/src/vm_secret.rs
+++ b/microdroid_manager/src/vm_secret.rs
@@ -101,7 +101,7 @@
         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 mut session = SkSession::new(sk_service, &explicit_dice, None)?;
         let mut skp_secret = Zeroizing::new([0u8; SECRET_SIZE]);
         if super::is_strict_boot() {
             if super::is_new_instance() {
diff --git a/pvmfw/src/device_assignment.rs b/pvmfw/src/device_assignment.rs
index e427710..2c47f9e 100644
--- a/pvmfw/src/device_assignment.rs
+++ b/pvmfw/src/device_assignment.rs
@@ -294,6 +294,26 @@
         .map_or(false, |name| name == b"__overlay__")
 }
 
+fn filter_dangling_symbols(fdt: &mut Fdt) -> Result<()> {
+    if let Some(symbols) = fdt.symbols()? {
+        let mut removed = vec![];
+        for prop in symbols.properties()? {
+            let path = CStr::from_bytes_with_nul(prop.value()?)
+                .map_err(|_| DeviceAssignmentError::Internal)?;
+            if fdt.node(path)?.is_none() {
+                let name = prop.name()?;
+                removed.push(CString::from(name));
+            }
+        }
+
+        let mut symbols = fdt.symbols_mut()?.unwrap();
+        for name in removed {
+            symbols.nop_property(&name)?;
+        }
+    }
+    Ok(())
+}
+
 impl AsRef<Fdt> for VmDtbo {
     fn as_ref(&self) -> &Fdt {
         &self.0
@@ -744,7 +764,8 @@
             device.patch(fdt, &pviommu_phandles)?;
         }
 
-        Ok(())
+        // Removes any dangling references in __symbols__ (e.g. removed pvIOMMUs)
+        filter_dangling_symbols(fdt)
     }
 }
 
@@ -1020,6 +1041,39 @@
     }
 
     #[test]
+    fn device_info_patch_no_pviommus() {
+        let mut fdt_data = fs::read(FDT_WITHOUT_IOMMUS_FILE_PATH).unwrap();
+        let mut vm_dtbo_data = fs::read(VM_DTBO_FILE_PATH).unwrap();
+        let mut data = vec![0_u8; fdt_data.len() + vm_dtbo_data.len()];
+        let fdt = Fdt::from_mut_slice(&mut fdt_data).unwrap();
+        let vm_dtbo = VmDtbo::from_mut_slice(&mut vm_dtbo_data).unwrap();
+        let platform_dt = Fdt::create_empty_tree(data.as_mut_slice()).unwrap();
+
+        let hypervisor = MockHypervisor {
+            mmio_tokens: [((0x9, 0xFF), 0x300)].into(),
+            iommu_tokens: BTreeMap::new(),
+        };
+        let device_info = DeviceAssignmentInfo::parse(fdt, vm_dtbo, &hypervisor).unwrap().unwrap();
+        device_info.filter(vm_dtbo).unwrap();
+
+        // SAFETY: Damaged VM DTBO wouldn't be used after this unsafe block.
+        unsafe {
+            platform_dt.apply_overlay(vm_dtbo.as_mut()).unwrap();
+        }
+        device_info.patch(platform_dt).unwrap();
+
+        let compatible = platform_dt.root().next_compatible(cstr!("pkvm,pviommu")).unwrap();
+        assert_eq!(None, compatible);
+
+        if let Some(symbols) = platform_dt.symbols().unwrap() {
+            for prop in symbols.properties().unwrap() {
+                let path = CStr::from_bytes_with_nul(prop.value().unwrap()).unwrap();
+                assert_ne!(None, platform_dt.node(path).unwrap());
+            }
+        }
+    }
+
+    #[test]
     fn device_info_overlay_iommu() {
         let mut fdt_data = fs::read(FDT_FILE_PATH).unwrap();
         let mut vm_dtbo_data = fs::read(VM_DTBO_FILE_PATH).unwrap();
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/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..c40a312 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");
     }
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_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..3c20a0f
--- /dev/null
+++ b/tests/testapk_no_perm/src/java/com/android/microdroid/test/MicrodroidTestAppNoPerm.java
@@ -0,0 +1,58 @@
+/*
+ * 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.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/**
+ * Test that the android.permission.MANAGE_VIRTUAL_MACHINE is enforced and that an app cannot launch
+ * a VM without said permission.
+ */
+@RunWith(JUnit4.class)
+public class MicrodroidTestAppNoPerm extends MicrodroidDeviceTestBase {
+    @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 6ddf952..961bb24 100644
--- a/virtualizationmanager/src/aidl.rs
+++ b/virtualizationmanager/src/aidl.rs
@@ -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,
@@ -399,9 +403,9 @@
             vec![]
         };
 
+        let instance_id;
         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";
+            instance_id = extract_instance_id(config);
             vec![(cstr!("instance-id"), &instance_id[..])]
         } else {
             vec![]
@@ -1306,6 +1310,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),
diff --git a/virtualizationservice/Android.bp b/virtualizationservice/Android.bp
index 38a9ec5..5dd1e0f 100644
--- a/virtualizationservice/Android.bp
+++ b/virtualizationservice/Android.bp
@@ -32,12 +32,14 @@
         "libanyhow",
         "libavflog",
         "libbinder_rs",
+        "libhex",
         "libhypervisor_props",
         "liblazy_static",
         "liblibc",
         "liblog_rust",
         "libnix",
         "libopenssl",
+        "librand",
         "librkpd_client",
         "librustutils",
         "libstatslog_virtualization_rust",
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/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..fc36190 100644
--- a/virtualizationservice/aidl/android/system/virtualizationservice_internal/IVirtualizationServiceInternal.aidl
+++ b/virtualizationservice/aidl/android/system/virtualizationservice_internal/IVirtualizationServiceInternal.aidl
@@ -91,4 +91,9 @@
 
     /** 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();
 }
diff --git a/virtualizationservice/src/aidl.rs b/virtualizationservice/src/aidl.rs
index c7a33ba..3bc7caf 100644
--- a/virtualizationservice/src/aidl.rs
+++ b/virtualizationservice/src/aidl.rs
@@ -35,6 +35,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;
@@ -378,6 +379,17 @@
         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)
+    }
 }
 
 // KEEP IN SYNC WITH assignable_devices.xsd
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)
 }