Skip to main content

bootloader_fdt_parser/
lib.rs

1// Copyright (c) Microsoft Corporation.
2// Licensed under the MIT License.
3
4//! Parsing code for the devicetree provided by openhcl_boot used by underhill
5//! usermode. Unlike `host_fdt_parser`, this code requires std as it is intended
6//! to be only used in usermode.
7
8#![forbid(unsafe_code)]
9
10use anyhow::Context;
11use anyhow::bail;
12use fdt::parser::Node;
13use fdt::parser::Parser;
14use fdt::parser::Property;
15use igvm_defs::MemoryMapEntryType;
16use igvm_defs::dt::IGVM_DT_IGVM_TYPE_PROPERTY;
17use inspect::Inspect;
18use loader_defs::shim::MemoryVtlType;
19use memory_range::MemoryRange;
20use vm_topology::memory::MemoryRangeWithNode;
21use vm_topology::processor::aarch64::Aarch64PlatformConfig;
22
23/// A parsed cpu.
24#[derive(Debug, Inspect, Clone, Copy, PartialEq, Eq)]
25pub struct Cpu {
26    /// Architecture specific "reg" value for this CPU. For x64, this is the
27    /// APIC ID. For ARM v8 64-bit, this should match the MPIDR_EL1 register
28    /// affinity bits.
29    pub reg: u64,
30    /// The vnode field of a cpu dt node, which describes the numa node id.
31    pub vnode: u32,
32}
33
34/// Information about a guest memory range.
35#[derive(Debug, Inspect, Clone, PartialEq, Eq)]
36pub struct Memory {
37    /// The range of memory.
38    pub range: MemoryRangeWithNode,
39    /// The VTL this memory is for.
40    #[inspect(debug)]
41    pub vtl_usage: MemoryVtlType,
42    /// The host provided IGVM type for this memory.
43    #[inspect(debug)]
44    pub igvm_type: MemoryMapEntryType,
45}
46
47/// Vtls for mmio.
48#[derive(Debug, Inspect, Clone, PartialEq, Eq)]
49pub enum Vtl {
50    /// VTL0.
51    Vtl0,
52    /// VTL2.
53    Vtl2,
54}
55
56/// Information about guest mmio.
57#[derive(Debug, Inspect, Clone, PartialEq, Eq)]
58pub struct Mmio {
59    /// The address range of mmio.
60    pub range: MemoryRange,
61    /// The VTL this mmio is for.
62    pub vtl: Vtl,
63}
64
65/// Information about a section of the guest's address space.
66#[derive(Debug, Inspect, Clone, PartialEq, Eq)]
67#[inspect(tag = "type")]
68pub enum AddressRange {
69    /// This range describes memory.
70    Memory(#[inspect(flatten)] Memory),
71    /// This range describes mmio.
72    Mmio(#[inspect(flatten)] Mmio),
73}
74
75impl AddressRange {
76    /// The [`MemoryRange`] for this address range.
77    pub fn range(&self) -> &MemoryRange {
78        match self {
79            AddressRange::Memory(memory) => &memory.range.range,
80            AddressRange::Mmio(mmio) => &mmio.range,
81        }
82    }
83
84    /// The [`MemoryVtlType`] for this address range.
85    pub fn vtl_usage(&self) -> MemoryVtlType {
86        match self {
87            AddressRange::Memory(memory) => memory.vtl_usage,
88            AddressRange::Mmio(Mmio { vtl, .. }) => match vtl {
89                Vtl::Vtl0 => MemoryVtlType::VTL0_MMIO,
90                Vtl::Vtl2 => MemoryVtlType::VTL2_MMIO,
91            },
92        }
93    }
94}
95
96/// The isolation type of the partition.
97#[derive(Debug, Clone, Copy, PartialEq, Eq, Inspect)]
98pub enum IsolationType {
99    /// No isolation.
100    None,
101    /// Hyper-V based isolation.
102    Vbs,
103    /// AMD SNP.
104    Snp,
105    /// Intel TDX.
106    Tdx,
107}
108
109/// The memory allocation mode provided by the host. This reports how the
110/// bootloader decided to provide memory for the kernel.
111#[derive(Debug, Clone, Copy, PartialEq, Eq, Inspect)]
112#[inspect(external_tag)]
113pub enum MemoryAllocationMode {
114    /// Use the host provided memory topology, and use VTL2_PROTECTABLE entries
115    /// as VTL2 ram. This is the default if no
116    /// `openhcl/memory-allocation-property` mode is provided by the host.
117    Host,
118    /// Allow VTL2 to select its own ranges from the address space to use for
119    /// memory, with a size provided by the host.
120    Vtl2 {
121        /// The number of bytes VTL2 should allocate for memory for itself.
122        /// Encoded as `openhcl/memory-size` in device tree.
123        #[inspect(hex)]
124        memory_size: Option<u64>,
125        /// The number of bytes VTL2 should allocate for mmio for itself.
126        /// Encoded as `openhcl/mmio-size` in device tree.
127        #[inspect(hex)]
128        mmio_size: Option<u64>,
129    },
130}
131
132/// Information parsed from the device tree provided by openhcl_boot. These
133/// values are trusted, as it's expected that openhcl_boot has already validated
134/// the host provided device tree.
135#[derive(Debug, Inspect, PartialEq, Eq)]
136pub struct ParsedBootDtInfo {
137    /// The cpus in the system. The index in the vector is also the mshv VP
138    /// index.
139    #[inspect(iter_by_index)]
140    pub cpus: Vec<Cpu>,
141    /// The physical address of the VTL0 alias mapping, if one is configured.
142    pub vtl0_alias_map: Option<u64>,
143    /// The memory ranges for VTL2 that were reported to the kernel. This is
144    /// sorted in ascending order.
145    #[inspect(iter_by_index)]
146    pub vtl2_memory: Vec<MemoryRangeWithNode>,
147    /// The unified memory map for the partition, from the bootloader. Sorted in
148    /// ascending order. Note that this includes mmio gaps as well.
149    #[inspect(with = "inspect_helpers::memory_internal")]
150    pub partition_memory_map: Vec<AddressRange>,
151    /// The mmio to report to VTL0.
152    #[inspect(iter_by_index)]
153    pub vtl0_mmio: Vec<MemoryRange>,
154    /// The ranges config regions are stored at.
155    #[inspect(iter_by_index)]
156    pub config_ranges: Vec<MemoryRange>,
157    /// The VTL2 reserved range.
158    pub vtl2_reserved_range: MemoryRange,
159    /// The memory range that contains the persisted header describing the
160    /// persisted protobuf region.
161    pub vtl2_persisted_header: MemoryRange,
162    /// The memory range that contains the persisted protobuf region.
163    pub vtl2_persisted_protobuf_region: MemoryRange,
164    /// The ranges that were accepted at load time by the host on behalf of the
165    /// guest.
166    #[inspect(iter_by_index)]
167    pub accepted_ranges: Vec<MemoryRange>,
168    /// The memory allocation mode the bootloader decided to use.
169    pub memory_allocation_mode: MemoryAllocationMode,
170    /// The isolation type of the partition.
171    pub isolation: IsolationType,
172    /// VTL2 range for private pool memory.
173    #[inspect(iter_by_index)]
174    pub private_pool_ranges: Vec<MemoryRangeWithNode>,
175
176    /// GIC and platform interrupt configuration, on AArch64.
177    pub gic: Option<Aarch64PlatformConfig>,
178}
179
180fn err_to_owned(e: fdt::parser::Error<'_>) -> anyhow::Error {
181    anyhow::Error::msg(e.to_string())
182}
183
184/// Try to find a given property on a node, returning None if not found. As the
185/// bootloader should be producing a well formed device tree, errors are not
186/// expected and flattened into `None`.
187fn try_find_property<'a>(node: &Node<'a>, name: &str) -> Option<Property<'a>> {
188    node.find_property(name).ok().flatten()
189}
190
191fn address_cells(node: &Node<'_>) -> anyhow::Result<u32> {
192    let prop = try_find_property(node, "#address-cells")
193        .context("missing address cells on {node.name}")?;
194    prop.read_u32(0).map_err(err_to_owned)
195}
196
197fn property_to_u64_vec(node: &Node<'_>, name: &str) -> anyhow::Result<Vec<u64>> {
198    let prop = try_find_property(node, name).context("missing prop {name} on {node.name}")?;
199    Ok(prop
200        .as_64_list()
201        .map_err(err_to_owned)
202        .context("prop {name} is not a list of u64s")?
203        .collect())
204}
205
206struct OpenhclInfo {
207    vtl0_mmio: Vec<MemoryRange>,
208    config_ranges: Vec<MemoryRange>,
209    partition_memory_map: Vec<AddressRange>,
210    accepted_memory: Vec<MemoryRange>,
211    vtl2_reserved_range: MemoryRange,
212    vtl0_alias_map: Option<u64>,
213    memory_allocation_mode: MemoryAllocationMode,
214    isolation: IsolationType,
215    private_pool_ranges: Vec<MemoryRangeWithNode>,
216    vtl2_persisted_header: MemoryRange,
217    vtl2_persisted_protobuf_region: MemoryRange,
218}
219
220fn parse_memory_openhcl(node: &Node<'_>) -> anyhow::Result<AddressRange> {
221    let vtl_usage = {
222        let prop = try_find_property(node, "openhcl,memory-type")
223            .context(format!("missing openhcl,memory-type on node {}", node.name))?;
224
225        MemoryVtlType(prop.read_u32(0).map_err(err_to_owned).context(format!(
226            "openhcl memory node {} openhcl,memory-type invalid",
227            node.name
228        ))?)
229    };
230
231    if vtl_usage.ram() {
232        // Parse this entry as memory.
233        let range = parse_memory(node).context("unable to parse base memory")?;
234
235        let igvm_type = {
236            let prop = try_find_property(node, IGVM_DT_IGVM_TYPE_PROPERTY)
237                .context(format!("missing igvm type on node {}", node.name))?;
238            let value = prop
239                .read_u32(0)
240                .map_err(err_to_owned)
241                .context(format!("memory node {} invalid igvm type", node.name))?;
242            MemoryMapEntryType(value as u16)
243        };
244
245        Ok(AddressRange::Memory(Memory {
246            range,
247            vtl_usage,
248            igvm_type,
249        }))
250    } else {
251        // Parse this type as just mmio.
252        let range = {
253            let reg = property_to_u64_vec(node, "reg")?;
254
255            if reg.len() != 2 {
256                bail!("mmio node {} does not have 2 u64s", node.name);
257            }
258
259            let base = reg[0];
260            let len = reg[1];
261            MemoryRange::try_new(base..(base + len)).context("invalid mmio range")?
262        };
263
264        let vtl = match vtl_usage {
265            MemoryVtlType::VTL0_MMIO => Vtl::Vtl0,
266            MemoryVtlType::VTL2_MMIO => Vtl::Vtl2,
267            _ => bail!(
268                "invalid vtl_usage {vtl_usage:?} type for mmio node {}",
269                node.name
270            ),
271        };
272
273        Ok(AddressRange::Mmio(Mmio { range, vtl }))
274    }
275}
276
277fn parse_accepted_memory(node: &Node<'_>) -> anyhow::Result<MemoryRange> {
278    let reg = property_to_u64_vec(node, "reg")?;
279
280    if reg.len() != 2 {
281        bail!("accepted memory node {} does not have 2 u64s", node.name);
282    }
283
284    let base = reg[0];
285    let len = reg[1];
286    MemoryRange::try_new(base..(base + len)).context("invalid preaccepted memory")
287}
288
289fn parse_openhcl(node: &Node<'_>) -> anyhow::Result<OpenhclInfo> {
290    let mut memory = Vec::new();
291    let mut accepted_memory = Vec::new();
292
293    for child in node.children() {
294        let child = child.map_err(err_to_owned).context("child invalid")?;
295
296        match child.name {
297            name if name.starts_with("memory@") => {
298                memory.push(parse_memory_openhcl(&child)?);
299            }
300
301            name if name.starts_with("accepted-memory@") => {
302                accepted_memory.push(parse_accepted_memory(&child)?);
303            }
304
305            name if name.starts_with("memory-allocation-mode") => {}
306
307            _ => {
308                // Ignore other nodes.
309            }
310        }
311    }
312
313    let isolation = {
314        let prop = try_find_property(node, "isolation-type").context("missing isolation-type")?;
315
316        match prop.read_str().map_err(err_to_owned)? {
317            "none" => IsolationType::None,
318            "vbs" => IsolationType::Vbs,
319            "snp" => IsolationType::Snp,
320            "tdx" => IsolationType::Tdx,
321            ty => bail!("invalid isolation-type {ty}"),
322        }
323    };
324
325    let memory_allocation_mode = {
326        let prop = try_find_property(node, "memory-allocation-mode")
327            .context("missing memory-allocation-mode")?;
328
329        match prop.read_str().map_err(err_to_owned)? {
330            "host" => MemoryAllocationMode::Host,
331            "vtl2" => {
332                let memory_size = try_find_property(node, "memory-size")
333                    .map(|p| p.read_u64(0))
334                    .transpose()
335                    .map_err(err_to_owned)?;
336
337                let mmio_size = try_find_property(node, "mmio-size")
338                    .map(|p| p.read_u64(0))
339                    .transpose()
340                    .map_err(err_to_owned)?;
341
342                MemoryAllocationMode::Vtl2 {
343                    memory_size,
344                    mmio_size,
345                }
346            }
347            mode => bail!("invalid memory-allocation-mode {mode}"),
348        }
349    };
350
351    memory.sort_by_key(|r| r.range().start());
352    accepted_memory.sort_by_key(|r| r.start());
353
354    // Report config ranges in a separate vec as well, for convenience.
355    let config_ranges = memory
356        .iter()
357        .filter_map(|entry| {
358            if entry.vtl_usage() == MemoryVtlType::VTL2_CONFIG {
359                Some(*entry.range())
360            } else {
361                None
362            }
363        })
364        .collect();
365
366    // Report the reserved range. There should only be one.
367    let vtl2_reserved_range = {
368        let mut reserved_range_iter = memory.iter().filter_map(|entry| {
369            if entry.vtl_usage() == MemoryVtlType::VTL2_RESERVED {
370                Some(*entry.range())
371            } else {
372                None
373            }
374        });
375
376        let reserved_range = reserved_range_iter.next().unwrap_or(MemoryRange::EMPTY);
377
378        if reserved_range_iter.next().is_some() {
379            bail!("multiple VTL2 reserved ranges found");
380        }
381
382        reserved_range
383    };
384
385    // Report private pool ranges in a separate vec, for convenience.
386    let private_pool_ranges = memory
387        .iter()
388        .filter_map(|entry| match entry {
389            AddressRange::Memory(memory) => {
390                if memory.vtl_usage == MemoryVtlType::VTL2_GPA_POOL {
391                    Some(memory.range.clone())
392                } else {
393                    None
394                }
395            }
396            AddressRange::Mmio(_) => None,
397        })
398        .collect();
399
400    // Report the persisted header range.
401    let vtl2_persisted_header = {
402        let mut header_iter = memory.iter().filter_map(|entry| {
403            if entry.vtl_usage() == MemoryVtlType::VTL2_PERSISTED_STATE_HEADER {
404                Some(*entry.range())
405            } else {
406                None
407            }
408        });
409
410        let header = header_iter
411            .next()
412            .ok_or(anyhow::anyhow!("no VTL2 persisted header range found"))?;
413
414        if header_iter.next().is_some() {
415            bail!("multiple VTL2 persisted header ranges found");
416        }
417
418        header
419    };
420
421    // Report the persisted protobuf region range.
422    let vtl2_persisted_protobuf_region = {
423        let mut region_iter = memory.iter().filter_map(|entry| {
424            if entry.vtl_usage() == MemoryVtlType::VTL2_PERSISTED_STATE_PROTOBUF {
425                Some(*entry.range())
426            } else {
427                None
428            }
429        });
430
431        let region = region_iter.next().ok_or(anyhow::anyhow!(
432            "no VTL2 persisted protobuf region range found"
433        ))?;
434
435        if region_iter.next().is_some() {
436            bail!("multiple VTL2 persisted protobuf region ranges found");
437        }
438
439        region
440    };
441
442    let vtl0_alias_map = try_find_property(node, "vtl0-alias-map")
443        .map(|prop| prop.read_u64(0).map_err(err_to_owned))
444        .transpose()
445        .context("unable to read vtl0-alias-map")?;
446
447    // Extract vmbus mmio information from the overall memory map.
448    let vtl0_mmio = memory
449        .iter()
450        .filter_map(|range| match range {
451            AddressRange::Memory(_) => None,
452            AddressRange::Mmio(mmio) => match mmio.vtl {
453                Vtl::Vtl0 => Some(mmio.range),
454                Vtl::Vtl2 => None,
455            },
456        })
457        .collect();
458
459    Ok(OpenhclInfo {
460        vtl0_mmio,
461        config_ranges,
462        partition_memory_map: memory,
463        accepted_memory,
464        vtl2_reserved_range,
465        vtl0_alias_map,
466        memory_allocation_mode,
467        isolation,
468        private_pool_ranges,
469        vtl2_persisted_header,
470        vtl2_persisted_protobuf_region,
471    })
472}
473
474fn parse_cpus(node: &Node<'_>) -> anyhow::Result<Vec<Cpu>> {
475    let address_cells = address_cells(node)?;
476
477    if address_cells > 2 {
478        bail!("cpus address-cells > 2 unexpected");
479    }
480
481    let mut cpus = Vec::new();
482
483    for cpu in node.children() {
484        let cpu = cpu.map_err(err_to_owned).context("cpu invalid")?;
485        let reg = try_find_property(&cpu, "reg").context("{cpu.name} missing reg")?;
486
487        let reg = match address_cells {
488            1 => reg.read_u32(0).map_err(err_to_owned)? as u64,
489            2 => reg.read_u64(0).map_err(err_to_owned)?,
490            _ => unreachable!(),
491        };
492
493        let vnode = try_find_property(&cpu, "numa-node-id")
494            .context("{cpu.name} missing numa-node-id")?
495            .read_u32(0)
496            .map_err(err_to_owned)?;
497
498        cpus.push(Cpu { reg, vnode });
499    }
500
501    Ok(cpus)
502}
503
504/// Parse a single memory node.
505fn parse_memory(node: &Node<'_>) -> anyhow::Result<MemoryRangeWithNode> {
506    let reg = property_to_u64_vec(node, "reg")?;
507
508    if reg.len() != 2 {
509        bail!("memory node {} does not have 2 u64s", node.name);
510    }
511
512    let base = reg[0];
513    let len = reg[1];
514    let numa_node_id = try_find_property(node, "numa-node-id")
515        .context(format!("{} missing numa-node-id", node.name))?
516        .read_u32(0)
517        .map_err(err_to_owned)
518        .context("unable to read numa-node-id")?;
519
520    Ok(MemoryRangeWithNode {
521        range: MemoryRange::try_new(base..base + len).context("invalid memory range")?,
522        vnode: numa_node_id,
523    })
524}
525
526/// Parse GIC config
527fn parse_gic(node: &Node<'_>) -> anyhow::Result<Aarch64PlatformConfig> {
528    let reg = property_to_u64_vec(node, "reg")?;
529
530    if reg.len() != 4 {
531        bail!("gic node {} does not have 4 u64s", node.name);
532    }
533
534    Ok(Aarch64PlatformConfig {
535        gic_distributor_base: reg[0],
536        // The OpenHCL paravisor boot path always receives a GICv3 layout.
537        gic_version: vm_topology::processor::aarch64::GicVersion::V3 {
538            redistributors_base: reg[2],
539        },
540        gic_msi: vm_topology::processor::aarch64::GicMsiController::None,
541        pmu_gsiv: None,
542        // TODO: parse from the DT timer node instead of hardcoding.
543        virt_timer_ppi: 20,
544        gic_nr_irqs: 992,
545    })
546}
547
548fn parse_pmu(node: &Node<'_>) -> anyhow::Result<u32> {
549    let interrupts =
550        try_find_property(node, "interrupts").context("pmu node missing interrupts")?;
551    let interrupts_typ = interrupts
552        .read_u32(0)
553        .map_err(err_to_owned)
554        .context("missing pmu interrupts typ")?;
555    let interrupts_ppi_index = interrupts
556        .read_u32(1)
557        .map_err(err_to_owned)
558        .context("missing pmu interrupts index")?;
559
560    // The interrupt is expected to be a PPI.
561    const GIC_PPI: u32 = 1;
562    if interrupts_typ != GIC_PPI {
563        bail!("pmu node has unexpected interrupt type {interrupts_typ}");
564    }
565
566    // The index is the index off of the PPI base of 16. PPIs only exist from 16
567    // to 31, so the index should be < 16.
568    if interrupts_ppi_index >= 16 {
569        bail!("pmu node has unexpected interrupt index {interrupts_ppi_index}");
570    }
571
572    const PPI_BASE: u32 = 16;
573    Ok(PPI_BASE + interrupts_ppi_index)
574}
575
576impl ParsedBootDtInfo {
577    /// Read parameters passed via device tree by openhcl_boot, at
578    /// /sys/firmware/fdt.
579    ///
580    /// The device tree is expected to be well formed from the bootloader, so
581    /// any errors here are not expected.
582    pub fn new() -> anyhow::Result<Self> {
583        let raw = fs_err::read("/sys/firmware/fdt").context("reading fdt")?;
584        Self::new_from_raw(&raw)
585    }
586
587    fn new_from_raw(raw: &[u8]) -> anyhow::Result<Self> {
588        let mut cpus = Vec::new();
589        let mut vtl0_mmio = Vec::new();
590        let mut config_ranges = Vec::new();
591        let mut vtl2_memory = Vec::new();
592        let mut gic = None;
593        let mut pmu_gsiv = None;
594        let mut partition_memory_map = Vec::new();
595        let mut accepted_ranges = Vec::new();
596        let mut vtl0_alias_map = None;
597        let mut memory_allocation_mode = MemoryAllocationMode::Host;
598        let mut isolation = IsolationType::None;
599        let mut vtl2_reserved_range = MemoryRange::EMPTY;
600        let mut private_pool_ranges = Vec::new();
601        let mut vtl2_persisted_header = MemoryRange::EMPTY;
602        let mut vtl2_persisted_protobuf_region = MemoryRange::EMPTY;
603
604        let parser = Parser::new(raw)
605            .map_err(err_to_owned)
606            .context("failed to create fdt parser")?;
607
608        for child in parser
609            .root()
610            .map_err(err_to_owned)
611            .context("root invalid")?
612            .children()
613        {
614            let child = child.map_err(err_to_owned).context("child invalid")?;
615
616            match child.name {
617                "cpus" => {
618                    cpus = parse_cpus(&child)?;
619                }
620
621                "openhcl" => {
622                    let OpenhclInfo {
623                        vtl0_mmio: n_vtl0_mmio,
624                        config_ranges: n_config_ranges,
625                        partition_memory_map: n_partition_memory_map,
626                        vtl2_reserved_range: n_vtl2_reserved_range,
627                        accepted_memory: n_accepted_memory,
628                        vtl0_alias_map: n_vtl0_alias_map,
629                        memory_allocation_mode: n_memory_allocation_mode,
630                        isolation: n_isolation,
631                        private_pool_ranges: n_private_pool_ranges,
632                        vtl2_persisted_header: n_vtl2_persisted_header,
633                        vtl2_persisted_protobuf_region: n_vtl2_persisted_protobuf_region,
634                    } = parse_openhcl(&child)?;
635                    vtl0_mmio = n_vtl0_mmio;
636                    config_ranges = n_config_ranges;
637                    partition_memory_map = n_partition_memory_map;
638                    accepted_ranges = n_accepted_memory;
639                    vtl0_alias_map = n_vtl0_alias_map;
640                    memory_allocation_mode = n_memory_allocation_mode;
641                    isolation = n_isolation;
642                    vtl2_reserved_range = n_vtl2_reserved_range;
643                    private_pool_ranges = n_private_pool_ranges;
644                    vtl2_persisted_header = n_vtl2_persisted_header;
645                    vtl2_persisted_protobuf_region = n_vtl2_persisted_protobuf_region;
646                }
647
648                _ if child.name.starts_with("memory@") => {
649                    vtl2_memory.push(parse_memory(&child)?);
650                }
651
652                _ if child.name.starts_with("intc@") => {
653                    // TODO: make sure we are on aarch64
654                    gic = Some(parse_gic(&child)?);
655                }
656
657                _ if child.name.starts_with("pmu") => {
658                    // TODO: make sure we are on aarch64
659                    pmu_gsiv = Some(parse_pmu(&child)?);
660                }
661
662                _ => {
663                    // Ignore other nodes.
664                }
665            }
666        }
667
668        vtl2_memory.sort_by_key(|r| r.range.start());
669
670        // Merge PMU GSIV into the GIC platform config if both were parsed.
671        if let (Some(gic), Some(pmu_gsiv)) = (&mut gic, pmu_gsiv) {
672            gic.pmu_gsiv = Some(pmu_gsiv);
673        }
674
675        Ok(Self {
676            cpus,
677            vtl0_mmio,
678            config_ranges,
679            vtl2_memory,
680            partition_memory_map,
681            vtl0_alias_map,
682            accepted_ranges,
683            gic,
684            memory_allocation_mode,
685            isolation,
686            vtl2_reserved_range,
687            private_pool_ranges,
688            vtl2_persisted_header,
689            vtl2_persisted_protobuf_region,
690        })
691    }
692}
693
694/// Boot times reported by the bootloader.
695#[derive(Debug, Clone, Copy, PartialEq, Eq)]
696pub struct BootTimes {
697    /// Kernel start time.
698    pub start: Option<u64>,
699    /// Kernel end time.
700    pub end: Option<u64>,
701    /// Sidecar start time.
702    pub sidecar_start: Option<u64>,
703    /// Sidecar end time.
704    pub sidecar_end: Option<u64>,
705}
706
707impl BootTimes {
708    /// Read the boot times passed via device tree by openhcl_boot, at
709    /// /sys/firmware/fdt.
710    ///
711    /// The device tree is expected to be well formed from the bootloader, so
712    /// any errors here are not expected.
713    pub fn new() -> anyhow::Result<Self> {
714        let raw = fs_err::read("/sys/firmware/fdt").context("reading fdt")?;
715        Self::new_from_raw(&raw)
716    }
717
718    fn new_from_raw(raw: &[u8]) -> anyhow::Result<Self> {
719        let mut start = None;
720        let mut end = None;
721        let mut sidecar_start = None;
722        let mut sidecar_end = None;
723        let parser = Parser::new(raw)
724            .map_err(err_to_owned)
725            .context("failed to create fdt parser")?;
726
727        let root = parser
728            .root()
729            .map_err(err_to_owned)
730            .context("root invalid")?;
731
732        if let Some(prop) = try_find_property(&root, "reftime_boot_start") {
733            start = Some(prop.read_u64(0).map_err(err_to_owned)?);
734        }
735
736        if let Some(prop) = try_find_property(&root, "reftime_boot_end") {
737            end = Some(prop.read_u64(0).map_err(err_to_owned)?);
738        }
739
740        if let Some(prop) = try_find_property(&root, "reftime_sidecar_start") {
741            sidecar_start = Some(prop.read_u64(0).map_err(err_to_owned)?);
742        }
743
744        if let Some(prop) = try_find_property(&root, "reftime_sidecar_end") {
745            sidecar_end = Some(prop.read_u64(0).map_err(err_to_owned)?);
746        }
747
748        Ok(Self {
749            start,
750            end,
751            sidecar_start,
752            sidecar_end,
753        })
754    }
755}
756
757mod inspect_helpers {
758    use super::*;
759
760    pub(super) fn memory_internal(ranges: &[AddressRange]) -> impl Inspect + '_ {
761        inspect::iter_by_key(ranges.iter().map(|entry| (entry.range(), entry)))
762    }
763}
764
765#[cfg(test)]
766mod tests {
767    use super::*;
768    use fdt::builder::Builder;
769
770    fn build_dt(info: &ParsedBootDtInfo) -> anyhow::Result<Vec<u8>> {
771        let mut buf = vec![0; 4096];
772
773        let mut builder = Builder::new(fdt::builder::BuilderConfig {
774            blob_buffer: &mut buf,
775            string_table_cap: 1024,
776            memory_reservations: &[],
777        })?;
778        let p_address_cells = builder.add_string("#address-cells")?;
779        let p_size_cells = builder.add_string("#size-cells")?;
780        let p_reg = builder.add_string("reg")?;
781        let p_device_type = builder.add_string("device_type")?;
782        let p_compatible = builder.add_string("compatible")?;
783        let p_ranges = builder.add_string("ranges")?;
784        let p_numa_node_id = builder.add_string("numa-node-id")?;
785        let p_igvm_type = builder.add_string(IGVM_DT_IGVM_TYPE_PROPERTY)?;
786        let p_openhcl_memory = builder.add_string("openhcl,memory-type")?;
787        let p_interrupts = builder.add_string("interrupts")?;
788
789        let mut root_builder = builder
790            .start_node("")?
791            .add_u32(p_address_cells, 2)?
792            .add_u32(p_size_cells, 2)?
793            .add_str(p_compatible, "microsoft,openvmm")?;
794
795        let mut cpu_builder = root_builder
796            .start_node("cpus")?
797            .add_u32(p_address_cells, 1)?
798            .add_u32(p_size_cells, 0)?;
799
800        // add cpus
801        for (index, cpu) in info.cpus.iter().enumerate() {
802            let name = format!("cpu@{}", index + 1);
803
804            cpu_builder = cpu_builder
805                .start_node(&name)?
806                .add_str(p_device_type, "cpu")?
807                .add_u32(p_reg, cpu.reg as u32)?
808                .add_u32(p_numa_node_id, cpu.vnode)?
809                .end_node()?;
810        }
811
812        root_builder = cpu_builder.end_node()?;
813
814        // add memory, in reverse order.
815        for memory in info.vtl2_memory.iter().rev() {
816            let name = format!("memory@{:x}", memory.range.start());
817
818            root_builder = root_builder
819                .start_node(&name)?
820                .add_str(p_device_type, "memory")?
821                .add_u64_list(p_reg, [memory.range.start(), memory.range.len()])?
822                .add_u32(p_numa_node_id, memory.vnode)?
823                .end_node()?;
824        }
825
826        // GIC
827        if let Some(gic) = info.gic {
828            let p_interrupt_cells = root_builder.add_string("#interrupt-cells")?;
829            let p_redist_regions = root_builder.add_string("#redistributor-regions")?;
830            let p_redist_stride = root_builder.add_string("redistributor-stride")?;
831            let p_interrupt_controller = root_builder.add_string("interrupt-controller")?;
832            let p_phandle = root_builder.add_string("phandle")?;
833            let gic_redist_base = match gic.gic_version {
834                vm_topology::processor::aarch64::GicVersion::V3 {
835                    redistributors_base,
836                } => redistributors_base,
837                vm_topology::processor::aarch64::GicVersion::V2 { cpu_interface_base } => {
838                    cpu_interface_base
839                }
840            };
841            let name = format!("intc@{}", gic.gic_distributor_base);
842            root_builder = root_builder
843                .start_node(name.as_ref())?
844                .add_str(p_compatible, "arm,gic-v3")?
845                .add_u32(p_redist_regions, 1)?
846                .add_u64(p_redist_stride, 0)?
847                .add_u64_array(p_reg, &[gic.gic_distributor_base, 0, gic_redist_base, 0])?
848                .add_u32(p_address_cells, 2)?
849                .add_u32(p_size_cells, 2)?
850                .add_u32(p_interrupt_cells, 3)?
851                .add_null(p_interrupt_controller)?
852                .add_u32(p_phandle, 1)?
853                .add_null(p_ranges)?
854                .end_node()?;
855        }
856
857        // PMU
858        if let Some(gic) = &info.gic {
859            if let Some(pmu_gsiv) = gic.pmu_gsiv {
860                anyhow::ensure!(
861                    (16..32).contains(&pmu_gsiv),
862                    "PMU GSIV {pmu_gsiv} is not a valid PPI (expected 16..32)"
863                );
864                const GIC_PPI: u32 = 1;
865                const IRQ_TYPE_LEVEL_HIGH: u32 = 4;
866                root_builder = root_builder
867                    .start_node("pmu")?
868                    .add_str(p_compatible, "arm,armv8-pmuv3")?
869                    .add_u32_array(p_interrupts, &[GIC_PPI, pmu_gsiv - 16, IRQ_TYPE_LEVEL_HIGH])?
870                    .end_node()?;
871            }
872        }
873
874        let mut openhcl_builder = root_builder.start_node("openhcl")?;
875        let p_isolation_type = openhcl_builder.add_string("isolation-type")?;
876        openhcl_builder = openhcl_builder.add_str(
877            p_isolation_type,
878            match info.isolation {
879                IsolationType::None => "none",
880                IsolationType::Vbs => "vbs",
881                IsolationType::Snp => "snp",
882                IsolationType::Tdx => "tdx",
883            },
884        )?;
885
886        let p_memory_allocation_mode = openhcl_builder.add_string("memory-allocation-mode")?;
887        match info.memory_allocation_mode {
888            MemoryAllocationMode::Host => {
889                openhcl_builder = openhcl_builder.add_str(p_memory_allocation_mode, "host")?;
890            }
891            MemoryAllocationMode::Vtl2 {
892                memory_size,
893                mmio_size,
894            } => {
895                let p_memory_size = openhcl_builder.add_string("memory-size")?;
896                let p_mmio_size = openhcl_builder.add_string("mmio-size")?;
897                openhcl_builder = openhcl_builder.add_str(p_memory_allocation_mode, "vtl2")?;
898                if let Some(memory_size) = memory_size {
899                    openhcl_builder = openhcl_builder.add_u64(p_memory_size, memory_size)?;
900                }
901                if let Some(mmio_size) = mmio_size {
902                    openhcl_builder = openhcl_builder.add_u64(p_mmio_size, mmio_size)?;
903                }
904            }
905        }
906
907        if let Some(data) = info.vtl0_alias_map {
908            let p_vtl0_alias_map = openhcl_builder.add_string("vtl0-alias-map")?;
909            openhcl_builder = openhcl_builder.add_u64(p_vtl0_alias_map, data)?;
910        }
911
912        openhcl_builder = openhcl_builder
913            .start_node("vmbus-vtl0")?
914            .add_u32(p_address_cells, 2)?
915            .add_u32(p_size_cells, 2)?
916            .add_str(p_compatible, "microsoft,vmbus")?
917            .add_u64_list(
918                p_ranges,
919                info.vtl0_mmio
920                    .iter()
921                    .flat_map(|r| [r.start(), r.start(), r.len()]),
922            )?
923            .end_node()?;
924
925        for range in &info.partition_memory_map {
926            let name = format!("memory@{:x}", range.range().start());
927
928            let node_builder = openhcl_builder
929                .start_node(&name)?
930                .add_str(p_device_type, "memory-openhcl")?
931                .add_u64_list(p_reg, [range.range().start(), range.range().len()])?
932                .add_u32(p_openhcl_memory, range.vtl_usage().0)?;
933
934            openhcl_builder = match range {
935                AddressRange::Memory(memory) => {
936                    // Add as a memory node, with numa info and igvm type.
937                    node_builder
938                        .add_u32(p_numa_node_id, memory.range.vnode)?
939                        .add_u32(p_igvm_type, memory.igvm_type.0 as u32)?
940                }
941                AddressRange::Mmio(_) => {
942                    // Nothing to do here, mmio already contains the min
943                    // required info of range and vtl via vtl_usage.
944                    node_builder
945                }
946            }
947            .end_node()?;
948        }
949
950        for range in &info.accepted_ranges {
951            let name = format!("accepted-memory@{:x}", range.start());
952
953            openhcl_builder = openhcl_builder
954                .start_node(&name)?
955                .add_str(p_device_type, "memory-openhcl")?
956                .add_u64_list(p_reg, [range.start(), range.len()])?
957                .end_node()?;
958        }
959
960        root_builder = openhcl_builder.end_node()?;
961
962        root_builder.end_node()?.build(info.cpus[0].reg as u32)?;
963
964        Ok(buf)
965    }
966
967    #[test]
968    fn test_basic() {
969        let orig_info = ParsedBootDtInfo {
970            cpus: (0..4).map(|i| Cpu { reg: i, vnode: 0 }).collect(),
971            vtl2_memory: vec![
972                MemoryRangeWithNode {
973                    range: MemoryRange::new(0x10000..0x20000),
974                    vnode: 0,
975                },
976                MemoryRangeWithNode {
977                    range: MemoryRange::new(0x20000..0x30000),
978                    vnode: 1,
979                },
980            ],
981            partition_memory_map: vec![
982                AddressRange::Memory(Memory {
983                    range: MemoryRangeWithNode {
984                        range: MemoryRange::new(0..0x1000),
985                        vnode: 0,
986                    },
987                    vtl_usage: MemoryVtlType::VTL0,
988                    igvm_type: MemoryMapEntryType::MEMORY,
989                }),
990                AddressRange::Mmio(Mmio {
991                    range: MemoryRange::new(0x1000..0x2000),
992                    vtl: Vtl::Vtl0,
993                }),
994                AddressRange::Mmio(Mmio {
995                    range: MemoryRange::new(0x3000..0x4000),
996                    vtl: Vtl::Vtl0,
997                }),
998                AddressRange::Memory(Memory {
999                    range: MemoryRangeWithNode {
1000                        range: MemoryRange::new(0x10000..0x20000),
1001                        vnode: 0,
1002                    },
1003                    vtl_usage: MemoryVtlType::VTL2_RAM,
1004                    igvm_type: MemoryMapEntryType::VTL2_PROTECTABLE,
1005                }),
1006                AddressRange::Memory(Memory {
1007                    range: MemoryRangeWithNode {
1008                        range: MemoryRange::new(0x20000..0x30000),
1009                        vnode: 1,
1010                    },
1011                    vtl_usage: MemoryVtlType::VTL2_CONFIG,
1012                    igvm_type: MemoryMapEntryType::VTL2_PROTECTABLE,
1013                }),
1014                AddressRange::Memory(Memory {
1015                    range: MemoryRangeWithNode {
1016                        range: MemoryRange::new(0x30000..0x40000),
1017                        vnode: 1,
1018                    },
1019                    vtl_usage: MemoryVtlType::VTL2_CONFIG,
1020                    igvm_type: MemoryMapEntryType::VTL2_PROTECTABLE,
1021                }),
1022                AddressRange::Memory(Memory {
1023                    range: MemoryRangeWithNode {
1024                        range: MemoryRange::new(0x40000..0x50000),
1025                        vnode: 1,
1026                    },
1027                    vtl_usage: MemoryVtlType::VTL2_RESERVED,
1028                    igvm_type: MemoryMapEntryType::VTL2_PROTECTABLE,
1029                }),
1030                AddressRange::Memory(Memory {
1031                    range: MemoryRangeWithNode {
1032                        range: MemoryRange::new(0x50000..0x51000),
1033                        vnode: 0,
1034                    },
1035                    vtl_usage: MemoryVtlType::VTL2_PERSISTED_STATE_HEADER,
1036                    igvm_type: MemoryMapEntryType::VTL2_PROTECTABLE,
1037                }),
1038                AddressRange::Memory(Memory {
1039                    range: MemoryRangeWithNode {
1040                        range: MemoryRange::new(0x51000..0x52000),
1041                        vnode: 0,
1042                    },
1043                    vtl_usage: MemoryVtlType::VTL2_PERSISTED_STATE_PROTOBUF,
1044                    igvm_type: MemoryMapEntryType::VTL2_PROTECTABLE,
1045                }),
1046                AddressRange::Memory(Memory {
1047                    range: MemoryRangeWithNode {
1048                        range: MemoryRange::new(0x60000..0x70000),
1049                        vnode: 0,
1050                    },
1051                    vtl_usage: MemoryVtlType::VTL2_GPA_POOL,
1052                    igvm_type: MemoryMapEntryType::VTL2_PROTECTABLE,
1053                }),
1054                AddressRange::Memory(Memory {
1055                    range: MemoryRangeWithNode {
1056                        range: MemoryRange::new(0x1000000..0x2000000),
1057                        vnode: 0,
1058                    },
1059                    vtl_usage: MemoryVtlType::VTL0,
1060                    igvm_type: MemoryMapEntryType::MEMORY,
1061                }),
1062                AddressRange::Mmio(Mmio {
1063                    range: MemoryRange::new(0x3000000..0x4000000),
1064                    vtl: Vtl::Vtl2,
1065                }),
1066            ],
1067            vtl0_mmio: vec![
1068                MemoryRange::new(0x1000..0x2000),
1069                MemoryRange::new(0x3000..0x4000),
1070            ],
1071            config_ranges: vec![
1072                MemoryRange::new(0x20000..0x30000),
1073                MemoryRange::new(0x30000..0x40000),
1074            ],
1075            vtl0_alias_map: Some(1 << 48),
1076            gic: Some(Aarch64PlatformConfig {
1077                gic_distributor_base: 0x10000,
1078                gic_version: vm_topology::processor::aarch64::GicVersion::V3 {
1079                    redistributors_base: 0x20000,
1080                },
1081                gic_msi: vm_topology::processor::aarch64::GicMsiController::None,
1082                pmu_gsiv: Some(0x17),
1083                virt_timer_ppi: 20,
1084                gic_nr_irqs: 992,
1085            }),
1086            accepted_ranges: vec![
1087                MemoryRange::new(0x10000..0x20000),
1088                MemoryRange::new(0x1000000..0x1500000),
1089            ],
1090            memory_allocation_mode: MemoryAllocationMode::Vtl2 {
1091                memory_size: Some(0x1000),
1092                mmio_size: Some(0x2000),
1093            },
1094            isolation: IsolationType::Vbs,
1095            vtl2_reserved_range: MemoryRange::new(0x40000..0x50000),
1096            private_pool_ranges: vec![MemoryRangeWithNode {
1097                range: MemoryRange::new(0x60000..0x70000),
1098                vnode: 0,
1099            }],
1100            vtl2_persisted_header: MemoryRange::new(0x50000..0x51000),
1101            vtl2_persisted_protobuf_region: MemoryRange::new(0x51000..0x52000),
1102        };
1103
1104        let dt = build_dt(&orig_info).unwrap();
1105        let parsed = ParsedBootDtInfo::new_from_raw(&dt).unwrap();
1106
1107        assert_eq!(orig_info, parsed);
1108    }
1109
1110    fn build_boottime_dt(boot_times: BootTimes) -> anyhow::Result<Vec<u8>> {
1111        let mut buf = vec![0; 4096];
1112
1113        let mut builder = Builder::new(fdt::builder::BuilderConfig {
1114            blob_buffer: &mut buf,
1115            string_table_cap: 1024,
1116            memory_reservations: &[],
1117        })?;
1118        let p_address_cells = builder.add_string("#address-cells")?;
1119        let p_size_cells = builder.add_string("#size-cells")?;
1120        let p_reftime_boot_start = builder.add_string("reftime_boot_start")?;
1121        let p_reftime_boot_end = builder.add_string("reftime_boot_end")?;
1122        let p_reftime_sidecar_start = builder.add_string("reftime_sidecar_start")?;
1123        let p_reftime_sidecar_end = builder.add_string("reftime_sidecar_end")?;
1124
1125        let mut root_builder = builder
1126            .start_node("")?
1127            .add_u32(p_address_cells, 2)?
1128            .add_u32(p_size_cells, 2)?;
1129
1130        if let Some(start) = boot_times.start {
1131            root_builder = root_builder.add_u64(p_reftime_boot_start, start)?;
1132        }
1133
1134        if let Some(end) = boot_times.end {
1135            root_builder = root_builder.add_u64(p_reftime_boot_end, end)?;
1136        }
1137
1138        if let Some(start) = boot_times.sidecar_start {
1139            root_builder = root_builder.add_u64(p_reftime_sidecar_start, start)?;
1140        }
1141
1142        if let Some(end) = boot_times.sidecar_end {
1143            root_builder = root_builder.add_u64(p_reftime_sidecar_end, end)?;
1144        }
1145
1146        root_builder.end_node()?.build(0)?;
1147
1148        Ok(buf)
1149    }
1150
1151    #[test]
1152    fn test_basic_boottime() {
1153        let orig_info = BootTimes {
1154            start: Some(0x1000),
1155            end: Some(0x2000),
1156            sidecar_start: Some(0x3000),
1157            sidecar_end: Some(0x4000),
1158        };
1159
1160        let dt = build_boottime_dt(orig_info).unwrap();
1161        let parsed = BootTimes::new_from_raw(&dt).unwrap();
1162
1163        assert_eq!(orig_info, parsed);
1164
1165        // test no boot times.
1166        let orig_info = BootTimes {
1167            start: None,
1168            end: None,
1169            sidecar_start: None,
1170            sidecar_end: None,
1171        };
1172
1173        let dt = build_boottime_dt(orig_info).unwrap();
1174        let parsed = BootTimes::new_from_raw(&dt).unwrap();
1175
1176        assert_eq!(orig_info, parsed);
1177    }
1178}