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