Skip to main content

petri/vm/openvmm/
modify.rs

1// Copyright (c) Microsoft Corporation.
2// Licensed under the MIT License.
3
4//! Helpers to modify a [`PetriVmConfigOpenVmm`] from its defaults.
5
6// TODO: Delete all modification functions that are not backend-specific
7// from this file, add necessary settings to the backend-agnostic
8// `PetriVmConfig`, and add corresponding functions to `PetriVmBuilder`.
9
10use super::MANA_INSTANCE;
11use super::NIC_MAC_ADDRESS;
12use super::PetriVmConfigOpenVmm;
13use chipset_resources::battery::BatteryDeviceHandleX64;
14use chipset_resources::battery::HostBatteryUpdate;
15use disk_backend_resources::LayeredDiskHandle;
16use disk_backend_resources::layer::RamDiskLayerHandle;
17use gdma_resources::GdmaDeviceHandle;
18use gdma_resources::VportDefinition;
19use get_resources::ged::IgvmAttestTestConfig;
20use guid::Guid;
21use net_backend_resources::mac_address::MacAddress;
22use nvme_resources::NamespaceDefinition;
23use nvme_resources::NvmeControllerHandle;
24use openvmm_defs::config::Config;
25use openvmm_defs::config::DeviceVtl;
26use openvmm_defs::config::LoadMode;
27use openvmm_defs::config::PcieDeviceConfig;
28use openvmm_defs::config::PcieIommuConfig;
29use openvmm_defs::config::PcieMmioRangeConfig;
30use openvmm_defs::config::PciePortConfig;
31use openvmm_defs::config::PcieRootComplexConfig;
32use openvmm_defs::config::PcieSwitchConfig;
33use openvmm_defs::config::VpciDeviceConfig;
34use openvmm_defs::config::Vtl2BaseAddressType;
35use vm_resource::IntoResource;
36use vmotherboard::ChipsetDeviceHandle;
37
38impl PetriVmConfigOpenVmm {
39    /// Enable the VTL0 alias map.
40    // TODO: Remove once #912 is fixed.
41    pub fn with_vtl0_alias_map(mut self) -> Self {
42        self.config
43            .hypervisor
44            .with_vtl2
45            .as_mut()
46            .expect("Not an openhcl config.")
47            .vtl0_alias_map = true;
48        self
49    }
50
51    /// Enable the battery for the VM.
52    pub fn with_battery(mut self) -> Self {
53        if self.resources.properties.is_openhcl {
54            self.ged.as_mut().unwrap().enable_battery = true;
55        } else {
56            self.config.chipset_devices.push(ChipsetDeviceHandle {
57                name: "battery".to_string(),
58                resource: BatteryDeviceHandleX64 {
59                    battery_status_recv: {
60                        let (tx, rx) = mesh::channel();
61                        tx.send(HostBatteryUpdate::default_present());
62                        rx
63                    },
64                }
65                .into_resource(),
66            });
67            if let LoadMode::Uefi { enable_battery, .. } = &mut self.config.load_mode {
68                *enable_battery = true;
69            }
70        }
71        self
72    }
73
74    /// Set test config for the GED's IGVM attest request handler
75    pub fn with_igvm_attest_test_config(mut self, config: IgvmAttestTestConfig) -> Self {
76        if !self.resources.properties.is_openhcl {
77            panic!("IGVM Attest test config is only supported for OpenHCL.")
78        };
79
80        let ged = self.ged.as_mut().expect("No GED to configure TPM");
81
82        ged.igvm_attest_test_config = Some(config);
83
84        self
85    }
86
87    /// Override the SMBIOS identity delivered to the guest, regardless of how
88    /// the VM is loaded.
89    ///
90    /// For OpenHCL the identity is forwarded to the paravisor over the Guest
91    /// Emulation Transport (GET), which synthesizes the guest's DMI tables from
92    /// it. For direct OpenVMM boot (Linux direct, UEFI, or PCAT) it is applied
93    /// to the loader's SMBIOS config. Each load path honors only the subset of
94    /// fields it can express and fails closed on the rest.
95    pub fn with_smbios(mut self, f: impl FnOnce(&mut smbios_defs::SmbiosConfig)) -> Self {
96        if self.resources.properties.is_openhcl {
97            let ged = self.ged.as_mut().expect("OpenHCL config must have a GED.");
98            f(&mut ged.smbios);
99        } else {
100            let smbios = match &mut self.config.load_mode {
101                LoadMode::Linux { smbios, .. }
102                | LoadMode::Uefi { smbios, .. }
103                | LoadMode::Pcat { smbios, .. } => &mut **smbios,
104                LoadMode::Igvm { .. } | LoadMode::None => {
105                    panic!("SMBIOS configuration is not supported for this load mode.")
106                }
107            };
108            f(smbios);
109        }
110        self
111    }
112
113    /// Enable a synthnic for the VM.
114    ///
115    /// Uses a mana emulator and the paravisor if a paravisor is present.
116    pub fn with_nic(mut self) -> Self {
117        let endpoint = net_backend_resources::consomme::ConsommeHandle {
118            cidr: None,
119            ports: Vec::new(),
120            recv: None,
121        }
122        .into_resource();
123        if let Some(vtl2_settings) = self.runtime_config.vtl2_settings.as_mut() {
124            self.config.vpci_devices.push(VpciDeviceConfig {
125                vtl: DeviceVtl::Vtl2,
126                instance_id: MANA_INSTANCE,
127                resource: GdmaDeviceHandle {
128                    vports: vec![VportDefinition {
129                        mac_address: NIC_MAC_ADDRESS,
130                        endpoint,
131                    }],
132                }
133                .into_resource(),
134                vnode: None,
135            });
136
137            vtl2_settings.dynamic.as_mut().unwrap().nic_devices.push(
138                vtl2_settings_proto::NicDeviceLegacy {
139                    instance_id: MANA_INSTANCE.to_string(),
140                    subordinate_instance_id: None,
141                    max_sub_channels: None,
142                },
143            );
144        } else {
145            const NETVSP_INSTANCE: Guid = guid::guid!("c6c46cc3-9302-4344-b206-aef65e5bd0a2");
146            self.config.vmbus_devices.push((
147                DeviceVtl::Vtl0,
148                netvsp_resources::NetvspHandle {
149                    instance_id: NETVSP_INSTANCE,
150                    mac_address: NIC_MAC_ADDRESS,
151                    endpoint,
152                    max_queues: None,
153                }
154                .into_resource(),
155            ));
156        }
157
158        self
159    }
160
161    /// Add a single-vport MANA VF to VTL2 and expose its synthetic NIC to VTL0.
162    pub fn with_mana_vf(self, instance_id: Guid, mac_address: MacAddress) -> Self {
163        self.with_mana_vf_vports(instance_id, [mac_address])
164    }
165
166    /// Add a MANA VF to VTL2 and expose one synthetic NIC per vport to VTL0.
167    pub fn with_mana_vf_vports(
168        mut self,
169        instance_id: Guid,
170        mac_addresses: impl IntoIterator<Item = MacAddress>,
171    ) -> Self {
172        let vtl2_settings = self
173            .runtime_config
174            .vtl2_settings
175            .as_mut()
176            .expect("a VTL2 MANA VF requires OpenHCL firmware");
177
178        let vports = mac_addresses
179            .into_iter()
180            .map(|mac_address| VportDefinition {
181                mac_address,
182                endpoint: net_backend_resources::consomme::ConsommeHandle {
183                    cidr: None,
184                    ports: Vec::new(),
185                    recv: None,
186                }
187                .into_resource(),
188            })
189            .collect::<Vec<_>>();
190
191        assert!(!vports.is_empty(), "a MANA VF requires at least one vport");
192
193        self.config.vpci_devices.push(VpciDeviceConfig {
194            vtl: DeviceVtl::Vtl2,
195            instance_id,
196            resource: GdmaDeviceHandle { vports }.into_resource(),
197            vnode: None,
198        });
199        vtl2_settings.dynamic.as_mut().unwrap().nic_devices.push(
200            vtl2_settings_proto::NicDeviceLegacy {
201                instance_id: instance_id.to_string(),
202                subordinate_instance_id: None,
203                max_sub_channels: None,
204            },
205        );
206
207        self
208    }
209
210    /// Add a PCIe NIC to the VM using the MANA emulator.
211    pub fn with_pcie_nic(mut self, port_name: &str, mac_address: MacAddress) -> Self {
212        let endpoint = net_backend_resources::consomme::ConsommeHandle {
213            cidr: None,
214            ports: Vec::new(),
215            recv: None,
216        }
217        .into_resource();
218        self.config.pcie_devices.push(PcieDeviceConfig {
219            port_name: port_name.to_string(),
220            resource: GdmaDeviceHandle {
221                vports: vec![VportDefinition {
222                    mac_address,
223                    endpoint,
224                }],
225            }
226            .into_resource(),
227        });
228
229        self
230    }
231
232    /// Add a PCIe NVMe device to the VM using the NVMe emulator.
233    pub fn with_pcie_nvme(mut self, port_name: &str, subsystem_id: Guid) -> Self {
234        self.config.pcie_devices.push(PcieDeviceConfig {
235            port_name: port_name.to_string(),
236            resource: NvmeControllerHandle {
237                subsystem_id,
238                max_io_queues: 64,
239                msix_count: 64,
240                namespaces: vec![NamespaceDefinition {
241                    nsid: 1,
242                    disk: LayeredDiskHandle::single_layer(RamDiskLayerHandle {
243                        len: Some(1024 * 1024),
244                        sector_size: None,
245                    })
246                    .into_resource(),
247                    read_only: false,
248                }],
249                requests: None,
250            }
251            .into_resource(),
252        });
253
254        self
255    }
256
257    /// Enable a virtio-net NIC for the VM backed by Consomme.
258    ///
259    /// This exposes a virtio-net device on a PCIe root port, suitable for
260    /// guests running virtio drivers (e.g. Linux with UEFI boot).
261    pub fn with_virtio_nic(mut self, port_name: &str, mac_address: MacAddress) -> Self {
262        let endpoint = net_backend_resources::consomme::ConsommeHandle {
263            cidr: None,
264            ports: Vec::new(),
265            recv: None,
266        }
267        .into_resource();
268
269        self.config.pcie_devices.push(PcieDeviceConfig {
270            port_name: port_name.to_string(),
271            resource: virtio_resources::VirtioPciDeviceHandle(
272                virtio_resources::net::VirtioNetHandle {
273                    max_queues: None,
274                    mac_address,
275                    endpoint,
276                }
277                .into_resource(),
278            )
279            .into_resource(),
280        });
281
282        self
283    }
284
285    /// Add a virtio-net NIC with consomme and TCP port forwarding for
286    /// pipette. Used for Windows no-vmbus guests where virtio-vsock is
287    /// unavailable.
288    ///
289    /// This configures consomme to forward the pipette TCP port from the
290    /// host into the guest, so the petri framework can connect to the
291    /// pipette agent over TCP.
292    pub fn with_tcp_pipette_nic(mut self, port_name: &str, mac_address: MacAddress) -> Self {
293        let (port_send, port_recv) = mesh::oneshot();
294        let endpoint = net_backend_resources::consomme::ConsommeHandle {
295            cidr: None,
296            ports: vec![net_backend_resources::consomme::HostPortConfig {
297                protocol: net_backend_resources::consomme::HostPortProtocol::Tcp,
298                host_address: Some(net_backend_resources::consomme::HostIpAddress::Ipv4(
299                    std::net::Ipv4Addr::LOCALHOST,
300                )),
301                host_port: net_backend_resources::consomme::HostPort::Dynamic(port_send),
302                guest_port: pipette_client::PIPETTE_PORT as u16,
303            }],
304            recv: None,
305        }
306        .into_resource();
307        self.config.pcie_devices.push(PcieDeviceConfig {
308            port_name: port_name.to_string(),
309            resource: virtio_resources::VirtioPciDeviceHandle(
310                virtio_resources::net::VirtioNetHandle {
311                    max_queues: None,
312                    mac_address,
313                    endpoint,
314                }
315                .into_resource(),
316            )
317            .into_resource(),
318        });
319        self.resources.tcp_pipette_port = Some(port_recv);
320        self
321    }
322
323    /// Request nested virtualization support from the host hypervisor.
324    pub fn with_nested_virt(mut self) -> Self {
325        self.config.hypervisor.nested_virt = true;
326        self
327    }
328
329    /// Enable a synthnic for the VM backed by the Windows vmswitch
330    /// DirectIO (`-net dio`) backend.
331    ///
332    /// `switch_id`, when `None`, defaults to the Hyper-V Default Switch.
333    /// This requires the host to have Hyper-V installed and the chosen
334    /// switch available; tests that call this method should pre-resolve
335    /// a switch via [`super::find_switch`] (or an equivalent runtime
336    /// probe) and bail out with a clear error when the host does not
337    /// meet those requirements. The method itself panics if the switch
338    /// cannot be opened or a port cannot be created.
339    ///
340    /// The created vmswitch port handle is held in the petri (parent)
341    /// process for the lifetime of the VM. The kernel switch port object
342    /// is reference counted, so keeping the handle alive in this process
343    /// keeps the port usable from the child VMM process.
344    #[cfg(windows)]
345    pub fn with_dio_nic(mut self, switch_id: Option<Guid>) -> Self {
346        let switch_port_id = vmswitch::kernel::SwitchPortId {
347            switch: switch_id.unwrap_or(vmswitch::hcn::DEFAULT_SWITCH),
348            port: Guid::new_random(),
349        };
350        let _ = vmswitch::hcn::Network::open(&switch_port_id.switch)
351            .unwrap_or_else(|e| panic!("could not find switch {}: {e}", switch_port_id.switch));
352        let switch_port = vmswitch::kernel::SwitchPort::new(&switch_port_id)
353            .expect("failed to create vmswitch DIO port");
354        self.resources._switch_ports.push(switch_port);
355
356        let endpoint = net_backend_resources::dio::WindowsDirectIoHandle {
357            switch_port_id: net_backend_resources::dio::SwitchPortId {
358                switch: switch_port_id.switch,
359                port: switch_port_id.port,
360            },
361        }
362        .into_resource();
363
364        if let Some(vtl2_settings) = self.runtime_config.vtl2_settings.as_mut() {
365            self.config.vpci_devices.push(VpciDeviceConfig {
366                vtl: DeviceVtl::Vtl2,
367                instance_id: MANA_INSTANCE,
368                resource: GdmaDeviceHandle {
369                    vports: vec![VportDefinition {
370                        mac_address: NIC_MAC_ADDRESS,
371                        endpoint,
372                    }],
373                }
374                .into_resource(),
375                vnode: None,
376            });
377
378            vtl2_settings.dynamic.as_mut().unwrap().nic_devices.push(
379                vtl2_settings_proto::NicDeviceLegacy {
380                    instance_id: MANA_INSTANCE.to_string(),
381                    subordinate_instance_id: None,
382                    max_sub_channels: None,
383                },
384            );
385        } else {
386            const NETVSP_DIO_INSTANCE: Guid = guid::guid!("d1ff4c5a-1b3c-4f0d-8e10-1b9d8b1d1cee");
387            self.config.vmbus_devices.push((
388                DeviceVtl::Vtl0,
389                netvsp_resources::NetvspHandle {
390                    instance_id: NETVSP_DIO_INSTANCE,
391                    mac_address: NIC_MAC_ADDRESS,
392                    endpoint,
393                    max_queues: None,
394                }
395                .into_resource(),
396            ));
397        }
398
399        self
400    }
401
402    /// Load with the specified VTL2 relocation mode.
403    pub fn with_vtl2_relocation_mode(mut self, mode: Vtl2BaseAddressType) -> Self {
404        let LoadMode::Igvm {
405            vtl2_base_address, ..
406        } = &mut self.config.load_mode
407        else {
408            panic!("vtl2 relocation mode is only supported for OpenHCL firmware")
409        };
410        *vtl2_base_address = mode;
411        self
412    }
413
414    /// Use a file-backed memory region instead of anonymous RAM.
415    ///
416    /// The file at the given path will be created (or opened) and sized to
417    /// match the VM's configured memory. Guest memory is then backed by
418    /// this file, which persists across snapshot save/restore.
419    ///
420    /// This forces shared (non-private) memory, since a file-backed mapping
421    /// is incompatible with private anonymous RAM. Panics if the caller
422    /// explicitly requested private memory via
423    /// [`MemoryConfig::private_memory`](crate::MemoryConfig::private_memory),
424    /// rather than silently downgrading it.
425    pub fn with_memory_backing_file(mut self, path: impl Into<std::path::PathBuf>) -> Self {
426        assert_ne!(
427            self.requested_private_memory,
428            Some(true),
429            "with_memory_backing_file forces shared memory, which conflicts with \
430             the explicitly requested private memory"
431        );
432        self.memory_backing_file = Some(path.into());
433        for node in &mut self.config.numa.nodes {
434            if let Some(mem) = &mut node.mem {
435                mem.private_memory = false;
436            }
437        }
438        self
439    }
440
441    /// Use explicit hugetlb-backed guest memory.
442    ///
443    /// This forces shared (non-private) memory, since hugetlb backing
444    /// requires a file-backed mapping rather than private anonymous RAM.
445    /// Panics if the caller explicitly requested private memory via
446    /// [`MemoryConfig::private_memory`](crate::MemoryConfig::private_memory),
447    /// rather than silently downgrading it.
448    pub fn with_hugepages(mut self, hugepage_size: Option<u64>) -> Self {
449        assert_ne!(
450            self.requested_private_memory,
451            Some(true),
452            "with_hugepages forces shared memory, which conflicts with the \
453             explicitly requested private memory"
454        );
455        for node in &mut self.config.numa.nodes {
456            if let Some(mem) = &mut node.mem {
457                mem.hugepages = true;
458                mem.hugepage_size = hugepage_size;
459                mem.private_memory = false;
460            }
461        }
462        self
463    }
464
465    /// Add a symmetric PCIe topology to the VM based on some basic scale factors
466    ///
467    /// All root ports are named according to their index within their parent
468    /// using the naming scheme `sXrcYrpZ`. For example, the third root port on
469    /// the fourth root complex in segment 0 would be named `s0rc3rp2`.
470    ///
471    /// This may be called multiple times to build asymmetric topologies (e.g. a
472    /// different number of root complexes per segment). Each call appends its
473    /// root complexes to segments numbered after any added by previous calls,
474    /// so the segment numbers in the `sXrcY` names continue from where the last
475    /// call left off.
476    pub fn with_pcie_root_topology(
477        mut self,
478        segment_count: u64,
479        root_complex_per_segment: u64,
480        root_ports_per_root_complex: u64,
481    ) -> Self {
482        const LOW_MMIO_SIZE: u64 = 64 * 1024 * 1024; // 64 MB
483        const HIGH_MMIO_SIZE: u64 = 1024 * 1024 * 1024; // 1 GB
484
485        // Offset the segments and global indices added by this call so that it
486        // can be called multiple times. New segments are numbered after any
487        // existing ones, and the global index continues from the existing
488        // root complex count.
489        let segment_base = self
490            .config
491            .pcie_root_complexes
492            .iter()
493            .map(|rc| u64::from(rc.segment) + 1)
494            .max()
495            .unwrap_or(0);
496        let index_base = self.config.pcie_root_complexes.len() as u64;
497
498        // Add the root complexes to the VM
499        for segment_offset in 0..segment_count {
500            let segment = segment_base + segment_offset;
501            let bus_count_per_rc = 256 / root_complex_per_segment;
502            for rc_index_in_segment in 0..root_complex_per_segment {
503                let index =
504                    index_base + segment_offset * root_complex_per_segment + rc_index_in_segment;
505                let name = format!("s{}rc{}", segment, rc_index_in_segment);
506
507                let start_bus = rc_index_in_segment * bus_count_per_rc;
508                let end_bus = start_bus + bus_count_per_rc - 1;
509
510                let ports = (0..root_ports_per_root_complex)
511                    .map(|i| PciePortConfig {
512                        name: format!("s{}rc{}rp{}", segment, rc_index_in_segment, i),
513                        devfn: None,
514                        hotplug: true,
515                        acs_capabilities_supported: Some(0),
516                        cxl: false,
517                        pasid: false,
518                    })
519                    .collect();
520
521                self.config.pcie_root_complexes.push(PcieRootComplexConfig {
522                    index: index.try_into().unwrap(),
523                    name,
524                    segment: segment.try_into().unwrap(),
525                    start_bus: start_bus.try_into().unwrap(),
526                    end_bus: end_bus.try_into().unwrap(),
527                    low_mmio: PcieMmioRangeConfig::Dynamic {
528                        size: LOW_MMIO_SIZE,
529                    },
530                    high_mmio: PcieMmioRangeConfig::Dynamic {
531                        size: HIGH_MMIO_SIZE,
532                    },
533                    cxl: None,
534                    ports,
535                    iommu: None,
536                    vnode: None,
537                    preserve_bars: false,
538                });
539            }
540        }
541
542        self
543    }
544
545    /// Add a PCIe switch to the VM.
546    pub fn with_pcie_switch(
547        mut self,
548        port_name: &str,
549        switch_name: &str,
550        port_count: u8,
551        hotplug: bool,
552    ) -> Self {
553        self.config.pcie_switches.push(PcieSwitchConfig {
554            name: switch_name.to_string(),
555            parent_port: port_name.to_string(),
556            ports: (0..port_count)
557                .map(|i| PciePortConfig {
558                    name: format!("{switch_name}-downstream-{i}"),
559                    devfn: None,
560                    hotplug,
561                    acs_capabilities_supported: Some(0),
562                    cxl: false,
563                    pasid: false,
564                })
565                .collect(),
566        });
567        self
568    }
569
570    /// Enable SMMUv3 IOMMU on the specified root complexes (aarch64 only).
571    ///
572    /// Each name must match a root complex added via
573    /// [`with_pcie_root_topology`](Self::with_pcie_root_topology). The SMMU
574    /// provides stage 1 IOVA translation for devices behind those root
575    /// complexes.
576    pub fn with_smmu(mut self, rc_names: &[&str]) -> Self {
577        for name in rc_names {
578            self.pending_iommu.push((
579                name.to_string(),
580                PcieIommuConfig::Smmu {
581                    accel: false,
582                    oas: openvmm_defs::config::SmmuOas::Auto,
583                },
584            ));
585        }
586        self
587    }
588
589    /// Enable an accelerated (iommufd-nested) SMMUv3 on the specified root
590    /// complexes (aarch64 only).
591    ///
592    /// Like [`with_smmu`](Self::with_smmu), but the SMMU programs the host
593    /// IOMMU for hardware nested stage-1 translation, so VFIO devices behind
594    /// these root complexes are permitted (and their guest-programmed stage-1
595    /// tables are honored via a host nested HWPT). Requires a host SMMU that
596    /// supports iommufd nesting.
597    pub fn with_smmu_accel(mut self, rc_names: &[&str]) -> Self {
598        for name in rc_names {
599            self.pending_iommu.push((
600                name.to_string(),
601                PcieIommuConfig::Smmu {
602                    accel: true,
603                    oas: openvmm_defs::config::SmmuOas::Auto,
604                },
605            ));
606        }
607        self
608    }
609
610    /// Enable AMD IOMMU (AMD-Vi) on the specified root complexes.
611    ///
612    /// Each name must match a root complex added via
613    /// [`with_pcie_root_topology`](Self::with_pcie_root_topology). The IOMMU
614    /// appears at device 0 function 0 on each listed root complex; PCIe
615    /// devices behind those root complexes have DMA translated through
616    /// guest-programmed page tables and MSIs remapped through the interrupt
617    /// remapping table.
618    pub fn with_amd_iommu(mut self, rc_names: &[&str]) -> Self {
619        for name in rc_names {
620            self.pending_iommu
621                .push((name.to_string(), PcieIommuConfig::AmdVi));
622        }
623        self
624    }
625
626    /// Enable Intel VT-d IOMMU on the specified root complexes.
627    ///
628    /// Each name must match a root complex added via
629    /// [`with_pcie_root_topology`](Self::with_pcie_root_topology). The IOMMU
630    /// is a platform device discovered via the ACPI DMAR table; PCIe devices
631    /// behind those root complexes have DMA translated through
632    /// guest-programmed page tables and MSIs remapped through the interrupt
633    /// remapping table.
634    pub fn with_intel_vtd(mut self, rc_names: &[&str]) -> Self {
635        for name in rc_names {
636            self.pending_iommu
637                .push((name.to_string(), PcieIommuConfig::IntelVtd));
638        }
639        self
640    }
641
642    /// This is intended for special one-off use cases. As soon as something
643    /// is needed in multiple tests we should consider making it a supported
644    /// pattern.
645    pub fn with_custom_config(mut self, f: impl FnOnce(&mut Config)) -> Self {
646        f(&mut self.config);
647        self
648    }
649
650    /// Specifies whether VTL2 should be allowed to access VTL0 memory before it
651    /// sets any VTL protections.
652    ///
653    /// This is needed just for the TMK VMM, and only until it gains support for
654    /// setting VTL protections.
655    pub fn with_allow_early_vtl0_access(mut self, allow: bool) -> Self {
656        self.config
657            .hypervisor
658            .with_vtl2
659            .as_mut()
660            .unwrap()
661            .late_map_vtl0_memory =
662            (!allow).then_some(openvmm_defs::config::LateMapVtl0MemoryPolicy::InjectException);
663
664        self
665    }
666}