pvmfw: Apply VM DTBO

This CL only applies assigned VM DTBO for the simplest case, which
iommu, phandle, nor aliases aren't involved.

Next CLs will handle following cases:
  - Apply iommu. Platform DT will be also updated to have pre-populated
    pvmiommu node
  - Validate patched values (reg, iommu, ..)
  - Handle __local_fixup__, __fixups__ (i.e. handle phandle in VM DTBO)
  - Handle /alias in VM DTBO
  - ...

Bug: 277993056
Test: atest libpvmfw.device_assignment.test, launch protected VM
Change-Id: I4e4aea0885da925ae419921d729380a1d71707e0
diff --git a/pvmfw/Android.bp b/pvmfw/Android.bp
index 8c21030..946ed85 100644
--- a/pvmfw/Android.bp
+++ b/pvmfw/Android.bp
@@ -59,6 +59,55 @@
     ],
 }
 
+genrule {
+    name: "test_pvmfw_devices_vm_dtbo",
+    defaults: ["dts_to_dtb"],
+    srcs: ["testdata/test_pvmfw_devices_vm_dtbo.dts"],
+    out: ["test_pvmfw_devices_vm_dtbo.dtbo"],
+}
+
+genrule {
+    name: "test_pvmfw_devices_vm_dtbo_without_symbols",
+    defaults: ["dts_to_dtb"],
+    srcs: ["testdata/test_pvmfw_devices_vm_dtbo_without_symbols.dts"],
+    out: ["test_pvmfw_devices_vm_dtbo_without_symbols.dtbo"],
+}
+
+genrule {
+    name: "test_pvmfw_devices_with_rng",
+    defaults: ["dts_to_dtb"],
+    srcs: ["testdata/test_pvmfw_devices_with_rng.dts"],
+    out: ["test_pvmfw_devices_with_rng.dtb"],
+}
+
+rust_test {
+    name: "libpvmfw.device_assignment.test",
+    srcs: ["src/device_assignment.rs"],
+    defaults: ["avf_build_flags_rust"],
+    test_suites: ["general-tests"],
+    test_options: {
+        unit_test: true,
+    },
+    prefer_rlib: true,
+    rustlibs: [
+        "liblibfdt",
+        "liblog_rust",
+        "libpvmfw_fdt_template",
+    ],
+    data: [
+        ":test_pvmfw_devices_vm_dtbo",
+        ":test_pvmfw_devices_vm_dtbo_without_symbols",
+        ":test_pvmfw_devices_with_rng",
+    ],
+    // To use libpvmfw_fdt_template for testing
+    enabled: false,
+    target: {
+        android_arm64: {
+            enabled: true,
+        },
+    },
+}
+
 cc_binary {
     name: "pvmfw",
     defaults: ["vmbase_elf_defaults"],
diff --git a/pvmfw/TEST_MAPPING b/pvmfw/TEST_MAPPING
index d77e651..f21318e 100644
--- a/pvmfw/TEST_MAPPING
+++ b/pvmfw/TEST_MAPPING
@@ -7,6 +7,9 @@
     },
     {
       "name" : "libpvmfw.bootargs.test"
+    },
+    {
+      "name" : "libpvmfw.device_assignment.test"
     }
   ]
 }
diff --git a/pvmfw/src/config.rs b/pvmfw/src/config.rs
index 78b6323..7023b95 100644
--- a/pvmfw/src/config.rs
+++ b/pvmfw/src/config.rs
@@ -260,7 +260,7 @@
     }
 
     /// Get slice containing the platform BCC.
-    pub fn get_entries(&mut self) -> (&mut [u8], Option<&mut [u8]>) {
+    pub fn get_entries(&mut self) -> (&mut [u8], Option<&mut [u8]>, Option<&mut [u8]>) {
         // This assumes that the blobs are in-order w.r.t. the entries.
         let bcc_range = self.get_entry_range(Entry::Bcc);
         let dp_range = self.get_entry_range(Entry::DebugPolicy);
@@ -277,6 +277,7 @@
             (
                 Self::from_raw_range_mut(ptr, bcc_range.unwrap()),
                 dp_range.map(|dp_range| Self::from_raw_range_mut(ptr, dp_range)),
+                vm_dtbo_range.map(|vm_dtbo_range| Self::from_raw_range_mut(ptr, vm_dtbo_range)),
             )
         }
     }
diff --git a/pvmfw/src/device_assignment.rs b/pvmfw/src/device_assignment.rs
new file mode 100644
index 0000000..a2816c4
--- /dev/null
+++ b/pvmfw/src/device_assignment.rs
@@ -0,0 +1,430 @@
+// Copyright 2023, 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.
+
+//! Validate device assignment written in crosvm DT with VM DTBO, and apply it
+//! to platform DT.
+//! Declared in separated libs for adding unit tests, which requires libstd.
+
+#[cfg(test)]
+extern crate alloc;
+
+use alloc::ffi::CString;
+use alloc::fmt;
+use alloc::vec;
+use alloc::vec::Vec;
+use core::ffi::CStr;
+use core::iter::Iterator;
+use core::mem;
+use libfdt::{Fdt, FdtError, FdtNode};
+
+// TODO(b/308694211): Move this to the vmbase
+macro_rules! const_cstr {
+    ($str:literal) => {{
+        #[allow(unused_unsafe)] // In case the macro is used within an unsafe block.
+        // SAFETY: Trailing null is guaranteed by concat!()
+        unsafe {
+            CStr::from_bytes_with_nul_unchecked(concat!($str, "\0").as_bytes())
+        }
+    }};
+}
+
+// TODO(b/308694211): Use cstr! from vmbase instead.
+macro_rules! cstr {
+    ($str:literal) => {{
+        CStr::from_bytes_with_nul(concat!($str, "\0").as_bytes()).unwrap()
+    }};
+}
+
+const FILTERED_VM_DTBO_PROP: [&CStr; 3] = [
+    const_cstr!("android,pvmfw,phy-reg"),
+    const_cstr!("android,pvmfw,phy-iommu"),
+    const_cstr!("android,pvmfw,phy-sid"),
+];
+
+const REG_PROP_NAME: &CStr = const_cstr!("reg");
+const INTERRUPTS_PROP_NAME: &CStr = const_cstr!("interrupts");
+// TODO(b/277993056): Keep constants derived from platform.dts in one place.
+const CELLS_PER_INTERRUPT: usize = 3; // from /intc node in platform.dts
+
+/// Errors in device assignment.
+#[derive(Clone, Copy, Debug, Eq, PartialEq)]
+pub enum DeviceAssignmentError {
+    // Invalid VM DTBO
+    InvalidDtbo,
+    /// Invalid __symbols__
+    InvalidSymbols,
+    /// Invalid <interrupts>
+    InvalidInterrupts,
+    /// Unsupported overlay target syntax. Only supports <target-path> with full path.
+    UnsupportedOverlayTarget,
+    /// Unexpected error from libfdt
+    UnexpectedFdtError(FdtError),
+}
+
+impl From<FdtError> for DeviceAssignmentError {
+    fn from(e: FdtError) -> Self {
+        DeviceAssignmentError::UnexpectedFdtError(e)
+    }
+}
+
+impl fmt::Display for DeviceAssignmentError {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        match self {
+            Self::InvalidDtbo => write!(f, "Invalid DTBO"),
+            Self::InvalidSymbols => write!(
+                f,
+                "Invalid property in /__symbols__. Must point to valid assignable device node."
+            ),
+            Self::InvalidInterrupts => write!(f, "Invalid <interrupts>"),
+            Self::UnsupportedOverlayTarget => {
+                write!(f, "Unsupported overlay target. Only supports 'target-path = \"/\"'")
+            }
+            Self::UnexpectedFdtError(e) => write!(f, "Unexpected Error from libfdt: {e}"),
+        }
+    }
+}
+
+pub type Result<T> = core::result::Result<T, DeviceAssignmentError>;
+
+/// Represents VM DTBO
+#[repr(transparent)]
+pub struct VmDtbo(Fdt);
+
+impl VmDtbo {
+    const OVERLAY_NODE_NAME: &CStr = const_cstr!("__overlay__");
+    const TARGET_PATH_PROP: &CStr = const_cstr!("target-path");
+    const SYMBOLS_NODE_PATH: &CStr = const_cstr!("/__symbols__");
+
+    /// Wraps a mutable slice containing a VM DTBO.
+    ///
+    /// Fails if the VM DTBO does not pass validation.
+    pub fn from_mut_slice(dtbo: &mut [u8]) -> Result<&mut Self> {
+        // This validates DTBO
+        let fdt = Fdt::from_mut_slice(dtbo)?;
+        // SAFETY: VmDtbo is a transparent wrapper around Fdt, so representation is the same.
+        Ok(unsafe { mem::transmute::<&mut Fdt, &mut Self>(fdt) })
+    }
+
+    // Locates device node path as if the given dtbo node path is assigned and VM DTBO is overlaid.
+    // For given dtbo node path, this concatenates <target-path> of the enclosing fragment and
+    // relative path from __overlay__ node.
+    //
+    // Here's an example with sample VM DTBO:
+    //    / {
+    //       fragment@rng {
+    //         target-path = "/";  // Always 'target-path = "/"'. Disallows <target> or other path.
+    //         __overlay__ {
+    //           rng { ... };      // Actual device node is here. If overlaid, path would be "/rng"
+    //         };
+    //       };
+    //       __symbols__ {         // List of assignable devices
+    //         // Each property describes an assigned device device information.
+    //         // property name is the device label, and property value is the path in the VM DTBO.
+    //         rng = "/fragment@rng/__overlay__/rng";
+    //       };
+    //    };
+    //
+    // Then locate_overlay_target_path(cstr!("/fragment@rng/__overlay__/rng")) is Ok("/rng")
+    //
+    // Contrary to fdt_overlay_target_offset(), this API enforces overlay target property
+    // 'target-path = "/"', so the overlay doesn't modify and/or append platform DT's existing
+    // node and/or properties. The enforcement is for compatibility reason.
+    fn locate_overlay_target_path(&self, dtbo_node_path: &CStr) -> Result<CString> {
+        let dtbo_node_path_bytes = dtbo_node_path.to_bytes();
+        if dtbo_node_path_bytes.first() != Some(&b'/') {
+            return Err(DeviceAssignmentError::UnsupportedOverlayTarget);
+        }
+
+        let node = self.0.node(dtbo_node_path)?.ok_or(DeviceAssignmentError::InvalidSymbols)?;
+
+        let fragment_node = node.supernode_at_depth(1)?;
+        let target_path = fragment_node
+            .getprop_str(Self::TARGET_PATH_PROP)?
+            .ok_or(DeviceAssignmentError::InvalidDtbo)?;
+        if target_path != cstr!("/") {
+            return Err(DeviceAssignmentError::UnsupportedOverlayTarget);
+        }
+
+        let mut components = dtbo_node_path_bytes
+            .split(|char| *char == b'/')
+            .filter(|&component| !component.is_empty())
+            .skip(1);
+        let overlay_node_name = components.next();
+        if overlay_node_name != Some(Self::OVERLAY_NODE_NAME.to_bytes()) {
+            return Err(DeviceAssignmentError::InvalidDtbo);
+        }
+        let mut overlaid_path = Vec::with_capacity(dtbo_node_path_bytes.len());
+        for component in components {
+            overlaid_path.push(b'/');
+            overlaid_path.extend_from_slice(component);
+        }
+        overlaid_path.push(b'\0');
+
+        Ok(CString::from_vec_with_nul(overlaid_path).unwrap())
+    }
+}
+
+impl AsRef<Fdt> for VmDtbo {
+    fn as_ref(&self) -> &Fdt {
+        &self.0
+    }
+}
+
+impl AsMut<Fdt> for VmDtbo {
+    fn as_mut(&mut self) -> &mut Fdt {
+        &mut self.0
+    }
+}
+
+/// Assigned device information parsed from crosvm DT.
+/// Keeps everything in the owned data because underlying FDT will be reused for platform DT.
+#[derive(Debug, Eq, PartialEq)]
+struct AssignedDeviceInfo {
+    // Node path of assigned device (e.g. "/rng")
+    node_path: CString,
+    // DTBO node path of the assigned device (e.g. "/fragment@rng/__overlay__/rng")
+    dtbo_node_path: CString,
+    // <reg> property from the crosvm DT
+    reg: Vec<u8>,
+    // <interrupts> property from the crosvm DT
+    interrupts: Vec<u8>,
+}
+
+impl AssignedDeviceInfo {
+    fn parse_interrupts(node: &FdtNode) -> Result<Vec<u8>> {
+        // Validation: Validate if interrupts cell numbers are multiple of #interrupt-cells.
+        // We can't know how many interrupts would exist.
+        let interrupts_cells = node
+            .getprop_cells(INTERRUPTS_PROP_NAME)?
+            .ok_or(DeviceAssignmentError::InvalidInterrupts)?
+            .count();
+        if interrupts_cells % CELLS_PER_INTERRUPT != 0 {
+            return Err(DeviceAssignmentError::InvalidInterrupts);
+        }
+
+        // Once validated, keep the raw bytes so patch can be done with setprop()
+        Ok(node.getprop(INTERRUPTS_PROP_NAME).unwrap().unwrap().into())
+    }
+
+    // TODO(b/277993056): Read and validate iommu
+    fn parse(fdt: &Fdt, vm_dtbo: &VmDtbo, dtbo_node_path: &CStr) -> Result<Option<Self>> {
+        let node_path = vm_dtbo.locate_overlay_target_path(dtbo_node_path)?;
+
+        let Some(node) = fdt.node(&node_path)? else { return Ok(None) };
+
+        // TODO(b/277993056): Validate reg with HVC, and keep reg with FdtNode::reg()
+        let reg = node.getprop(REG_PROP_NAME).unwrap().unwrap();
+
+        let interrupts = Self::parse_interrupts(&node)?;
+
+        Ok(Some(Self {
+            node_path,
+            dtbo_node_path: dtbo_node_path.into(),
+            reg: reg.to_vec(),
+            interrupts: interrupts.to_vec(),
+        }))
+    }
+
+    fn patch(&self, fdt: &mut Fdt) -> Result<()> {
+        let mut dst = fdt.node_mut(&self.node_path)?.unwrap();
+        dst.setprop(REG_PROP_NAME, &self.reg)?;
+        dst.setprop(INTERRUPTS_PROP_NAME, &self.interrupts)?;
+        // TODO(b/277993056): Read and patch iommu
+        Ok(())
+    }
+}
+
+#[derive(Debug, Default, Eq, PartialEq)]
+pub struct DeviceAssignmentInfo {
+    assigned_devices: Vec<AssignedDeviceInfo>,
+    filtered_dtbo_paths: Vec<CString>,
+}
+
+impl DeviceAssignmentInfo {
+    /// Parses fdt and vm_dtbo, and creates new DeviceAssignmentInfo
+    // TODO(b/277993056): Parse __local_fixups__
+    // TODO(b/277993056): Parse __fixups__
+    pub fn parse(fdt: &Fdt, vm_dtbo: &VmDtbo) -> Result<Option<Self>> {
+        let Some(symbols_node) = vm_dtbo.as_ref().symbols()? else {
+            // /__symbols__ should contain all assignable devices.
+            // If empty, then nothing can be assigned.
+            return Ok(None);
+        };
+
+        let mut assigned_devices = vec![];
+        let mut filtered_dtbo_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)
+                .or(Err(DeviceAssignmentError::InvalidSymbols))?;
+            let assigned_device = AssignedDeviceInfo::parse(fdt, vm_dtbo, dtbo_node_path)?;
+            if let Some(assigned_device) = assigned_device {
+                assigned_devices.push(assigned_device);
+            } else {
+                filtered_dtbo_paths.push(dtbo_node_path.into());
+            }
+        }
+        filtered_dtbo_paths.push(VmDtbo::SYMBOLS_NODE_PATH.into());
+
+        if assigned_devices.is_empty() {
+            return Ok(None);
+        }
+        Ok(Some(Self { assigned_devices, filtered_dtbo_paths }))
+    }
+
+    /// 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
+    ///   - Removes /__symbols__ node
+    // 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()?;
+        }
+
+        // Filters unused properties in assigned device node
+        for assigned_device in &self.assigned_devices {
+            let mut node = vm_dtbo.node_mut(&assigned_device.dtbo_node_path).unwrap().unwrap();
+            for prop in FILTERED_VM_DTBO_PROP {
+                node.nop_property(prop)?;
+            }
+        }
+        Ok(())
+    }
+
+    pub fn patch(&self, fdt: &mut Fdt) -> Result<()> {
+        for device in &self.assigned_devices {
+            device.patch(fdt)?
+        }
+        Ok(())
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use std::fs;
+
+    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 FDT_FILE_PATH: &str = "test_pvmfw_devices_with_rng.dtb";
+
+    fn into_fdt_prop(native_bytes: Vec<u32>) -> Vec<u8> {
+        let mut v = Vec::with_capacity(native_bytes.len() * 4);
+        for byte in native_bytes {
+            v.extend_from_slice(&byte.to_be_bytes());
+        }
+        v
+    }
+
+    #[test]
+    fn device_info_new_without_symbols() {
+        let mut fdt_data = fs::read(FDT_FILE_PATH).unwrap();
+        let mut vm_dtbo_data = fs::read(VM_DTBO_WITHOUT_SYMBOLS_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 device_info = DeviceAssignmentInfo::parse(fdt, vm_dtbo).unwrap();
+        assert_eq!(device_info, None);
+    }
+
+    #[test]
+    fn device_info_assigned_info() {
+        let mut fdt_data = fs::read(FDT_FILE_PATH).unwrap();
+        let mut vm_dtbo_data = fs::read(VM_DTBO_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 device_info = DeviceAssignmentInfo::parse(fdt, vm_dtbo).unwrap().unwrap();
+
+        let expected = [AssignedDeviceInfo {
+            node_path: CString::new("/rng").unwrap(),
+            dtbo_node_path: cstr!("/fragment@rng/__overlay__/rng").into(),
+            reg: into_fdt_prop(vec![0x0, 0x9, 0x0, 0xFF]),
+            interrupts: into_fdt_prop(vec![0x0, 0xF, 0x4]),
+        }];
+
+        assert_eq!(device_info.assigned_devices, expected);
+    }
+
+    #[test]
+    fn device_info_new_without_assigned_devices() {
+        let mut fdt_data: Vec<u8> = pvmfw_fdt_template::RAW.into();
+        let mut vm_dtbo_data = fs::read(VM_DTBO_FILE_PATH).unwrap();
+        let fdt = Fdt::from_mut_slice(fdt_data.as_mut_slice()).unwrap();
+        let vm_dtbo = VmDtbo::from_mut_slice(&mut vm_dtbo_data).unwrap();
+
+        let device_info = DeviceAssignmentInfo::parse(fdt, vm_dtbo).unwrap();
+        assert_eq!(device_info, None);
+    }
+
+    #[test]
+    fn device_info_filter() {
+        let mut fdt_data = fs::read(FDT_FILE_PATH).unwrap();
+        let mut vm_dtbo_data = fs::read(VM_DTBO_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 device_info = DeviceAssignmentInfo::parse(fdt, vm_dtbo).unwrap().unwrap();
+        device_info.filter(vm_dtbo).unwrap();
+
+        let vm_dtbo = vm_dtbo.as_mut();
+
+        let rng = vm_dtbo.node(cstr!("/fragment@rng/__overlay__/rng")).unwrap();
+        assert_ne!(rng, None);
+
+        let light = vm_dtbo.node(cstr!("/fragment@rng/__overlay__/light")).unwrap();
+        assert_eq!(light, None);
+
+        let symbols_node = vm_dtbo.symbols().unwrap();
+        assert_eq!(symbols_node, None);
+    }
+
+    #[test]
+    fn device_info_patch() {
+        let mut fdt_data = fs::read(FDT_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 device_info = DeviceAssignmentInfo::parse(fdt, vm_dtbo).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();
+        }
+
+        let rng_node = platform_dt.node(cstr!("/rng")).unwrap().unwrap();
+        let expected: Vec<(&CStr, Vec<u8>)> = vec![
+            (cstr!("android,rng,ignore-gctrl-reset"), Vec::<u8>::new()),
+            (cstr!("compatible"), b"android,rng\0".to_vec()),
+            (cstr!("reg"), into_fdt_prop(vec![0x0, 0x9, 0x0, 0xFF])),
+            (cstr!("interrupts"), into_fdt_prop(vec![0x0, 0xF, 0x4])),
+        ];
+
+        for (prop, (prop_name, prop_value)) in rng_node.properties().unwrap().zip(expected) {
+            assert_eq!((prop.name(), prop.value()), (Ok(prop_name), Ok(prop_value.as_slice())));
+        }
+    }
+}
diff --git a/pvmfw/src/entry.rs b/pvmfw/src/entry.rs
index 9c929a9..ed73bc9 100644
--- a/pvmfw/src/entry.rs
+++ b/pvmfw/src/entry.rs
@@ -83,7 +83,12 @@
 }
 
 impl<'a> MemorySlices<'a> {
-    fn new(fdt: usize, kernel: usize, kernel_size: usize) -> Result<Self, RebootReason> {
+    fn new(
+        fdt: usize,
+        kernel: usize,
+        kernel_size: usize,
+        vm_dtbo: Option<&mut [u8]>,
+    ) -> Result<Self, RebootReason> {
         let fdt_size = NonZeroUsize::new(crosvm::FDT_MAX_SIZE).unwrap();
         // TODO - Only map the FDT as read-only, until we modify it right before jump_to_payload()
         // e.g. by generating a DTBO for a template DT in main() and, on return, re-map DT as RW,
@@ -95,12 +100,12 @@
 
         // SAFETY: The tracker validated the range to be in main memory, mapped, and not overlap.
         let fdt = unsafe { slice::from_raw_parts_mut(range.start as *mut u8, range.len()) };
+
+        let info = fdt::sanitize_device_tree(fdt, vm_dtbo)?;
         let fdt = libfdt::Fdt::from_mut_slice(fdt).map_err(|e| {
-            error!("Failed to spawn the FDT wrapper: {e}");
+            error!("Failed to load sanitized FDT: {e}");
             RebootReason::InvalidFdt
         })?;
-
-        let info = fdt::sanitize_device_tree(fdt)?;
         debug!("Fdt passed validation!");
 
         let memory_range = info.memory_range;
@@ -207,7 +212,7 @@
         RebootReason::InvalidConfig
     })?;
 
-    let (bcc_slice, debug_policy) = appended.get_entries();
+    let (bcc_slice, debug_policy, vm_dtbo) = appended.get_entries();
 
     // Up to this point, we were using the built-in static (from .rodata) page tables.
     MEMORY.lock().replace(MemoryTracker::new(
@@ -217,7 +222,7 @@
         Some(memory::appended_payload_range()),
     ));
 
-    let slices = MemorySlices::new(fdt, payload, payload_size)?;
+    let slices = MemorySlices::new(fdt, payload, payload_size, vm_dtbo)?;
 
     // This wrapper allows main() to be blissfully ignorant of platform details.
     let next_bcc = crate::main(slices.fdt, slices.kernel, slices.ramdisk, bcc_slice, debug_policy)?;
@@ -427,10 +432,10 @@
         }
     }
 
-    fn get_entries(&mut self) -> (&mut [u8], Option<&mut [u8]>) {
+    fn get_entries(&mut self) -> (&mut [u8], Option<&mut [u8]>, Option<&mut [u8]>) {
         match self {
             Self::Config(ref mut cfg) => cfg.get_entries(),
-            Self::LegacyBcc(ref mut bcc) => (bcc, None),
+            Self::LegacyBcc(ref mut bcc) => (bcc, None, None),
         }
     }
 }
diff --git a/pvmfw/src/fdt.rs b/pvmfw/src/fdt.rs
index 1f87dcc..7655614 100644
--- a/pvmfw/src/fdt.rs
+++ b/pvmfw/src/fdt.rs
@@ -15,6 +15,8 @@
 //! High-level FDT functions.
 
 use crate::bootargs::BootArgsIterator;
+use crate::device_assignment::DeviceAssignmentInfo;
+use crate::device_assignment::VmDtbo;
 use crate::helpers::GUEST_PAGE_SIZE;
 use crate::Box;
 use crate::RebootReason;
@@ -590,6 +592,7 @@
     pci_info: PciInfo,
     serial_info: SerialInfo,
     pub swiotlb_info: SwiotlbInfo,
+    device_assignment: Option<DeviceAssignmentInfo>,
 }
 
 impl DeviceTreeInfo {
@@ -600,20 +603,53 @@
     }
 }
 
-pub fn sanitize_device_tree(fdt: &mut Fdt) -> Result<DeviceTreeInfo, RebootReason> {
-    let info = parse_device_tree(fdt)?;
-    debug!("Device tree info: {:?}", info);
+pub fn sanitize_device_tree(
+    fdt: &mut [u8],
+    vm_dtbo: Option<&mut [u8]>,
+) -> Result<DeviceTreeInfo, RebootReason> {
+    let fdt = Fdt::from_mut_slice(fdt).map_err(|e| {
+        error!("Failed to load FDT: {e}");
+        RebootReason::InvalidFdt
+    })?;
+
+    let vm_dtbo = match vm_dtbo {
+        Some(vm_dtbo) => Some(VmDtbo::from_mut_slice(vm_dtbo).map_err(|e| {
+            error!("Failed to load VM DTBO: {e}");
+            RebootReason::InvalidFdt
+        })?),
+        None => None,
+    };
+
+    let info = parse_device_tree(fdt, vm_dtbo.as_deref())?;
 
     fdt.copy_from_slice(pvmfw_fdt_template::RAW).map_err(|e| {
         error!("Failed to instantiate FDT from the template DT: {e}");
         RebootReason::InvalidFdt
     })?;
 
+    if let Some(device_assignment_info) = &info.device_assignment {
+        let vm_dtbo = vm_dtbo.unwrap();
+        device_assignment_info.filter(vm_dtbo).map_err(|e| {
+            error!("Failed to filter VM DTBO: {e}");
+            RebootReason::InvalidFdt
+        })?;
+        // SAFETY: Damaged VM DTBO isn't used in this API after this unsafe block.
+        // VM DTBO can't be reused in any way as Fdt nor VmDtbo outside of this API because
+        // it can only be instantiated after validation.
+        unsafe {
+            fdt.apply_overlay(vm_dtbo.as_mut()).map_err(|e| {
+                error!("Failed to apply filtered VM DTBO: {e}");
+                RebootReason::InvalidFdt
+            })?;
+        }
+    }
+
     patch_device_tree(fdt, &info)?;
+
     Ok(info)
 }
 
-fn parse_device_tree(fdt: &libfdt::Fdt) -> Result<DeviceTreeInfo, RebootReason> {
+fn parse_device_tree(fdt: &Fdt, vm_dtbo: Option<&VmDtbo>) -> Result<DeviceTreeInfo, RebootReason> {
     let kernel_range = read_kernel_range_from(fdt).map_err(|e| {
         error!("Failed to read kernel range from DT: {e}");
         RebootReason::InvalidFdt
@@ -657,6 +693,14 @@
     })?;
     validate_swiotlb_info(&swiotlb_info, &memory_range)?;
 
+    let device_assignment = match vm_dtbo {
+        Some(vm_dtbo) => DeviceAssignmentInfo::parse(fdt, vm_dtbo).map_err(|e| {
+            error!("Failed to parse device assignment from DT and VM DTBO: {e}");
+            RebootReason::InvalidFdt
+        })?,
+        None => None,
+    };
+
     Ok(DeviceTreeInfo {
         kernel_range,
         initrd_range,
@@ -666,6 +710,7 @@
         pci_info,
         serial_info,
         swiotlb_info,
+        device_assignment,
     })
 }
 
@@ -715,6 +760,14 @@
         error!("Failed to patch timer info to DT: {e}");
         RebootReason::InvalidFdt
     })?;
+    if let Some(device_assignment) = &info.device_assignment {
+        // Note: We patch values after VM DTBO is overlaid because patch may require more space
+        // then VM DTBO's underlying slice is allocated.
+        device_assignment.patch(fdt).map_err(|e| {
+            error!("Failed to patch device assignment info to DT: {e}");
+            RebootReason::InvalidFdt
+        })?;
+    }
 
     fdt.pack().map_err(|e| {
         error!("Failed to pack DT after patching: {e}");
diff --git a/pvmfw/src/main.rs b/pvmfw/src/main.rs
index b8cbf1b..8aa5274 100644
--- a/pvmfw/src/main.rs
+++ b/pvmfw/src/main.rs
@@ -23,6 +23,7 @@
 mod bootargs;
 mod config;
 mod crypto;
+mod device_assignment;
 mod dice;
 mod entry;
 mod exceptions;
diff --git a/pvmfw/testdata/test_pvmfw_devices_vm_dtbo.dts b/pvmfw/testdata/test_pvmfw_devices_vm_dtbo.dts
new file mode 100644
index 0000000..e85b55b
--- /dev/null
+++ b/pvmfw/testdata/test_pvmfw_devices_vm_dtbo.dts
@@ -0,0 +1,32 @@
+/dts-v1/;
+/plugin/;
+
+/ {
+	fragment@rng {
+		target-path = "/";
+		__overlay__ {
+			rng {
+				compatible = "android,rng";
+				android,rng,ignore-gctrl-reset;
+				android,pvmfw,phy-reg = <0x0 0x12F00000 0x1000>;
+				android,pvmfw,phy-iommu = <0x0 0x12E40000>;
+				android,pvmfw,phy-sid = <3>;
+			};
+		};
+	};
+
+	fragment@sensor {
+		target-path = "/";
+		__overlay__ {
+			light {
+				compatible = "android,light";
+				version = <0x1 0x2>;
+			};
+		};
+	};
+
+	__symbols__ {
+		rng = "/fragment@rng/__overlay__/rng";
+		sensor = "/fragment@sensor/__overlay__/light";
+	};
+};
diff --git a/pvmfw/testdata/test_pvmfw_devices_vm_dtbo_without_symbols.dts b/pvmfw/testdata/test_pvmfw_devices_vm_dtbo_without_symbols.dts
new file mode 100644
index 0000000..08444ac
--- /dev/null
+++ b/pvmfw/testdata/test_pvmfw_devices_vm_dtbo_without_symbols.dts
@@ -0,0 +1,27 @@
+/dts-v1/;
+/plugin/;
+
+/ {
+	fragment@rng {
+		target-path = "/";
+		__overlay__ {
+			rng {
+				compatible = "android,rng";
+				android,rng,ignore-gctrl-reset;
+				android,pvmfw,phy-reg = <0x0 0x12F00000 0x1000>;
+				android,pvmfw,phy-iommu = <0x0 0x12E40000>;
+				android,pvmfw,phy-sid = <3>;
+			};
+		};
+	};
+
+	fragment@sensor {
+		target-path = "/";
+		__overlay__ {
+			light {
+				compatible = "android,light";
+				version = <0x1 0x2>;
+			};
+		};
+	};
+};
diff --git a/pvmfw/testdata/test_pvmfw_devices_with_rng.dts b/pvmfw/testdata/test_pvmfw_devices_with_rng.dts
new file mode 100644
index 0000000..f24fd65
--- /dev/null
+++ b/pvmfw/testdata/test_pvmfw_devices_with_rng.dts
@@ -0,0 +1,52 @@
+/dts-v1/;
+/plugin/;
+
+/ {
+	chosen {
+		stdout-path = "/uart@3f8";
+		linux,pci-probe-only = <1>;
+	};
+
+	memory {
+		device_type = "memory";
+		reg = <0x00 0x80000000 0xFFFFFFFF>;
+	};
+
+	reserved-memory {
+		#address-cells = <2>;
+		#size-cells = <2>;
+		ranges;
+		swiotlb: restricted_dma_reserved {
+			compatible = "restricted-dma-pool";
+			reg = <0xFFFFFFFF>;
+			size = <0xFFFFFFFF>;
+			alignment = <0xFFFFFFFF>;
+		};
+
+		dice {
+			compatible = "google,open-dice";
+			no-map;
+			reg = <0xFFFFFFFF>;
+		};
+	};
+
+	cpus {
+		#address-cells = <1>;
+		#size-cells = <0>;
+		cpu@0 {
+			device_type = "cpu";
+		};
+		cpu@1 {
+			device_type = "cpu";
+		    reg = <0x00 0x80000000 0xFFFFFFFF>;
+		};
+    };
+
+    rng@90000000 {
+        compatible = "android,rng";
+        reg = <0x0 0x9 0x0 0xFF>;
+        interrupts = <0x0 0xF 0x4>;
+        google,eh,ignore-gctrl-reset;
+        status = "okay";
+    };
+};