Merge "Check whether remote attestation is supported with system property" into main
diff --git a/apex/sign_virt_apex_test.sh b/apex/sign_virt_apex_test.sh
index e6a892b..e4ac615 100644
--- a/apex/sign_virt_apex_test.sh
+++ b/apex/sign_virt_apex_test.sh
@@ -25,12 +25,16 @@
 DEBUGFS=$TEST_DIR/debugfs_static
 FSCKEROFS=$TEST_DIR/fsck.erofs
 
+echo "Extracting the virt apex ..."
 deapexer --debugfs_path $DEBUGFS --fsckerofs_path $FSCKEROFS \
   extract $TEST_DIR/com.android.virt.apex $TMP_ROOT
 
 if [ "$(ls -A $TMP_ROOT/etc/fs/)" ]; then
-  sign_virt_apex $TEST_DIR/test.com.android.virt.pem $TMP_ROOT
-  sign_virt_apex --verify $TEST_DIR/test.com.android.virt.pem $TMP_ROOT
+  echo "Re-signing the contents ..."
+  sign_virt_apex -v $TEST_DIR/test.com.android.virt.pem $TMP_ROOT
+  echo "Verifying the contents ..."
+  sign_virt_apex -v --verify $TEST_DIR/test.com.android.virt.pem $TMP_ROOT
+  echo "Done."
 else
   echo "No filesystem images. Skip."
 fi
diff --git a/compos/common/compos_client.rs b/compos/common/compos_client.rs
index 6914380..107f8d0 100644
--- a/compos/common/compos_client.rs
+++ b/compos/common/compos_client.rs
@@ -36,7 +36,6 @@
 use glob::glob;
 use log::{info, warn};
 use platformproperties::hypervisorproperties;
-use rustutils::system_properties;
 use std::fs::File;
 use std::path::{Path, PathBuf};
 use vmclient::{DeathReason, ErrorCode, VmInstance, VmWaitError};
@@ -80,7 +79,11 @@
         idsig_manifest_ext_apk: &Path,
         parameters: &VmParameters,
     ) -> Result<Self> {
-        let protected_vm = want_protected_vm()?;
+        let have_protected_vm =
+            hypervisorproperties::hypervisor_protected_vm_supported()?.unwrap_or(false);
+        if !have_protected_vm {
+            bail!("Protected VM not supported, unable to start VM");
+        }
 
         let instance_fd = ParcelFileDescriptor::new(instance_image);
 
@@ -133,7 +136,7 @@
             payload: Payload::ConfigPath(config_path),
             debugLevel: debug_level,
             extraIdsigs: extra_idsigs,
-            protectedVm: protected_vm,
+            protectedVm: true,
             memoryMib: parameters.memory_mib.unwrap_or(0), // 0 means use the default
             cpuTopology: cpu_topology,
             customConfig: custom_config,
@@ -236,28 +239,6 @@
     Ok(idsig_fd)
 }
 
-fn want_protected_vm() -> Result<bool> {
-    let have_protected_vm =
-        hypervisorproperties::hypervisor_protected_vm_supported()?.unwrap_or(false);
-    if have_protected_vm {
-        info!("Starting protected VM");
-        return Ok(true);
-    }
-
-    let is_debug_build = system_properties::read("ro.debuggable")?.as_deref().unwrap_or("0") == "1";
-    if !is_debug_build {
-        bail!("Protected VM not supported, unable to start VM");
-    }
-
-    let have_non_protected_vm = hypervisorproperties::hypervisor_vm_supported()?.unwrap_or(false);
-    if have_non_protected_vm {
-        warn!("Protected VM not supported, falling back to non-protected on debuggable build");
-        return Ok(false);
-    }
-
-    bail!("No VM support available")
-}
-
 struct Callback {}
 impl vmclient::VmCallback for Callback {
     fn on_payload_started(&self, cid: i32) {
diff --git a/java/framework/src/android/system/virtualmachine/VirtualMachineConfig.java b/java/framework/src/android/system/virtualmachine/VirtualMachineConfig.java
index 66d0f4b..4d3bf2d 100644
--- a/java/framework/src/android/system/virtualmachine/VirtualMachineConfig.java
+++ b/java/framework/src/android/system/virtualmachine/VirtualMachineConfig.java
@@ -90,6 +90,7 @@
     private static final String KEY_PROTECTED_VM = "protectedVm";
     private static final String KEY_MEMORY_BYTES = "memoryBytes";
     private static final String KEY_CPU_TOPOLOGY = "cpuTopology";
+    private static final String KEY_CONSOLE_INPUT_DEVICE = "consoleInputDevice";
     private static final String KEY_ENCRYPTED_STORAGE_BYTES = "encryptedStorageBytes";
     private static final String KEY_VM_OUTPUT_CAPTURED = "vmOutputCaptured";
     private static final String KEY_VM_CONSOLE_INPUT_SUPPORTED = "vmConsoleInputSupported";
@@ -173,6 +174,9 @@
     /** CPU topology configuration of the VM. */
     @CpuTopology private final int mCpuTopology;
 
+    /** The serial device for VM console input. */
+    @Nullable private final String mConsoleInputDevice;
+
     /**
      * Path within the APK to the payload config file that defines software aspects of the VM.
      */
@@ -229,6 +233,7 @@
             boolean protectedVm,
             long memoryBytes,
             @CpuTopology int cpuTopology,
+            @Nullable String consoleInputDevice,
             long encryptedStorageBytes,
             boolean vmOutputCaptured,
             boolean vmConsoleInputSupported,
@@ -250,6 +255,7 @@
         mProtectedVm = protectedVm;
         mMemoryBytes = memoryBytes;
         mCpuTopology = cpuTopology;
+        mConsoleInputDevice = consoleInputDevice;
         mEncryptedStorageBytes = encryptedStorageBytes;
         mVmOutputCaptured = vmOutputCaptured;
         mVmConsoleInputSupported = vmConsoleInputSupported;
@@ -330,6 +336,10 @@
             builder.setMemoryBytes(memoryBytes);
         }
         builder.setCpuTopology(b.getInt(KEY_CPU_TOPOLOGY));
+        String consoleInputDevice = b.getString(KEY_CONSOLE_INPUT_DEVICE);
+        if (consoleInputDevice != null) {
+            builder.setConsoleInputDevice(consoleInputDevice);
+        }
         long encryptedStorageBytes = b.getLong(KEY_ENCRYPTED_STORAGE_BYTES);
         if (encryptedStorageBytes != 0) {
             builder.setEncryptedStorageBytes(encryptedStorageBytes);
@@ -382,6 +392,9 @@
         b.putInt(KEY_DEBUGLEVEL, mDebugLevel);
         b.putBoolean(KEY_PROTECTED_VM, mProtectedVm);
         b.putInt(KEY_CPU_TOPOLOGY, mCpuTopology);
+        if (mConsoleInputDevice != null) {
+            b.putString(KEY_CONSOLE_INPUT_DEVICE, mConsoleInputDevice);
+        }
         if (mMemoryBytes > 0) {
             b.putLong(KEY_MEMORY_BYTES, mMemoryBytes);
         }
@@ -595,6 +608,7 @@
                 && this.mVmOutputCaptured == other.mVmOutputCaptured
                 && this.mVmConsoleInputSupported == other.mVmConsoleInputSupported
                 && this.mConnectVmConsole == other.mConnectVmConsole
+                && this.mConsoleInputDevice == other.mConsoleInputDevice
                 && (this.mVendorDiskImage == null) == (other.mVendorDiskImage == null)
                 && Objects.equals(this.mPayloadConfigPath, other.mPayloadConfigPath)
                 && Objects.equals(this.mPayloadBinaryName, other.mPayloadBinaryName)
@@ -666,6 +680,7 @@
         config.protectedVm = this.mProtectedVm;
         config.memoryMib = bytesToMebiBytes(mMemoryBytes);
         config.cpuTopology = (byte) this.mCpuTopology;
+        config.consoleInputDevice = mConsoleInputDevice;
         config.devices = EMPTY_STRING_ARRAY;
         config.platformVersion = "~1.0";
         return config;
@@ -804,6 +819,7 @@
         private boolean mProtectedVmSet;
         private long mMemoryBytes;
         @CpuTopology private int mCpuTopology = CPU_TOPOLOGY_ONE_CPU;
+        @Nullable private String mConsoleInputDevice;
         private long mEncryptedStorageBytes;
         private boolean mVmOutputCaptured = false;
         private boolean mVmConsoleInputSupported = false;
@@ -897,6 +913,7 @@
                     mProtectedVm,
                     mMemoryBytes,
                     mCpuTopology,
+                    mConsoleInputDevice,
                     mEncryptedStorageBytes,
                     mVmOutputCaptured,
                     mVmConsoleInputSupported,
@@ -1080,6 +1097,17 @@
         }
 
         /**
+         * Sets the serial device for VM console input.
+         *
+         * @see android.system.virtualizationservice.ConsoleInputDevice
+         * @hide
+         */
+        public Builder setConsoleInputDevice(@Nullable String consoleInputDevice) {
+            mConsoleInputDevice = consoleInputDevice;
+            return this;
+        }
+
+        /**
          * Sets the size (in bytes) of encrypted storage available to the VM. If not set, no
          * encrypted storage is provided.
          *
diff --git a/libs/vmconfig/src/lib.rs b/libs/vmconfig/src/lib.rs
index 7c917b0..1413b51 100644
--- a/libs/vmconfig/src/lib.rs
+++ b/libs/vmconfig/src/lib.rs
@@ -65,6 +65,8 @@
     /// SysFS paths of devices assigned to the VM.
     #[serde(default)]
     pub devices: Vec<PathBuf>,
+    /// The serial device for VM console input.
+    pub console_input_device: Option<String>,
 }
 
 impl VmConfig {
@@ -124,6 +126,7 @@
                     x.to_str().map(String::from).ok_or(anyhow!("Failed to convert {x:?} to String"))
                 })
                 .collect::<Result<_>>()?,
+            consoleInputDevice: self.console_input_device.clone(),
             ..Default::default()
         })
     }
diff --git a/microdroid/microdroid.json b/microdroid/microdroid.json
index 00cedc8..e60c4ca 100644
--- a/microdroid/microdroid.json
+++ b/microdroid/microdroid.json
@@ -16,5 +16,6 @@
     }
   ],
   "memory_mib": 256,
+  "console_input_device": "hvc0",
   "platform_version": "~1.0"
 }
diff --git a/pvmfw/platform.dts b/pvmfw/platform.dts
index 99ecf8f..2df0768 100644
--- a/pvmfw/platform.dts
+++ b/pvmfw/platform.dts
@@ -366,6 +366,12 @@
 			0x4000 0x0 0x0 1 &intc 0 0 GIC_SPI (IRQ_BASE + 7) IRQ_TYPE_LEVEL_HIGH
 			0x4800 0x0 0x0 1 &intc 0 0 GIC_SPI (IRQ_BASE + 8) IRQ_TYPE_LEVEL_HIGH
 			0x5000 0x0 0x0 1 &intc 0 0 GIC_SPI (IRQ_BASE + 9) IRQ_TYPE_LEVEL_HIGH
+			0x5800 0x0 0x0 1 &intc 0 0 GIC_SPI (IRQ_BASE + 10) IRQ_TYPE_LEVEL_HIGH
+			0x6000 0x0 0x0 1 &intc 0 0 GIC_SPI (IRQ_BASE + 11) IRQ_TYPE_LEVEL_HIGH
+			0x6800 0x0 0x0 1 &intc 0 0 GIC_SPI (IRQ_BASE + 12) IRQ_TYPE_LEVEL_HIGH
+			0x7000 0x0 0x0 1 &intc 0 0 GIC_SPI (IRQ_BASE + 13) IRQ_TYPE_LEVEL_HIGH
+			0x7800 0x0 0x0 1 &intc 0 0 GIC_SPI (IRQ_BASE + 14) IRQ_TYPE_LEVEL_HIGH
+			0x8000 0x0 0x0 1 &intc 0 0 GIC_SPI (IRQ_BASE + 15) IRQ_TYPE_LEVEL_HIGH
 		>;
 		interrupt-map-mask = <0xf800 0x0 0x0 0x7
 				      0xf800 0x0 0x0 0x7
@@ -376,6 +382,12 @@
 				      0xf800 0x0 0x0 0x7
 				      0xf800 0x0 0x0 0x7
 				      0xf800 0x0 0x0 0x7
+				      0xf800 0x0 0x0 0x7
+				      0xf800 0x0 0x0 0x7
+				      0xf800 0x0 0x0 0x7
+				      0xf800 0x0 0x0 0x7
+				      0xf800 0x0 0x0 0x7
+				      0xf800 0x0 0x0 0x7
 				      0xf800 0x0 0x0 0x7>;
 	};
 
diff --git a/pvmfw/src/fdt.rs b/pvmfw/src/fdt.rs
index 9206588..84dc14d 100644
--- a/pvmfw/src/fdt.rs
+++ b/pvmfw/src/fdt.rs
@@ -515,7 +515,7 @@
 impl PciInfo {
     const IRQ_MASK_CELLS: usize = 4;
     const IRQ_MAP_CELLS: usize = 10;
-    const MAX_IRQS: usize = 10;
+    const MAX_IRQS: usize = 16;
 }
 
 type PciAddrRange = AddressRange<(u32, u64), u64, u64>;
diff --git a/pvmfw/src/main.rs b/pvmfw/src/main.rs
index 299d1c0..247aa6a 100644
--- a/pvmfw/src/main.rs
+++ b/pvmfw/src/main.rs
@@ -116,21 +116,6 @@
         info!("Please disregard any previous libavb ERROR about initrd_normal.");
     }
 
-    if verified_boot_data.has_capability(Capability::RemoteAttest) {
-        info!("Service VM capable of remote attestation detected");
-        if service_vm_version::VERSION != verified_boot_data.rollback_index {
-            // For RKP VM, we only boot if the version in the AVB footer of its kernel matches
-            // the one embedded in pvmfw at build time.
-            // This prevents the pvmfw from booting a roll backed RKP VM.
-            error!(
-                "Service VM version mismatch: expected {}, found {}",
-                service_vm_version::VERSION,
-                verified_boot_data.rollback_index
-            );
-            return Err(RebootReason::InvalidPayload);
-        }
-    }
-
     let next_bcc = heap::aligned_boxed_slice(NEXT_BCC_SIZE, GUEST_PAGE_SIZE).ok_or_else(|| {
         error!("Failed to allocate the next-stage BCC");
         RebootReason::InternalError
@@ -154,16 +139,30 @@
             error!("Expected positive rollback_index, found 0");
             return Err(RebootReason::InvalidPayload);
         };
-        // `new_instance` cannot be known to pvmfw
+        (false, instance_hash.unwrap())
+    } else if verified_boot_data.has_capability(Capability::RemoteAttest) {
+        info!("Service VM capable of remote attestation detected, performing version checks");
+        if service_vm_version::VERSION != verified_boot_data.rollback_index {
+            // For RKP VM, we only boot if the version in the AVB footer of its kernel matches
+            // the one embedded in pvmfw at build time.
+            // This prevents the pvmfw from booting a roll backed RKP VM.
+            error!(
+                "Service VM version mismatch: expected {}, found {}",
+                service_vm_version::VERSION,
+                verified_boot_data.rollback_index
+            );
+            return Err(RebootReason::InvalidPayload);
+        }
         (false, instance_hash.unwrap())
     } else {
+        info!("Fallback to instance.img based rollback checks");
         let (recorded_entry, mut instance_img, header_index) =
             get_recorded_entry(&mut pci_root, cdi_seal).map_err(|e| {
                 error!("Failed to get entry from instance.img: {e}");
                 RebootReason::InternalError
             })?;
         let (new_instance, salt) = if let Some(entry) = recorded_entry {
-            maybe_check_dice_measurements_match_entry(&dice_inputs, &entry)?;
+            check_dice_measurements_match_entry(&dice_inputs, &entry)?;
             let salt = instance_hash.unwrap_or(entry.salt);
             (false, salt)
         } else {
@@ -244,21 +243,10 @@
     Ok(bcc_range)
 }
 
-fn maybe_check_dice_measurements_match_entry(
+fn check_dice_measurements_match_entry(
     dice_inputs: &PartialInputs,
     entry: &EntryBody,
 ) -> Result<(), RebootReason> {
-    // The RKP VM is allowed to run if it has passed the verified boot check and
-    // contains the expected version in its AVB footer.
-    // The comparison below with the previous boot information is skipped to enable the
-    // simultaneous update of the pvmfw and RKP VM.
-    // For instance, when both the pvmfw and RKP VM are updated, the code hash of the
-    // RKP VM will differ from the one stored in the instance image. In this case, the
-    // RKP VM is still allowed to run.
-    // This ensures that the updated RKP VM will retain the same CDIs in the next stage.
-    if dice_inputs.rkp_vm_marker {
-        return Ok(());
-    }
     ensure_dice_measurements_match_entry(dice_inputs, entry).map_err(|e| {
         error!(
             "Dice measurements do not match recorded entry. \
diff --git a/tests/helper/Android.bp b/tests/helper/Android.bp
index 41d1ba2..1c38d12 100644
--- a/tests/helper/Android.bp
+++ b/tests/helper/Android.bp
@@ -33,4 +33,5 @@
     ],
     host_supported: true,
     device_supported: false,
+    sdk_version: "test_current",
 }
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 6040531..e02db39 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
@@ -219,7 +219,7 @@
     }
 
     protected static int getVendorApiLevel() {
-        return SystemProperties.getInt("ro.vendor.api_level", 0);
+        return SystemProperties.getInt("ro.board.api_level", 0);
     }
 
     protected void assumeSupportedDevice() {
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 46df011..1fc0f92 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
@@ -140,7 +140,7 @@
         assumeTrue("Requires VM support", testDevice.supportsMicrodroid());
 
         CommandRunner android = new CommandRunner(androidDevice);
-        long vendorApiLevel = androidDevice.getIntProperty("ro.vendor.api_level", 0);
+        long vendorApiLevel = androidDevice.getIntProperty("ro.board.api_level", 0);
         boolean isGsi =
                 android.runForResult("[ -e /system/system_ext/etc/init/init.gsi.rc ]").getStatus()
                         == CommandStatus.SUCCESS;
diff --git a/tests/hostside/java/com/android/microdroid/test/MicrodroidHostTests.java b/tests/hostside/java/com/android/microdroid/test/MicrodroidHostTests.java
index 9d0b04b..80d1fc6 100644
--- a/tests/hostside/java/com/android/microdroid/test/MicrodroidHostTests.java
+++ b/tests/hostside/java/com/android/microdroid/test/MicrodroidHostTests.java
@@ -1038,7 +1038,7 @@
         assumeFalse("Unlocked devices may have AVF debug policy", lockProp.equals("orange"));
 
         // Test that AVF debug policy doesn't exist.
-        boolean hasDebugPolicy = device.doesFileExist("/proc/device-tree/avf");
+        boolean hasDebugPolicy = device.doesFileExist("/proc/device-tree/avf/guest");
         assertThat(hasDebugPolicy).isFalse();
     }
 
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 4ffef3c..12a46f7 100644
--- a/tests/testapk/src/java/com/android/microdroid/test/MicrodroidTests.java
+++ b/tests/testapk/src/java/com/android/microdroid/test/MicrodroidTests.java
@@ -2107,12 +2107,9 @@
         IVmShareTestService service = connection.waitForService();
         assertWithMessage("Timed out connecting to " + serviceIntent).that(service).isNotNull();
 
+
         try {
-            // Send the VM descriptor to the other app. When received, it will reconstruct the VM
-            // from the descriptor, start it, connect to the ITestService in it, creates a "proxy"
-            // ITestService binder that delegates all the calls to the VM, and share it with this
-            // app. It will allow us to verify assertions on the running VM in the other app.
-            ITestService testServiceProxy = service.startVm(vmDesc);
+            ITestService testServiceProxy = transferAndStartVm(service, vmDesc, "vm_to_share");
 
             int result = testServiceProxy.addInteger(37, 73);
             assertThat(result).isEqualTo(110);
@@ -2163,12 +2160,7 @@
         assertWithMessage("Timed out connecting to " + serviceIntent).that(service).isNotNull();
 
         try {
-            // Send the VM descriptor to the other app. When received, it will reconstruct the VM
-            // from the descriptor, start it, connect to the ITestService in it, creates a "proxy"
-            // ITestService binder that delegates all the calls to the VM, and share it with this
-            // app. It will allow us to verify assertions on the running VM in the other app.
-            ITestService testServiceProxy = service.startVm(vmDesc);
-
+            ITestService testServiceProxy = transferAndStartVm(service, vmDesc, "vm_to_share");
             String result = testServiceProxy.readFromFile("/mnt/encryptedstore/private.key");
             assertThat(result).isEqualTo(EXAMPLE_STRING);
         } finally {
@@ -2176,6 +2168,25 @@
         }
     }
 
+    private ITestService transferAndStartVm(
+            IVmShareTestService service, VirtualMachineDescriptor vmDesc, String vmName)
+            throws Exception {
+        // Send the VM descriptor to the other app. When received, it will reconstruct the VM
+        // from the descriptor.
+        service.importVm(vmDesc);
+
+        // Now that the VM has been imported, we should be free to delete our copy (this is
+        // what we recommend for VM transfer).
+        getVirtualMachineManager().delete(vmName);
+
+        // Ask the other app to start the imported VM, connect to the ITestService in it, create
+        // a "proxy" ITestService binder that delegates all the calls to the VM, and share it
+        // with this app. It will allow us to verify assertions on the running VM in the other
+        // app.
+        ITestService testServiceProxy = service.startVm();
+        return testServiceProxy;
+    }
+
     @Test
     @CddTest(requirements = {"9.17/C-1-5"})
     public void testFileUnderBinHasExecutePermission() throws Exception {
diff --git a/tests/vmshareapp/aidl/com/android/microdroid/test/vmshare/IVmShareTestService.aidl b/tests/vmshareapp/aidl/com/android/microdroid/test/vmshare/IVmShareTestService.aidl
index fe6ca43..ac59610 100644
--- a/tests/vmshareapp/aidl/com/android/microdroid/test/vmshare/IVmShareTestService.aidl
+++ b/tests/vmshareapp/aidl/com/android/microdroid/test/vmshare/IVmShareTestService.aidl
@@ -20,5 +20,7 @@
 
 /** {@hide} */
 interface IVmShareTestService {
-    ITestService startVm(in VirtualMachineDescriptor vmDesc);
+    void importVm(in VirtualMachineDescriptor vmDesc);
+
+    ITestService startVm();
 }
diff --git a/tests/vmshareapp/src/java/com/android/microdroid/test/sharevm/VmShareServiceImpl.java b/tests/vmshareapp/src/java/com/android/microdroid/test/sharevm/VmShareServiceImpl.java
index dc8908b..109486c 100644
--- a/tests/vmshareapp/src/java/com/android/microdroid/test/sharevm/VmShareServiceImpl.java
+++ b/tests/vmshareapp/src/java/com/android/microdroid/test/sharevm/VmShareServiceImpl.java
@@ -93,16 +93,19 @@
         }
     }
 
-    public ITestService startVm(VirtualMachineDescriptor vmDesc) throws Exception {
+    public void importVm(VirtualMachineDescriptor vmDesc) throws Exception {
         // Cleanup VM left from the previous test.
         deleteVm();
 
-        VirtualMachineManager vmm = getSystemService(VirtualMachineManager.class);
-
         // Add random uuid to make sure that different tests that bind to this service don't trip
         // over each other.
         String vmName = "imported_vm" + UUID.randomUUID();
 
+        VirtualMachineManager vmm = getSystemService(VirtualMachineManager.class);
+        mVirtualMachine = vmm.importFromDescriptor(vmName, vmDesc);
+    }
+
+    public ITestService startVm() throws Exception {
         final CountDownLatch latch = new CountDownLatch(1);
         VirtualMachineCallback callback =
                 new VirtualMachineCallback() {
@@ -134,10 +137,9 @@
                     }
                 };
 
-        mVirtualMachine = vmm.importFromDescriptor(vmName, vmDesc);
         mVirtualMachine.setCallback(getMainExecutor(), callback);
 
-        Log.i(TAG, "Starting VM " + vmName);
+        Log.i(TAG, "Starting VM " + mVirtualMachine.getName());
         mVirtualMachine.run();
         if (!latch.await(1, TimeUnit.MINUTES)) {
             throw new TimeoutException("Timed out starting VM");
@@ -155,10 +157,21 @@
     final class ServiceImpl extends IVmShareTestService.Stub {
 
         @Override
-        public ITestService startVm(VirtualMachineDescriptor vmDesc) {
+        public void importVm(VirtualMachineDescriptor vmDesc) {
+            Log.i(TAG, "importVm binder call received");
+            try {
+                VmShareServiceImpl.this.importVm(vmDesc);
+            } catch (Exception e) {
+                Log.e(TAG, "Failed to importVm", e);
+                throw new IllegalStateException("Failed to importVm", e);
+            }
+        }
+
+        @Override
+        public ITestService startVm() {
             Log.i(TAG, "startVm binder call received");
             try {
-                return VmShareServiceImpl.this.startVm(vmDesc);
+                return VmShareServiceImpl.this.startVm();
             } catch (Exception e) {
                 Log.e(TAG, "Failed to startVm", e);
                 throw new IllegalStateException("Failed to startVm", e);
diff --git a/virtualizationmanager/src/aidl.rs b/virtualizationmanager/src/aidl.rs
index dd17b46..0055b3b 100644
--- a/virtualizationmanager/src/aidl.rs
+++ b/virtualizationmanager/src/aidl.rs
@@ -463,7 +463,7 @@
 
         let debug_config = DebugConfig::new(config);
 
-        let ramdump = if debug_config.is_ramdump_needed() {
+        let ramdump = if !uses_gki_kernel(config) && debug_config.is_ramdump_needed() {
             Some(prepare_ramdump_file(&temporary_directory)?)
         } else {
             None
@@ -657,6 +657,7 @@
             hugepages: config.hugePages,
             tap,
             virtio_snd_backend,
+            console_input_device: config.consoleInputDevice.clone(),
         };
         let instance = Arc::new(
             VmInstance::new(
@@ -868,6 +869,16 @@
     SUPPORTED_OS_NAMES.contains(os_name)
 }
 
+fn uses_gki_kernel(config: &VirtualMachineConfig) -> bool {
+    if !cfg!(vendor_modules) {
+        return false;
+    }
+    match config {
+        VirtualMachineConfig::RawConfig(_) => false,
+        VirtualMachineConfig::AppConfig(config) => config.osName.starts_with("microdroid_gki-"),
+    }
+}
+
 fn load_app_config(
     config: &VirtualMachineAppConfig,
     debug_config: &DebugConfig,
diff --git a/virtualizationmanager/src/crosvm.rs b/virtualizationmanager/src/crosvm.rs
index 371a908..6408b84 100644
--- a/virtualizationmanager/src/crosvm.rs
+++ b/virtualizationmanager/src/crosvm.rs
@@ -82,6 +82,12 @@
 
 const SYSPROP_CUSTOM_PVMFW_PATH: &str = "hypervisor.pvmfw.path";
 
+/// Serial device for VM console input.
+/// Hypervisor (virtio-console)
+const CONSOLE_HVC0: &str = "hvc0";
+/// Serial (emulated uart)
+const CONSOLE_TTYS0: &str = "ttyS0";
+
 lazy_static! {
     /// If the VM doesn't move to the Started state within this amount time, a hang-up error is
     /// triggered.
@@ -124,6 +130,7 @@
     pub hugepages: bool,
     pub tap: Option<File>,
     pub virtio_snd_backend: Option<String>,
+    pub console_input_device: Option<String>,
 }
 
 #[derive(Debug)]
@@ -919,19 +926,29 @@
     let log_arg = format_serial_out_arg(&mut preserved_fds, &config.log_fd);
     let failure_serial_path = add_preserved_fd(&mut preserved_fds, &failure_pipe_write);
     let ramdump_arg = format_serial_out_arg(&mut preserved_fds, &config.ramdump);
+    let console_input_device = config.console_input_device.as_deref().unwrap_or(CONSOLE_HVC0);
+    match console_input_device {
+        CONSOLE_HVC0 | CONSOLE_TTYS0 => {}
+        _ => bail!("Unsupported serial device {console_input_device}"),
+    };
 
     // Warning: Adding more serial devices requires you to shift the PCI device ID of the boot
     // disks in bootconfig.x86_64. This is because x86 crosvm puts serial devices and the block
     // devices in the same PCI bus and serial devices comes before the block devices. Arm crosvm
     // doesn't have the issue.
     // /dev/ttyS0
-    command.arg(format!("--serial={},hardware=serial,num=1", &console_out_arg));
+    command.arg(format!(
+        "--serial={}{},hardware=serial,num=1",
+        &console_out_arg,
+        if console_input_device == CONSOLE_TTYS0 { &console_in_arg } else { "" }
+    ));
     // /dev/ttyS1
     command.arg(format!("--serial=type=file,path={},hardware=serial,num=2", &failure_serial_path));
     // /dev/hvc0
     command.arg(format!(
         "--serial={}{},hardware=virtio-console,num=1",
-        &console_out_arg, &console_in_arg
+        &console_out_arg,
+        if console_input_device == CONSOLE_HVC0 { &console_in_arg } else { "" }
     ));
     // /dev/hvc1
     command.arg(format!("--serial={},hardware=virtio-console,num=2", &ramdump_arg));
diff --git a/virtualizationservice/aidl/android/system/virtualizationservice/VirtualMachineRawConfig.aidl b/virtualizationservice/aidl/android/system/virtualizationservice/VirtualMachineRawConfig.aidl
index a5a849a..c927c9b 100644
--- a/virtualizationservice/aidl/android/system/virtualizationservice/VirtualMachineRawConfig.aidl
+++ b/virtualizationservice/aidl/android/system/virtualizationservice/VirtualMachineRawConfig.aidl
@@ -88,4 +88,7 @@
 
     /** Whether the VM should have network feature. */
     boolean networkSupported;
+
+    /** The serial device for VM console input. */
+    @nullable @utf8InCpp String consoleInputDevice;
 }
diff --git a/virtualizationservice/src/aidl.rs b/virtualizationservice/src/aidl.rs
index 41d09bc..5592f14 100644
--- a/virtualizationservice/src/aidl.rs
+++ b/virtualizationservice/src/aidl.rs
@@ -471,8 +471,16 @@
     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]);
+            let uid = get_calling_uid();
+            info!(
+                "Removing a VM's instance_id: {:?}, for uid: {:?}",
+                hex::encode(instance_id),
+                uid
+            );
+
+            let user_id = multiuser_get_user_id(uid);
+            let app_id = multiuser_get_app_id(uid);
+            sk_state.delete_id(instance_id, user_id, app_id);
         } else {
             info!("ignoring removeVmInstance() as no ISecretkeeper");
         }
diff --git a/virtualizationservice/src/maintenance.rs b/virtualizationservice/src/maintenance.rs
index 4732e1f..8e04075 100644
--- a/virtualizationservice/src/maintenance.rs
+++ b/virtualizationservice/src/maintenance.rs
@@ -90,14 +90,15 @@
         self.get_inner()?.delete_ids_for_app(user_id, app_id)
     }
 
-    /// Delete the provided VM IDs from both Secretkeeper and the database.
-    pub fn delete_ids(&mut self, vm_ids: &[VmId]) {
+    /// Delete the provided VM ID associated with `(user_id, app_id)` from both Secretkeeper and
+    /// the database.
+    pub fn delete_id(&mut self, vm_id: &VmId, user_id: u32, app_id: u32) {
         let Ok(inner) = self.get_inner() else {
             warn!("No Secretkeeper available, not deleting secrets");
             return;
         };
 
-        inner.delete_ids(vm_ids)
+        inner.delete_id_for_app(vm_id, user_id, app_id)
     }
 
     /// Perform reconciliation to allow for possibly missed notifications of user or app removal.
@@ -157,6 +158,16 @@
         self.vm_id_db.add_vm_id(vm_id, user_id, app_id)
     }
 
+    fn delete_id_for_app(&mut self, vm_id: &VmId, user_id: u32, app_id: u32) {
+        if !self.vm_id_db.is_vm_id_for_app(vm_id, user_id, app_id).unwrap_or(false) {
+            info!(
+                "delete_id_for_app - VM id not associated with user_id={user_id}, app_id={app_id}"
+            );
+            return;
+        }
+        self.delete_ids(&[*vm_id])
+    }
+
     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!(
@@ -371,8 +382,8 @@
     #[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 sk_state = new_test_state(history.clone(), 2);
+        sk_state.inner.unwrap().delete_ids(&[VM_ID1, VM_ID2, VM_ID3, VM_ID4, VM_ID5]);
         let got = (*history.lock().unwrap()).clone();
         assert_eq!(
             got,
@@ -387,8 +398,8 @@
     #[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 sk_state = new_test_state(history.clone(), 6);
+        sk_state.inner.unwrap().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])]);
     }
@@ -402,7 +413,7 @@
         get_db(&mut sk_state).add_vm_id(&VM_ID2, USER1, APP_A).unwrap();
         get_db(&mut sk_state).add_vm_id(&VM_ID3, USER2, APP_B).unwrap();
         get_db(&mut sk_state).add_vm_id(&VM_ID4, USER3, APP_A).unwrap();
-        get_db(&mut sk_state).add_vm_id(&VM_ID5, USER3, APP_C).unwrap(); // Overwrites APP_A
+        get_db(&mut sk_state).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();
@@ -425,6 +436,36 @@
     }
 
     #[test]
+    fn test_sk_state_delete_id() {
+        let history = Arc::new(Mutex::new(Vec::new()));
+        let mut sk_state = new_test_state(history.clone(), 2);
+
+        get_db(&mut sk_state).add_vm_id(&VM_ID1, USER1, APP_A).unwrap();
+        get_db(&mut sk_state).add_vm_id(&VM_ID2, USER1, APP_A).unwrap();
+        get_db(&mut sk_state).add_vm_id(&VM_ID3, USER2, APP_B).unwrap();
+        assert_eq!((*history.lock().unwrap()).clone(), vec![]);
+
+        // A VM ID that doesn't exist anywhere - no delete
+        sk_state.delete_id(&VM_ID4, USER1 as u32, APP_A as u32);
+        assert_eq!((*history.lock().unwrap()).clone(), vec![]);
+
+        // Wrong app ID - no delete
+        sk_state.delete_id(&VM_ID1, USER1 as u32, APP_B as u32);
+        assert_eq!((*history.lock().unwrap()).clone(), vec![]);
+
+        // Wrong user ID - no delete
+        sk_state.delete_id(&VM_ID1, USER2 as u32, APP_A as u32);
+        assert_eq!((*history.lock().unwrap()).clone(), vec![]);
+
+        // This porridge is just right.
+        sk_state.delete_id(&VM_ID1, USER1 as u32, APP_A as u32);
+        assert_eq!((*history.lock().unwrap()).clone(), vec![SkOp::DeleteIds(vec![VM_ID1])]);
+
+        assert_eq!(vec![VM_ID2], get_db(&mut sk_state).vm_ids_for_user(USER1).unwrap());
+        assert_eq!(vec![VM_ID3], get_db(&mut sk_state).vm_ids_for_user(USER2).unwrap());
+    }
+
+    #[test]
     fn test_sk_state_reconcile() {
         let history = Arc::new(Mutex::new(Vec::new()));
         let mut sk_state = new_test_state(history.clone(), 20);
diff --git a/virtualizationservice/src/maintenance/vmdb.rs b/virtualizationservice/src/maintenance/vmdb.rs
index 273f340..3519015 100644
--- a/virtualizationservice/src/maintenance/vmdb.rs
+++ b/virtualizationservice/src/maintenance/vmdb.rs
@@ -272,6 +272,21 @@
         Ok(vm_ids)
     }
 
+    /// Determine whether the specified VM ID is associated with `(user_id, app_id)`. Returns false
+    /// if there is no such VM ID, or it exists but is not associated.
+    pub fn is_vm_id_for_app(&mut self, vm_id: &VmId, user_id: u32, app_id: u32) -> Result<bool> {
+        let mut stmt = self
+            .conn
+            .prepare(
+                "SELECT COUNT(*) FROM main.vmids \
+                        WHERE vm_id = ? AND user_id = ? AND app_id = ?;",
+            )
+            .context("failed to prepare SELECT stmt")?;
+        stmt.query_row(params![vm_id, user_id, app_id], |row| row.get(0))
+            .context("query failed")
+            .map(|n: usize| n != 0)
+    }
+
     /// 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
@@ -350,6 +365,7 @@
     const VM_ID3: VmId = [3u8; 64];
     const VM_ID4: VmId = [4u8; 64];
     const VM_ID5: VmId = [5u8; 64];
+    const VM_ID_UNKNOWN: VmId = [6u8; 64];
     const USER1: i32 = 1;
     const USER2: i32 = 2;
     const USER3: i32 = 3;
@@ -506,6 +522,13 @@
         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!(db.is_vm_id_for_app(&VM_ID1, USER1 as u32, APP_A as u32).unwrap());
+        assert!(!db.is_vm_id_for_app(&VM_ID1, USER2 as u32, APP_A as u32).unwrap());
+        assert!(!db.is_vm_id_for_app(&VM_ID1, USER1 as u32, APP_B as u32).unwrap());
+        assert!(!db.is_vm_id_for_app(&VM_ID_UNKNOWN, USER1 as u32, APP_A as u32).unwrap());
+        assert!(!db.is_vm_id_for_app(&VM_ID5, USER3 as u32, APP_A as u32).unwrap());
+        assert!(db.is_vm_id_for_app(&VM_ID5, USER3 as u32, APP_C as u32).unwrap());
+
         db.delete_vm_ids(&[VM_ID2, VM_ID3]).unwrap();
 
         assert_eq!(vec![VM_ID1], db.vm_ids_for_user(USER1).unwrap());
diff --git a/vmbase/src/virtio/hal.rs b/vmbase/src/virtio/hal.rs
index 0d3f445..52635c3 100644
--- a/vmbase/src/virtio/hal.rs
+++ b/vmbase/src/virtio/hal.rs
@@ -91,7 +91,7 @@
         let bounce = alloc_shared(bb_layout(size))
             .expect("Failed to allocate and share VirtIO bounce buffer with host");
         let paddr = virt_to_phys(bounce);
-        if direction == BufferDirection::DriverToDevice {
+        if direction != BufferDirection::DeviceToDriver {
             let src = buffer.cast::<u8>().as_ptr().cast_const();
             trace!("VirtIO bounce buffer at {bounce:?} (PA:{paddr:#x}) initialized from {src:?}");
             // SAFETY: Both regions are valid, properly aligned, and don't overlap.
@@ -104,7 +104,7 @@
     unsafe fn unshare(paddr: PhysAddr, buffer: NonNull<[u8]>, direction: BufferDirection) {
         let bounce = phys_to_virt(paddr);
         let size = buffer.len();
-        if direction == BufferDirection::DeviceToDriver {
+        if direction != BufferDirection::DriverToDevice {
             let dest = buffer.cast::<u8>().as_ptr();
             trace!("VirtIO bounce buffer at {bounce:?} (PA:{paddr:#x}) copied back to {dest:?}");
             // SAFETY: Both regions are valid, properly aligned, and don't overlap.
diff --git a/vmlauncher_app/java/com/android/virtualization/vmlauncher/MainActivity.java b/vmlauncher_app/java/com/android/virtualization/vmlauncher/MainActivity.java
index 4d79235..5355313 100644
--- a/vmlauncher_app/java/com/android/virtualization/vmlauncher/MainActivity.java
+++ b/vmlauncher_app/java/com/android/virtualization/vmlauncher/MainActivity.java
@@ -47,13 +47,12 @@
 import android.view.WindowManager;
 import android.view.WindowMetrics;
 
+import libcore.io.IoBridge;
 
 import org.json.JSONArray;
 import org.json.JSONException;
 import org.json.JSONObject;
 
-import libcore.io.IoBridge;
-
 import java.io.BufferedOutputStream;
 import java.io.BufferedReader;
 import java.io.IOException;
@@ -123,6 +122,9 @@
                     }
                 }
             }
+            if (json.has("console_input_device")) {
+                configBuilder.setConsoleInputDevice(json.getString("console_input_device"));
+            }
 
             configBuilder.setMemoryBytes(8L * 1024 * 1024 * 1024 /* 8 GB */);
             WindowMetrics windowMetrics = getWindowManager().getCurrentWindowMetrics();