pvmfw: Handle dependent nodes in VM DTBO

Test: atest libpvmfw.device_assignment.test
Bug: 317830919
Change-Id: I961519f5d89c043e3085853dda61046e0ad90848
diff --git a/pvmfw/Android.bp b/pvmfw/Android.bp
index cce0e73..4ee02c1 100644
--- a/pvmfw/Android.bp
+++ b/pvmfw/Android.bp
@@ -74,16 +74,19 @@
     srcs: ["src/device_assignment.rs"],
     defaults: ["libpvmfw.test.defaults"],
     rustlibs: [
+        "libdts",
         "libhyp",
         "liblibfdt",
         "liblog_rust",
         "libpvmfw_fdt_template",
+        "libzerocopy",
     ],
     data: [
         ":test_pvmfw_devices_vm_dtbo",
         ":test_pvmfw_devices_vm_dtbo_without_symbols",
         ":test_pvmfw_devices_vm_dtbo_with_duplicated_iommus",
         ":test_pvmfw_devices_overlapping_pvmfw",
+        ":test_pvmfw_devices_vm_dtbo_with_dependencies",
         ":test_pvmfw_devices_with_rng",
         ":test_pvmfw_devices_with_multiple_devices_iommus",
         ":test_pvmfw_devices_with_iommu_sharing",
@@ -92,7 +95,13 @@
         ":test_pvmfw_devices_without_iommus",
         ":test_pvmfw_devices_with_duplicated_pviommus",
         ":test_pvmfw_devices_with_multiple_reg_iommus",
+        ":test_pvmfw_devices_with_dependency",
+        ":test_pvmfw_devices_with_dependency_loop",
+        ":test_pvmfw_devices_with_multiple_dependencies",
+        ":test_pvmfw_devices_expected_dt",
     ],
+    data_bins: ["dtc_static"],
+    compile_multilib: "first",
     // To use libpvmfw_fdt_template for testing
     enabled: false,
     target: {
@@ -136,6 +145,14 @@
     out: ["test_pvmfw_devices_vm_dtbo_with_duplicated_iommus.dtbo"],
 }
 
+genrule {
+    name: "test_pvmfw_devices_vm_dtbo_with_dependencies",
+    tools: ["dtc"],
+    cmd: "$(location dtc) -@ -I dts -O dtb $(in) -o $(out)",
+    srcs: ["testdata/test_pvmfw_devices_vm_dtbo_with_dependencies.dts"],
+    out: ["test_pvmfw_devices_vm_dtbo_with_dependencies.dtbo"],
+}
+
 genrule_defaults {
     name: "test_device_assignment_dts_to_dtb",
     defaults: ["dts_to_dtb"],
@@ -205,6 +222,53 @@
     out: ["test_pvmfw_devices_with_multiple_reg_iommus.dtb"],
 }
 
+genrule {
+    name: "test_pvmfw_devices_with_dependency",
+    defaults: ["test_device_assignment_dts_to_dtb"],
+    srcs: ["testdata/test_pvmfw_devices_with_dependency.dts"],
+    out: ["test_pvmfw_devices_with_dependency.dtb"],
+}
+
+genrule {
+    name: "test_pvmfw_devices_with_multiple_dependencies",
+    defaults: ["test_device_assignment_dts_to_dtb"],
+    srcs: ["testdata/test_pvmfw_devices_with_multiple_dependencies.dts"],
+    out: ["test_pvmfw_devices_with_multiple_dependencies.dtb"],
+}
+
+genrule {
+    name: "test_pvmfw_devices_with_dependency_loop",
+    defaults: ["test_device_assignment_dts_to_dtb"],
+    srcs: ["testdata/test_pvmfw_devices_with_dependency_loop.dts"],
+    out: ["test_pvmfw_devices_with_dependency_loop.dtb"],
+}
+
+// We can't use genrule because preprocessed platform DT is built with cc_object.
+// cc_genrule doesn't support default, so we'll build all expected DTs in
+// a single build rule.
+cc_genrule {
+    name: "test_pvmfw_devices_expected_dt",
+    srcs: [
+        ":pvmfw_platform.dts.preprocessed",
+        "testdata/expected_dt_with_dependency.dts",
+        "testdata/expected_dt_with_multiple_dependencies.dts",
+        "testdata/expected_dt_with_dependency_loop.dts",
+    ],
+    out: [
+        "expected_dt_with_dependency.dtb",
+        "expected_dt_with_multiple_dependencies.dtb",
+        "expected_dt_with_dependency_loop.dtb",
+    ],
+    tools: ["dtc"],
+    cmd: "FILES=($(in));" +
+        "cp $${FILES[0]} $(genDir)/platform_preprocessed.dts;" +
+        "for DTS in $${FILES[@]:1}; do" +
+        "  DTB=$$(basename -s .dts $${DTS}).dtb;" +
+        "  $(location dtc) -@ -i $(genDir) -I dts -O dtb $${DTS} -o $(genDir)/$${DTB};" +
+        "done",
+    visibility: ["//visibility:private"],
+}
+
 cc_binary {
     name: "pvmfw",
     defaults: ["vmbase_elf_defaults"],
diff --git a/pvmfw/platform.dts b/pvmfw/platform.dts
index 275a1c9..8074188 100644
--- a/pvmfw/platform.dts
+++ b/pvmfw/platform.dts
@@ -706,6 +706,14 @@
 		timeout-sec = <8>;
 	};
 
+	cpufreq {
+		compatible = "virtual,android-v-only-cpufreq";
+		reg = <0x0 0x1040000 PLACEHOLDER2>;
+	};
+
+	// Keep pvIOMMUs at the last for making test happy.
+	// Otherwise, phandle of other nodes are changed when unused pvIOMMU nodes
+	// are removed, so hardcoded phandles in test data would mismatch.
 	pviommu_0: pviommu0 {
 		compatible = "pkvm,pviommu";
 		id = <PLACEHOLDER>;
@@ -766,8 +774,5 @@
 		#iommu-cells = <1>;
 	};
 
-	cpufreq {
-		compatible = "virtual,android-v-only-cpufreq";
-		reg = <0x0 0x1040000 PLACEHOLDER2>;
-	};
+	// Do not add new node below
 };
diff --git a/pvmfw/src/device_assignment.rs b/pvmfw/src/device_assignment.rs
index c3ccf96..885cd22 100644
--- a/pvmfw/src/device_assignment.rs
+++ b/pvmfw/src/device_assignment.rs
@@ -29,8 +29,10 @@
 use core::mem;
 use core::ops::Range;
 use hyp::DeviceAssigningHypervisor;
-use libfdt::{Fdt, FdtError, FdtNode, Phandle, Reg};
+use libfdt::{Fdt, FdtError, FdtNode, FdtNodeMut, Phandle, Reg};
 use log::error;
+use zerocopy::byteorder::big_endian::U32;
+use zerocopy::FromBytes as _;
 
 // TODO(b/308694211): Use cstr! from vmbase instead.
 macro_rules! cstr {
@@ -214,6 +216,72 @@
     }
 }
 
+#[derive(Debug, Eq, PartialEq)]
+enum DeviceTreeChildrenMask {
+    Partial(Vec<DeviceTreeMask>),
+    All,
+}
+
+#[derive(Eq, PartialEq)]
+struct DeviceTreeMask {
+    name_bytes: Vec<u8>,
+    children: DeviceTreeChildrenMask,
+}
+
+impl fmt::Debug for DeviceTreeMask {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        let name_bytes = [self.name_bytes.as_slice(), b"\0"].concat();
+
+        f.debug_struct("DeviceTreeMask")
+            .field("name", &CStr::from_bytes_with_nul(&name_bytes).unwrap())
+            .field("children", &self.children)
+            .finish()
+    }
+}
+
+impl DeviceTreeMask {
+    fn new() -> Self {
+        Self { name_bytes: b"/".to_vec(), children: DeviceTreeChildrenMask::Partial(Vec::new()) }
+    }
+
+    fn mask_internal(&mut self, path: &DtPathTokens, leaf_mask: DeviceTreeChildrenMask) -> bool {
+        let mut iter = self;
+        let mut newly_masked = false;
+        'next_token: for path_token in &path.tokens {
+            let DeviceTreeChildrenMask::Partial(ref mut children) = &mut iter.children else {
+                return false;
+            };
+
+            // Note: Can't use iterator for 'get or insert'. (a.k.a. polonius Rust)
+            #[allow(clippy::needless_range_loop)]
+            for i in 0..children.len() {
+                if children[i].name_bytes.as_slice() == *path_token {
+                    iter = &mut children[i];
+                    newly_masked = false;
+                    continue 'next_token;
+                }
+            }
+            let child = Self {
+                name_bytes: path_token.to_vec(),
+                children: DeviceTreeChildrenMask::Partial(Vec::new()),
+            };
+            children.push(child);
+            newly_masked = true;
+            iter = children.last_mut().unwrap()
+        }
+        iter.children = leaf_mask;
+        newly_masked
+    }
+
+    fn mask(&mut self, path: &DtPathTokens) -> bool {
+        self.mask_internal(path, DeviceTreeChildrenMask::Partial(Vec::new()))
+    }
+
+    fn mask_all(&mut self, path: &DtPathTokens) {
+        self.mask_internal(path, DeviceTreeChildrenMask::All);
+    }
+}
+
 /// Represents VM DTBO
 #[repr(transparent)]
 pub struct VmDtbo(Fdt);
@@ -347,6 +415,114 @@
         }
         Ok(Some(node))
     }
+
+    fn collect_overlayable_nodes_with_phandle(&self) -> Result<BTreeMap<Phandle, DtPathTokens>> {
+        let mut paths = BTreeMap::new();
+        let mut path: DtPathTokens = Default::default();
+        let root = self.as_ref().root();
+        for (node, depth) in root.descendants() {
+            path.tokens.truncate(depth - 1);
+            path.tokens.push(node.name()?.to_bytes());
+            if !path.is_overlayable_node() {
+                continue;
+            }
+            if let Some(phandle) = node.get_phandle()? {
+                paths.insert(phandle, path.clone());
+            }
+        }
+        Ok(paths)
+    }
+
+    fn collect_phandle_references_from_overlayable_nodes(
+        &self,
+    ) -> Result<BTreeMap<DtPathTokens, Vec<Phandle>>> {
+        const CELL_SIZE: usize = core::mem::size_of::<u32>();
+
+        let vm_dtbo = self.as_ref();
+
+        let mut phandle_map = BTreeMap::new();
+        let Some(local_fixups) = vm_dtbo.node(cstr!("/__local_fixups__"))? else {
+            return Ok(phandle_map);
+        };
+
+        let mut path: DtPathTokens = Default::default();
+        for (fixup_node, depth) in local_fixups.descendants() {
+            let node_name = fixup_node.name()?;
+            path.tokens.truncate(depth - 1);
+            path.tokens.push(node_name.to_bytes());
+            if path.tokens.len() != depth {
+                return Err(DeviceAssignmentError::Internal);
+            }
+            if !path.is_overlayable_node() {
+                continue;
+            }
+            let target_node = self.node(&path)?.ok_or(DeviceAssignmentError::InvalidDtbo)?;
+
+            let mut phandles = vec![];
+            for fixup_prop in fixup_node.properties()? {
+                let target_prop = target_node
+                    .getprop(fixup_prop.name()?)
+                    .or(Err(DeviceAssignmentError::InvalidDtbo))?
+                    .ok_or(DeviceAssignmentError::InvalidDtbo)?;
+                let fixup_prop_values = fixup_prop.value()?;
+                if fixup_prop_values.is_empty() || fixup_prop_values.len() % CELL_SIZE != 0 {
+                    return Err(DeviceAssignmentError::InvalidDtbo);
+                }
+
+                for fixup_prop_cell in fixup_prop_values.chunks(CELL_SIZE) {
+                    let phandle_offset: usize = u32::from_be_bytes(
+                        fixup_prop_cell.try_into().or(Err(DeviceAssignmentError::InvalidDtbo))?,
+                    )
+                    .try_into()
+                    .or(Err(DeviceAssignmentError::InvalidDtbo))?;
+                    if phandle_offset % CELL_SIZE != 0 {
+                        return Err(DeviceAssignmentError::InvalidDtbo);
+                    }
+                    let phandle_value = target_prop
+                        .get(phandle_offset..phandle_offset + CELL_SIZE)
+                        .ok_or(DeviceAssignmentError::InvalidDtbo)?;
+                    let phandle: Phandle = U32::ref_from(phandle_value)
+                        .unwrap()
+                        .get()
+                        .try_into()
+                        .or(Err(DeviceAssignmentError::InvalidDtbo))?;
+
+                    phandles.push(phandle);
+                }
+            }
+            if !phandles.is_empty() {
+                phandle_map.insert(path.clone(), phandles);
+            }
+        }
+
+        Ok(phandle_map)
+    }
+
+    fn build_mask(&self, assigned_devices: Vec<DtPathTokens>) -> Result<DeviceTreeMask> {
+        if assigned_devices.is_empty() {
+            return Err(DeviceAssignmentError::Internal);
+        }
+
+        let dependencies = self.collect_phandle_references_from_overlayable_nodes()?;
+        let paths = self.collect_overlayable_nodes_with_phandle()?;
+
+        let mut mask = DeviceTreeMask::new();
+        let mut stack = assigned_devices;
+        while let Some(path) = stack.pop() {
+            if !mask.mask(&path) {
+                continue;
+            }
+            let Some(dst_phandles) = dependencies.get(&path) else {
+                continue;
+            };
+            for dst_phandle in dst_phandles {
+                let dst_path = paths.get(dst_phandle).ok_or(DeviceAssignmentError::Internal)?;
+                stack.push(dst_path.clone());
+            }
+        }
+
+        Ok(mask)
+    }
 }
 
 fn filter_dangling_symbols(fdt: &mut Fdt) -> Result<()> {
@@ -381,6 +557,38 @@
     }
 }
 
+// Filter any node that isn't masked by DeviceTreeMask.
+fn filter_with_mask(anchor: FdtNodeMut, mask: &DeviceTreeMask) -> Result<()> {
+    let mut stack = vec![mask];
+    let mut iter = anchor.next_node(0)?;
+    while let Some((node, depth)) = iter {
+        stack.truncate(depth);
+        let parent_mask = stack.last().unwrap();
+        let DeviceTreeChildrenMask::Partial(parent_mask_children) = &parent_mask.children else {
+            // Shouldn't happen. We only step-in if parent has DeviceTreeChildrenMask::Partial.
+            return Err(DeviceAssignmentError::Internal);
+        };
+
+        let name = node.as_node().name()?.to_bytes();
+        let mask = parent_mask_children.iter().find(|child_mask| child_mask.name_bytes == name);
+        if let Some(masked) = mask {
+            if let DeviceTreeChildrenMask::Partial(_) = &masked.children {
+                // This node is partially masked. Stepping-in.
+                stack.push(masked);
+                iter = node.next_node(depth)?;
+            } else {
+                // This node is fully masked. Stepping-out.
+                iter = node.next_node_skip_subnodes(depth)?;
+            }
+        } else {
+            // This node isn't masked.
+            iter = node.delete_and_next_node(depth)?;
+        }
+    }
+
+    Ok(())
+}
+
 #[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd)]
 struct PvIommu {
     // ID from pvIOMMU node
@@ -689,11 +897,11 @@
     }
 }
 
-#[derive(Debug, Default, Eq, PartialEq)]
+#[derive(Debug, Eq, PartialEq)]
 pub struct DeviceAssignmentInfo {
     pviommus: BTreeSet<PvIommu>,
     assigned_devices: Vec<AssignedDeviceInfo>,
-    filtered_dtbo_paths: Vec<CString>,
+    vm_dtbo_mask: DeviceTreeMask,
 }
 
 impl DeviceAssignmentInfo {
@@ -751,7 +959,7 @@
         let physical_devices = vm_dtbo.parse_physical_devices()?;
 
         let mut assigned_devices = vec![];
-        let mut filtered_dtbo_paths = vec![];
+        let mut assigned_device_paths = vec![];
         for symbol_prop in symbols_node.properties()? {
             let symbol_prop_value = symbol_prop.value()?;
             let dtbo_node_path = CStr::from_bytes_with_nul(symbol_prop_value)
@@ -770,8 +978,7 @@
             )?;
             if let Some(assigned_device) = assigned_device {
                 assigned_devices.push(assigned_device);
-            } else {
-                filtered_dtbo_paths.push(dtbo_node_path.to_cstring());
+                assigned_device_paths.push(dtbo_node_path);
             }
         }
         if assigned_devices.is_empty() {
@@ -780,32 +987,29 @@
 
         Self::validate_pviommu_topology(&assigned_devices)?;
 
-        // Clean up any nodes that wouldn't be overlaid but may contain reference to filtered nodes.
-        // Otherwise, `fdt_apply_overlay()` would fail because of missing phandle reference.
-        // TODO(b/277993056): Also filter other unused nodes/props in __local_fixups__
-        filtered_dtbo_paths.push(CString::new("/__local_fixups__/host").unwrap());
+        let mut vm_dtbo_mask = vm_dtbo.build_mask(assigned_device_paths)?;
+        vm_dtbo_mask.mask_all(&DtPathTokens::new(cstr!("/__local_fixups__"))?);
+        vm_dtbo_mask.mask_all(&DtPathTokens::new(cstr!("/__symbols__"))?);
 
         // Note: Any node without __overlay__ will be ignored by fdt_apply_overlay,
         // so doesn't need to be filtered.
 
-        Ok(Some(Self { pviommus: unique_pviommus, assigned_devices, filtered_dtbo_paths }))
+        Ok(Some(Self { pviommus: unique_pviommus, assigned_devices, vm_dtbo_mask }))
     }
 
     /// Filters VM DTBO to only contain necessary information for booting pVM
-    /// In detail, this will remove followings by setting nop node / nop property.
-    ///   - Removes unassigned devices
-    // TODO(b/277993056): remove unused dependencies in VM DTBO.
-    // TODO(b/277993056): remove supernodes' properties.
-    // TODO(b/277993056): remove unused alises.
     pub fn filter(&self, vm_dtbo: &mut VmDtbo) -> Result<()> {
         let vm_dtbo = vm_dtbo.as_mut();
 
-        // Filters unused node in assigned devices
-        for filtered_dtbo_path in &self.filtered_dtbo_paths {
-            let node = vm_dtbo.node_mut(filtered_dtbo_path).unwrap().unwrap();
-            node.nop()?;
+        // Filter unused references in /__local_fixups__
+        if let Some(local_fixups) = vm_dtbo.node_mut(cstr!("/__local_fixups__"))? {
+            filter_with_mask(local_fixups, &self.vm_dtbo_mask)?;
         }
 
+        // Filter unused nodes in rest of tree
+        let root = vm_dtbo.root_mut();
+        filter_with_mask(root, &self.vm_dtbo_mask)?;
+
         filter_dangling_symbols(vm_dtbo)
     }
 
@@ -860,13 +1064,17 @@
 mod tests {
     use super::*;
     use alloc::collections::{BTreeMap, BTreeSet};
+    use dts::Dts;
     use std::fs;
+    use std::path::Path;
 
     const VM_DTBO_FILE_PATH: &str = "test_pvmfw_devices_vm_dtbo.dtbo";
     const VM_DTBO_WITHOUT_SYMBOLS_FILE_PATH: &str =
         "test_pvmfw_devices_vm_dtbo_without_symbols.dtbo";
     const VM_DTBO_WITH_DUPLICATED_IOMMUS_FILE_PATH: &str =
         "test_pvmfw_devices_vm_dtbo_with_duplicated_iommus.dtbo";
+    const VM_DTBO_WITH_DEPENDENCIES_FILE_PATH: &str =
+        "test_pvmfw_devices_vm_dtbo_with_dependencies.dtbo";
     const FDT_WITHOUT_IOMMUS_FILE_PATH: &str = "test_pvmfw_devices_without_iommus.dtb";
     const FDT_WITHOUT_DEVICE_FILE_PATH: &str = "test_pvmfw_devices_without_device.dtb";
     const FDT_FILE_PATH: &str = "test_pvmfw_devices_with_rng.dtb";
@@ -879,6 +1087,16 @@
         "test_pvmfw_devices_with_duplicated_pviommus.dtb";
     const FDT_WITH_MULTIPLE_REG_IOMMU_FILE_PATH: &str =
         "test_pvmfw_devices_with_multiple_reg_iommus.dtb";
+    const FDT_WITH_DEPENDENCY_FILE_PATH: &str = "test_pvmfw_devices_with_dependency.dtb";
+    const FDT_WITH_MULTIPLE_DEPENDENCIES_FILE_PATH: &str =
+        "test_pvmfw_devices_with_multiple_dependencies.dtb";
+    const FDT_WITH_DEPENDENCY_LOOP_FILE_PATH: &str = "test_pvmfw_devices_with_dependency_loop.dtb";
+
+    const EXPECTED_FDT_WITH_DEPENDENCY_FILE_PATH: &str = "expected_dt_with_dependency.dtb";
+    const EXPECTED_FDT_WITH_MULTIPLE_DEPENDENCIES_FILE_PATH: &str =
+        "expected_dt_with_multiple_dependencies.dtb";
+    const EXPECTED_FDT_WITH_DEPENDENCY_LOOP_FILE_PATH: &str =
+        "expected_dt_with_dependency_loop.dtb";
 
     #[derive(Debug, Default)]
     struct MockHypervisor {
@@ -1449,4 +1667,97 @@
         let compatible = platform_dt.root().next_compatible(cstr!("pkvm,pviommu"));
         assert_eq!(Ok(None), compatible);
     }
+
+    #[test]
+    fn device_info_dependency() {
+        let mut fdt_data = fs::read(FDT_WITH_DEPENDENCY_FILE_PATH).unwrap();
+        let mut vm_dtbo_data = fs::read(VM_DTBO_WITH_DEPENDENCIES_FILE_PATH).unwrap();
+        let fdt = Fdt::from_mut_slice(&mut fdt_data).unwrap();
+        let vm_dtbo = VmDtbo::from_mut_slice(&mut vm_dtbo_data).unwrap();
+        let mut platform_dt_data = pvmfw_fdt_template::RAW.to_vec();
+        platform_dt_data.resize(pvmfw_fdt_template::RAW.len() * 2, 0);
+        let platform_dt = Fdt::from_mut_slice(&mut platform_dt_data).unwrap();
+        platform_dt.unpack().unwrap();
+
+        let hypervisor = MockHypervisor {
+            mmio_tokens: [((0xFF000, 0x1), 0xF000)].into(),
+            iommu_tokens: Default::default(),
+        };
+
+        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 expected = Dts::from_dtb(Path::new(EXPECTED_FDT_WITH_DEPENDENCY_FILE_PATH)).unwrap();
+        let platform_dt = Dts::from_fdt(platform_dt).unwrap();
+
+        assert_eq!(expected, platform_dt);
+    }
+
+    #[test]
+    fn device_info_multiple_dependencies() {
+        let mut fdt_data = fs::read(FDT_WITH_MULTIPLE_DEPENDENCIES_FILE_PATH).unwrap();
+        let mut vm_dtbo_data = fs::read(VM_DTBO_WITH_DEPENDENCIES_FILE_PATH).unwrap();
+        let fdt = Fdt::from_mut_slice(&mut fdt_data).unwrap();
+        let vm_dtbo = VmDtbo::from_mut_slice(&mut vm_dtbo_data).unwrap();
+        let mut platform_dt_data = pvmfw_fdt_template::RAW.to_vec();
+        platform_dt_data.resize(pvmfw_fdt_template::RAW.len() * 2, 0);
+        let platform_dt = Fdt::from_mut_slice(&mut platform_dt_data).unwrap();
+        platform_dt.unpack().unwrap();
+
+        let hypervisor = MockHypervisor {
+            mmio_tokens: [((0xFF000, 0x1), 0xF000), ((0xFF100, 0x1), 0xF100)].into(),
+            iommu_tokens: Default::default(),
+        };
+        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 expected =
+            Dts::from_dtb(Path::new(EXPECTED_FDT_WITH_MULTIPLE_DEPENDENCIES_FILE_PATH)).unwrap();
+        let platform_dt = Dts::from_fdt(platform_dt).unwrap();
+
+        assert_eq!(expected, platform_dt);
+    }
+
+    #[test]
+    fn device_info_dependency_loop() {
+        let mut fdt_data = fs::read(FDT_WITH_DEPENDENCY_LOOP_FILE_PATH).unwrap();
+        let mut vm_dtbo_data = fs::read(VM_DTBO_WITH_DEPENDENCIES_FILE_PATH).unwrap();
+        let fdt = Fdt::from_mut_slice(&mut fdt_data).unwrap();
+        let vm_dtbo = VmDtbo::from_mut_slice(&mut vm_dtbo_data).unwrap();
+        let mut platform_dt_data = pvmfw_fdt_template::RAW.to_vec();
+        platform_dt_data.resize(pvmfw_fdt_template::RAW.len() * 2, 0);
+        let platform_dt = Fdt::from_mut_slice(&mut platform_dt_data).unwrap();
+        platform_dt.unpack().unwrap();
+
+        let hypervisor = MockHypervisor {
+            mmio_tokens: [((0xFF200, 0x1), 0xF200)].into(),
+            iommu_tokens: Default::default(),
+        };
+        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 expected =
+            Dts::from_dtb(Path::new(EXPECTED_FDT_WITH_DEPENDENCY_LOOP_FILE_PATH)).unwrap();
+        let platform_dt = Dts::from_fdt(platform_dt).unwrap();
+
+        assert_eq!(expected, platform_dt);
+    }
 }
diff --git a/pvmfw/testdata/expected_dt_with_dependency.dts b/pvmfw/testdata/expected_dt_with_dependency.dts
new file mode 100644
index 0000000..7e0ad20
--- /dev/null
+++ b/pvmfw/testdata/expected_dt_with_dependency.dts
@@ -0,0 +1,47 @@
+/dts-v1/;
+
+/include/ "platform_preprocessed.dts"
+
+// Note: This uses manually written __symbols__ so we don't
+
+/ {
+    node_a: node_a {
+        phandle = <0x2E>;
+        val = <0x6>;
+        dep = <&node_a_dep &common>;
+        reg = <0x0 0xFF000 0x0 0x1>;
+        interrupts = <0x0 0xF 0x4>;
+        iommus;
+    };
+
+    node_a_dep: node_a_dep {
+        phandle = <0x31>;
+        val = <0xFF>;
+        dep = <&node_aa_nested_dep>;
+    };
+
+    node_aa {
+        should_be_preserved = <0xFF>;
+
+        node_aa_nested_dep: node_aa_nested_dep {
+            phandle = <0x33>;
+            tag = <0x9>;
+        };
+    };
+
+    common: common {
+        phandle = <0x32>;
+        id = <0x9>;
+    };
+
+    /delete-node/ pviommu0;
+    /delete-node/ pviommu1;
+    /delete-node/ pviommu2;
+    /delete-node/ pviommu3;
+    /delete-node/ pviommu4;
+    /delete-node/ pviommu5;
+    /delete-node/ pviommu6;
+    /delete-node/ pviommu7;
+    /delete-node/ pviommu8;
+    /delete-node/ pviommu9;
+};
diff --git a/pvmfw/testdata/expected_dt_with_dependency_loop.dts b/pvmfw/testdata/expected_dt_with_dependency_loop.dts
new file mode 100644
index 0000000..61031ab
--- /dev/null
+++ b/pvmfw/testdata/expected_dt_with_dependency_loop.dts
@@ -0,0 +1,29 @@
+/dts-v1/;
+
+/include/ "platform_preprocessed.dts"
+
+/ {
+    node_c: node_c {
+        phandle = <0x30>;
+        loop_dep = <&node_c_loop>;
+        reg = <0x0 0xFF200 0x0 0x1>;
+        interrupts = <0x0 0xF 0x4>;
+        iommus;
+    };
+
+    node_c_loop: node_c_loop {
+        phandle = <0x36>;
+        loop_dep = <&node_c>;
+    };
+
+    /delete-node/ pviommu0;
+    /delete-node/ pviommu1;
+    /delete-node/ pviommu2;
+    /delete-node/ pviommu3;
+    /delete-node/ pviommu4;
+    /delete-node/ pviommu5;
+    /delete-node/ pviommu6;
+    /delete-node/ pviommu7;
+    /delete-node/ pviommu8;
+    /delete-node/ pviommu9;
+};
diff --git a/pvmfw/testdata/expected_dt_with_multiple_dependencies.dts b/pvmfw/testdata/expected_dt_with_multiple_dependencies.dts
new file mode 100644
index 0000000..dc8c357
--- /dev/null
+++ b/pvmfw/testdata/expected_dt_with_multiple_dependencies.dts
@@ -0,0 +1,70 @@
+/dts-v1/;
+
+// Note: We can't use label syntax here.
+// Implementation applies overlay after removing /__symbols__,
+// so using label syntax here wouldn't match with the actual reasult.
+
+/include/ "platform_preprocessed.dts"
+
+/ {
+    node_a: node_a {
+        phandle = <0x2E>;
+        val = <0x6>;
+        dep = <&node_a_dep &common>;
+        reg = <0x0 0xFF000 0x0 0x1>;
+        interrupts = <0x0 0xF 0x4>;
+        iommus;
+    };
+
+    node_a_dep: node_a_dep {
+        phandle = <0x31>;
+        val = <0xFF>;
+        dep = <&node_aa_nested_dep>;
+    };
+
+    node_aa {
+        should_be_preserved = <0xFF>;
+
+        node_aa_nested_dep: node_aa_nested_dep {
+            phandle = <0x33>;
+            tag = <0x9>;
+        };
+    };
+
+    node_b: node_b {
+        phandle = <0x2F>;
+        tag = <0x33>;
+        version = <0x1 0x2>;
+        dep = <&node_b_dep1 &node_b_dep2>;
+        reg = <0x00 0xFF100 0x00 0x01>;
+        interrupts = <0x00 0x0F 0x04>;
+        iommus;
+    };
+
+    node_b_dep1: node_b_dep1 {
+        phandle = <0x34>;
+        placeholder;
+    };
+
+    node_b_dep2: node_b_dep2 {
+        phandle = <0x35>;
+        placeholder;
+        dep = <&common>;
+    };
+
+    common: common {
+        phandle = <0x32>;
+        id = <0x9>;
+    };
+
+    /delete-node/ pviommu0;
+    /delete-node/ pviommu1;
+    /delete-node/ pviommu2;
+    /delete-node/ pviommu3;
+    /delete-node/ pviommu4;
+    /delete-node/ pviommu5;
+    /delete-node/ pviommu6;
+    /delete-node/ pviommu7;
+    /delete-node/ pviommu8;
+    /delete-node/ pviommu9;
+};
diff --git a/pvmfw/testdata/test_pvmfw_devices_vm_dtbo_with_dependencies.dts b/pvmfw/testdata/test_pvmfw_devices_vm_dtbo_with_dependencies.dts
new file mode 100644
index 0000000..21075e7
--- /dev/null
+++ b/pvmfw/testdata/test_pvmfw_devices_vm_dtbo_with_dependencies.dts
@@ -0,0 +1,77 @@
+/dts-v1/;
+/plugin/;
+
+/ {
+    host {
+        #address-cells = <0x2>;
+        #size-cells = <0x1>;
+        node_a {
+            reg = <0x0 0xF000 0x1>;
+            android,pvmfw,target = <&node_a>;
+        };
+        node_b {
+            reg = <0x0 0xF100 0x1>;
+            android,pvmfw,target = <&node_b>;
+        };
+        node_c {
+            reg = <0x0 0xF200 0x1>;
+            android,pvmfw,target = <&node_c>;
+        };
+    };
+};
+
+&{/} {
+    node_a: node_a {
+        val = <0x6>;
+        dep = <&node_a_dep &common>;
+    };
+
+    node_a_dep: node_a_dep {
+        val = <0xFF>;
+        dep = <&node_aa_nested_dep>;
+
+        node_a_internal {
+            val;
+        };
+    };
+
+    node_aa {
+        should_be_preserved = <0xFF>;
+        node_aa_nested_dep: node_aa_nested_dep {
+            tag = <0x9>;
+        };
+    };
+};
+
+&{/} {
+    node_b: node_b {
+        tag = <0x33>;
+        version = <0x1 0x2>;
+        dep = <&node_b_dep1 &node_b_dep2>;
+    };
+
+    node_b_dep1: node_b_dep1 {
+        placeholder;
+    };
+
+    node_b_dep2: node_b_dep2 {
+        placeholder;
+        dep = <&common>;
+    };
+};
+
+&{/} {
+    node_c: node_c {
+        loop_dep = <&node_c_loop>;
+    };
+
+    node_c_loop: node_c_loop {
+        loop_dep = <&node_c>;
+    };
+};
+
+&{/} {
+    common: common {
+        id = <0x9>;
+    };
+};
diff --git a/pvmfw/testdata/test_pvmfw_devices_with_dependency.dts b/pvmfw/testdata/test_pvmfw_devices_with_dependency.dts
new file mode 100644
index 0000000..b1cf6c7
--- /dev/null
+++ b/pvmfw/testdata/test_pvmfw_devices_with_dependency.dts
@@ -0,0 +1,36 @@
+/dts-v1/;
+
+/include/ "test_crosvm_dt_base.dtsi"
+
+/ {
+    node_a: node_a {
+        reg = <0x0 0xFF000 0x0 0x1>;
+        interrupts = <0x0 0xF 0x4>;
+        val = <0x6>;
+        dep = <&node_a_dep &common>;
+
+        node_a_internal {
+            parent = <&node_a>;
+        };
+    };
+
+    node_a_dep: node_a_dep {
+        val = <0xFF>;
+        dep = <&node_aa_nested_dep>;
+
+        node_a_dep_internal {
+            val;
+        };
+    };
+
+    node_aa {
+        should_be_preserved = <0xFF>;
+        node_aa_nested_dep: node_aa_nested_dep {
+            tag = <0x9>;
+        };
+    };
+
+    common: common {
+        id = <0x9>;
+    };
+};
diff --git a/pvmfw/testdata/test_pvmfw_devices_with_dependency_loop.dts b/pvmfw/testdata/test_pvmfw_devices_with_dependency_loop.dts
new file mode 100644
index 0000000..9a62cb5
--- /dev/null
+++ b/pvmfw/testdata/test_pvmfw_devices_with_dependency_loop.dts
@@ -0,0 +1,15 @@
+/dts-v1/;
+
+/include/ "test_crosvm_dt_base.dtsi"
+
+/ {
+    node_c: node_c {
+        reg = <0x0 0xFF200 0x0 0x1>;
+        interrupts = <0x0 0xF 0x4>;
+        loop_dep = <&node_c_loop>;
+    };
+
+    node_c_loop: node_c_loop {
+        loop_dep = <&node_c>;
+    };
+};
diff --git a/pvmfw/testdata/test_pvmfw_devices_with_multiple_dependencies.dts b/pvmfw/testdata/test_pvmfw_devices_with_multiple_dependencies.dts
new file mode 100644
index 0000000..573bdcf
--- /dev/null
+++ b/pvmfw/testdata/test_pvmfw_devices_with_multiple_dependencies.dts
@@ -0,0 +1,50 @@
+/dts-v1/;
+
+/include/ "test_crosvm_dt_base.dtsi"
+
+/ {
+    node_a: node_a {
+        reg = <0x0 0xFF000 0x0 0x1>;
+        interrupts = <0x0 0xF 0x4>;
+        val = <0x6>;
+        dep = <&node_a_dep &common>;
+    };
+
+    node_a_dep: node_a_dep {
+        val = <0xFF>;
+        dep = <&node_nested_dep>;
+
+        node_a_internal {
+            val;
+        };
+    };
+
+    node_aa {
+        should_be_preserved = <0xFF>;
+        node_nested_dep: node_aa_nested_dep {
+            tag = <0x9>;
+        };
+    };
+
+    node_b: node_b {
+        reg = <0x0 0xFF100 0x0 0x1>;
+        interrupts = <0x0 0xF 0x4>;
+        tag = <0x33>;
+        version = <0x1 0x2>;
+        phandle = <0x5>;
+        dep = <&node_b_dep1 &node_b_dep2>;
+    };
+
+    node_b_dep1: node_b_dep1 {
+        placeholder;
+    };
+
+    node_b_dep2: node_b_dep2 {
+        placeholder;
+        dep = <&common>;
+    };
+
+    common: common {
+        id = <0x9>;
+    };
+};