Merge "Upgrade nix to 0.28.0" into main
diff --git a/Android.bp b/Android.bp
index 7cedfb7..9734cba 100644
--- a/Android.bp
+++ b/Android.bp
@@ -30,6 +30,7 @@
         "release_avf_enable_remote_attestation",
         "release_avf_enable_vendor_modules",
         "release_avf_enable_virt_cpufreq",
+        "release_avf_support_custom_vm_with_paravirtualized_devices",
     ],
     properties: [
         "cfgs",
@@ -60,6 +61,9 @@
         release_avf_enable_virt_cpufreq: {
             cfgs: ["virt_cpufreq"],
         },
+        release_avf_support_custom_vm_with_paravirtualized_devices: {
+            cfgs: ["paravirtualized_devices"],
+        },
     },
 }
 
diff --git a/TEST_MAPPING b/TEST_MAPPING
index 58dcc06..1c4f5ca 100644
--- a/TEST_MAPPING
+++ b/TEST_MAPPING
@@ -36,15 +36,9 @@
       "name": "initrd_bootconfig.test"
     },
     {
-      "name": "libdice_policy.test"
-    },
-    {
       "name": "libapkzip.test"
     },
     {
-      "name": "libsecretkeeper_comm.test"
-    },
-    {
       "name": "libdice_driver_test"
     }
   ],
diff --git a/apex/Android.bp b/apex/Android.bp
index 3b5141e..e6c809c 100644
--- a/apex/Android.bp
+++ b/apex/Android.bp
@@ -42,6 +42,7 @@
         "release_avf_enable_remote_attestation",
         "release_avf_enable_vendor_modules",
         "release_avf_enable_virt_cpufreq",
+        "release_avf_support_custom_vm_with_paravirtualized_devices",
     ],
     properties: [
         "androidManifest",
@@ -50,6 +51,7 @@
         "prebuilts",
         "systemserverclasspath_fragments",
         "vintf_fragments",
+        "apps",
     ],
 }
 
@@ -96,6 +98,11 @@
                 canned_fs_config: "canned_fs_config",
             },
         },
+        release_avf_support_custom_vm_with_paravirtualized_devices: {
+            apps: [
+                "VmLauncherApp",
+            ],
+        },
     },
 }
 
diff --git a/java/framework/src/android/system/virtualmachine/VirtualMachineConfig.java b/java/framework/src/android/system/virtualmachine/VirtualMachineConfig.java
index a8f318c..d267763 100644
--- a/java/framework/src/android/system/virtualmachine/VirtualMachineConfig.java
+++ b/java/framework/src/android/system/virtualmachine/VirtualMachineConfig.java
@@ -40,6 +40,7 @@
 import android.sysprop.HypervisorProperties;
 import android.system.virtualizationservice.DiskImage;
 import android.system.virtualizationservice.Partition;
+import android.system.virtualizationservice.InputDevice;
 import android.system.virtualizationservice.VirtualMachineAppConfig;
 import android.system.virtualizationservice.VirtualMachinePayloadConfig;
 import android.system.virtualizationservice.VirtualMachineRawConfig;
@@ -650,6 +651,7 @@
         config.cpuTopology = (byte) this.mCpuTopology;
         config.devices = EMPTY_STRING_ARRAY;
         config.platformVersion = "~1.0";
+        config.inputDevices = new InputDevice[0];
         return config;
     }
 
diff --git a/libs/android_display_backend/Android.bp b/libs/android_display_backend/Android.bp
index 6ad5fab..f792a04 100644
--- a/libs/android_display_backend/Android.bp
+++ b/libs/android_display_backend/Android.bp
@@ -11,6 +11,9 @@
     backend: {
         java: {
             enabled: true,
+            apex_available: [
+                "com.android.virt",
+            ],
         },
         cpp: {
             enabled: false,
diff --git a/tests/helper/src/java/com/android/microdroid/test/device/MicrodroidDeviceTestBase.java b/tests/helper/src/java/com/android/microdroid/test/device/MicrodroidDeviceTestBase.java
index 6c82de8..f881909 100644
--- a/tests/helper/src/java/com/android/microdroid/test/device/MicrodroidDeviceTestBase.java
+++ b/tests/helper/src/java/com/android/microdroid/test/device/MicrodroidDeviceTestBase.java
@@ -155,6 +155,12 @@
     public VirtualMachine forceCreateNewVirtualMachine(String name, VirtualMachineConfig config)
             throws VirtualMachineException {
         final VirtualMachineManager vmm = getVirtualMachineManager();
+        deleteVirtualMachineIfExists(name);
+        return vmm.create(name, config);
+    }
+
+    protected void deleteVirtualMachineIfExists(String name) throws VirtualMachineException {
+        VirtualMachineManager vmm = getVirtualMachineManager();
         boolean deleteExisting;
         try {
             deleteExisting = vmm.get(name) != null;
@@ -166,7 +172,6 @@
         if (deleteExisting) {
             vmm.delete(name);
         }
-        return vmm.create(name, config);
     }
 
     public void prepareTestSetup(boolean protectedVm, String gki) {
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 8f4df63..29e9014 100644
--- a/tests/testapk/src/java/com/android/microdroid/test/MicrodroidTests.java
+++ b/tests/testapk/src/java/com/android/microdroid/test/MicrodroidTests.java
@@ -37,6 +37,7 @@
 import static org.junit.Assume.assumeTrue;
 
 import static java.nio.file.StandardCopyOption.REPLACE_EXISTING;
+import static java.util.stream.Collectors.toList;
 
 import android.app.Instrumentation;
 import android.app.UiAutomation;
@@ -110,6 +111,7 @@
 import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.atomic.AtomicReference;
+import java.util.stream.Stream;
 
 import co.nstant.in.cbor.CborDecoder;
 import co.nstant.in.cbor.model.Array;
@@ -1055,6 +1057,51 @@
         changeDebugLevel(DEBUG_LEVEL_NONE, DEBUG_LEVEL_FULL);
     }
 
+    // Copy the Vm directory, creating the target Vm directory if it does not already exist.
+    private void copyVmDirectory(String sourceVmName, String targetVmName) throws IOException {
+        Path sourceVm = getVmDirectory(sourceVmName);
+        Path targetVm = getVmDirectory(targetVmName);
+        if (!Files.exists(targetVm)) {
+            Files.createDirectories(targetVm);
+        }
+
+        try (Stream<Path> stream = Files.list(sourceVm)) {
+            for (Path f : stream.collect(toList())) {
+                Files.copy(f, targetVm.resolve(f.getFileName()), REPLACE_EXISTING);
+            }
+        }
+    }
+
+    private Path getVmDirectory(String vmName) {
+        Context context = getContext();
+        Path filePath = Paths.get(context.getDataDir().getPath(), "vm", vmName);
+        return filePath;
+    }
+
+    // Create a fresh VM with the given `vmName`, instance_id & instance.img. This function creates
+    // a Vm with a different temporary name & copies it to target VM directory. This ensures this
+    // VM is not in cache of `VirtualMachineManager` which makes it possible to modify underlying
+    // files.
+    private void createUncachedVmWithName(
+            String vmName, VirtualMachineConfig config, File vmIdBackup, File vmInstanceBackup)
+            throws Exception {
+        deleteVirtualMachineIfExists(vmName);
+        forceCreateNewVirtualMachine("test_vm_tmp", config);
+        copyVmDirectory("test_vm_tmp", vmName);
+        if (vmInstanceBackup != null) {
+            Files.copy(
+                    vmInstanceBackup.toPath(),
+                    getVmFile(vmName, "instance.img").toPath(),
+                    REPLACE_EXISTING);
+        }
+        if (vmIdBackup != null) {
+            Files.copy(
+                    vmIdBackup.toPath(),
+                    getVmFile(vmName, "instance_id").toPath(),
+                    REPLACE_EXISTING);
+        }
+    }
+
     @Test
     @CddTest(requirements = {"9.17/C-1-1", "9.17/C-2-7"})
     public void changingDebuggableVmNonDebuggableInvalidatesVmIdentity() throws Exception {
@@ -1089,29 +1136,17 @@
             Files.copy(vmId.toPath(), vmIdBackup.toPath(), REPLACE_EXISTING);
         }
 
-        forceCreateNewVirtualMachine("test_vm", normalConfig);
-
-        if (vmInstanceBackup != null) {
-            Files.copy(vmInstanceBackup.toPath(), vmInstance.toPath(), REPLACE_EXISTING);
-        }
-        if (vmIdBackup != null) {
-            Files.copy(vmIdBackup.toPath(), vmId.toPath(), REPLACE_EXISTING);
-        }
-        assertThat(tryBootVm(TAG, "test_vm").payloadStarted).isTrue();
+        createUncachedVmWithName("test_vm_rerun", normalConfig, vmIdBackup, vmInstanceBackup);
+        assertThat(tryBootVm(TAG, "test_vm_rerun").payloadStarted).isTrue();
 
         // Launch the same VM with a different debug level. The Java API prohibits this
         // (thankfully).
         // For testing, we do that by creating a new VM with debug level, and overwriting the old
         // instance data to the new VM instance data.
         VirtualMachineConfig debugConfig = builder.setDebugLevel(toLevel).build();
-        forceCreateNewVirtualMachine("test_vm", debugConfig);
-        if (vmInstanceBackup != null) {
-            Files.copy(vmInstanceBackup.toPath(), vmInstance.toPath(), REPLACE_EXISTING);
-        }
-        if (vmIdBackup != null) {
-            Files.copy(vmIdBackup.toPath(), vmId.toPath(), REPLACE_EXISTING);
-        }
-        assertThat(tryBootVm(TAG, "test_vm").payloadStarted).isFalse();
+        createUncachedVmWithName(
+                "test_vm_changed_debug_level", debugConfig, vmIdBackup, vmInstanceBackup);
+        assertThat(tryBootVm(TAG, "test_vm_changed_debug_level").payloadStarted).isFalse();
     }
 
     private static class VmCdis {
@@ -1555,7 +1590,6 @@
             assertFileContentsAreEqualInTwoVms("storage.img", vmNameOrig, vmNameImport);
         }
         assertThat(vmImport).isNotEqualTo(vmOrig);
-        vmm.delete(vmNameOrig);
         assertThat(vmImport).isEqualTo(vmm.get(vmNameImport));
         TestResults testResults =
                 runVmTestService(
diff --git a/virtualizationmanager/src/aidl.rs b/virtualizationmanager/src/aidl.rs
index 001c070..c5f1ab7 100644
--- a/virtualizationmanager/src/aidl.rs
+++ b/virtualizationmanager/src/aidl.rs
@@ -17,7 +17,7 @@
 use crate::{get_calling_pid, get_calling_uid};
 use crate::atom::{write_vm_booted_stats, write_vm_creation_stats};
 use crate::composite::make_composite_image;
-use crate::crosvm::{CrosvmConfig, DiskFile, DisplayConfig, PayloadState, VmContext, VmInstance, VmState};
+use crate::crosvm::{CrosvmConfig, DiskFile, DisplayConfig, InputDeviceOption, PayloadState, VmContext, VmInstance, VmState};
 use crate::debug_config::DebugConfig;
 use crate::dt_overlay::{create_device_tree_overlay, VM_DT_OVERLAY_MAX_SIZE, VM_DT_OVERLAY_PATH};
 use crate::payload::{add_microdroid_payload_images, add_microdroid_system_images, add_microdroid_vendor_image};
@@ -32,6 +32,7 @@
     AssignableDevice::AssignableDevice,
     CpuTopology::CpuTopology,
     DiskImage::DiskImage,
+    InputDevice::InputDevice,
     IVirtualMachine::{BnVirtualMachine, IVirtualMachine},
     IVirtualMachineCallback::IVirtualMachineCallback,
     IVirtualizationService::IVirtualizationService,
@@ -581,13 +582,27 @@
         } else {
             (vec![], None)
         };
+        let display_config = if cfg!(paravirtualized_devices) {
+            config
+                .displayConfig
+                .as_ref()
+                .map(DisplayConfig::new)
+                .transpose()
+                .or_binder_exception(ExceptionCode::ILLEGAL_ARGUMENT)?
+        } else {
+            None
+        };
 
-        let display_config = config
-            .displayConfig
-            .as_ref()
-            .map(DisplayConfig::new)
-            .transpose()
-            .or_binder_exception(ExceptionCode::ILLEGAL_ARGUMENT)?;
+        let input_device_options = if cfg!(paravirtualized_devices) {
+            config
+                .inputDevices
+                .iter()
+                .map(to_input_device_option_from)
+                .collect::<Result<Vec<InputDeviceOption>, _>>()
+                .or_binder_exception(ExceptionCode::ILLEGAL_ARGUMENT)?
+        } else {
+            vec![]
+        };
 
         // Actually start the VM.
         let crosvm_config = CrosvmConfig {
@@ -615,6 +630,7 @@
             dtbo,
             device_tree_overlay,
             display_config,
+            input_device_options,
         };
         let instance = Arc::new(
             VmInstance::new(
@@ -720,6 +736,23 @@
     (result / granularity) * granularity
 }
 
+fn to_input_device_option_from(input_device: &InputDevice) -> Result<InputDeviceOption> {
+    Ok(match input_device {
+        InputDevice::SingleTouch(single_touch) => InputDeviceOption::SingleTouch {
+            file: clone_file(single_touch.pfd.as_ref().ok_or(anyhow!("pfd should have value"))?)?,
+            height: u32::try_from(single_touch.height)?,
+            width: u32::try_from(single_touch.width)?,
+            name: if !single_touch.name.is_empty() {
+                Some(single_touch.name.clone())
+            } else {
+                None
+            },
+        },
+        InputDevice::EvDev(evdev) => InputDeviceOption::EvDev(clone_file(
+            evdev.pfd.as_ref().ok_or(anyhow!("pfd should have value"))?,
+        )?),
+    })
+}
 /// Given the configuration for a disk image, assembles the `DiskFile` to pass to crosvm.
 ///
 /// This may involve assembling a composite disk from a set of partition images.
diff --git a/virtualizationmanager/src/crosvm.rs b/virtualizationmanager/src/crosvm.rs
index 58f54cd..4be48a5 100644
--- a/virtualizationmanager/src/crosvm.rs
+++ b/virtualizationmanager/src/crosvm.rs
@@ -120,6 +120,7 @@
     pub dtbo: Option<File>,
     pub device_tree_overlay: Option<File>,
     pub display_config: Option<DisplayConfig>,
+    pub input_device_options: Vec<InputDeviceOption>,
 }
 
 #[derive(Debug)]
@@ -154,6 +155,14 @@
     pub writable: bool,
 }
 
+/// virtio-input device configuration from `external/crosvm/src/crosvm/config.rs`
+#[derive(Debug)]
+#[allow(dead_code)]
+pub enum InputDeviceOption {
+    EvDev(File),
+    SingleTouch { file: File, width: u32, height: u32, name: Option<String> },
+}
+
 type VfioDevice = Strong<dyn IBoundDevice>;
 
 /// The lifecycle state which the payload in the VM has reported itself to be in.
@@ -955,14 +964,34 @@
     if let Some(dt_overlay) = &config.device_tree_overlay {
         command.arg("--device-tree-overlay").arg(add_preserved_fd(&mut preserved_fds, dt_overlay));
     }
-    if let Some(display_config) = &config.display_config {
-        command.arg("--gpu")
-        // TODO(b/331708504): support backend config as well
-        .arg("backend=virglrenderer,context-types=virgl2,egl=true,surfaceless=true,glx=false,gles=true")
-        .arg(format!("--gpu-display=mode=windowed[{},{}],dpi=[{},{}],refresh-rate={}", display_config.width, display_config.height, display_config.horizontal_dpi, display_config.vertical_dpi, display_config.refresh_rate))
-        .arg(format!("--android-display-service={}", config.name));
+
+    if cfg!(paravirtualized_devices) {
+        if let Some(display_config) = &config.display_config {
+            command.arg("--gpu")
+            // TODO(b/331708504): support backend config as well
+            .arg("backend=virglrenderer,context-types=virgl2,egl=true,surfaceless=true,glx=false,gles=true")
+            .arg(format!("--gpu-display=mode=windowed[{},{}],dpi=[{},{}],refresh-rate={}", display_config.width, display_config.height, display_config.horizontal_dpi, display_config.vertical_dpi, display_config.refresh_rate))
+            .arg(format!("--android-display-service={}", config.name));
+        }
     }
 
+    if cfg!(paravirtualized_devices) {
+        for input_device_option in config.input_device_options.iter() {
+            command.arg("--input");
+            command.arg(match input_device_option {
+                InputDeviceOption::EvDev(file) => {
+                    format!("evdev[path={}]", add_preserved_fd(&mut preserved_fds, file))
+                }
+                InputDeviceOption::SingleTouch { file, width, height, name } => format!(
+                    "single-touch[path={},width={},height={}{}]",
+                    add_preserved_fd(&mut preserved_fds, file),
+                    width,
+                    height,
+                    name.as_ref().map_or("".into(), |n| format!(",name={}", n))
+                ),
+            });
+        }
+    }
     append_platform_devices(&mut command, &mut preserved_fds, &config)?;
 
     debug!("Preserving FDs {:?}", preserved_fds);
diff --git a/virtualizationservice/aidl/Android.bp b/virtualizationservice/aidl/Android.bp
index c479691..fb89772 100644
--- a/virtualizationservice/aidl/Android.bp
+++ b/virtualizationservice/aidl/Android.bp
@@ -48,7 +48,7 @@
         java: {
             sdk_version: "module_current",
             apex_available: [
-                "//apex_available:platform",
+                "com.android.virt",
             ],
         },
         rust: {
diff --git a/virtualizationservice/aidl/android/system/virtualizationservice/InputDevice.aidl b/virtualizationservice/aidl/android/system/virtualizationservice/InputDevice.aidl
new file mode 100644
index 0000000..fe12291
--- /dev/null
+++ b/virtualizationservice/aidl/android/system/virtualizationservice/InputDevice.aidl
@@ -0,0 +1,36 @@
+/*
+ * 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.virtualizationservice;
+
+// Refer to https://crosvm.dev/book/devices/input.html
+union InputDevice {
+    // Add a single-touch touchscreen virtio-input device.
+    parcelable SingleTouch {
+        ParcelFileDescriptor pfd;
+        // Default values come from https://crosvm.dev/book/devices/input.html#single-touch
+        int width = 1280;
+        int height = 1080;
+        @utf8InCpp String name = "";
+    }
+    // Passes an event device node into the VM. The device will be grabbed (unusable from the host)
+    // and made available to the guest with the same configuration it shows on the host.
+    parcelable EvDev {
+        ParcelFileDescriptor pfd;
+    }
+
+    SingleTouch singleTouch;
+    EvDev evDev;
+}
diff --git a/virtualizationservice/aidl/android/system/virtualizationservice/VirtualMachineRawConfig.aidl b/virtualizationservice/aidl/android/system/virtualizationservice/VirtualMachineRawConfig.aidl
index 1a18bf8..86e26da 100644
--- a/virtualizationservice/aidl/android/system/virtualizationservice/VirtualMachineRawConfig.aidl
+++ b/virtualizationservice/aidl/android/system/virtualizationservice/VirtualMachineRawConfig.aidl
@@ -18,6 +18,7 @@
 import android.system.virtualizationservice.CpuTopology;
 import android.system.virtualizationservice.DiskImage;
 import android.system.virtualizationservice.DisplayConfig;
+import android.system.virtualizationservice.InputDevice;
 
 /** Raw configuration for running a VM. */
 parcelable VirtualMachineRawConfig {
@@ -73,4 +74,7 @@
     String[] devices;
 
     @nullable DisplayConfig displayConfig;
+
+    /** List of input devices to the VM */
+    InputDevice[] inputDevices;
 }
diff --git a/virtualizationservice/src/maintenance.rs b/virtualizationservice/src/maintenance.rs
index f950db9..8efc58d 100644
--- a/virtualizationservice/src/maintenance.rs
+++ b/virtualizationservice/src/maintenance.rs
@@ -40,6 +40,9 @@
 /// parcel fits within max AIDL message size.
 const DELETE_MAX_BATCH_SIZE: usize = 100;
 
+/// Maximum number of VM IDs that a single app can have.
+const MAX_VM_IDS_PER_APP: usize = 400;
+
 /// State related to VM secrets.
 pub struct State {
     sk: binder::Strong<dyn ISecretkeeper>,
@@ -101,6 +104,24 @@
     pub fn add_id(&mut self, vm_id: &VmId, user_id: u32, app_id: u32) -> Result<()> {
         let user_id: i32 = user_id.try_into().context(format!("user_id {user_id} out of range"))?;
         let app_id: i32 = app_id.try_into().context(format!("app_id {app_id} out of range"))?;
+
+        // To prevent unbounded growth of VM IDs (and the associated state) for an app, limit the
+        // number of VM IDs per app.
+        let count = self
+            .vm_id_db
+            .count_vm_ids_for_app(user_id, app_id)
+            .context("failed to determine VM count")?;
+        if count >= MAX_VM_IDS_PER_APP {
+            // The owner has too many VM IDs, so delete the oldest IDs so that the new VM ID
+            // creation can progress/succeed.
+            let purge = 1 + count - MAX_VM_IDS_PER_APP;
+            let old_vm_ids = self
+                .vm_id_db
+                .oldest_vm_ids_for_app(user_id, app_id, purge)
+                .context("failed to find oldest VM IDs")?;
+            error!("Deleting {purge} of {count} VM IDs for user_id={user_id}, app_id={app_id}");
+            self.delete_ids(&old_vm_ids);
+        }
         self.vm_id_db.add_vm_id(vm_id, user_id, app_id)
     }
 
@@ -396,6 +417,39 @@
         assert_eq!(vec![VM_ID5], sk_state.vm_id_db.vm_ids_for_user(USER3).unwrap());
     }
 
+    #[test]
+    fn test_sk_state_too_many_vms() {
+        let history = Arc::new(Mutex::new(Vec::new()));
+        let mut sk_state = new_test_state(history.clone(), 20);
+
+        // Every VM ID added up to the limit is kept.
+        for idx in 0..MAX_VM_IDS_PER_APP {
+            let mut vm_id = [0u8; 64];
+            vm_id[0..8].copy_from_slice(&(idx as u64).to_be_bytes());
+            sk_state.add_id(&vm_id, USER1 as u32, APP_A as u32).unwrap();
+            assert_eq!(idx + 1, sk_state.vm_id_db.count_vm_ids_for_app(USER1, APP_A).unwrap());
+        }
+        assert_eq!(
+            MAX_VM_IDS_PER_APP,
+            sk_state.vm_id_db.count_vm_ids_for_app(USER1, APP_A).unwrap()
+        );
+
+        // Beyond the limit it's one in, one out.
+        for idx in MAX_VM_IDS_PER_APP..MAX_VM_IDS_PER_APP + 10 {
+            let mut vm_id = [0u8; 64];
+            vm_id[0..8].copy_from_slice(&(idx as u64).to_be_bytes());
+            sk_state.add_id(&vm_id, USER1 as u32, APP_A as u32).unwrap();
+            assert_eq!(
+                MAX_VM_IDS_PER_APP,
+                sk_state.vm_id_db.count_vm_ids_for_app(USER1, APP_A).unwrap()
+            );
+        }
+        assert_eq!(
+            MAX_VM_IDS_PER_APP,
+            sk_state.vm_id_db.count_vm_ids_for_app(USER1, APP_A).unwrap()
+        );
+    }
+
     struct Irreconcilable;
 
     impl IVirtualizationReconciliationCallback for Irreconcilable {
diff --git a/virtualizationservice/src/maintenance/vmdb.rs b/virtualizationservice/src/maintenance/vmdb.rs
index 47704bc..273f340 100644
--- a/virtualizationservice/src/maintenance/vmdb.rs
+++ b/virtualizationservice/src/maintenance/vmdb.rs
@@ -272,6 +272,34 @@
         Ok(vm_ids)
     }
 
+    /// Determine the number of VM IDs associated with `(user_id, app_id)`.
+    pub fn count_vm_ids_for_app(&mut self, user_id: i32, app_id: i32) -> Result<usize> {
+        let mut stmt = self
+            .conn
+            .prepare("SELECT COUNT(vm_id) FROM main.vmids WHERE user_id = ? AND app_id = ?;")
+            .context("failed to prepare SELECT stmt")?;
+        stmt.query_row(params![user_id, app_id], |row| row.get(0)).context("query failed")
+    }
+
+    /// Return the `count` oldest VM IDs associated with `(user_id, app_id)`.
+    pub fn oldest_vm_ids_for_app(
+        &mut self,
+        user_id: i32,
+        app_id: i32,
+        count: usize,
+    ) -> Result<Vec<VmId>> {
+        // SQLite considers NULL columns to be smaller than values, so rows left over from a v0
+        // database will be listed first.
+        let mut stmt = self
+            .conn
+            .prepare(
+                "SELECT vm_id FROM main.vmids WHERE user_id = ? AND app_id = ? ORDER BY created LIMIT ?;",
+            )
+            .context("failed to prepare SELECT stmt")?;
+        let rows = stmt.query(params![user_id, app_id, count]).context("query failed")?;
+        Self::vm_ids_from_rows(rows)
+    }
+
     /// Return all of the `(user_id, app_id)` pairs present in the database.
     pub fn get_all_owners(&mut self) -> Result<Vec<(i32, i32)>> {
         let mut stmt = self
@@ -344,6 +372,19 @@
     fn show_contents(db: &VmIdDb) {
         let mut stmt = db.conn.prepare("SELECT * FROM main.vmids;").unwrap();
         let mut rows = stmt.query(()).unwrap();
+        println!("DB contents:");
+        while let Some(row) = rows.next().unwrap() {
+            println!("  {row:?}");
+        }
+    }
+
+    fn show_contents_for_app(db: &VmIdDb, user_id: i32, app_id: i32, count: usize) {
+        let mut stmt = db
+            .conn
+            .prepare("SELECT vm_id, created FROM main.vmids WHERE user_id = ? AND app_id = ? ORDER BY created LIMIT ?;")
+            .unwrap();
+        let mut rows = stmt.query(params![user_id, app_id, count]).unwrap();
+        println!("First (by created) {count} rows for app_id={app_id}");
         while let Some(row) = rows.next().unwrap() {
             println!("  {row:?}");
         }
@@ -457,31 +498,39 @@
 
         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!(3, db.count_vm_ids_for_app(USER1, APP_A).unwrap());
         assert_eq!(vec![VM_ID4], db.vm_ids_for_app(USER2, APP_B).unwrap());
+        assert_eq!(1, db.count_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());
+        assert_eq!(0, db.count_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());
+        assert_eq!(1, db.count_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());
+        assert_eq!(1, db.count_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!(3, db.count_vm_ids_for_app(USER1, APP_A).unwrap());
         assert_eq!(vec![VM_ID4], db.vm_ids_for_app(USER2, APP_B).unwrap());
+        assert_eq!(1, db.count_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());
+        assert_eq!(0, db.count_vm_ids_for_app(USER1, APP_UNKNOWN).unwrap());
 
         assert_eq!(
             vec![(USER1, APP_A), (USER2, APP_B), (USER3, APP_C)],
@@ -513,4 +562,47 @@
         assert_eq!(vec![VM_ID1, VM_ID2, VM_ID3], db.vm_ids_for_user(USER1).unwrap());
         show_contents(&db);
     }
+
+    #[test]
+    fn test_remove_oldest_with_upgrade() {
+        let mut db = new_test_db_version(0);
+        let version = db.schema_version().unwrap();
+        assert_eq!(0, version);
+
+        let remove_count = 10;
+        let mut want = vec![];
+
+        // Manually insert rows before upgrade.
+        const V0_COUNT: usize = 5;
+        for idx in 0..V0_COUNT {
+            let mut vm_id = [0u8; 64];
+            vm_id[0..8].copy_from_slice(&(idx as u64).to_be_bytes());
+            if want.len() < remove_count {
+                want.push(vm_id);
+            }
+            db.conn
+                .execute(
+                    "REPLACE INTO main.vmids (vm_id, user_id, app_id) VALUES (?1, ?2, ?3);",
+                    params![&vm_id, &USER1, APP_A],
+                )
+                .unwrap();
+        }
+
+        // Now move to v1.
+        db.upgrade_tables_v0_v1().unwrap();
+        let version = db.schema_version().unwrap();
+        assert_eq!(1, version);
+
+        for idx in V0_COUNT..40 {
+            let mut vm_id = [0u8; 64];
+            vm_id[0..8].copy_from_slice(&(idx as u64).to_be_bytes());
+            if want.len() < remove_count {
+                want.push(vm_id);
+            }
+            db.add_vm_id(&vm_id, USER1, APP_A).unwrap();
+        }
+        show_contents_for_app(&db, USER1, APP_A, 10);
+        let got = db.oldest_vm_ids_for_app(USER1, APP_A, 10).unwrap();
+        assert_eq!(got, want);
+    }
 }
diff --git a/vmbase/example/src/main.rs b/vmbase/example/src/main.rs
index 48b24be..61dda04 100644
--- a/vmbase/example/src/main.rs
+++ b/vmbase/example/src/main.rs
@@ -28,6 +28,7 @@
 use aarch64_paging::paging::MemoryRegion;
 use aarch64_paging::MapError;
 use alloc::{vec, vec::Vec};
+use core::ptr::addr_of_mut;
 use cstr::cstr;
 use fdtpci::PciInfo;
 use libfdt::Fdt;
@@ -138,14 +139,15 @@
 
     // SAFETY: Nowhere else in the program accesses this static mutable variable, so there is no
     // chance of concurrent access.
-    let zeroed_data = unsafe { &mut ZEROED_DATA };
+    let zeroed_data = unsafe { &mut *addr_of_mut!(ZEROED_DATA) };
     // SAFETY: Nowhere else in the program accesses this static mutable variable, so there is no
     // chance of concurrent access.
-    let mutable_data = unsafe { &mut MUTABLE_DATA };
+    let mutable_data = unsafe {&mut *addr_of_mut!(MUTABLE_DATA) };
 
     for element in zeroed_data.iter() {
         assert_eq!(*element, 0);
     }
+
     zeroed_data[0] = 13;
     assert_eq!(zeroed_data[0], 13);
     zeroed_data[0] = 0;
@@ -161,6 +163,7 @@
     assert_eq!(mutable_data[0], 1);
 
     info!("Data looks good");
+
 }
 
 fn check_fdt(reader: &Fdt) {
diff --git a/vmlauncher_app/Android.bp b/vmlauncher_app/Android.bp
index 06dcf7a..f9c325c 100644
--- a/vmlauncher_app/Android.bp
+++ b/vmlauncher_app/Android.bp
@@ -21,4 +21,7 @@
     ],
     platform_apis: true,
     privileged: true,
+    apex_available: [
+        "com.android.virt",
+    ],
 }
diff --git a/vmlauncher_app/AndroidManifest.xml b/vmlauncher_app/AndroidManifest.xml
index 860c03f..607a895 100644
--- a/vmlauncher_app/AndroidManifest.xml
+++ b/vmlauncher_app/AndroidManifest.xml
@@ -8,6 +8,7 @@
     <application
         android:label="VmLauncherApp">
         <activity android:name=".MainActivity"
+                  android:enabled="false"
                   android:screenOrientation="landscape"
                   android:configChanges="orientation|screenSize|keyboard|keyboardHidden|navigation|uiMode"
                   android:theme="@style/MyTheme"