Skip to main content

openhcl_boot/
dt.rs

1// Copyright (c) Microsoft Corporation.
2// Licensed under the MIT License.
3
4//! Module used to write the device tree used by the OpenHCL kernel and
5//! usermode.
6
7use crate::host_params::COMMAND_LINE_SIZE;
8use crate::host_params::PartitionInfo;
9use crate::host_params::shim_params::IsolationType;
10use crate::memory::AddressSpaceManager;
11use crate::memory::MAX_RESERVED_MEM_RANGES;
12use crate::sidecar::SidecarConfig;
13use crate::single_threaded::off_stack;
14use arrayvec::ArrayString;
15use arrayvec::ArrayVec;
16use core::fmt;
17use core::ops::Range;
18use fdt::builder::Builder;
19use fdt::builder::StringId;
20use host_fdt_parser::ComInfo;
21use host_fdt_parser::GicInfo;
22use host_fdt_parser::MemoryAllocationMode;
23use host_fdt_parser::VmbusInfo;
24use hvdef::Vtl;
25use igvm_defs::dt::IGVM_DT_IGVM_TYPE_PROPERTY;
26use loader_defs::shim::MemoryVtlType;
27use memory_range::MemoryRange;
28use memory_range::RangeWalkResult;
29use memory_range::walk_ranges;
30#[cfg(target_arch = "x86_64")]
31use x86defs::tdx::RESET_VECTOR_PAGE;
32
33/// AArch64 defines
34mod aarch64 {
35    // For compatibility with older hosts, use these legacy Hyper-V defaults if
36    // GIC addresses aren't passed in via the host device tree
37    pub const DEFAULT_GIC_DISTRIBUTOR_BASE: u64 = 0xFFFF_0000;
38    pub const DEFAULT_GIC_REDISTRIBUTORS_BASE: u64 = 0xEFFE_E000;
39
40    // The interrupt IDs (INTID's) in the ARM64 DeviceTree must be _relative_
41    // to its base. See `gic_irq_domain_translate` in the Linux kernel, could not
42    // find a specification for that.
43    //
44    // Architecturally, PPIs occupy INTID's in the [16..32) range. In DeviceTree,
45    // the type of the interrupt is specified first (PPI) and then the _relative_ INTID:
46    // for PPI INTID `27` `[GIC_PPI, 27-16, flags]` goes into the DT description.
47    /// VMBus PPI offset for the DT `interrupts` property.
48    /// Canonical INTID is DEFAULT_VMBUS_PPI (18) in openvmm_defs.
49    pub const VMBUS_PPI_OFFSET: u32 = 2;
50    pub const TIMER_INTID: u32 = 4; // Note: the hardware INTID will be 16 + 4
51
52    /// The Hyper-V default PMU_GSIV value.
53    pub const PMU_GSIV: u32 = 0x17;
54    pub const PMU_GSIV_INT_INDEX: u32 = PMU_GSIV - 16;
55
56    pub const GIC_PHANDLE: u32 = 1;
57    pub const GIC_PPI: u32 = 1;
58    pub const IRQ_TYPE_EDGE_RISING: u32 = 1;
59    pub const IRQ_TYPE_LEVEL_LOW: u32 = 8;
60    pub const IRQ_TYPE_LEVEL_HIGH: u32 = 4;
61}
62
63#[derive(Debug)]
64pub enum DtError {
65    // Field is stored solely for logging via debug, not actually dead.
66    Fdt(#[expect(dead_code)] fdt::builder::Error),
67}
68
69impl From<fdt::builder::Error> for DtError {
70    fn from(err: fdt::builder::Error) -> Self {
71        DtError::Fdt(err)
72    }
73}
74
75macro_rules! format_fixed {
76    ($n:expr, $($arg:tt)*) => {
77        {
78            let mut buf = ArrayString::<$n>::new();
79            fmt::write(&mut buf, format_args!($($arg)*)).unwrap();
80            buf
81        }
82    };
83}
84
85pub struct BootTimes {
86    pub start: u64,
87    pub end: u64,
88}
89
90/// Info needed about the current device tree being built to add the vmbus node.
91#[derive(Clone, Copy)]
92pub struct VmbusDeviceTreeInfo {
93    p_address_cells: StringId,
94    p_size_cells: StringId,
95    p_compatible: StringId,
96    p_ranges: StringId,
97    p_vtl: StringId,
98    p_vmbus_connection_id: StringId,
99    p_dma_coherent: StringId,
100    p_interrupt_parent: StringId,
101    p_interrupts: StringId,
102    interrupt_cell_value: Option<u32>,
103}
104
105/// Write a vmbus node to the device tree.
106fn write_vmbus<'a, T>(
107    parent: Builder<'a, T>,
108    name: &str,
109    vtl: Vtl,
110    vmbus: &VmbusInfo,
111    dt: VmbusDeviceTreeInfo,
112) -> Result<Builder<'a, T>, DtError> {
113    let VmbusDeviceTreeInfo {
114        p_address_cells,
115        p_size_cells,
116        p_compatible,
117        p_ranges,
118        p_vtl,
119        p_vmbus_connection_id,
120        p_dma_coherent,
121        p_interrupt_parent,
122        p_interrupts,
123        interrupt_cell_value,
124    } = dt;
125
126    let mut vmbus_builder = parent
127        .start_node(name)?
128        .add_u32(p_address_cells, 2)?
129        .add_u32(p_size_cells, 2)?
130        .add_null(p_dma_coherent)?
131        .add_str(p_compatible, "microsoft,vmbus")?
132        .add_u32(p_vtl, u8::from(vtl).into())?
133        .add_u32(p_vmbus_connection_id, vmbus.connection_id)?;
134
135    let mut mmio_ranges = ArrayVec::<u64, 6>::new();
136    for entry in vmbus.mmio.iter() {
137        mmio_ranges
138            .try_extend_from_slice(&[entry.start(), entry.start(), entry.len()])
139            .expect("should always fit");
140    }
141    vmbus_builder = vmbus_builder.add_u64_array(p_ranges, mmio_ranges.as_slice())?;
142
143    if cfg!(target_arch = "aarch64") {
144        vmbus_builder = vmbus_builder
145            .add_u32(p_interrupt_parent, aarch64::GIC_PHANDLE)?
146            .add_u32_array(
147                p_interrupts,
148                // Here 3 parameters are used as the "#interrupt-cells"
149                // above specifies.
150                &[
151                    aarch64::GIC_PPI,
152                    aarch64::VMBUS_PPI_OFFSET,
153                    interrupt_cell_value.expect("must be set on aarch64"),
154                ],
155            )?;
156    }
157
158    Ok(vmbus_builder.end_node()?)
159}
160
161/// Writes the device tree blob into `buffer`.
162pub fn write_dt(
163    buffer: &mut [u8],
164    partition_info: &PartitionInfo,
165    address_space: &AddressSpaceManager,
166    accepted_ranges: impl IntoIterator<Item = MemoryRange>,
167    initrd: Range<u64>,
168    cmdline: &ArrayString<COMMAND_LINE_SIZE>,
169    sidecar: Option<&SidecarConfig<'_>>,
170    boot_times: Option<BootTimes>,
171    #[cfg_attr(target_arch = "aarch64", expect(unused_variables))]
172    // isolation_type is unused on aarch64
173    isolation_type: IsolationType,
174) -> Result<(), DtError> {
175    // First, the reservation map is built. That keyes off of the x86 E820 memory map.
176    // The `/memreserve/` is used to tell the kernel that the reserved memory is RAM
177    // but it is reserved. That way the kernel allows mapping it via `/dev/mem` without
178    // inhibiting the cache thus disabling the unaligned access on some architectures.
179
180    let mut memory_reservations =
181        off_stack!(ArrayVec<fdt::ReserveEntry, MAX_RESERVED_MEM_RANGES>, ArrayVec::new_const());
182
183    memory_reservations.extend(address_space.reserved_vtl2_ranges().map(|(r, _)| {
184        fdt::ReserveEntry {
185            address: r.start().into(),
186            size: r.len().into(),
187        }
188    }));
189
190    // Build the actual device tree.
191    let builder_config = fdt::builder::BuilderConfig {
192        blob_buffer: buffer,
193        string_table_cap: 1024,
194        memory_reservations: &memory_reservations,
195    };
196    let mut builder = Builder::new(builder_config)?;
197
198    // These StringIds are common across many nodes.
199    let p_address_cells = builder.add_string("#address-cells")?;
200    let p_size_cells = builder.add_string("#size-cells")?;
201    let p_reg = builder.add_string("reg")?;
202    let p_reg_names = builder.add_string("reg-names")?;
203    let p_device_type = builder.add_string("device_type")?;
204    let p_status = builder.add_string("status")?;
205    let p_compatible = builder.add_string("compatible")?;
206    let p_ranges = builder.add_string("ranges")?;
207    let p_numa_node_id = builder.add_string("numa-node-id")?;
208    let p_reftime_boot_start = builder.add_string("reftime_boot_start")?;
209    let p_reftime_boot_end = builder.add_string("reftime_boot_end")?;
210    let p_reftime_sidecar_start = builder.add_string("reftime_sidecar_start")?;
211    let p_reftime_sidecar_end = builder.add_string("reftime_sidecar_end")?;
212    let p_vtl = builder.add_string(igvm_defs::dt::IGVM_DT_VTL_PROPERTY)?;
213    let p_vmbus_connection_id = builder.add_string("microsoft,message-connection-id")?;
214    let p_dma_coherent = builder.add_string("dma-coherent")?;
215    let p_igvm_type = builder.add_string(IGVM_DT_IGVM_TYPE_PROPERTY)?;
216    let p_openhcl_memory = builder.add_string("openhcl,memory-type")?;
217
218    // These StringIds are used across multiple AArch64 nodes.
219    //
220    // TODO: If we add support for an associative map based add_string/add_prop
221    // interface to the fdt builder, these explicit definitions would go away.
222    // That would require either alloc support, or an alloc-free associative
223    // datastructure.
224    let p_interrupt_parent = builder.add_string("interrupt-parent")?;
225    let p_interrupts = builder.add_string("interrupts")?;
226    let p_enable_method = builder.add_string("enable-method")?;
227    let p_current_speed = builder.add_string("current-speed")?;
228
229    let num_cpus = partition_info.cpus.len();
230
231    let mut root_builder = builder
232        .start_node("")?
233        .add_u32(p_address_cells, 2)?
234        .add_u32(p_size_cells, 2)?
235        .add_str(p_compatible, "microsoft,openvmm")?;
236
237    if let Some(boot_times) = boot_times {
238        let BootTimes { start, end } = boot_times;
239        root_builder = root_builder
240            .add_u64(p_reftime_boot_start, start)?
241            .add_u64(p_reftime_boot_end, end)?;
242    }
243
244    if let Some(sidecar) = sidecar {
245        root_builder = root_builder
246            .add_u64(p_reftime_sidecar_start, sidecar.start_reftime)?
247            .add_u64(p_reftime_sidecar_end, sidecar.end_reftime)?;
248    }
249
250    let hypervisor_builder = root_builder
251        .start_node("hypervisor")?
252        .add_str(p_compatible, "microsoft,hyperv")?;
253    root_builder = hypervisor_builder.end_node()?;
254
255    #[cfg(target_arch = "x86_64")]
256    if isolation_type == IsolationType::Tdx {
257        // Linux ignores the whole `/reserved-memory` subtree unless its cell
258        // counts match the root node's, so these must stay in sync with the
259        // root node above.
260        let mut mailbox_builder = root_builder
261            .start_node("reserved-memory")?
262            .add_u32(p_address_cells, 2)?
263            .add_u32(p_size_cells, 2)?
264            .add_null(p_ranges)?;
265
266        let name = format_fixed!(32, "wakeup_table@{:x}", RESET_VECTOR_PAGE);
267        let mailbox_addr_builder = mailbox_builder
268            .start_node(name.as_ref())?
269            .add_str(p_compatible, "intel,wakeup-mailbox")?
270            .add_u32_array(
271                p_reg,
272                &[0x0, RESET_VECTOR_PAGE.try_into().unwrap(), 0x0, 0x1000],
273            )?;
274
275        mailbox_builder = mailbox_addr_builder.end_node()?;
276
277        root_builder = mailbox_builder.end_node()?;
278    }
279
280    // For ARM v8, always specify two register cells, which can accommodate
281    // higher number of VPs.
282    let address_cells = if cfg!(target_arch = "aarch64") { 2 } else { 1 };
283    let mut cpu_builder = root_builder
284        .start_node("cpus")?
285        .add_u32(p_address_cells, address_cells)?
286        .add_u32(p_size_cells, 0)?;
287
288    // Add a CPU node for each cpu.
289    for (vp_index, cpu_entry) in partition_info.cpus.iter().enumerate() {
290        let name = format_fixed!(32, "cpu@{}", vp_index + 1);
291
292        let mut cpu = cpu_builder
293            .start_node(name.as_ref())?
294            .add_str(p_device_type, "cpu")?
295            .add_u32(p_numa_node_id, cpu_entry.vnode)?;
296
297        if cfg!(target_arch = "aarch64") {
298            cpu = cpu
299                .add_u64(p_reg, cpu_entry.reg)?
300                .add_str(p_compatible, "arm,arm-v8")?;
301
302            if num_cpus > 1 {
303                cpu = cpu.add_str(p_enable_method, "psci")?;
304            }
305
306            if vp_index == 0 {
307                cpu = cpu.add_str(p_status, "okay")?;
308            } else {
309                cpu = cpu.add_str(p_status, "disabled")?;
310            }
311        } else {
312            cpu = cpu
313                .add_u32(p_reg, cpu_entry.reg as u32)?
314                .add_str(p_status, "okay")?;
315        }
316
317        cpu_builder = cpu.end_node()?;
318    }
319    root_builder = cpu_builder.end_node()?;
320
321    if cfg!(target_arch = "aarch64") {
322        let p_method = root_builder.add_string("method")?;
323        let p_cpu_off = root_builder.add_string("cpu_off")?;
324        let p_cpu_on = root_builder.add_string("cpu_on")?;
325        let psci = root_builder
326            .start_node("psci")?
327            .add_str(p_compatible, "arm,psci-0.2")?
328            .add_str(p_method, "hvc")?
329            .add_u32(p_cpu_off, 1)?
330            .add_u32(p_cpu_on, 2)?;
331        root_builder = psci.end_node()?;
332    }
333
334    // Add a memory node for each VTL2 range.
335    for mem_entry in partition_info.vtl2_ram.iter() {
336        let name = format_fixed!(32, "memory@{:x}", mem_entry.range.start());
337        let mut mem = root_builder.start_node(&name)?;
338        mem = mem.add_str(p_device_type, "memory")?;
339        mem = mem.add_u64_array(p_reg, &[mem_entry.range.start(), mem_entry.range.len()])?;
340        mem = mem.add_u32(p_numa_node_id, mem_entry.vnode)?;
341        root_builder = mem.end_node()?;
342    }
343
344    if cfg!(target_arch = "aarch64") {
345        // ARM64 Generic Interrupt Controller aka GIC, v3.
346
347        // Use legacy Hyper-V defaults if not specified in the host device tree.
348        let default = GicInfo {
349            gic_distributor_base: aarch64::DEFAULT_GIC_DISTRIBUTOR_BASE,
350            gic_distributor_size: aarch64defs::GIC_DISTRIBUTOR_SIZE,
351            gic_redistributors_base: aarch64::DEFAULT_GIC_REDISTRIBUTORS_BASE,
352            gic_redistributors_size: aarch64defs::GIC_REDISTRIBUTOR_SIZE * num_cpus as u64,
353            gic_redistributor_stride: aarch64defs::GIC_REDISTRIBUTOR_SIZE,
354        };
355        let gic = partition_info.gic.as_ref().unwrap_or(&default);
356
357        // Validate sizes
358        assert_eq!(gic.gic_distributor_size, default.gic_distributor_size);
359        assert_eq!(gic.gic_redistributors_size, default.gic_redistributors_size);
360        assert_eq!(
361            gic.gic_redistributor_stride,
362            default.gic_redistributor_stride
363        );
364
365        let p_interrupt_cells = root_builder.add_string("#interrupt-cells")?;
366        let p_redist_regions = root_builder.add_string("#redistributor-regions")?;
367        let p_redist_stride = root_builder.add_string("redistributor-stride")?;
368        let p_interrupt_controller = root_builder.add_string("interrupt-controller")?;
369        let p_phandle = root_builder.add_string("phandle")?;
370        let p_interrupt_names = root_builder.add_string("interrupt-names")?;
371        let p_always_on = root_builder.add_string("always-on")?;
372        let name = format_fixed!(32, "intc@{}", gic.gic_distributor_base);
373        let gicv3 = root_builder
374            .start_node(name.as_ref())?
375            .add_str(p_compatible, "arm,gic-v3")?
376            .add_u32(p_redist_regions, 1)?
377            .add_u64(p_redist_stride, gic.gic_redistributor_stride)?
378            .add_u64_array(
379                p_reg,
380                &[
381                    gic.gic_distributor_base,
382                    gic.gic_distributor_size,
383                    gic.gic_redistributors_base,
384                    gic.gic_redistributors_size,
385                ],
386            )?
387            .add_u32(p_address_cells, 2)?
388            .add_u32(p_size_cells, 2)?
389            .add_u32(p_interrupt_cells, 3)?
390            .add_null(p_interrupt_controller)?
391            .add_u32(p_phandle, aarch64::GIC_PHANDLE)?
392            .add_null(p_ranges)?;
393        root_builder = gicv3.end_node()?;
394
395        // ARM64 Architectural Timer.
396        let timer = root_builder
397            .start_node("timer")?
398            .add_str(p_compatible, "arm,armv8-timer")?
399            .add_u32(p_interrupt_parent, aarch64::GIC_PHANDLE)?
400            .add_str(p_interrupt_names, "virt")?
401            .add_u32_array(
402                p_interrupts,
403                // Here 3 parameters are used as the "#interrupt-cells"
404                // above specifies. The only interrupt employed is
405                // the one for the virtualized environment, it is a
406                // Private Peripheral Interrupt.
407                &[
408                    aarch64::GIC_PPI,
409                    aarch64::TIMER_INTID,
410                    aarch64::IRQ_TYPE_LEVEL_LOW,
411                ],
412            )?
413            .add_null(p_always_on)?;
414        root_builder = timer.end_node()?;
415
416        // Add PMU.
417        //
418        // TODO: This may be insufficient to get perf to work as the kernel
419        // prints:
420        // `armv8-pmu pmu: hw perfevents: no irqs for PMU, sampling events not supported`
421        //
422        // Tracked by issue 1808.
423        //
424        // NOTE: The host may not provide this value in device tree, so use the
425        // Hyper-V platform default if not provided.
426        let pmu_gsiv_index = partition_info
427            .pmu_gsiv
428            .map(|gsiv| {
429                assert!(
430                    (16..32).contains(&gsiv),
431                    "PMU GSIV must be a PPI in [16, 32) range"
432                );
433                gsiv - 16
434            })
435            .unwrap_or(aarch64::PMU_GSIV_INT_INDEX);
436        let pmu = root_builder
437            .start_node("pmu")?
438            .add_str(p_compatible, "arm,armv8-pmuv3")?
439            .add_u32_array(
440                p_interrupts,
441                &[
442                    aarch64::GIC_PPI,
443                    pmu_gsiv_index,
444                    aarch64::IRQ_TYPE_LEVEL_HIGH,
445                ],
446            )?;
447        root_builder = pmu.end_node()?;
448
449        // ARM64 PL011 Serial device for COM3 OpenHCL logs
450        if let ComInfo::Pl011 {
451            base,
452            intid,
453            current_speed,
454        } = partition_info.com3_serial
455        {
456            let name = format_fixed!(32, "serial@{:x}", base);
457            let serial = root_builder
458                .start_node(&name)?
459                .add_str(p_compatible, "arm,sbsa-uart")?
460                .add_u32_array(p_reg, &[0, base, 0, 0x1000])?
461                .add_u32(p_interrupt_parent, aarch64::GIC_PHANDLE)?
462                .add_u32_array(p_interrupts, &[0, intid, 4])?
463                .add_u32(p_current_speed, current_speed)?
464                .add_str(p_status, "okay")?;
465            root_builder = serial.end_node()?;
466        }
467    }
468
469    // Linux requires vmbus to be under a simple-bus node.
470    let mut simple_bus_builder = root_builder
471        .start_node("bus")?
472        .add_str(p_compatible, "simple-bus")?
473        .add_u32(p_address_cells, 2)?
474        .add_u32(p_size_cells, 2)?;
475    simple_bus_builder = simple_bus_builder.add_prop_array(p_ranges, &[])?;
476
477    let vmbus_info = VmbusDeviceTreeInfo {
478        p_address_cells,
479        p_size_cells,
480        p_compatible,
481        p_ranges,
482        p_vtl,
483        p_vmbus_connection_id,
484        p_dma_coherent,
485        p_interrupt_parent,
486        p_interrupts,
487        interrupt_cell_value: if cfg!(target_arch = "aarch64") {
488            Some(aarch64::IRQ_TYPE_EDGE_RISING)
489        } else {
490            None
491        },
492    };
493
494    simple_bus_builder = write_vmbus(
495        simple_bus_builder,
496        "vmbus",
497        Vtl::Vtl2,
498        &partition_info.vmbus_vtl2,
499        vmbus_info,
500    )?;
501
502    if let Some(sidecar) = sidecar {
503        for node in sidecar.nodes {
504            let name = format_fixed!(64, "sidecar@{:x}", node.control_page);
505            simple_bus_builder = simple_bus_builder
506                .start_node(&name)?
507                .add_str(p_compatible, "microsoft,openhcl-sidecar")?
508                .add_u64_array(
509                    p_reg,
510                    &[
511                        node.control_page,
512                        sidecar_defs::PAGE_SIZE as u64,
513                        node.shmem_pages_base,
514                        node.shmem_pages_size,
515                    ],
516                )?
517                .add_str_array(p_reg_names, &["ctrl", "shmem"])?
518                .end_node()?;
519        }
520    }
521
522    root_builder = simple_bus_builder.end_node()?;
523
524    if cfg!(target_arch = "aarch64") {
525        let p_bootargs = root_builder.add_string("bootargs")?;
526        let p_initrd_start = root_builder.add_string("linux,initrd-start")?;
527        let p_initrd_end = root_builder.add_string("linux,initrd-end")?;
528
529        let chosen = root_builder
530            .start_node("chosen")?
531            .add_str(p_bootargs, cmdline.as_str())?
532            .add_u64(p_initrd_start, initrd.start)?
533            .add_u64(p_initrd_end, initrd.end)?;
534        root_builder = chosen.end_node()?;
535    }
536
537    // Add information used by openhcl usermode.
538    let mut openhcl_builder = root_builder.start_node("openhcl")?;
539
540    let p_isolation_type = openhcl_builder.add_string("isolation-type")?;
541    let isolation_type = match partition_info.isolation {
542        IsolationType::None => "none",
543        IsolationType::Vbs => "vbs",
544        IsolationType::Snp => "snp",
545        IsolationType::Tdx => "tdx",
546        IsolationType::Cca => "cca",
547    };
548    openhcl_builder = openhcl_builder.add_str(p_isolation_type, isolation_type)?;
549
550    // Indicate what kind of memory allocation mode was done by the bootloader
551    // to usermode.
552    let p_memory_allocation_mode = openhcl_builder.add_string("memory-allocation-mode")?;
553    match partition_info.memory_allocation_mode {
554        MemoryAllocationMode::Host => {
555            openhcl_builder = openhcl_builder.add_str(p_memory_allocation_mode, "host")?;
556        }
557        MemoryAllocationMode::Vtl2 {
558            memory_size,
559            mmio_size,
560        } => {
561            let p_memory_size = openhcl_builder.add_string("memory-size")?;
562            let p_mmio_size = openhcl_builder.add_string("mmio-size")?;
563            openhcl_builder = openhcl_builder.add_str(p_memory_allocation_mode, "vtl2")?;
564            if let Some(memory_size) = memory_size {
565                openhcl_builder = openhcl_builder.add_u64(p_memory_size, memory_size)?;
566            }
567            if let Some(mmio_size) = mmio_size {
568                openhcl_builder = openhcl_builder.add_u64(p_mmio_size, mmio_size)?;
569            }
570        }
571    }
572
573    if let Some(data) = partition_info.vtl0_alias_map {
574        let p_vtl0_alias_map = openhcl_builder.add_string("vtl0-alias-map")?;
575        openhcl_builder = openhcl_builder.add_u64(p_vtl0_alias_map, data)?;
576    }
577
578    // Report the unified memory map to usermode describing which memory is
579    // used by what.
580    //
581    // NOTE: Use a different device type for memory ranges, as the Linux kernel
582    // will treat every device tree node with device type as memory, and attempt
583    // to parse numa information from it.
584    let memory_openhcl_type = "memory-openhcl";
585    for (range, result) in walk_ranges(
586        partition_info.partition_ram.iter().map(|r| (r.range, r)),
587        address_space.vtl2_ranges(),
588    ) {
589        match result {
590            RangeWalkResult::Left(entry) => {
591                // This range is usable by VTL0.
592                let name = format_fixed!(64, "memory@{:x}", range.start());
593                openhcl_builder = openhcl_builder
594                    .start_node(&name)?
595                    .add_str(p_device_type, memory_openhcl_type)?
596                    .add_u64_array(p_reg, &[range.start(), range.len()])?
597                    .add_u32(p_numa_node_id, entry.vnode)?
598                    .add_u32(p_igvm_type, entry.mem_type.0.into())?
599                    .add_u32(p_openhcl_memory, MemoryVtlType::VTL0.0)?
600                    .end_node()?;
601            }
602            RangeWalkResult::Both(partition_entry, vtl2_type) => {
603                // This range is in use by VTL2. Indicate that.
604                let name = format_fixed!(64, "memory@{:x}", range.start());
605                openhcl_builder = openhcl_builder
606                    .start_node(&name)?
607                    .add_str(p_device_type, memory_openhcl_type)?
608                    .add_u64_array(p_reg, &[range.start(), range.len()])?
609                    .add_u32(p_numa_node_id, partition_entry.vnode)?
610                    .add_u32(p_igvm_type, partition_entry.mem_type.0.into())?
611                    .add_u32(p_openhcl_memory, vtl2_type.0)?
612                    .end_node()?;
613            }
614            RangeWalkResult::Right(..) => {
615                panic!("vtl2 range {:?} not contained in partition ram", range)
616            }
617            // Ignore ranges not described in either.
618            RangeWalkResult::Neither => {}
619        }
620    }
621
622    // Add mmio ranges for both VTL0 and VTL2.
623    for entry in &partition_info.vmbus_vtl0.mmio {
624        let name = format_fixed!(64, "memory@{:x}", entry.start());
625        openhcl_builder = openhcl_builder
626            .start_node(&name)?
627            .add_str(p_device_type, memory_openhcl_type)?
628            .add_u64_array(p_reg, &[entry.start(), entry.len()])?
629            .add_u32(p_openhcl_memory, MemoryVtlType::VTL0_MMIO.0)?
630            .end_node()?;
631    }
632
633    for entry in &partition_info.vmbus_vtl2.mmio {
634        let name = format_fixed!(64, "memory@{:x}", entry.start());
635        openhcl_builder = openhcl_builder
636            .start_node(&name)?
637            .add_str(p_device_type, memory_openhcl_type)?
638            .add_u64_array(p_reg, &[entry.start(), entry.len()])?
639            .add_u32(p_openhcl_memory, MemoryVtlType::VTL2_MMIO.0)?
640            .end_node()?;
641    }
642
643    // Report accepted ranges underhil openhcl node.
644    for range in accepted_ranges {
645        let name = format_fixed!(64, "accepted-memory@{:x}", range.start());
646        openhcl_builder = openhcl_builder
647            .start_node(&name)?
648            .add_u64_array(p_reg, &[range.start(), range.len()])?
649            .end_node()?;
650    }
651
652    // Pass through host-provided entropy to the init process for seeding
653    // the OpenHCL kernel random number generator
654    if let Some(entropy) = &partition_info.entropy {
655        openhcl_builder = openhcl_builder
656            .start_node("entropy")?
657            .add_prop_array(p_reg, &[entropy])?
658            .end_node()?;
659    }
660
661    let root_builder = openhcl_builder.end_node()?;
662
663    root_builder.end_node()?.build(partition_info.bsp_reg)?;
664    Ok(())
665}