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