Skip to main content

petri/vm/openvmm/
construct.rs

1// Copyright (c) Microsoft Corporation.
2// Licensed under the MIT License.
3
4//! Contains [`PetriVmConfigOpenVmm::new`], which builds a [`PetriVmConfigOpenVmm`] with all
5//! default settings for a given [`Firmware`] and [`MachineArch`].
6
7use super::PetriVmConfigOpenVmm;
8use super::PetriVmResourcesOpenVmm;
9use crate::Drive;
10use crate::EfiDiagnosticsLogLevel;
11use crate::Firmware;
12use crate::IsolationType;
13use crate::MemoryConfig;
14use crate::OpenHclConfig;
15use crate::PcieNvmeDrive;
16use crate::PcieVirtioBlkDrive;
17use crate::PetriLogSource;
18use crate::PetriVmConfig;
19use crate::PetriVmResources;
20use crate::PetriVmgsResource;
21use crate::ProcessorTopology;
22use crate::SecureBootTemplate;
23use crate::TpmConfig;
24use crate::UefiConfig;
25use crate::VmbusStorageType;
26use crate::linux_direct_serial_agent::LinuxDirectSerialAgent;
27
28use crate::SIZE_1_MB;
29use crate::VmbusStorageController;
30use crate::openvmm::memdiff_vmgs;
31use crate::openvmm::petri_disk_to_openvmm;
32use crate::vm::PetriVmProperties;
33use crate::vm::append_cmdline;
34use anyhow::Context;
35use framebuffer::FRAMEBUFFER_SIZE;
36use framebuffer::Framebuffer;
37use framebuffer::FramebufferAccess;
38use fs_err::File;
39use futures::StreamExt;
40use get_resources::crash::GuestCrashDeviceHandle;
41use get_resources::ged::FirmwareEvent;
42use guid::Guid;
43use hyperv_ic_resources::shutdown::ShutdownIcHandle;
44use ide_resources::GuestMedia;
45use ide_resources::IdeDeviceConfig;
46use mesh_process::Mesh;
47use nvme_resources::NamespaceDefinition;
48use nvme_resources::NvmeControllerHandle;
49use openvmm_defs::config::Config;
50use openvmm_defs::config::DEFAULT_PCAT_BOOT_ORDER;
51use openvmm_defs::config::DeviceVtl;
52use openvmm_defs::config::HypervisorConfig;
53use openvmm_defs::config::LateMapVtl0MemoryPolicy;
54use openvmm_defs::config::LoadMode;
55use openvmm_defs::config::NumaNode;
56use openvmm_defs::config::NumaTopology;
57use openvmm_defs::config::PcieDeviceConfig;
58use openvmm_defs::config::ProcessorTopologyConfig;
59use openvmm_defs::config::SerialInformation;
60use openvmm_defs::config::VmbusConfig;
61use openvmm_defs::config::VpAssignment;
62use openvmm_defs::config::VpciDeviceConfig;
63use openvmm_defs::config::Vtl2BaseAddressType;
64use openvmm_defs::config::Vtl2Config;
65use openvmm_pcat_locator::RomFileLocation;
66use pal_async::DefaultDriver;
67use pal_async::socket::PolledSocket;
68use pal_async::task::Spawn;
69use pal_async::task::Task;
70use petri_artifacts_common::tags::MachineArch;
71use petri_artifacts_core::ResolvedArtifact;
72use pipette_client::PIPETTE_PORT;
73use scsidisk_resources::SimpleScsiDiskHandle;
74use scsidisk_resources::SimpleScsiDvdHandle;
75use serial_16550_resources::ComPort;
76use serial_core::resources::DisconnectedSerialBackendHandle;
77use serial_socket::net::OpenSocketSerialConfig;
78use sparse_mmap::alloc_shared_memory;
79use std::collections::HashMap;
80use storvsp_resources::ScsiControllerHandle;
81use storvsp_resources::ScsiDeviceAndPath;
82use storvsp_resources::ScsiPath;
83use tempfile::TempPath;
84use tpm_resources::TpmDeviceHandle;
85use tpm_resources::TpmRegisterLayout;
86use uidevices_resources::SynthVideoHandle;
87use unix_socket::UnixListener;
88use unix_socket::UnixStream;
89use video_core::SharedFramebufferHandle;
90use virtio_resources::VirtioPciDeviceHandle;
91use virtio_resources::blk::VirtioBlkHandle;
92use virtio_resources::vsock::VirtioVsockHandle;
93#[cfg(target_os = "linux")]
94use virtio_resources::vsock::VirtioVsockVhostHandle;
95use vm_manifest_builder::VmChipsetResult;
96use vm_manifest_builder::VmManifestBuilder;
97use vm_resource::IntoResource;
98use vm_resource::Resource;
99use vm_resource::kind::SerialBackendHandle;
100use vm_resource::kind::VirtioDeviceHandle;
101use vm_resource::kind::VmbusDeviceHandleKind;
102use vmbus_serial_resources::VmbusSerialDeviceHandle;
103use vmbus_serial_resources::VmbusSerialPort;
104use vmcore::non_volatile_store::resources::EphemeralNonVolatileStoreHandle;
105use vmgs_resources::GuestStateEncryptionPolicy;
106use vmgs_resources::VmgsFileHandle;
107use vmotherboard::ChipsetDeviceHandle;
108
109impl PetriVmConfigOpenVmm {
110    /// Create a new VM configuration.
111    pub async fn new(
112        openvmm_path: &ResolvedArtifact,
113        petri_vm_config: PetriVmConfig,
114        resources: &PetriVmResources,
115        properties: PetriVmProperties,
116    ) -> anyhow::Result<Self> {
117        let PetriVmConfig {
118            name: _,
119            arch,
120            host_log_levels,
121            firmware,
122            memory,
123            proc_topology,
124            vmgs,
125            tpm: tpm_config,
126            vmbus_storage_controllers,
127            pcie_nvme_drives,
128            pcie_virtio_blk_drives,
129            physical_nvme_devices,
130        } = petri_vm_config;
131
132        if !physical_nvme_devices.is_empty() {
133            anyhow::bail!("Physical NVMe devices are only supported with the Hyper-V backend");
134        }
135
136        tracing::debug!(?firmware, ?arch, "Petri VM firmware configuration");
137
138        let PetriVmResources { driver, log_source } = resources;
139        #[cfg(target_os = "linux")]
140        let vhost_vsock_guest_cid = properties.vhost_vsock_guest_cid;
141        #[cfg(not(target_os = "linux"))]
142        let vhost_vsock_guest_cid: Option<u32> = None;
143
144        let mesh = Mesh::new("petri_mesh".to_string())?;
145
146        let setup = PetriVmConfigSetupCore {
147            arch,
148            firmware: &firmware,
149            driver,
150            logger: log_source,
151            vmgs: &vmgs,
152            tpm_config: tpm_config.as_ref(),
153            mesh: &mesh,
154            openvmm_path,
155            uses_pipette_as_init: properties.uses_pipette_as_init,
156            enable_serial: properties.enable_serial,
157            use_virtio_vsock: properties.use_virtio_vsock,
158            no_vmbus: properties.no_vmbus,
159            no_hv: properties.no_hv,
160        };
161
162        let mut chipset = VmManifestBuilder::new(
163            match firmware {
164                Firmware::LinuxDirect { .. } => {
165                    vm_manifest_builder::BaseChipsetType::HyperVGen2LinuxDirect
166                }
167                Firmware::OpenhclLinuxDirect { .. } => {
168                    vm_manifest_builder::BaseChipsetType::HclHost
169                }
170                Firmware::OpenhclUefi { .. } => vm_manifest_builder::BaseChipsetType::HclHost,
171                Firmware::Pcat { .. } => vm_manifest_builder::BaseChipsetType::HypervGen1,
172                Firmware::Uefi { .. } => vm_manifest_builder::BaseChipsetType::HypervGen2Uefi,
173                Firmware::OpenhclPcat { .. } => todo!("OpenVMM OpenHCL PCAT"),
174            },
175            match arch {
176                MachineArch::X86_64 => vm_manifest_builder::MachineArch::X86_64,
177                MachineArch::Aarch64 => vm_manifest_builder::MachineArch::Aarch64,
178            },
179        );
180
181        let mut load_mode = setup.load_firmware()?;
182
183        // If using pipette-as-init, replace the initrd with the pre-built
184        // one that has pipette injected. run_core() guarantees that
185        // prebuilt_initrd is set when uses_pipette_as_init is true.
186        if properties.uses_pipette_as_init {
187            if let LoadMode::Linux { initrd, .. } = &mut load_mode {
188                let prebuilt = properties
189                    .prebuilt_initrd
190                    .as_ref()
191                    .expect("uses_pipette_as_init requires prebuilt_initrd");
192                let file = std::fs::File::open(prebuilt).with_context(|| {
193                    format!("failed to open prebuilt initrd at {}", prebuilt.display())
194                })?;
195                *initrd = Some(file);
196            }
197        }
198
199        let (emulated_serial_config, log_stream_tasks, linux_direct_serial_agent) =
200            if !properties.enable_serial {
201                // No emulated serial backends (OpenHCL VMBus serial stubs may still exist)
202                ([None, None, None, None], Vec::new(), None)
203            } else {
204                let SerialData {
205                    emulated_serial_config,
206                    serial_tasks,
207                    linux_direct_serial_agent,
208                } = setup.configure_serial(log_source)?;
209                (
210                    emulated_serial_config,
211                    serial_tasks,
212                    linux_direct_serial_agent,
213                )
214            };
215        let mut emulated_serial_config = emulated_serial_config;
216
217        let (video_dev, framebuffer, framebuffer_view) = match setup.config_video()? {
218            Some((v, fb, fba)) => {
219                chipset = chipset.with_framebuffer();
220                (Some(v), Some(fb), Some(fba.view()?))
221            }
222            None => (None, None, None),
223        };
224
225        if properties.no_vmbus {
226            chipset = chipset.without_vmbus();
227        }
228
229        let (ide_disks, storvsp_ide_handles) =
230            ide_controllers_to_openvmm(firmware.ide_controllers()).await?;
231        let (mut vmbus_devices, vpci_devices) =
232            vmbus_storage_controllers_to_openvmm(&vmbus_storage_controllers).await?;
233
234        let mut pcie_devices = Vec::new();
235        for PcieNvmeDrive {
236            port_name,
237            nsid,
238            drive: Drive { disk, .. },
239        } in pcie_nvme_drives
240        {
241            let disk = disk.ok_or_else(|| {
242                anyhow::anyhow!(
243                    "missing disk for PCIe NVMe drive on port '{port_name}' (nsid {nsid})"
244                )
245            })?;
246            let disk = petri_disk_to_openvmm(&disk).await?;
247            pcie_devices.push(PcieDeviceConfig {
248                port_name,
249                resource: NvmeControllerHandle {
250                    subsystem_id: Guid::new_random(),
251                    max_io_queues: 64,
252                    msix_count: 64,
253                    namespaces: vec![NamespaceDefinition {
254                        nsid,
255                        read_only: false,
256                        disk,
257                    }],
258                    requests: None,
259                }
260                .into_resource(),
261            });
262        }
263
264        for PcieVirtioBlkDrive {
265            port_name,
266            drive: Drive { disk, .. },
267        } in pcie_virtio_blk_drives
268        {
269            let disk = disk.ok_or_else(|| {
270                anyhow::anyhow!("missing disk for PCIe virtio-blk drive on port '{port_name}'")
271            })?;
272            let disk = petri_disk_to_openvmm(&disk).await?;
273            pcie_devices.push(PcieDeviceConfig {
274                port_name,
275                resource: VirtioPciDeviceHandle(
276                    VirtioBlkHandle {
277                        disk,
278                        read_only: false,
279                    }
280                    .into_resource(),
281                )
282                .into_resource(),
283            });
284        }
285
286        if !storvsp_ide_handles.is_empty() {
287            anyhow::ensure!(
288                !properties.no_vmbus,
289                "IDE accelerator requires VMBus to be enabled"
290            );
291            vmbus_devices.extend(storvsp_ide_handles);
292        }
293
294        let (firmware_event_send, firmware_event_recv) = mesh::mpsc_channel();
295
296        let make_vsock_listener = || -> anyhow::Result<(UnixListener, TempPath)> {
297            Ok(tempfile::Builder::new()
298                .make(|path| UnixListener::bind(path))?
299                .into_parts())
300        };
301
302        let (with_vtl2, vtl2_vmbus, ged, ged_send, vtl2_vsock_path) = if firmware.is_openhcl() {
303            let (ged, ged_send) = setup
304                .config_openhcl_vmbus_devices(
305                    &mut emulated_serial_config,
306                    &mut vmbus_devices,
307                    &firmware_event_send,
308                    framebuffer.is_some(),
309                )
310                .await?;
311
312            let late_map_vtl0_memory = match load_mode {
313                LoadMode::Igvm {
314                    vtl2_base_address: Vtl2BaseAddressType::Vtl2Allocate { .. },
315                    ..
316                } => {
317                    // Late Map VTL0 memory not supported when test supplies Vtl2Allocate
318                    None
319                }
320                _ => Some(LateMapVtl0MemoryPolicy::InjectException),
321            };
322
323            let (vtl2_vsock_listener, vtl2_vsock_path) = make_vsock_listener()?;
324            (
325                Some(Vtl2Config {
326                    vtl0_alias_map: false, // TODO: enable when OpenVMM supports it for DMA
327                    late_map_vtl0_memory,
328                }),
329                Some(VmbusConfig {
330                    vsock_listener: Some(vtl2_vsock_listener),
331                    vsock_path: Some(vtl2_vsock_path.to_string_lossy().into_owned()),
332                    vmbus_max_version: None,
333                    vtl2_redirect: false,
334                    #[cfg(windows)]
335                    vmbusproxy_handle: None,
336                }),
337                Some(ged),
338                Some(ged_send),
339                Some(vtl2_vsock_path),
340            )
341        } else {
342            (None, None, None, None, None)
343        };
344
345        // Configure the serial ports now that they have been updated by the
346        // OpenHCL configuration.
347        if properties.enable_serial {
348            chipset = chipset.with_serial(emulated_serial_config);
349            // Set so that we don't pull serial data until the guest is
350            // ready. Otherwise, Linux will drop the input serial data
351            // on the floor during boot.
352            if matches!(firmware, Firmware::LinuxDirect { .. }) && !properties.uses_pipette_as_init
353            {
354                chipset = chipset.with_serial_wait_for_rts();
355            }
356        }
357
358        // Extract video configuration
359        let vga_firmware = match video_dev {
360            Some(VideoDevice::Vga(firmware)) => Some(firmware),
361            Some(VideoDevice::Synth(vtl, resource)) => {
362                vmbus_devices.push((vtl, resource));
363                None
364            }
365            None => None,
366        };
367
368        // Add default VMBus devices (skipped in minimal mode and no-vmbus mode).
369        let (shutdown_ic_send, kvp_ic_send) = if !properties.minimal_mode && !properties.no_vmbus {
370            let (shutdown_ic_send, shutdown_ic_recv) = mesh::channel();
371            vmbus_devices.push((
372                DeviceVtl::Vtl0,
373                ShutdownIcHandle {
374                    recv: shutdown_ic_recv,
375                }
376                .into_resource(),
377            ));
378
379            let (kvp_ic_send, kvp_ic_recv) = mesh::channel();
380            vmbus_devices.push((
381                DeviceVtl::Vtl0,
382                hyperv_ic_resources::kvp::KvpIcHandle { recv: kvp_ic_recv }.into_resource(),
383            ));
384
385            vmbus_devices.push((
386                DeviceVtl::Vtl0,
387                hyperv_ic_resources::timesync::TimesyncIcHandle.into_resource(),
388            ));
389
390            (Some(shutdown_ic_send), Some(kvp_ic_send))
391        } else {
392            (None, None)
393        };
394
395        // Make a vmbus or virtio vsock path for pipette connections
396        let (vsock_listener, vsock_path) = make_vsock_listener()?;
397        let mut vsock_listener = Some(vsock_listener);
398        let vsock_path_string = vsock_path.to_string_lossy();
399
400        // Configure the UEFI helper device on the chipset for Firmware::Uefi.
401        // OpenhclUefi uses BaseChipsetType::HclHost, so it does not need this.
402        if matches!(firmware, Firmware::Uefi { .. }) {
403            use firmware_uefi_resources::aarch64_secure_boot_templates;
404            use firmware_uefi_resources::x64_secure_boot_templates;
405
406            let uefi_cfg = firmware.uefi_config();
407            let base_template =
408                uefi_cfg
409                    .and_then(|c| c.secure_boot_template)
410                    .map(|template| match (arch, template) {
411                        (MachineArch::X86_64, SecureBootTemplate::MicrosoftWindows) => {
412                            x64_secure_boot_templates::microsoft_windows()
413                        }
414                        (
415                            MachineArch::X86_64,
416                            SecureBootTemplate::MicrosoftUefiCertificateAuthority,
417                        ) => x64_secure_boot_templates::microsoft_uefi_ca(),
418                        (MachineArch::Aarch64, SecureBootTemplate::MicrosoftWindows) => {
419                            aarch64_secure_boot_templates::microsoft_windows()
420                        }
421                        (
422                            MachineArch::Aarch64,
423                            SecureBootTemplate::MicrosoftUefiCertificateAuthority,
424                        ) => aarch64_secure_boot_templates::microsoft_uefi_ca(),
425                    });
426            let custom_uefi_json = uefi_cfg
427                .and_then(|c| c.custom_uefi_json.clone())
428                .map(Into::into);
429            let secure_boot = uefi_cfg.is_some_and(|c| c.secure_boot_enabled);
430            let log_level = match uefi_cfg
431                .map(|c| c.efi_diagnostics_log_level)
432                .unwrap_or_default()
433            {
434                EfiDiagnosticsLogLevel::Default => {
435                    firmware_uefi_resources::LogLevel::make_default()
436                }
437                EfiDiagnosticsLogLevel::Info => firmware_uefi_resources::LogLevel::make_info(),
438                EfiDiagnosticsLogLevel::Full => firmware_uefi_resources::LogLevel::make_full(),
439            };
440            let diagnostics_rate_limit = uefi_cfg.and_then(|c| c.efi_diagnostics_rate_limit);
441            let nvram_storage = if vmgs.disk().is_some() {
442                VmgsFileHandle::new(vmgs_format::FileId::BIOS_NVRAM, true).into_resource()
443            } else {
444                EphemeralNonVolatileStoreHandle.into_resource()
445            };
446            chipset = chipset.with_uefi(vm_manifest_builder::UefiManifest::new(
447                match arch {
448                    MachineArch::X86_64 => vm_manifest_builder::MachineArch::X86_64,
449                    MachineArch::Aarch64 => vm_manifest_builder::MachineArch::Aarch64,
450                },
451                base_template,
452                custom_uefi_json,
453                secure_boot,
454                log_level,
455                diagnostics_rate_limit,
456                nvram_storage,
457                None,
458            ));
459        }
460
461        let layout_config = chipset.layout_config();
462        let chipset = chipset
463            .build()
464            .context("failed to build chipset configuration")?;
465
466        // Preserve the caller's explicit private-memory request so that backend
467        // methods which force shared memory can fail on an explicit conflict.
468        let requested_private_memory = memory.private_memory;
469
470        let numa = {
471            let MemoryConfig {
472                startup_bytes,
473                dynamic_memory_range,
474                numa_mem_sizes,
475                private_memory,
476                transparent_hugepages,
477            } = memory;
478
479            if dynamic_memory_range.is_some() {
480                anyhow::bail!("dynamic memory not supported in OpenVMM");
481            }
482
483            // Private (anonymous) guest memory is incompatible with two
484            // OpenVMM features that petri enables based on the firmware:
485            // - OpenHCL uses a remote VA mapper to share VTL0 RAM with VTL2,
486            //   which requires a shareable memory section.
487            // - PCAT (Gen1) relies on x86 legacy support (the VGA hole and
488            //   PAM registers), which toggles low RAM visibility in a way
489            //   that requires shared, file-backed memory.
490            let private_incompatible =
491                firmware.is_openhcl() || firmware.is_pcat() || vhost_vsock_guest_cid.is_some();
492            let private_memory = match private_memory {
493                // An explicit request for private memory that the firmware
494                // cannot honor is an error, rather than a silent downgrade.
495                Some(true) if private_incompatible => {
496                    anyhow::bail!(
497                        "private guest memory was explicitly requested but is \
498                         not supported with this configuration (OpenHCL, \
499                         PCAT/Gen1, and kernel vhost-vsock require shared memory)"
500                    );
501                }
502                Some(explicit) => explicit,
503                // Default: prefer private memory for performance, falling back
504                // to shared when the firmware requires it.
505                None => !private_incompatible,
506            };
507
508            // THP applies to both private anonymous and shared (file/memfd)
509            // guest RAM, and on both Linux (madvise-based) and Windows
510            // (soft large pages). The membacking layer suppresses it where it
511            // does not apply (e.g. explicit hugetlb backings), so pass the
512            // requested value through unchanged.
513            let make_mem = |size: u64| openvmm_defs::config::MemoryConfig {
514                mem_size: size,
515                prefetch_memory: false,
516                private_memory,
517                transparent_hugepages,
518                hugepages: false,
519                hugepage_size: None,
520                host_numa_node: None,
521            };
522
523            if let Some(sizes) = numa_mem_sizes {
524                NumaTopology {
525                    nodes: sizes
526                        .into_iter()
527                        .map(|size| NumaNode {
528                            mem: if size > 0 { Some(make_mem(size)) } else { None },
529                            vps: VpAssignment::FromTopology,
530                        })
531                        .collect(),
532                    distances: vec![],
533                }
534            } else {
535                NumaTopology {
536                    nodes: vec![NumaNode {
537                        mem: Some(make_mem(startup_bytes)),
538                        vps: VpAssignment::FromTopology,
539                    }],
540                    distances: vec![],
541                }
542            }
543        };
544
545        let processor_topology = {
546            let ProcessorTopology {
547                vp_count,
548                enable_smt,
549                vps_per_socket,
550                apic_mode,
551            } = proc_topology;
552
553            ProcessorTopologyConfig {
554                proc_count: vp_count,
555                vps_per_socket,
556                enable_smt,
557                arch: Some(match arch {
558                    MachineArch::X86_64 => openvmm_defs::config::ArchTopologyConfig::X86(
559                        openvmm_defs::config::X86TopologyConfig {
560                            x2apic: match apic_mode {
561                                None => openvmm_defs::config::X2ApicConfig::Auto,
562                                Some(x) => match x {
563                                    crate::ApicMode::Xapic => {
564                                        openvmm_defs::config::X2ApicConfig::Unsupported
565                                    }
566                                    crate::ApicMode::X2apicSupported => {
567                                        openvmm_defs::config::X2ApicConfig::Supported
568                                    }
569                                    crate::ApicMode::X2apicEnabled => {
570                                        openvmm_defs::config::X2ApicConfig::Enabled
571                                    }
572                                },
573                            },
574                            ..Default::default()
575                        },
576                    ),
577                    MachineArch::Aarch64 => openvmm_defs::config::ArchTopologyConfig::Aarch64(
578                        openvmm_defs::config::Aarch64TopologyConfig::default(),
579                    ),
580                }),
581            }
582        };
583
584        let vmgs = if firmware.is_openhcl() {
585            None
586        } else {
587            Some(memdiff_vmgs(&vmgs).await?)
588        };
589
590        let VmChipsetResult {
591            chipset,
592            mut chipset_devices,
593            pci_chipset_devices,
594            isa_dma_controller,
595            capabilities,
596        } = chipset;
597
598        // Add the TPM
599        if let Some(tpm) = setup.config_tpm().await? {
600            chipset_devices.push(tpm);
601        }
602
603        // Set up virtio-vsock if enabled.
604        // Find the first unused PCIe root port to avoid conflicting with
605        // NVMe devices that were already assigned.
606        if properties.use_virtio_vsock {
607            let vsock_port = (0..)
608                .map(|i| format!("s0rc0rp{i}"))
609                .find(|name| !pcie_devices.iter().any(|d| d.port_name == *name))
610                .unwrap();
611            let resource: Resource<VirtioDeviceHandle> = match vhost_vsock_guest_cid {
612                #[cfg(target_os = "linux")]
613                Some(guest_cid) => {
614                    let vhost = std::fs::OpenOptions::new()
615                        .read(true)
616                        .write(true)
617                        .open("/dev/vhost-vsock")
618                        .context("failed to open /dev/vhost-vsock")?
619                        .into();
620                    // The kernel backend does not use the Unix relay. Clear it
621                    // so VmbusConfig below does not receive the listener.
622                    vsock_listener = None;
623                    VirtioVsockVhostHandle { vhost, guest_cid }.into_resource()
624                }
625                #[cfg(not(target_os = "linux"))]
626                Some(_) => unreachable!("kernel vhost-vsock is Linux-only"),
627                None => VirtioVsockHandle {
628                    guest_cid: 0x3,
629                    base_path: vsock_path_string.to_string(),
630                    listener: vsock_listener.take().unwrap(),
631                }
632                .into_resource(),
633            };
634            pcie_devices.push(PcieDeviceConfig {
635                port_name: vsock_port,
636                resource: VirtioPciDeviceHandle(resource).into_resource(),
637            });
638        }
639
640        let config = Config {
641            // Firmware
642            load_mode,
643            firmware_event_send: Some(firmware_event_send),
644
645            // CPU and RAM
646            numa,
647            processor_topology,
648
649            // Base chipset
650            chipset,
651            chipset_devices,
652            pci_chipset_devices,
653            isa_dma_controller,
654            chipset_capabilities: capabilities,
655            layout: layout_config,
656
657            // Basic virtualization device support
658            hypervisor: HypervisorConfig {
659                with_hv: !properties.no_hv,
660                with_vtl2,
661                with_isolation: match firmware.isolation() {
662                    Some(IsolationType::Vbs) => Some(openvmm_defs::config::IsolationType::Vbs),
663                    None => None,
664                    _ => anyhow::bail!("unsupported isolation type"),
665                },
666                nested_virt: false,
667            },
668            vmbus: if properties.no_vmbus {
669                None
670            } else {
671                Some(VmbusConfig {
672                    // If virtio vsock is enabled, the vsock_listener will have already been taken
673                    // and is now None.
674                    vsock_listener,
675                    vsock_path: (!properties.use_virtio_vsock)
676                        .then(|| vsock_path_string.to_string()),
677                    vmbus_max_version: None,
678                    vtl2_redirect: firmware.openhcl_config().is_some_and(|c| c.vmbus_redirect),
679                    #[cfg(windows)]
680                    vmbusproxy_handle: None,
681                })
682            },
683            vtl2_vmbus,
684
685            // Devices
686            floppy_disks: vec![],
687            ide_disks,
688            pcie_root_complexes: vec![],
689            pcie_ecam_below_4gb: false,
690            pcie_devices,
691            pcie_switches: vec![],
692            pcie_generic_initiators: vec![],
693            vpci_devices,
694            vmbus_devices,
695
696            // Video support
697            framebuffer,
698            vga_firmware,
699
700            vmgs,
701
702            // Disabled for VMM tests by default
703            #[cfg(windows)]
704            kernel_vmnics: vec![],
705            input: mesh::Receiver::new(),
706            vtl2_gfx: false,
707            virtio_devices: vec![],
708            #[cfg(windows)]
709            vpci_resources: vec![],
710            debugger_rpc: None,
711            rtc_delta_milliseconds: 0,
712        };
713
714        // Make the pipette connection listener.
715        let path = format!("{vsock_path_string}_{PIPETTE_PORT}");
716        let pipette_listener = PolledSocket::new(
717            driver,
718            UnixListener::bind(path).context("failed to bind to pipette listener")?,
719        )?;
720
721        // Make the vtl2 pipette connection listener.
722        let vtl2_pipette_listener = if let Some(vtl2_vmbus) = &config.vtl2_vmbus {
723            let path = vtl2_vmbus.vsock_path.as_ref().unwrap();
724            let path = format!("{path}_{PIPETTE_PORT}");
725            Some(PolledSocket::new(
726                driver,
727                UnixListener::bind(path).context("failed to bind to vtl2 pipette listener")?,
728            )?)
729        } else {
730            None
731        };
732
733        Ok(Self {
734            runtime_config: firmware.into_runtime_config(vmbus_storage_controllers),
735            arch,
736            host_log_levels,
737            config,
738            mesh,
739
740            resources: PetriVmResourcesOpenVmm {
741                log_stream_tasks,
742                firmware_event_recv,
743                shutdown_ic_send,
744                kvp_ic_send,
745                ged_send,
746                pipette_listener,
747                vtl2_pipette_listener,
748                linux_direct_serial_agent,
749                tcp_pipette_port: None,
750                driver: driver.clone(),
751                output_dir: log_source.output_dir().to_owned(),
752                openvmm_path: openvmm_path.clone(),
753                vtl2_vsock_path,
754                _vsock_path: vsock_path,
755                properties,
756                #[cfg(windows)]
757                _switch_ports: Vec::new(),
758            },
759
760            openvmm_log_file: log_source.log_file("openvmm")?,
761
762            memory_backing_file: None,
763            requested_private_memory,
764
765            ged,
766            framebuffer_view,
767
768            pending_iommu: Vec::new(),
769        })
770    }
771}
772
773struct PetriVmConfigSetupCore<'a> {
774    arch: MachineArch,
775    firmware: &'a Firmware,
776    driver: &'a DefaultDriver,
777    logger: &'a PetriLogSource,
778    vmgs: &'a PetriVmgsResource,
779    tpm_config: Option<&'a TpmConfig>,
780    mesh: &'a Mesh,
781    openvmm_path: &'a ResolvedArtifact,
782    uses_pipette_as_init: bool,
783    enable_serial: bool,
784    use_virtio_vsock: bool,
785    no_vmbus: bool,
786    no_hv: bool,
787}
788
789struct SerialData {
790    emulated_serial_config: [Option<Resource<SerialBackendHandle>>; 4],
791    serial_tasks: Vec<Task<anyhow::Result<()>>>,
792    linux_direct_serial_agent: Option<LinuxDirectSerialAgent>,
793}
794
795enum VideoDevice {
796    Vga(RomFileLocation),
797    Synth(DeviceVtl, Resource<VmbusDeviceHandleKind>),
798}
799
800impl PetriVmConfigSetupCore<'_> {
801    fn configure_serial(&self, logger: &PetriLogSource) -> anyhow::Result<SerialData> {
802        let mut serial_tasks = Vec::new();
803
804        let serial0_log_file = logger.log_file(match self.firmware {
805            Firmware::LinuxDirect { .. } | Firmware::OpenhclLinuxDirect { .. } => "linux",
806            Firmware::Pcat { .. } | Firmware::OpenhclPcat { .. } => "pcat",
807            Firmware::Uefi { .. } | Firmware::OpenhclUefi { .. } => "uefi",
808        })?;
809
810        let (serial0_host, serial0) = self
811            .create_serial_stream()
812            .context("failed to create serial0 stream")?;
813        let (serial0_read, serial0_write) = serial0_host.split();
814        let serial0_task = self.driver.spawn(
815            "serial0-console",
816            crate::log_task(serial0_log_file, serial0_read, "serial0-console"),
817        );
818        serial_tasks.push(serial0_task);
819
820        let serial2 = if self.firmware.is_openhcl() {
821            let (serial2_host, serial2) = self
822                .create_serial_stream()
823                .context("failed to create serial2 stream")?;
824            let serial2_task = self.driver.spawn(
825                "serial2-openhcl",
826                crate::log_task(logger.log_file("openhcl")?, serial2_host, "serial2-openhcl"),
827            );
828            serial_tasks.push(serial2_task);
829            serial2
830        } else {
831            None
832        };
833
834        if self.firmware.is_linux_direct() && !self.uses_pipette_as_init {
835            // Non-pipette-as-init Linux direct: create serial1 and a serial
836            // agent so we can send shell commands to launch pipette.
837            let (serial1_host, serial1) = self.create_serial_stream()?;
838            let (serial1_read, _serial1_write) = serial1_host.split();
839            let linux_direct_serial_agent =
840                LinuxDirectSerialAgent::new(serial1_read, serial0_write);
841            Ok(SerialData {
842                emulated_serial_config: [serial0, serial1, serial2, None],
843                serial_tasks,
844                linux_direct_serial_agent: Some(linux_direct_serial_agent),
845            })
846        } else {
847            Ok(SerialData {
848                emulated_serial_config: [serial0, None, serial2, None],
849                serial_tasks,
850                linux_direct_serial_agent: None,
851            })
852        }
853    }
854
855    fn create_serial_stream(
856        &self,
857    ) -> anyhow::Result<(
858        PolledSocket<UnixStream>,
859        Option<Resource<SerialBackendHandle>>,
860    )> {
861        let (host_side, guest_side) = UnixStream::pair()?;
862        let host_side = PolledSocket::new(self.driver, host_side)?;
863        let serial = OpenSocketSerialConfig::from(guest_side).into_resource();
864        Ok((host_side, Some(serial)))
865    }
866
867    fn load_firmware(&self) -> anyhow::Result<LoadMode> {
868        // The test kernel has both CONFIG_VIRTIO_VSOCK=y and
869        // CONFIG_HYPERV_VSOCKETS=y built in. The kernel only allows one G2H
870        // vsock transport, and virtio_vsock_init runs first, claiming the
871        // slot. This causes hv_sock registration to fail with -EBUSY,
872        // breaking pipette's AF_VSOCK connection. Blacklist either
873        // virtio_vsock_init or hv_sock_init depending on which vsock transport
874        // is being used.
875        const VIRTIO_VSOCK_BLACKLIST: &str = "initcall_blacklist=virtio_vsock_init";
876        let vsock_blacklist = if self.use_virtio_vsock {
877            "initcall_blacklist=hv_sock_init"
878        } else {
879            VIRTIO_VSOCK_BLACKLIST
880        };
881
882        Ok(match (self.arch, &self.firmware) {
883            (arch, Firmware::LinuxDirect { kernel, initrd }) => {
884                let console = match arch {
885                    MachineArch::X86_64 => "console=ttyS0",
886                    MachineArch::Aarch64 => "console=ttyAMA0 earlycon",
887                };
888                let kernel = File::open(kernel.clone())
889                    .context("Failed to open kernel")?
890                    .into();
891                let initrd = File::open(initrd.clone())
892                    .context("Failed to open initrd")?
893                    .into();
894
895                let init = if self.uses_pipette_as_init {
896                    "/pipette"
897                } else {
898                    "/bin/sh"
899                };
900
901                let serial_args = if self.enable_serial {
902                    format!("{console} debug ")
903                } else {
904                    String::new()
905                };
906
907                let cmdline = format!("{serial_args}panic=-1 rdinit={init} {vsock_blacklist}");
908
909                LoadMode::Linux {
910                    kernel,
911                    initrd: Some(initrd),
912                    cmdline,
913                    enable_serial: self.enable_serial,
914                    boot_mode: openvmm_defs::config::LinuxDirectBootMode::Acpi,
915                }
916            }
917            (
918                MachineArch::X86_64,
919                Firmware::Pcat {
920                    bios_firmware: firmware,
921                    guest: _,         // load_boot_disk
922                    svga_firmware: _, // config_video
923                    ide_controllers: _,
924                },
925            ) => {
926                let firmware = openvmm_pcat_locator::find_pcat_bios(firmware.get())
927                    .context("Failed to load packaged PCAT binary")?;
928                LoadMode::Pcat {
929                    firmware,
930                    boot_order: DEFAULT_PCAT_BOOT_ORDER,
931                }
932            }
933            (
934                _,
935                Firmware::Uefi {
936                    uefi_firmware: firmware,
937                    guest: _, // load_boot_disk
938                    uefi_config:
939                        UefiConfig {
940                            secure_boot_enabled: _,  // new
941                            secure_boot_template: _, // new
942                            custom_uefi_json: _,     // applied device-side via UefiManifest::new
943                            disable_frontpage,
944                            default_boot_always_attempt,
945                            enable_vpci_boot,
946                            force_dma_bounce,
947                            efi_diagnostics_log_level: _, // applied device-side via UefiManifest::new
948                            efi_diagnostics_rate_limit: _, // applied device-side via UefiManifest::new
949                        },
950                },
951            ) => {
952                anyhow::ensure!(
953                    !(self.no_hv && self.arch == MachineArch::X86_64),
954                    "x86_64 UEFI firmware requires Hyper-V enlightenments"
955                );
956                let firmware = File::open(firmware.clone())
957                    .context("Failed to open uefi firmware file")?
958                    .into();
959                LoadMode::Uefi {
960                    firmware,
961                    enable_debugging: false,
962                    enable_memory_protections: false,
963                    disable_frontpage: *disable_frontpage,
964                    enable_tpm: self.tpm_config.is_some(),
965                    enable_battery: false,
966                    enable_serial: true,
967                    enable_vpci_boot: *enable_vpci_boot,
968                    uefi_console_mode: Some(openvmm_defs::config::UefiConsoleMode::Com1),
969                    default_boot_always_attempt: *default_boot_always_attempt,
970                    bios_guid: Guid::new_random(),
971                    enable_vmbus: !self.no_vmbus,
972                    force_dma_bounce: *force_dma_bounce,
973                    enable_hv: !self.no_hv,
974                }
975            }
976            (
977                MachineArch::X86_64,
978                Firmware::OpenhclLinuxDirect {
979                    igvm_path,
980                    openhcl_config,
981                }
982                | Firmware::OpenhclUefi {
983                    igvm_path,
984                    guest: _,       // load_boot_disk
985                    isolation: _,   // new via Firmware::isolation
986                    uefi_config: _, // config_openhcl_vmbus_devices
987                    openhcl_config,
988                },
989            ) => {
990                let OpenHclConfig {
991                    vmbus_redirect: _, // config_openhcl_vmbus_devices
992                    custom_command_line: _,
993                    log_levels: _,
994                    vtl2_base_address_type,
995                    vtl2_settings: _, // run_core
996                } = openhcl_config;
997
998                let mut cmdline = Some(openhcl_config.command_line());
999
1000                append_cmdline(&mut cmdline, "panic=-1 reboot=triple");
1001
1002                let isolated = match self.firmware {
1003                    Firmware::OpenhclLinuxDirect { .. } => {
1004                        // Set UNDERHILL_SERIAL_WAIT_FOR_RTS=1 so that we don't pull serial data
1005                        // until the guest is ready. Otherwise, Linux will drop the input serial
1006                        // data on the floor during boot.
1007                        append_cmdline(
1008                            &mut cmdline,
1009                            format!(
1010                                "UNDERHILL_SERIAL_WAIT_FOR_RTS=1 UNDERHILL_CMDLINE_APPEND=\"rdinit=/bin/sh {vsock_blacklist}\""
1011                            ),
1012                        );
1013                        false
1014                    }
1015                    Firmware::OpenhclUefi { isolation, .. } if isolation.is_some() => true,
1016                    _ => false,
1017                };
1018
1019                // For certain configurations, we need to override the override
1020                // in new_underhill_vm.
1021                //
1022                // TODO: remove this (and OpenHCL override) once host changes
1023                // are saturated.
1024                if let Firmware::OpenhclUefi {
1025                    uefi_config:
1026                        UefiConfig {
1027                            default_boot_always_attempt,
1028                            secure_boot_enabled,
1029                            ..
1030                        },
1031                    ..
1032                } = self.firmware
1033                {
1034                    if !isolated
1035                        && !secure_boot_enabled
1036                        && self.tpm_config.is_none()
1037                        && !default_boot_always_attempt
1038                    {
1039                        append_cmdline(&mut cmdline, "HCL_DEFAULT_BOOT_ALWAYS_ATTEMPT=0");
1040                    }
1041                }
1042
1043                // Plumb the EFI diagnostics rate-limit override to the
1044                // OpenHCL-side UEFI device via env var.
1045                if let Firmware::OpenhclUefi {
1046                    uefi_config:
1047                        UefiConfig {
1048                            efi_diagnostics_rate_limit: Some(limit),
1049                            ..
1050                        },
1051                    ..
1052                } = self.firmware
1053                {
1054                    append_cmdline(
1055                        &mut cmdline,
1056                        format!("HCL_EFI_DIAGNOSTICS_RATE_LIMIT={limit}"),
1057                    );
1058                }
1059
1060                let vtl2_base_address = vtl2_base_address_type.unwrap_or_else(|| {
1061                    if isolated {
1062                        // Isolated VMs must load at the location specified by
1063                        // the file, as they do not support relocation.
1064                        Vtl2BaseAddressType::File
1065                    } else {
1066                        // By default, utilize IGVM relocation and tell OpenVMM
1067                        // to place VTL2 at 512MB. This tests both relocation
1068                        // support in OpenVMM, and relocation support within
1069                        // OpenHCL.
1070                        Vtl2BaseAddressType::Absolute(512 * SIZE_1_MB)
1071                    }
1072                });
1073
1074                let file = File::open(igvm_path.clone())
1075                    .context("failed to open openhcl firmware file")?
1076                    .into();
1077                LoadMode::Igvm {
1078                    file,
1079                    cmdline: cmdline.unwrap_or_default(),
1080                    vtl2_base_address,
1081                    com_serial: Some(SerialInformation {
1082                        io_port: ComPort::Com3.io_port(),
1083                        irq: ComPort::Com3.irq().into(),
1084                    }),
1085                }
1086            }
1087            (a, f) => anyhow::bail!("Unsupported firmware {f:?} for arch {a:?}"),
1088        })
1089    }
1090
1091    async fn config_openhcl_vmbus_devices(
1092        &self,
1093        serial: &mut [Option<Resource<SerialBackendHandle>>],
1094        devices: &mut impl Extend<(DeviceVtl, Resource<VmbusDeviceHandleKind>)>,
1095        firmware_event_send: &mesh::Sender<FirmwareEvent>,
1096        framebuffer: bool,
1097    ) -> anyhow::Result<(
1098        get_resources::ged::GuestEmulationDeviceHandle,
1099        mesh::Sender<get_resources::ged::GuestEmulationRequest>,
1100    )> {
1101        let serial0 = serial[0].take();
1102        devices.extend([(
1103            DeviceVtl::Vtl2,
1104            VmbusSerialDeviceHandle {
1105                port: VmbusSerialPort::Com1,
1106                backend: serial0.unwrap_or_else(|| DisconnectedSerialBackendHandle.into_resource()),
1107            }
1108            .into_resource(),
1109        )]);
1110        let serial1 = serial[1].take();
1111        devices.extend([(
1112            DeviceVtl::Vtl2,
1113            VmbusSerialDeviceHandle {
1114                port: VmbusSerialPort::Com2,
1115                backend: serial1.unwrap_or_else(|| DisconnectedSerialBackendHandle.into_resource()),
1116            }
1117            .into_resource(),
1118        )]);
1119
1120        let crash = spawn_dump_handler(self.driver, self.logger).into_resource();
1121        devices.extend([(DeviceVtl::Vtl2, crash)]);
1122
1123        let (guest_request_send, guest_request_recv) = mesh::channel();
1124
1125        let (
1126            UefiConfig {
1127                secure_boot_enabled,
1128                secure_boot_template,
1129                custom_uefi_json: _, // OpenHCL reads this from VMGS CUSTOM_UEFI.
1130                disable_frontpage,
1131                default_boot_always_attempt,
1132                enable_vpci_boot,
1133                force_dma_bounce,
1134                efi_diagnostics_log_level,
1135                efi_diagnostics_rate_limit: _, // applied device-side via UefiManifest::new
1136            },
1137            OpenHclConfig { vmbus_redirect, .. },
1138        ) = match self.firmware {
1139            Firmware::OpenhclUefi {
1140                uefi_config,
1141                openhcl_config,
1142                ..
1143            } => (uefi_config, openhcl_config),
1144            Firmware::OpenhclLinuxDirect { openhcl_config, .. } => {
1145                (&UefiConfig::default(), openhcl_config)
1146            }
1147            _ => anyhow::bail!("not a supported openhcl firmware config"),
1148        };
1149
1150        let test_gsp_by_id = matches!(
1151            self.vmgs.encryption_policy(),
1152            Some(GuestStateEncryptionPolicy::GspById(_))
1153        );
1154
1155        // Save the GED handle to add later after configuration is complete.
1156        let ged = get_resources::ged::GuestEmulationDeviceHandle {
1157            firmware: get_resources::ged::GuestFirmwareConfig::Uefi {
1158                firmware_debug: false,
1159                disable_frontpage: *disable_frontpage,
1160                enable_vpci_boot: *enable_vpci_boot,
1161                console_mode: get_resources::ged::UefiConsoleMode::COM1,
1162                default_boot_always_attempt: *default_boot_always_attempt,
1163            },
1164            com1: true,
1165            com2: true,
1166            serial_tx_only: false,
1167            vmbus_redirection: *vmbus_redirect,
1168            vtl2_settings: None, // Will be added at startup to allow tests to modify
1169            vmgs: memdiff_vmgs(self.vmgs).await?,
1170            framebuffer: framebuffer.then(|| SharedFramebufferHandle.into_resource()),
1171            guest_request_recv,
1172            enable_tpm: self.tpm_config.is_some(),
1173            firmware_event_send: Some(firmware_event_send.clone()),
1174            secure_boot_enabled: *secure_boot_enabled,
1175            secure_boot_template: match secure_boot_template {
1176                Some(SecureBootTemplate::MicrosoftWindows) => {
1177                    get_resources::ged::GuestSecureBootTemplateType::MicrosoftWindows
1178                }
1179                Some(SecureBootTemplate::MicrosoftUefiCertificateAuthority) => {
1180                    get_resources::ged::GuestSecureBootTemplateType::MicrosoftUefiCertificateAuthority
1181                }
1182                None => get_resources::ged::GuestSecureBootTemplateType::None,
1183            },
1184            enable_battery: false,
1185            no_persistent_secrets: self.tpm_config.as_ref().is_some_and(|c| c.no_persistent_secrets),
1186            igvm_attest_test_config: None,
1187            test_gsp_by_id,
1188            efi_diagnostics_log_level: match efi_diagnostics_log_level {
1189                EfiDiagnosticsLogLevel::Default => {
1190                    get_resources::ged::EfiDiagnosticsLogLevelType::Default
1191                }
1192                EfiDiagnosticsLogLevel::Info => {
1193                    get_resources::ged::EfiDiagnosticsLogLevelType::Info
1194                }
1195                EfiDiagnosticsLogLevel::Full => {
1196                    get_resources::ged::EfiDiagnosticsLogLevelType::Full
1197                }
1198            },
1199            force_dma_bounce_enabled: *force_dma_bounce,
1200        };
1201
1202        Ok((ged, guest_request_send))
1203    }
1204
1205    fn config_video(
1206        &self,
1207    ) -> anyhow::Result<Option<(VideoDevice, Framebuffer, FramebufferAccess)>> {
1208        if self.firmware.isolation().is_some() {
1209            return Ok(None);
1210        }
1211
1212        let video_dev = match self.firmware {
1213            Firmware::Pcat { svga_firmware, .. } | Firmware::OpenhclPcat { svga_firmware, .. } => {
1214                Some(VideoDevice::Vga(
1215                    openvmm_pcat_locator::find_svga_bios(svga_firmware.get())
1216                        .context("Failed to load VGA BIOS")?,
1217                ))
1218            }
1219            Firmware::Uefi { .. } | Firmware::OpenhclUefi { .. } if !self.no_vmbus => {
1220                Some(VideoDevice::Synth(
1221                    DeviceVtl::Vtl0,
1222                    SynthVideoHandle {
1223                        framebuffer: SharedFramebufferHandle.into_resource(),
1224                        dirt_send: None,
1225                    }
1226                    .into_resource(),
1227                ))
1228            }
1229            Firmware::OpenhclLinuxDirect { .. }
1230            | Firmware::LinuxDirect { .. }
1231            | Firmware::Uefi { .. }
1232            | Firmware::OpenhclUefi { .. } => None,
1233        };
1234
1235        Ok(if let Some(vdev) = video_dev {
1236            let vram =
1237                alloc_shared_memory(FRAMEBUFFER_SIZE, "vram").context("allocating framebuffer")?;
1238            let (fb, fba) = framebuffer::framebuffer(vram, FRAMEBUFFER_SIZE, 0)
1239                .context("creating framebuffer")?;
1240            Some((vdev, fb, fba))
1241        } else {
1242            None
1243        })
1244    }
1245
1246    async fn config_tpm(&self) -> anyhow::Result<Option<ChipsetDeviceHandle>> {
1247        if !self.firmware.is_openhcl()
1248            && let Some(TpmConfig {
1249                no_persistent_secrets,
1250                ..
1251            }) = self.tpm_config
1252        {
1253            let register_layout = match self.arch {
1254                MachineArch::X86_64 => TpmRegisterLayout::IoPort,
1255                MachineArch::Aarch64 => TpmRegisterLayout::Mmio,
1256            };
1257
1258            let (ppi_store, nvram_store) = if self.vmgs.disk().is_none() || *no_persistent_secrets {
1259                (
1260                    EphemeralNonVolatileStoreHandle.into_resource(),
1261                    EphemeralNonVolatileStoreHandle.into_resource(),
1262                )
1263            } else {
1264                (
1265                    VmgsFileHandle::new(vmgs_format::FileId::TPM_PPI, true).into_resource(),
1266                    VmgsFileHandle::new(vmgs_format::FileId::TPM_NVRAM, true).into_resource(),
1267                )
1268            };
1269
1270            Ok(Some(ChipsetDeviceHandle {
1271                name: "tpm".to_string(),
1272                resource: chipset_device_worker_defs::RemoteChipsetDeviceHandle {
1273                    device: TpmDeviceHandle {
1274                        ppi_store,
1275                        nvram_store,
1276                        refresh_tpm_seeds: false,
1277                        ak_cert_type: tpm_resources::TpmAkCertTypeResource::None,
1278                        register_layout,
1279                        guest_secret_key: None,
1280                        logger: None,
1281                        is_confidential_vm: self.firmware.isolation().is_some(),
1282                        // TODO: generate an actual BIOS GUID and put it here
1283                        bios_guid: Guid::ZERO,
1284                        nvram_size: None,
1285                    }
1286                    .into_resource(),
1287                    worker_host: self.make_device_worker("tpm").await?,
1288                }
1289                .into_resource(),
1290            }))
1291        } else {
1292            Ok(None)
1293        }
1294    }
1295
1296    async fn make_device_worker(&self, name: &str) -> anyhow::Result<mesh_worker::WorkerHost> {
1297        let (host, runner) = mesh_worker::worker_host();
1298        self.mesh
1299            .launch_host(
1300                mesh_process::ProcessConfig::new(name).process_name(self.openvmm_path),
1301                openvmm_defs::entrypoint::MeshHostParams { runner },
1302            )
1303            .await?;
1304        Ok(host)
1305    }
1306}
1307
1308fn spawn_dump_handler(driver: &DefaultDriver, logger: &PetriLogSource) -> GuestCrashDeviceHandle {
1309    let (send, mut recv) = mesh::channel();
1310    let handle = GuestCrashDeviceHandle {
1311        request_dump: send,
1312        max_dump_size: 256 * 1024 * 1024,
1313    };
1314    driver
1315        .spawn("openhcl-dump-handler", {
1316            let logger = logger.clone();
1317            let driver = driver.clone();
1318            async move {
1319                while let Some(rpc) = recv.next().await {
1320                    rpc.handle_failable_sync(|done| {
1321                        let (file, path) = logger.create_attachment("openhcl.core")?.into_parts();
1322                        driver
1323                            .spawn("crash-waiter", async move {
1324                                let filename = path.file_name().unwrap().to_str().unwrap();
1325                                if done.await.is_ok() {
1326                                    tracing::warn!(filename, "openhcl crash dump complete");
1327                                } else {
1328                                    tracing::error!(
1329                                        filename,
1330                                        "openhcl crash dump incomplete, may be corrupted"
1331                                    );
1332                                }
1333                            })
1334                            .detach();
1335                        anyhow::Ok(file)
1336                    })
1337                }
1338            }
1339        })
1340        .detach();
1341    handle
1342}
1343
1344/// Convert the generic IDE configuration to OpenVMM IDE disks and storvsp
1345/// IDE accelerator handles.
1346async fn ide_controllers_to_openvmm(
1347    ide_controllers: Option<&[[Option<Drive>; 2]; 2]>,
1348) -> anyhow::Result<(
1349    Vec<IdeDeviceConfig>,
1350    Vec<(DeviceVtl, Resource<VmbusDeviceHandleKind>)>,
1351)> {
1352    let mut ide_disks = Vec::new();
1353    let mut storvsp_ide_handles = Vec::new();
1354
1355    if let Some(ide_controllers) = ide_controllers {
1356        for (controller_number, controller) in ide_controllers.iter().enumerate() {
1357            for (controller_location, drive) in controller.iter().enumerate() {
1358                if let Some(drive) = drive {
1359                    if let Some(disk) = &drive.disk {
1360                        // Create storvsp accelerator resource before consuming
1361                        // the disk reference, since petri_disk_to_openvmm
1362                        // shadows the binding.
1363                        let storvsp_disk = if !drive.is_dvd {
1364                            Some(petri_disk_to_openvmm(disk).await?)
1365                        } else {
1366                            None
1367                        };
1368
1369                        let disk = petri_disk_to_openvmm(disk).await?;
1370                        let guest_media = if drive.is_dvd {
1371                            GuestMedia::Dvd(
1372                                SimpleScsiDvdHandle {
1373                                    media: Some(disk),
1374                                    requests: None,
1375                                }
1376                                .into_resource(),
1377                            )
1378                        } else {
1379                            GuestMedia::Disk {
1380                                disk_type: disk,
1381                                read_only: false,
1382                            }
1383                        };
1384
1385                        let channel = controller_number as u8;
1386                        let device = controller_location as u8;
1387
1388                        ide_disks.push(IdeDeviceConfig {
1389                            path: ide_resources::IdePath {
1390                                channel,
1391                                drive: device,
1392                            },
1393                            guest_media,
1394                        });
1395
1396                        // Hard disks also get a storvsp IDE accelerator channel.
1397                        if let Some(storvsp_disk) = storvsp_disk {
1398                            storvsp_ide_handles.push((
1399                                DeviceVtl::Vtl0,
1400                                storvsp_resources::StorvspIdeDeviceHandle {
1401                                    channel_id: channel,
1402                                    device_id: device,
1403                                    disk: SimpleScsiDiskHandle {
1404                                        disk: storvsp_disk,
1405                                        read_only: false,
1406                                        parameters: Default::default(),
1407                                    }
1408                                    .into_resource(),
1409                                    io_queue_depth: None,
1410                                }
1411                                .into_resource(),
1412                            ));
1413                        }
1414                    }
1415                }
1416            }
1417        }
1418    }
1419
1420    Ok((ide_disks, storvsp_ide_handles))
1421}
1422
1423/// Convert the generic VMBUS storage configuration to OpenVMM VMBUS and VPCI devices.
1424async fn vmbus_storage_controllers_to_openvmm(
1425    vmbus_storage_controllers: &HashMap<Guid, VmbusStorageController>,
1426) -> anyhow::Result<(
1427    Vec<(DeviceVtl, Resource<VmbusDeviceHandleKind>)>,
1428    Vec<VpciDeviceConfig>,
1429)> {
1430    let mut vmbus_devices = Vec::new();
1431    let mut vpci_devices = Vec::new();
1432
1433    // Add VMBus storage
1434    for (instance_id, controller) in vmbus_storage_controllers {
1435        let vtl = match controller.target_vtl {
1436            crate::Vtl::Vtl0 => DeviceVtl::Vtl0,
1437            crate::Vtl::Vtl1 => DeviceVtl::Vtl1,
1438            crate::Vtl::Vtl2 => DeviceVtl::Vtl2,
1439        };
1440        match controller.controller_type {
1441            VmbusStorageType::Scsi => {
1442                let mut devices = Vec::new();
1443                for (lun, Drive { disk, is_dvd }) in &controller.drives {
1444                    if !*is_dvd && let Some(disk) = disk {
1445                        devices.push(ScsiDeviceAndPath {
1446                            path: ScsiPath {
1447                                path: 0,
1448                                target: 0,
1449                                lun: (*lun).try_into().expect("invalid scsi lun"),
1450                            },
1451                            device: SimpleScsiDiskHandle {
1452                                disk: petri_disk_to_openvmm(disk).await?,
1453                                read_only: false,
1454                                parameters: Default::default(),
1455                            }
1456                            .into_resource(),
1457                        });
1458                    } else {
1459                        todo!("dvd ({}) or empty ({})", *is_dvd, disk.is_none())
1460                    }
1461                }
1462
1463                vmbus_devices.push((
1464                    vtl,
1465                    ScsiControllerHandle {
1466                        instance_id: *instance_id,
1467                        max_sub_channel_count: 1,
1468                        io_queue_depth: None,
1469                        devices,
1470                        requests: None,
1471                        poll_mode_queue_depth: None,
1472                    }
1473                    .into_resource(),
1474                ));
1475            }
1476            VmbusStorageType::Nvme => {
1477                let mut namespaces = Vec::new();
1478                for (nsid, Drive { disk, is_dvd }) in &controller.drives {
1479                    if !*is_dvd && let Some(disk) = disk {
1480                        namespaces.push(NamespaceDefinition {
1481                            nsid: *nsid,
1482                            read_only: false,
1483                            disk: petri_disk_to_openvmm(disk).await?,
1484                        });
1485                    } else {
1486                        todo!("dvd ({}) or empty ({})", *is_dvd, disk.is_none())
1487                    }
1488                }
1489
1490                vpci_devices.push(VpciDeviceConfig {
1491                    vtl,
1492                    instance_id: *instance_id,
1493                    resource: NvmeControllerHandle {
1494                        subsystem_id: *instance_id,
1495                        max_io_queues: 64,
1496                        msix_count: 64,
1497                        namespaces,
1498                        requests: None,
1499                    }
1500                    .into_resource(),
1501                    vnode: None,
1502                });
1503            }
1504            VmbusStorageType::VirtioBlk => {
1505                // Each virtio-blk drive needs a unique VPCI instance ID.
1506                // Use a fixed template GUID with data1 set to the LUN.
1507                const VIRTIO_BLK_INSTANCE_ID_TEMPLATE: Guid = Guid {
1508                    data1: 0,
1509                    data2: 0x1234,
1510                    data3: 0x5678,
1511                    data4: [0xab, 0xcd, 0xef, 0x01, 0x23, 0x45, 0x67, 0x89],
1512                };
1513                for (lun, Drive { disk, is_dvd }) in &controller.drives {
1514                    if *is_dvd {
1515                        anyhow::bail!("dvd not supported with virtio-blk");
1516                    }
1517                    let Some(disk) = disk else {
1518                        anyhow::bail!("empty drive not supported with virtio-blk");
1519                    };
1520                    let mut drive_id = VIRTIO_BLK_INSTANCE_ID_TEMPLATE;
1521                    drive_id.data1 = *lun;
1522                    vpci_devices.push(VpciDeviceConfig {
1523                        vtl,
1524                        instance_id: drive_id,
1525                        resource: VirtioPciDeviceHandle(
1526                            VirtioBlkHandle {
1527                                disk: petri_disk_to_openvmm(disk).await?,
1528                                read_only: false,
1529                            }
1530                            .into_resource(),
1531                        )
1532                        .into_resource(),
1533                        vnode: None,
1534                    });
1535                }
1536            }
1537        }
1538    }
1539
1540    Ok((vmbus_devices, vpci_devices))
1541}