Merge "Notify VS when users or apps are removed" into main
diff --git a/TEST_MAPPING b/TEST_MAPPING
index f146b4e..89d4552 100644
--- a/TEST_MAPPING
+++ b/TEST_MAPPING
@@ -12,6 +12,9 @@
       "name": "MicrodroidTestApp"
     },
     {
+      "name": "MicrodroidTestAppNoPerm"
+    },
+    {
       "name": "VmAttestationTestApp"
     },
     {
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/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/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/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");
+    }
+}