// Copyright (c) Microsoft Corporation. // Licensed under the MIT License. //! Parsing code for the devicetree provided by openhcl_boot used by underhill //! usermode. Unlike `host_fdt_parser`, this code requires std as it is intended //! to be only used in usermode. #![forbid(unsafe_code)] use anyhow::Context; use anyhow::bail; use fdt::parser::Node; use fdt::parser::Parser; use fdt::parser::Property; use igvm_defs::MemoryMapEntryType; use igvm_defs::dt::IGVM_DT_IGVM_TYPE_PROPERTY; use inspect::Inspect; use loader_defs::shim::MemoryVtlType; use memory_range::MemoryRange; use vm_topology::memory::MemoryRangeWithNode; use vm_topology::processor::aarch64::Aarch64PlatformConfig; /// A parsed cpu. #[derive(Debug, Inspect, Clone, Copy, PartialEq, Eq)] pub struct Cpu { /// Architecture specific "reg" value for this CPU. For x64, this is the /// APIC ID. For ARM v8 64-bit, this should match the MPIDR_EL1 register /// affinity bits. pub reg: u64, /// The vnode field of a cpu dt node, which describes the numa node id. pub vnode: u32, } /// Information about a guest memory range. #[derive(Debug, Inspect, Clone, PartialEq, Eq)] pub struct Memory { /// The range of memory. pub range: MemoryRangeWithNode, /// The VTL this memory is for. #[inspect(debug)] pub vtl_usage: MemoryVtlType, /// The host provided IGVM type for this memory. #[inspect(debug)] pub igvm_type: MemoryMapEntryType, } /// Vtls for mmio. #[derive(Debug, Inspect, Clone, PartialEq, Eq)] pub enum Vtl { /// VTL0. Vtl0, /// VTL2. Vtl2, } /// Information about guest mmio. #[derive(Debug, Inspect, Clone, PartialEq, Eq)] pub struct Mmio { /// The address range of mmio. pub range: MemoryRange, /// The VTL this mmio is for. pub vtl: Vtl, } /// Information about a section of the guest's address space. #[derive(Debug, Inspect, Clone, PartialEq, Eq)] #[inspect(tag = "type")] pub enum AddressRange { /// This range describes memory. Memory(#[inspect(flatten)] Memory), /// This range describes mmio. Mmio(#[inspect(flatten)] Mmio), } impl AddressRange { /// The [`MemoryRange`] for this address range. pub fn range(&self) -> &MemoryRange { match self { AddressRange::Memory(memory) => &memory.range.range, AddressRange::Mmio(mmio) => &mmio.range, } } /// The [`MemoryVtlType`] for this address range. pub fn vtl_usage(&self) -> MemoryVtlType { match self { AddressRange::Memory(memory) => memory.vtl_usage, AddressRange::Mmio(Mmio { vtl, .. }) => match vtl { Vtl::Vtl0 => MemoryVtlType::VTL0_MMIO, Vtl::Vtl2 => MemoryVtlType::VTL2_MMIO, }, } } } /// The isolation type of the partition. #[derive(Debug, Clone, Copy, PartialEq, Eq, Inspect)] pub enum IsolationType { /// No isolation. None, /// Hyper-V based isolation. Vbs, /// AMD SNP. Snp, /// Intel TDX. Tdx, } /// The memory allocation mode provided by the host. This reports how the /// bootloader decided to provide memory for the kernel. #[derive(Debug, Clone, Copy, PartialEq, Eq, Inspect)] #[inspect(external_tag)] pub enum MemoryAllocationMode { /// Use the host provided memory topology, and use VTL2_PROTECTABLE entries /// as VTL2 ram. This is the default if no /// `openhcl/memory-allocation-property` mode is provided by the host. Host, /// Allow VTL2 to select its own ranges from the address space to use for /// memory, with a size provided by the host. Vtl2 { /// The number of bytes VTL2 should allocate for memory for itself. /// Encoded as `openhcl/memory-size` in device tree. #[inspect(hex)] memory_size: Option, /// The number of bytes VTL2 should allocate for mmio for itself. /// Encoded as `openhcl/mmio-size` in device tree. #[inspect(hex)] mmio_size: Option, }, } /// Information parsed from the device tree provided by openhcl_boot. These /// values are trusted, as it's expected that openhcl_boot has already validated /// the host provided device tree. #[derive(Debug, Inspect, PartialEq, Eq)] pub struct ParsedBootDtInfo { /// The cpus in the system. The index in the vector is also the mshv VP /// index. #[inspect(iter_by_index)] pub cpus: Vec, /// The physical address of the VTL0 alias mapping, if one is configured. pub vtl0_alias_map: Option, /// The memory ranges for VTL2 that were reported to the kernel. This is /// sorted in ascending order. #[inspect(iter_by_index)] pub vtl2_memory: Vec, /// The unified memory map for the partition, from the bootloader. Sorted in /// ascending order. Note that this includes mmio gaps as well. #[inspect(with = "inspect_helpers::memory_internal")] pub partition_memory_map: Vec, /// The mmio to report to VTL0. #[inspect(iter_by_index)] pub vtl0_mmio: Vec, /// The ranges config regions are stored at. #[inspect(iter_by_index)] pub config_ranges: Vec, /// The VTL2 reserved range. pub vtl2_reserved_range: MemoryRange, /// The memory range that contains the persisted header describing the /// persisted protobuf region. pub vtl2_persisted_header: MemoryRange, /// The memory range that contains the persisted protobuf region. pub vtl2_persisted_protobuf_region: MemoryRange, /// The ranges that were accepted at load time by the host on behalf of the /// guest. #[inspect(iter_by_index)] pub accepted_ranges: Vec, /// The memory allocation mode the bootloader decided to use. pub memory_allocation_mode: MemoryAllocationMode, /// The isolation type of the partition. pub isolation: IsolationType, /// VTL2 range for private pool memory. #[inspect(iter_by_index)] pub private_pool_ranges: Vec, /// GIC and platform interrupt configuration, on AArch64. pub gic: Option, } fn err_to_owned(e: fdt::parser::Error<'_>) -> anyhow::Error { anyhow::Error::msg(e.to_string()) } /// Try to find a given property on a node, returning None if not found. As the /// bootloader should be producing a well formed device tree, errors are not /// expected and flattened into `None`. fn try_find_property<'a>(node: &Node<'a>, name: &str) -> Option> { node.find_property(name).ok().flatten() } fn address_cells(node: &Node<'_>) -> anyhow::Result { let prop = try_find_property(node, "#address-cells") .context("missing address cells on {node.name}")?; prop.read_u32(0).map_err(err_to_owned) } fn property_to_u64_vec(node: &Node<'_>, name: &str) -> anyhow::Result> { let prop = try_find_property(node, name).context("missing prop {name} on {node.name}")?; Ok(prop .as_64_list() .map_err(err_to_owned) .context("prop {name} is not a list of u64s")? .collect()) } struct OpenhclInfo { vtl0_mmio: Vec, config_ranges: Vec, partition_memory_map: Vec, accepted_memory: Vec, vtl2_reserved_range: MemoryRange, vtl0_alias_map: Option, memory_allocation_mode: MemoryAllocationMode, isolation: IsolationType, private_pool_ranges: Vec, vtl2_persisted_header: MemoryRange, vtl2_persisted_protobuf_region: MemoryRange, } fn parse_memory_openhcl(node: &Node<'_>) -> anyhow::Result { let vtl_usage = { let prop = try_find_property(node, "openhcl,memory-type") .context(format!("missing openhcl,memory-type on node {}", node.name))?; MemoryVtlType(prop.read_u32(0).map_err(err_to_owned).context(format!( "openhcl memory node {} openhcl,memory-type invalid", node.name ))?) }; if vtl_usage.ram() { // Parse this entry as memory. let range = parse_memory(node).context("unable to parse base memory")?; let igvm_type = { let prop = try_find_property(node, IGVM_DT_IGVM_TYPE_PROPERTY) .context(format!("missing igvm type on node {}", node.name))?; let value = prop .read_u32(0) .map_err(err_to_owned) .context(format!("memory node {} invalid igvm type", node.name))?; MemoryMapEntryType(value as u16) }; Ok(AddressRange::Memory(Memory { range, vtl_usage, igvm_type, })) } else { // Parse this type as just mmio. let range = { let reg = property_to_u64_vec(node, "reg")?; if reg.len() != 2 { bail!("mmio node {} does not have 2 u64s", node.name); } let base = reg[0]; let len = reg[1]; MemoryRange::try_new(base..(base + len)).context("invalid mmio range")? }; let vtl = match vtl_usage { MemoryVtlType::VTL0_MMIO => Vtl::Vtl0, MemoryVtlType::VTL2_MMIO => Vtl::Vtl2, _ => bail!( "invalid vtl_usage {vtl_usage:?} type for mmio node {}", node.name ), }; Ok(AddressRange::Mmio(Mmio { range, vtl })) } } fn parse_accepted_memory(node: &Node<'_>) -> anyhow::Result { let reg = property_to_u64_vec(node, "reg")?; if reg.len() != 2 { bail!("accepted memory node {} does not have 2 u64s", node.name); } let base = reg[0]; let len = reg[1]; MemoryRange::try_new(base..(base + len)).context("invalid preaccepted memory") } fn parse_openhcl(node: &Node<'_>) -> anyhow::Result { let mut memory = Vec::new(); let mut accepted_memory = Vec::new(); for child in node.children() { let child = child.map_err(err_to_owned).context("child invalid")?; match child.name { name if name.starts_with("memory@") => { memory.push(parse_memory_openhcl(&child)?); } name if name.starts_with("accepted-memory@") => { accepted_memory.push(parse_accepted_memory(&child)?); } name if name.starts_with("memory-allocation-mode") => {} _ => { // Ignore other nodes. } } } let isolation = { let prop = try_find_property(node, "isolation-type").context("missing isolation-type")?; match prop.read_str().map_err(err_to_owned)? { "none" => IsolationType::None, "vbs" => IsolationType::Vbs, "snp" => IsolationType::Snp, "tdx" => IsolationType::Tdx, ty => bail!("invalid isolation-type {ty}"), } }; let memory_allocation_mode = { let prop = try_find_property(node, "memory-allocation-mode") .context("missing memory-allocation-mode")?; match prop.read_str().map_err(err_to_owned)? { "host" => MemoryAllocationMode::Host, "vtl2" => { let memory_size = try_find_property(node, "memory-size") .map(|p| p.read_u64(0)) .transpose() .map_err(err_to_owned)?; let mmio_size = try_find_property(node, "mmio-size") .map(|p| p.read_u64(0)) .transpose() .map_err(err_to_owned)?; MemoryAllocationMode::Vtl2 { memory_size, mmio_size, } } mode => bail!("invalid memory-allocation-mode {mode}"), } }; memory.sort_by_key(|r| r.range().start()); accepted_memory.sort_by_key(|r| r.start()); // Report config ranges in a separate vec as well, for convenience. let config_ranges = memory .iter() .filter_map(|entry| { if entry.vtl_usage() == MemoryVtlType::VTL2_CONFIG { Some(*entry.range()) } else { None } }) .collect(); // Report the reserved range. There should only be one. let vtl2_reserved_range = { let mut reserved_range_iter = memory.iter().filter_map(|entry| { if entry.vtl_usage() == MemoryVtlType::VTL2_RESERVED { Some(*entry.range()) } else { None } }); let reserved_range = reserved_range_iter.next().unwrap_or(MemoryRange::EMPTY); if reserved_range_iter.next().is_some() { bail!("multiple VTL2 reserved ranges found"); } reserved_range }; // Report private pool ranges in a separate vec, for convenience. let private_pool_ranges = memory .iter() .filter_map(|entry| match entry { AddressRange::Memory(memory) => { if memory.vtl_usage == MemoryVtlType::VTL2_GPA_POOL { Some(memory.range.clone()) } else { None } } AddressRange::Mmio(_) => None, }) .collect(); // Report the persisted header range. let vtl2_persisted_header = { let mut header_iter = memory.iter().filter_map(|entry| { if entry.vtl_usage() == MemoryVtlType::VTL2_PERSISTED_STATE_HEADER { Some(*entry.range()) } else { None } }); let header = header_iter .next() .ok_or(anyhow::anyhow!("no VTL2 persisted header range found"))?; if header_iter.next().is_some() { bail!("multiple VTL2 persisted header ranges found"); } header }; // Report the persisted protobuf region range. let vtl2_persisted_protobuf_region = { let mut region_iter = memory.iter().filter_map(|entry| { if entry.vtl_usage() == MemoryVtlType::VTL2_PERSISTED_STATE_PROTOBUF { Some(*entry.range()) } else { None } }); let region = region_iter.next().ok_or(anyhow::anyhow!( "no VTL2 persisted protobuf region range found" ))?; if region_iter.next().is_some() { bail!("multiple VTL2 persisted protobuf region ranges found"); } region }; let vtl0_alias_map = try_find_property(node, "vtl0-alias-map") .map(|prop| prop.read_u64(0).map_err(err_to_owned)) .transpose() .context("unable to read vtl0-alias-map")?; // Extract vmbus mmio information from the overall memory map. let vtl0_mmio = memory .iter() .filter_map(|range| match range { AddressRange::Memory(_) => None, AddressRange::Mmio(mmio) => match mmio.vtl { Vtl::Vtl0 => Some(mmio.range), Vtl::Vtl2 => None, }, }) .collect(); Ok(OpenhclInfo { vtl0_mmio, config_ranges, partition_memory_map: memory, accepted_memory, vtl2_reserved_range, vtl0_alias_map, memory_allocation_mode, isolation, private_pool_ranges, vtl2_persisted_header, vtl2_persisted_protobuf_region, }) } fn parse_cpus(node: &Node<'_>) -> anyhow::Result> { let address_cells = address_cells(node)?; if address_cells > 2 { bail!("cpus address-cells > 2 unexpected"); } let mut cpus = Vec::new(); for cpu in node.children() { let cpu = cpu.map_err(err_to_owned).context("cpu invalid")?; let reg = try_find_property(&cpu, "reg").context("{cpu.name} missing reg")?; let reg = match address_cells { 1 => reg.read_u32(0).map_err(err_to_owned)? as u64, 2 => reg.read_u64(0).map_err(err_to_owned)?, _ => unreachable!(), }; let vnode = try_find_property(&cpu, "numa-node-id") .context("{cpu.name} missing numa-node-id")? .read_u32(0) .map_err(err_to_owned)?; cpus.push(Cpu { reg, vnode }); } Ok(cpus) } /// Parse a single memory node. fn parse_memory(node: &Node<'_>) -> anyhow::Result { let reg = property_to_u64_vec(node, "reg")?; if reg.len() != 2 { bail!("memory node {} does not have 2 u64s", node.name); } let base = reg[0]; let len = reg[1]; let numa_node_id = try_find_property(node, "numa-node-id") .context(format!("{} missing numa-node-id", node.name))? .read_u32(0) .map_err(err_to_owned) .context("unable to read numa-node-id")?; Ok(MemoryRangeWithNode { range: MemoryRange::try_new(base..base + len).context("invalid memory range")?, vnode: numa_node_id, }) } /// Parse GIC config fn parse_gic(node: &Node<'_>) -> anyhow::Result { let reg = property_to_u64_vec(node, "reg")?; if reg.len() != 4 { bail!("gic node {} does not have 4 u64s", node.name); } Ok(Aarch64PlatformConfig { gic_distributor_base: reg[0], // The OpenHCL paravisor boot path always receives a GICv3 layout. gic_version: vm_topology::processor::aarch64::GicVersion::V3 { redistributors_base: reg[2], }, gic_msi: vm_topology::processor::aarch64::GicMsiController::None, pmu_gsiv: None, // TODO: parse from the DT timer node instead of hardcoding. virt_timer_ppi: 20, gic_nr_irqs: 992, }) } fn parse_pmu(node: &Node<'_>) -> anyhow::Result { let interrupts = try_find_property(node, "interrupts").context("pmu node missing interrupts")?; let interrupts_typ = interrupts .read_u32(0) .map_err(err_to_owned) .context("missing pmu interrupts typ")?; let interrupts_ppi_index = interrupts .read_u32(1) .map_err(err_to_owned) .context("missing pmu interrupts index")?; // The interrupt is expected to be a PPI. const GIC_PPI: u32 = 1; if interrupts_typ != GIC_PPI { bail!("pmu node has unexpected interrupt type {interrupts_typ}"); } // The index is the index off of the PPI base of 16. PPIs only exist from 16 // to 31, so the index should be < 16. if interrupts_ppi_index >= 16 { bail!("pmu node has unexpected interrupt index {interrupts_ppi_index}"); } const PPI_BASE: u32 = 16; Ok(PPI_BASE + interrupts_ppi_index) } impl ParsedBootDtInfo { /// Read parameters passed via device tree by openhcl_boot, at /// /sys/firmware/fdt. /// /// The device tree is expected to be well formed from the bootloader, so /// any errors here are not expected. pub fn new() -> anyhow::Result { let raw = fs_err::read("/sys/firmware/fdt").context("reading fdt")?; Self::new_from_raw(&raw) } fn new_from_raw(raw: &[u8]) -> anyhow::Result { let mut cpus = Vec::new(); let mut vtl0_mmio = Vec::new(); let mut config_ranges = Vec::new(); let mut vtl2_memory = Vec::new(); let mut gic = None; let mut pmu_gsiv = None; let mut partition_memory_map = Vec::new(); let mut accepted_ranges = Vec::new(); let mut vtl0_alias_map = None; let mut memory_allocation_mode = MemoryAllocationMode::Host; let mut isolation = IsolationType::None; let mut vtl2_reserved_range = MemoryRange::EMPTY; let mut private_pool_ranges = Vec::new(); let mut vtl2_persisted_header = MemoryRange::EMPTY; let mut vtl2_persisted_protobuf_region = MemoryRange::EMPTY; let parser = Parser::new(raw) .map_err(err_to_owned) .context("failed to create fdt parser")?; for child in parser .root() .map_err(err_to_owned) .context("root invalid")? .children() { let child = child.map_err(err_to_owned).context("child invalid")?; match child.name { "cpus" => { cpus = parse_cpus(&child)?; } "openhcl" => { let OpenhclInfo { vtl0_mmio: n_vtl0_mmio, config_ranges: n_config_ranges, partition_memory_map: n_partition_memory_map, vtl2_reserved_range: n_vtl2_reserved_range, accepted_memory: n_accepted_memory, vtl0_alias_map: n_vtl0_alias_map, memory_allocation_mode: n_memory_allocation_mode, isolation: n_isolation, private_pool_ranges: n_private_pool_ranges, vtl2_persisted_header: n_vtl2_persisted_header, vtl2_persisted_protobuf_region: n_vtl2_persisted_protobuf_region, } = parse_openhcl(&child)?; vtl0_mmio = n_vtl0_mmio; config_ranges = n_config_ranges; partition_memory_map = n_partition_memory_map; accepted_ranges = n_accepted_memory; vtl0_alias_map = n_vtl0_alias_map; memory_allocation_mode = n_memory_allocation_mode; isolation = n_isolation; vtl2_reserved_range = n_vtl2_reserved_range; private_pool_ranges = n_private_pool_ranges; vtl2_persisted_header = n_vtl2_persisted_header; vtl2_persisted_protobuf_region = n_vtl2_persisted_protobuf_region; } _ if child.name.starts_with("memory@") => { vtl2_memory.push(parse_memory(&child)?); } _ if child.name.starts_with("intc@") => { // TODO: make sure we are on aarch64 gic = Some(parse_gic(&child)?); } _ if child.name.starts_with("pmu") => { // TODO: make sure we are on aarch64 pmu_gsiv = Some(parse_pmu(&child)?); } _ => { // Ignore other nodes. } } } vtl2_memory.sort_by_key(|r| r.range.start()); // Merge PMU GSIV into the GIC platform config if both were parsed. if let (Some(gic), Some(pmu_gsiv)) = (&mut gic, pmu_gsiv) { gic.pmu_gsiv = Some(pmu_gsiv); } Ok(Self { cpus, vtl0_mmio, config_ranges, vtl2_memory, partition_memory_map, vtl0_alias_map, accepted_ranges, gic, memory_allocation_mode, isolation, vtl2_reserved_range, private_pool_ranges, vtl2_persisted_header, vtl2_persisted_protobuf_region, }) } } /// Boot times reported by the bootloader. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct BootTimes { /// Kernel start time. pub start: Option, /// Kernel end time. pub end: Option, /// Sidecar start time. pub sidecar_start: Option, /// Sidecar end time. pub sidecar_end: Option, } impl BootTimes { /// Read the boot times passed via device tree by openhcl_boot, at /// /sys/firmware/fdt. /// /// The device tree is expected to be well formed from the bootloader, so /// any errors here are not expected. pub fn new() -> anyhow::Result { let raw = fs_err::read("/sys/firmware/fdt").context("reading fdt")?; Self::new_from_raw(&raw) } fn new_from_raw(raw: &[u8]) -> anyhow::Result { let mut start = None; let mut end = None; let mut sidecar_start = None; let mut sidecar_end = None; let parser = Parser::new(raw) .map_err(err_to_owned) .context("failed to create fdt parser")?; let root = parser .root() .map_err(err_to_owned) .context("root invalid")?; if let Some(prop) = try_find_property(&root, "reftime_boot_start") { start = Some(prop.read_u64(0).map_err(err_to_owned)?); } if let Some(prop) = try_find_property(&root, "reftime_boot_end") { end = Some(prop.read_u64(0).map_err(err_to_owned)?); } if let Some(prop) = try_find_property(&root, "reftime_sidecar_start") { sidecar_start = Some(prop.read_u64(0).map_err(err_to_owned)?); } if let Some(prop) = try_find_property(&root, "reftime_sidecar_end") { sidecar_end = Some(prop.read_u64(0).map_err(err_to_owned)?); } Ok(Self { start, end, sidecar_start, sidecar_end, }) } } mod inspect_helpers { use super::*; pub(super) fn memory_internal(ranges: &[AddressRange]) -> impl Inspect + '_ { inspect::iter_by_key(ranges.iter().map(|entry| (entry.range(), entry))) } } #[cfg(test)] mod tests { use super::*; use fdt::builder::Builder; fn build_dt(info: &ParsedBootDtInfo) -> anyhow::Result> { let mut buf = vec![0; 4096]; let mut builder = Builder::new(fdt::builder::BuilderConfig { blob_buffer: &mut buf, string_table_cap: 1024, memory_reservations: &[], })?; let p_address_cells = builder.add_string("#address-cells")?; let p_size_cells = builder.add_string("#size-cells")?; let p_reg = builder.add_string("reg")?; let p_device_type = builder.add_string("device_type")?; let p_compatible = builder.add_string("compatible")?; let p_ranges = builder.add_string("ranges")?; let p_numa_node_id = builder.add_string("numa-node-id")?; let p_igvm_type = builder.add_string(IGVM_DT_IGVM_TYPE_PROPERTY)?; let p_openhcl_memory = builder.add_string("openhcl,memory-type")?; let p_interrupts = builder.add_string("interrupts")?; let mut root_builder = builder .start_node("")? .add_u32(p_address_cells, 2)? .add_u32(p_size_cells, 2)? .add_str(p_compatible, "microsoft,openvmm")?; let mut cpu_builder = root_builder .start_node("cpus")? .add_u32(p_address_cells, 1)? .add_u32(p_size_cells, 0)?; // add cpus for (index, cpu) in info.cpus.iter().enumerate() { let name = format!("cpu@{}", index + 1); cpu_builder = cpu_builder .start_node(&name)? .add_str(p_device_type, "cpu")? .add_u32(p_reg, cpu.reg as u32)? .add_u32(p_numa_node_id, cpu.vnode)? .end_node()?; } root_builder = cpu_builder.end_node()?; // add memory, in reverse order. for memory in info.vtl2_memory.iter().rev() { let name = format!("memory@{:x}", memory.range.start()); root_builder = root_builder .start_node(&name)? .add_str(p_device_type, "memory")? .add_u64_list(p_reg, [memory.range.start(), memory.range.len()])? .add_u32(p_numa_node_id, memory.vnode)? .end_node()?; } // GIC if let Some(gic) = info.gic { let p_interrupt_cells = root_builder.add_string("#interrupt-cells")?; let p_redist_regions = root_builder.add_string("#redistributor-regions")?; let p_redist_stride = root_builder.add_string("redistributor-stride")?; let p_interrupt_controller = root_builder.add_string("interrupt-controller")?; let p_phandle = root_builder.add_string("phandle")?; let gic_redist_base = match gic.gic_version { vm_topology::processor::aarch64::GicVersion::V3 { redistributors_base, } => redistributors_base, vm_topology::processor::aarch64::GicVersion::V2 { cpu_interface_base } => { cpu_interface_base } }; let name = format!("intc@{}", gic.gic_distributor_base); root_builder = root_builder .start_node(name.as_ref())? .add_str(p_compatible, "arm,gic-v3")? .add_u32(p_redist_regions, 1)? .add_u64(p_redist_stride, 0)? .add_u64_array(p_reg, &[gic.gic_distributor_base, 0, gic_redist_base, 0])? .add_u32(p_address_cells, 2)? .add_u32(p_size_cells, 2)? .add_u32(p_interrupt_cells, 3)? .add_null(p_interrupt_controller)? .add_u32(p_phandle, 1)? .add_null(p_ranges)? .end_node()?; } // PMU if let Some(gic) = &info.gic { if let Some(pmu_gsiv) = gic.pmu_gsiv { anyhow::ensure!( (16..32).contains(&pmu_gsiv), "PMU GSIV {pmu_gsiv} is not a valid PPI (expected 16..32)" ); const GIC_PPI: u32 = 1; const IRQ_TYPE_LEVEL_HIGH: u32 = 4; root_builder = root_builder .start_node("pmu")? .add_str(p_compatible, "arm,armv8-pmuv3")? .add_u32_array(p_interrupts, &[GIC_PPI, pmu_gsiv - 16, IRQ_TYPE_LEVEL_HIGH])? .end_node()?; } } let mut openhcl_builder = root_builder.start_node("openhcl")?; let p_isolation_type = openhcl_builder.add_string("isolation-type")?; openhcl_builder = openhcl_builder.add_str( p_isolation_type, match info.isolation { IsolationType::None => "none", IsolationType::Vbs => "vbs", IsolationType::Snp => "snp", IsolationType::Tdx => "tdx", }, )?; let p_memory_allocation_mode = openhcl_builder.add_string("memory-allocation-mode")?; match info.memory_allocation_mode { MemoryAllocationMode::Host => { openhcl_builder = openhcl_builder.add_str(p_memory_allocation_mode, "host")?; } MemoryAllocationMode::Vtl2 { memory_size, mmio_size, } => { let p_memory_size = openhcl_builder.add_string("memory-size")?; let p_mmio_size = openhcl_builder.add_string("mmio-size")?; openhcl_builder = openhcl_builder.add_str(p_memory_allocation_mode, "vtl2")?; if let Some(memory_size) = memory_size { openhcl_builder = openhcl_builder.add_u64(p_memory_size, memory_size)?; } if let Some(mmio_size) = mmio_size { openhcl_builder = openhcl_builder.add_u64(p_mmio_size, mmio_size)?; } } } if let Some(data) = info.vtl0_alias_map { let p_vtl0_alias_map = openhcl_builder.add_string("vtl0-alias-map")?; openhcl_builder = openhcl_builder.add_u64(p_vtl0_alias_map, data)?; } openhcl_builder = openhcl_builder .start_node("vmbus-vtl0")? .add_u32(p_address_cells, 2)? .add_u32(p_size_cells, 2)? .add_str(p_compatible, "microsoft,vmbus")? .add_u64_list( p_ranges, info.vtl0_mmio .iter() .flat_map(|r| [r.start(), r.start(), r.len()]), )? .end_node()?; for range in &info.partition_memory_map { let name = format!("memory@{:x}", range.range().start()); let node_builder = openhcl_builder .start_node(&name)? .add_str(p_device_type, "memory-openhcl")? .add_u64_list(p_reg, [range.range().start(), range.range().len()])? .add_u32(p_openhcl_memory, range.vtl_usage().0)?; openhcl_builder = match range { AddressRange::Memory(memory) => { // Add as a memory node, with numa info and igvm type. node_builder .add_u32(p_numa_node_id, memory.range.vnode)? .add_u32(p_igvm_type, memory.igvm_type.0 as u32)? } AddressRange::Mmio(_) => { // Nothing to do here, mmio already contains the min // required info of range and vtl via vtl_usage. node_builder } } .end_node()?; } for range in &info.accepted_ranges { let name = format!("accepted-memory@{:x}", range.start()); openhcl_builder = openhcl_builder .start_node(&name)? .add_str(p_device_type, "memory-openhcl")? .add_u64_list(p_reg, [range.start(), range.len()])? .end_node()?; } root_builder = openhcl_builder.end_node()?; root_builder.end_node()?.build(info.cpus[0].reg as u32)?; Ok(buf) } #[test] fn test_basic() { let orig_info = ParsedBootDtInfo { cpus: (0..4).map(|i| Cpu { reg: i, vnode: 0 }).collect(), vtl2_memory: vec![ MemoryRangeWithNode { range: MemoryRange::new(0x10000..0x20000), vnode: 0, }, MemoryRangeWithNode { range: MemoryRange::new(0x20000..0x30000), vnode: 1, }, ], partition_memory_map: vec![ AddressRange::Memory(Memory { range: MemoryRangeWithNode { range: MemoryRange::new(0..0x1000), vnode: 0, }, vtl_usage: MemoryVtlType::VTL0, igvm_type: MemoryMapEntryType::MEMORY, }), AddressRange::Mmio(Mmio { range: MemoryRange::new(0x1000..0x2000), vtl: Vtl::Vtl0, }), AddressRange::Mmio(Mmio { range: MemoryRange::new(0x3000..0x4000), vtl: Vtl::Vtl0, }), AddressRange::Memory(Memory { range: MemoryRangeWithNode { range: MemoryRange::new(0x10000..0x20000), vnode: 0, }, vtl_usage: MemoryVtlType::VTL2_RAM, igvm_type: MemoryMapEntryType::VTL2_PROTECTABLE, }), AddressRange::Memory(Memory { range: MemoryRangeWithNode { range: MemoryRange::new(0x20000..0x30000), vnode: 1, }, vtl_usage: MemoryVtlType::VTL2_CONFIG, igvm_type: MemoryMapEntryType::VTL2_PROTECTABLE, }), AddressRange::Memory(Memory { range: MemoryRangeWithNode { range: MemoryRange::new(0x30000..0x40000), vnode: 1, }, vtl_usage: MemoryVtlType::VTL2_CONFIG, igvm_type: MemoryMapEntryType::VTL2_PROTECTABLE, }), AddressRange::Memory(Memory { range: MemoryRangeWithNode { range: MemoryRange::new(0x40000..0x50000), vnode: 1, }, vtl_usage: MemoryVtlType::VTL2_RESERVED, igvm_type: MemoryMapEntryType::VTL2_PROTECTABLE, }), AddressRange::Memory(Memory { range: MemoryRangeWithNode { range: MemoryRange::new(0x50000..0x51000), vnode: 0, }, vtl_usage: MemoryVtlType::VTL2_PERSISTED_STATE_HEADER, igvm_type: MemoryMapEntryType::VTL2_PROTECTABLE, }), AddressRange::Memory(Memory { range: MemoryRangeWithNode { range: MemoryRange::new(0x51000..0x52000), vnode: 0, }, vtl_usage: MemoryVtlType::VTL2_PERSISTED_STATE_PROTOBUF, igvm_type: MemoryMapEntryType::VTL2_PROTECTABLE, }), AddressRange::Memory(Memory { range: MemoryRangeWithNode { range: MemoryRange::new(0x60000..0x70000), vnode: 0, }, vtl_usage: MemoryVtlType::VTL2_GPA_POOL, igvm_type: MemoryMapEntryType::VTL2_PROTECTABLE, }), AddressRange::Memory(Memory { range: MemoryRangeWithNode { range: MemoryRange::new(0x1000000..0x2000000), vnode: 0, }, vtl_usage: MemoryVtlType::VTL0, igvm_type: MemoryMapEntryType::MEMORY, }), AddressRange::Mmio(Mmio { range: MemoryRange::new(0x3000000..0x4000000), vtl: Vtl::Vtl2, }), ], vtl0_mmio: vec![ MemoryRange::new(0x1000..0x2000), MemoryRange::new(0x3000..0x4000), ], config_ranges: vec![ MemoryRange::new(0x20000..0x30000), MemoryRange::new(0x30000..0x40000), ], vtl0_alias_map: Some(1 << 48), gic: Some(Aarch64PlatformConfig { gic_distributor_base: 0x10000, gic_version: vm_topology::processor::aarch64::GicVersion::V3 { redistributors_base: 0x20000, }, gic_msi: vm_topology::processor::aarch64::GicMsiController::None, pmu_gsiv: Some(0x17), virt_timer_ppi: 20, gic_nr_irqs: 992, }), accepted_ranges: vec![ MemoryRange::new(0x10000..0x20000), MemoryRange::new(0x1000000..0x1500000), ], memory_allocation_mode: MemoryAllocationMode::Vtl2 { memory_size: Some(0x1000), mmio_size: Some(0x2000), }, isolation: IsolationType::Vbs, vtl2_reserved_range: MemoryRange::new(0x40000..0x50000), private_pool_ranges: vec![MemoryRangeWithNode { range: MemoryRange::new(0x60000..0x70000), vnode: 0, }], vtl2_persisted_header: MemoryRange::new(0x50000..0x51000), vtl2_persisted_protobuf_region: MemoryRange::new(0x51000..0x52000), }; let dt = build_dt(&orig_info).unwrap(); let parsed = ParsedBootDtInfo::new_from_raw(&dt).unwrap(); assert_eq!(orig_info, parsed); } fn build_boottime_dt(boot_times: BootTimes) -> anyhow::Result> { let mut buf = vec![0; 4096]; let mut builder = Builder::new(fdt::builder::BuilderConfig { blob_buffer: &mut buf, string_table_cap: 1024, memory_reservations: &[], })?; let p_address_cells = builder.add_string("#address-cells")?; let p_size_cells = builder.add_string("#size-cells")?; let p_reftime_boot_start = builder.add_string("reftime_boot_start")?; let p_reftime_boot_end = builder.add_string("reftime_boot_end")?; let p_reftime_sidecar_start = builder.add_string("reftime_sidecar_start")?; let p_reftime_sidecar_end = builder.add_string("reftime_sidecar_end")?; let mut root_builder = builder .start_node("")? .add_u32(p_address_cells, 2)? .add_u32(p_size_cells, 2)?; if let Some(start) = boot_times.start { root_builder = root_builder.add_u64(p_reftime_boot_start, start)?; } if let Some(end) = boot_times.end { root_builder = root_builder.add_u64(p_reftime_boot_end, end)?; } if let Some(start) = boot_times.sidecar_start { root_builder = root_builder.add_u64(p_reftime_sidecar_start, start)?; } if let Some(end) = boot_times.sidecar_end { root_builder = root_builder.add_u64(p_reftime_sidecar_end, end)?; } root_builder.end_node()?.build(0)?; Ok(buf) } #[test] fn test_basic_boottime() { let orig_info = BootTimes { start: Some(0x1000), end: Some(0x2000), sidecar_start: Some(0x3000), sidecar_end: Some(0x4000), }; let dt = build_boottime_dt(orig_info).unwrap(); let parsed = BootTimes::new_from_raw(&dt).unwrap(); assert_eq!(orig_info, parsed); // test no boot times. let orig_info = BootTimes { start: None, end: None, sidecar_start: None, sidecar_end: None, }; let dt = build_boottime_dt(orig_info).unwrap(); let parsed = BootTimes::new_from_raw(&dt).unwrap(); assert_eq!(orig_info, parsed); } }