pvmfw: Handle dependent nodes in VM DTBO
Test: atest libpvmfw.device_assignment.test
Bug: 317830919
Change-Id: I961519f5d89c043e3085853dda61046e0ad90848
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);
+ }
}