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