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