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::Firmware;
11use crate::IsolationType;
12use crate::MemoryConfig;
13use crate::OpenHclConfig;
14use crate::PetriLogSource;
15use crate::PetriVmConfig;
16use crate::PetriVmResources;
17use crate::PetriVmgsResource;
18use crate::ProcessorTopology;
19use crate::SecureBootTemplate;
20use crate::TpmConfig;
21use crate::UefiConfig;
22use crate::VmbusStorageType;
23use crate::linux_direct_serial_agent::LinuxDirectSerialAgent;
24
25use crate::MmioConfig;
26use crate::SIZE_1_MB;
27use crate::VmbusStorageController;
28use crate::openvmm::memdiff_vmgs;
29use crate::openvmm::petri_disk_to_openvmm;
30use crate::vm::PetriVmProperties;
31use crate::vm::append_cmdline;
32use anyhow::Context;
33use framebuffer::FRAMEBUFFER_SIZE;
34use framebuffer::Framebuffer;
35use framebuffer::FramebufferAccess;
36use fs_err::File;
37use futures::StreamExt;
38use get_resources::crash::GuestCrashDeviceHandle;
39use get_resources::ged::FirmwareEvent;
40use guid::Guid;
41use hyperv_ic_resources::shutdown::ShutdownIcHandle;
42use ide_resources::GuestMedia;
43use ide_resources::IdeDeviceConfig;
44use mesh_process::Mesh;
45use nvme_resources::NamespaceDefinition;
46use nvme_resources::NvmeControllerHandle;
47use openvmm_defs::config::Config;
48use openvmm_defs::config::DEFAULT_MMIO_GAPS_AARCH64;
49use openvmm_defs::config::DEFAULT_MMIO_GAPS_AARCH64_WITH_VTL2;
50use openvmm_defs::config::DEFAULT_MMIO_GAPS_X86;
51use openvmm_defs::config::DEFAULT_MMIO_GAPS_X86_WITH_VTL2;
52use openvmm_defs::config::DEFAULT_PCAT_BOOT_ORDER;
53use openvmm_defs::config::DeviceVtl;
54use openvmm_defs::config::HypervisorConfig;
55use openvmm_defs::config::LateMapVtl0MemoryPolicy;
56use openvmm_defs::config::LoadMode;
57use openvmm_defs::config::ProcessorTopologyConfig;
58use openvmm_defs::config::SerialInformation;
59use openvmm_defs::config::VmbusConfig;
60use openvmm_defs::config::VpciDeviceConfig;
61use openvmm_defs::config::Vtl2BaseAddressType;
62use openvmm_defs::config::Vtl2Config;
63use openvmm_pcat_locator::RomFileLocation;
64use pal_async::DefaultDriver;
65use pal_async::socket::PolledSocket;
66use pal_async::task::Spawn;
67use pal_async::task::Task;
68use petri_artifacts_common::tags::MachineArch;
69use petri_artifacts_core::ResolvedArtifact;
70use pipette_client::PIPETTE_VSOCK_PORT;
71use scsidisk_resources::SimpleScsiDiskHandle;
72use scsidisk_resources::SimpleScsiDvdHandle;
73use serial_16550_resources::ComPort;
74use serial_core::resources::DisconnectedSerialBackendHandle;
75use serial_socket::net::OpenSocketSerialConfig;
76use sparse_mmap::alloc_shared_memory;
77use std::collections::HashMap;
78use storvsp_resources::ScsiControllerHandle;
79use storvsp_resources::ScsiDeviceAndPath;
80use storvsp_resources::ScsiPath;
81use tempfile::TempPath;
82use tpm_resources::TpmDeviceHandle;
83use tpm_resources::TpmRegisterLayout;
84use uidevices_resources::SynthVideoHandle;
85use unix_socket::UnixListener;
86use unix_socket::UnixStream;
87use video_core::SharedFramebufferHandle;
88use virtio_resources::VirtioPciDeviceHandle;
89use virtio_resources::blk::VirtioBlkHandle;
90use vm_manifest_builder::VmChipsetResult;
91use vm_manifest_builder::VmManifestBuilder;
92use vm_resource::IntoResource;
93use vm_resource::Resource;
94use vm_resource::kind::SerialBackendHandle;
95use vm_resource::kind::VmbusDeviceHandleKind;
96use vmbus_serial_resources::VmbusSerialDeviceHandle;
97use vmbus_serial_resources::VmbusSerialPort;
98use vmcore::non_volatile_store::resources::EphemeralNonVolatileStoreHandle;
99use vmgs_resources::GuestStateEncryptionPolicy;
100use vmgs_resources::VmgsFileHandle;
101use vmotherboard::ChipsetDeviceHandle;
102
103impl PetriVmConfigOpenVmm {
104    /// Create a new VM configuration.
105    pub async fn new(
106        openvmm_path: &ResolvedArtifact,
107        petri_vm_config: PetriVmConfig,
108        resources: &PetriVmResources,
109        properties: PetriVmProperties,
110    ) -> anyhow::Result<Self> {
111        let PetriVmConfig {
112            name: _,
113            arch,
114            host_log_levels,
115            firmware,
116            memory,
117            proc_topology,
118            vmgs,
119            tpm: tpm_config,
120            vmbus_storage_controllers,
121        } = petri_vm_config;
122
123        tracing::debug!(?firmware, ?arch, "Petri VM firmware configuration");
124
125        let PetriVmResources { driver, log_source } = resources;
126
127        let mesh = Mesh::new("petri_mesh".to_string())?;
128
129        let setup = PetriVmConfigSetupCore {
130            arch,
131            firmware: &firmware,
132            driver,
133            logger: log_source,
134            vmgs: &vmgs,
135            tpm_config: tpm_config.as_ref(),
136            mesh: &mesh,
137            openvmm_path,
138            uses_pipette_as_init: properties.uses_pipette_as_init,
139            enable_serial: properties.enable_serial,
140        };
141
142        let mut chipset = VmManifestBuilder::new(
143            match firmware {
144                Firmware::LinuxDirect { .. } => {
145                    vm_manifest_builder::BaseChipsetType::HyperVGen2LinuxDirect
146                }
147                Firmware::OpenhclLinuxDirect { .. } => {
148                    vm_manifest_builder::BaseChipsetType::HclHost
149                }
150                Firmware::OpenhclUefi { .. } => vm_manifest_builder::BaseChipsetType::HclHost,
151                Firmware::Pcat { .. } => vm_manifest_builder::BaseChipsetType::HypervGen1,
152                Firmware::Uefi { .. } => vm_manifest_builder::BaseChipsetType::HypervGen2Uefi,
153                Firmware::OpenhclPcat { .. } => todo!("OpenVMM OpenHCL PCAT"),
154            },
155            match arch {
156                MachineArch::X86_64 => vm_manifest_builder::MachineArch::X86_64,
157                MachineArch::Aarch64 => vm_manifest_builder::MachineArch::Aarch64,
158            },
159        );
160
161        let mut load_mode = setup.load_firmware()?;
162
163        // If using pipette-as-init, replace the initrd with the pre-built
164        // one that has pipette injected. run_core() guarantees that
165        // prebuilt_initrd is set when uses_pipette_as_init is true.
166        if properties.uses_pipette_as_init {
167            if let LoadMode::Linux { initrd, .. } = &mut load_mode {
168                let prebuilt = properties
169                    .prebuilt_initrd
170                    .as_ref()
171                    .expect("uses_pipette_as_init requires prebuilt_initrd");
172                let file = std::fs::File::open(prebuilt).with_context(|| {
173                    format!("failed to open prebuilt initrd at {}", prebuilt.display())
174                })?;
175                *initrd = Some(file);
176            }
177        }
178
179        let (emulated_serial_config, log_stream_tasks, linux_direct_serial_agent) =
180            if !properties.enable_serial {
181                // No emulated serial backends (OpenHCL VMBus serial stubs may still exist)
182                ([None, None, None, None], Vec::new(), None)
183            } else {
184                let SerialData {
185                    emulated_serial_config,
186                    serial_tasks,
187                    linux_direct_serial_agent,
188                } = setup.configure_serial(log_source)?;
189                (
190                    emulated_serial_config,
191                    serial_tasks,
192                    linux_direct_serial_agent,
193                )
194            };
195        let mut emulated_serial_config = emulated_serial_config;
196
197        let (video_dev, framebuffer, framebuffer_view) = match setup.config_video()? {
198            Some((v, fb, fba)) => {
199                chipset = chipset.with_framebuffer();
200                (Some(v), Some(fb), Some(fba.view()?))
201            }
202            None => (None, None, None),
203        };
204
205        let ide_disks = ide_controllers_to_openvmm(firmware.ide_controllers())?;
206        let (mut vmbus_devices, vpci_devices) =
207            vmbus_storage_controllers_to_openvmm(&vmbus_storage_controllers)?;
208
209        let (firmware_event_send, firmware_event_recv) = mesh::mpsc_channel();
210
211        let make_vsock_listener = || -> anyhow::Result<(UnixListener, TempPath)> {
212            Ok(tempfile::Builder::new()
213                .make(|path| UnixListener::bind(path))?
214                .into_parts())
215        };
216
217        let (with_vtl2, vtl2_vmbus, ged, ged_send, vtl2_vsock_path) = if firmware.is_openhcl() {
218            let (ged, ged_send) = setup.config_openhcl_vmbus_devices(
219                &mut emulated_serial_config,
220                &mut vmbus_devices,
221                &firmware_event_send,
222                framebuffer.is_some(),
223            )?;
224
225            let late_map_vtl0_memory = match load_mode {
226                LoadMode::Igvm {
227                    vtl2_base_address: Vtl2BaseAddressType::Vtl2Allocate { .. },
228                    ..
229                } => {
230                    // Late Map VTL0 memory not supported when test supplies Vtl2Allocate
231                    None
232                }
233                _ => Some(LateMapVtl0MemoryPolicy::InjectException),
234            };
235
236            let (vtl2_vsock_listener, vtl2_vsock_path) = make_vsock_listener()?;
237            (
238                Some(Vtl2Config {
239                    vtl0_alias_map: false, // TODO: enable when OpenVMM supports it for DMA
240                    late_map_vtl0_memory,
241                }),
242                Some(VmbusConfig {
243                    vsock_listener: Some(vtl2_vsock_listener),
244                    vsock_path: Some(vtl2_vsock_path.to_string_lossy().into_owned()),
245                    vmbus_max_version: None,
246                    vtl2_redirect: false,
247                    #[cfg(windows)]
248                    vmbusproxy_handle: None,
249                }),
250                Some(ged),
251                Some(ged_send),
252                Some(vtl2_vsock_path),
253            )
254        } else {
255            (None, None, None, None, None)
256        };
257
258        // Configure the serial ports now that they have been updated by the
259        // OpenHCL configuration.
260        if properties.enable_serial {
261            chipset = chipset.with_serial(emulated_serial_config);
262            // Set so that we don't pull serial data until the guest is
263            // ready. Otherwise, Linux will drop the input serial data
264            // on the floor during boot.
265            if matches!(firmware, Firmware::LinuxDirect { .. }) && !properties.uses_pipette_as_init
266            {
267                chipset = chipset.with_serial_wait_for_rts();
268            }
269        }
270
271        // Extract video configuration
272        let vga_firmware = match video_dev {
273            Some(VideoDevice::Vga(firmware)) => Some(firmware),
274            Some(VideoDevice::Synth(vtl, resource)) => {
275                vmbus_devices.push((vtl, resource));
276                None
277            }
278            None => None,
279        };
280
281        // Add default VMBus devices (skipped in minimal mode).
282        let (shutdown_ic_send, kvp_ic_send) = if !properties.minimal_mode {
283            let (shutdown_ic_send, shutdown_ic_recv) = mesh::channel();
284            vmbus_devices.push((
285                DeviceVtl::Vtl0,
286                ShutdownIcHandle {
287                    recv: shutdown_ic_recv,
288                }
289                .into_resource(),
290            ));
291
292            let (kvp_ic_send, kvp_ic_recv) = mesh::channel();
293            vmbus_devices.push((
294                DeviceVtl::Vtl0,
295                hyperv_ic_resources::kvp::KvpIcHandle { recv: kvp_ic_recv }.into_resource(),
296            ));
297
298            vmbus_devices.push((
299                DeviceVtl::Vtl0,
300                hyperv_ic_resources::timesync::TimesyncIcHandle.into_resource(),
301            ));
302
303            (shutdown_ic_send, kvp_ic_send)
304        } else {
305            // Minimal mode: no ICs. Create dummy senders so the fields
306            // are populated (calls to send_enlightened_shutdown will fail
307            // with a channel error, which is fine — minimal VMs shut down
308            // via reboot(2) directly).
309            let (shutdown_ic_send, _) = mesh::channel();
310            let (kvp_ic_send, _) = mesh::channel();
311            (shutdown_ic_send, kvp_ic_send)
312        };
313
314        // Make a vmbus vsock path for pipette connections
315        let (vmbus_vsock_listener, vmbus_vsock_path) = make_vsock_listener()?;
316
317        let chipset = chipset
318            .build()
319            .context("failed to build chipset configuration")?;
320
321        let memory = {
322            let MemoryConfig {
323                startup_bytes,
324                dynamic_memory_range,
325                mmio_gaps,
326            } = memory;
327
328            if dynamic_memory_range.is_some() {
329                anyhow::bail!("dynamic memory not supported in OpenVMM");
330            }
331
332            openvmm_defs::config::MemoryConfig {
333                mem_size: startup_bytes,
334                mmio_gaps: match mmio_gaps {
335                    MmioConfig::Platform => {
336                        if firmware.is_openhcl() {
337                            match arch {
338                                MachineArch::X86_64 => DEFAULT_MMIO_GAPS_X86_WITH_VTL2.into(),
339                                MachineArch::Aarch64 => DEFAULT_MMIO_GAPS_AARCH64_WITH_VTL2.into(),
340                            }
341                        } else {
342                            match arch {
343                                MachineArch::X86_64 => DEFAULT_MMIO_GAPS_X86.into(),
344                                MachineArch::Aarch64 => DEFAULT_MMIO_GAPS_AARCH64.into(),
345                            }
346                        }
347                    }
348                    MmioConfig::Custom(ranges) => ranges,
349                },
350                pci_ecam_gaps: vec![],
351                pci_mmio_gaps: vec![],
352                prefetch_memory: false,
353                private_memory: false,
354                transparent_hugepages: false,
355            }
356        };
357
358        let processor_topology = {
359            let ProcessorTopology {
360                vp_count,
361                enable_smt,
362                vps_per_socket,
363                apic_mode,
364            } = proc_topology;
365
366            ProcessorTopologyConfig {
367                proc_count: vp_count,
368                vps_per_socket,
369                enable_smt,
370                arch: Some(match arch {
371                    MachineArch::X86_64 => openvmm_defs::config::ArchTopologyConfig::X86(
372                        openvmm_defs::config::X86TopologyConfig {
373                            x2apic: match apic_mode {
374                                None => openvmm_defs::config::X2ApicConfig::Auto,
375                                Some(x) => match x {
376                                    crate::ApicMode::Xapic => {
377                                        openvmm_defs::config::X2ApicConfig::Unsupported
378                                    }
379                                    crate::ApicMode::X2apicSupported => {
380                                        openvmm_defs::config::X2ApicConfig::Supported
381                                    }
382                                    crate::ApicMode::X2apicEnabled => {
383                                        openvmm_defs::config::X2ApicConfig::Enabled
384                                    }
385                                },
386                            },
387                            ..Default::default()
388                        },
389                    ),
390                    MachineArch::Aarch64 => openvmm_defs::config::ArchTopologyConfig::Aarch64(
391                        openvmm_defs::config::Aarch64TopologyConfig::default(),
392                    ),
393                }),
394            }
395        };
396
397        let (secure_boot_enabled, custom_uefi_vars) = firmware.uefi_config().map_or_else(
398            || (false, Default::default()),
399            |c| {
400                (
401                    c.secure_boot_enabled,
402                    match (arch, c.secure_boot_template) {
403                        (MachineArch::X86_64, Some(SecureBootTemplate::MicrosoftWindows)) => {
404                            hyperv_secure_boot_templates::x64::microsoft_windows()
405                        }
406                        (
407                            MachineArch::X86_64,
408                            Some(SecureBootTemplate::MicrosoftUefiCertificateAuthority),
409                        ) => hyperv_secure_boot_templates::x64::microsoft_uefi_ca(),
410                        (MachineArch::Aarch64, Some(SecureBootTemplate::MicrosoftWindows)) => {
411                            hyperv_secure_boot_templates::aarch64::microsoft_windows()
412                        }
413                        (
414                            MachineArch::Aarch64,
415                            Some(SecureBootTemplate::MicrosoftUefiCertificateAuthority),
416                        ) => hyperv_secure_boot_templates::aarch64::microsoft_uefi_ca(),
417                        (_, None) => Default::default(),
418                    },
419                )
420            },
421        );
422
423        let vmgs = if firmware.is_openhcl() {
424            None
425        } else {
426            Some(memdiff_vmgs(&vmgs)?)
427        };
428
429        let VmChipsetResult {
430            chipset,
431            mut chipset_devices,
432        } = chipset;
433
434        // Add the TPM
435        if let Some(tpm) = setup.config_tpm().await? {
436            chipset_devices.push(tpm);
437        }
438
439        let config = Config {
440            // Firmware
441            load_mode,
442            firmware_event_send: Some(firmware_event_send),
443
444            // CPU and RAM
445            memory,
446            processor_topology,
447
448            // Base chipset
449            chipset,
450            chipset_devices,
451
452            // Basic virtualization device support
453            hypervisor: HypervisorConfig {
454                with_hv: true,
455                user_mode_hv_enlightenments: false,
456                user_mode_apic: false,
457                with_vtl2,
458                with_isolation: match firmware.isolation() {
459                    Some(IsolationType::Vbs) => Some(openvmm_defs::config::IsolationType::Vbs),
460                    None => None,
461                    _ => anyhow::bail!("unsupported isolation type"),
462                },
463            },
464            vmbus: Some(VmbusConfig {
465                vsock_listener: Some(vmbus_vsock_listener),
466                vsock_path: Some(vmbus_vsock_path.to_string_lossy().into_owned()),
467                vmbus_max_version: None,
468                vtl2_redirect: firmware.openhcl_config().is_some_and(|c| c.vmbus_redirect),
469                #[cfg(windows)]
470                vmbusproxy_handle: None,
471            }),
472            vtl2_vmbus,
473
474            // Devices
475            floppy_disks: vec![],
476            ide_disks,
477            pcie_root_complexes: vec![],
478            pcie_devices: vec![],
479            pcie_switches: vec![],
480            vpci_devices,
481            vmbus_devices,
482
483            // Video support
484            framebuffer,
485            vga_firmware,
486
487            secure_boot_enabled,
488            custom_uefi_vars,
489            vmgs,
490
491            // Don't automatically reset the guest by default
492            automatic_guest_reset: false,
493
494            // Disabled for VMM tests by default
495            #[cfg(windows)]
496            kernel_vmnics: vec![],
497            input: mesh::Receiver::new(),
498            vtl2_gfx: false,
499            virtio_devices: vec![],
500            #[cfg(windows)]
501            vpci_resources: vec![],
502            debugger_rpc: None,
503            generation_id_recv: None,
504            rtc_delta_milliseconds: 0,
505            efi_diagnostics_log_level: Default::default(), // TODO: Add config for tests
506        };
507
508        // Make the pipette connection listener.
509        let path = config.vmbus.as_ref().unwrap().vsock_path.as_ref().unwrap();
510        let path = format!("{path}_{PIPETTE_VSOCK_PORT}");
511        let pipette_listener = PolledSocket::new(
512            driver,
513            UnixListener::bind(path).context("failed to bind to pipette listener")?,
514        )?;
515
516        // Make the vtl2 pipette connection listener.
517        let vtl2_pipette_listener = if let Some(vtl2_vmbus) = &config.vtl2_vmbus {
518            let path = vtl2_vmbus.vsock_path.as_ref().unwrap();
519            let path = format!("{path}_{PIPETTE_VSOCK_PORT}");
520            Some(PolledSocket::new(
521                driver,
522                UnixListener::bind(path).context("failed to bind to vtl2 pipette listener")?,
523            )?)
524        } else {
525            None
526        };
527
528        Ok(Self {
529            runtime_config: firmware.into_runtime_config(vmbus_storage_controllers),
530            arch,
531            host_log_levels,
532            config,
533            mesh,
534
535            resources: PetriVmResourcesOpenVmm {
536                log_stream_tasks,
537                firmware_event_recv,
538                shutdown_ic_send,
539                kvp_ic_send,
540                ged_send,
541                pipette_listener,
542                vtl2_pipette_listener,
543                linux_direct_serial_agent,
544                driver: driver.clone(),
545                output_dir: log_source.output_dir().to_owned(),
546                openvmm_path: openvmm_path.clone(),
547                vtl2_vsock_path,
548                _vmbus_vsock_path: vmbus_vsock_path,
549                properties,
550            },
551
552            openvmm_log_file: log_source.log_file("openvmm")?,
553
554            memory_backing_file: None,
555
556            ged,
557            framebuffer_view,
558        })
559    }
560}
561
562struct PetriVmConfigSetupCore<'a> {
563    arch: MachineArch,
564    firmware: &'a Firmware,
565    driver: &'a DefaultDriver,
566    logger: &'a PetriLogSource,
567    vmgs: &'a PetriVmgsResource,
568    tpm_config: Option<&'a TpmConfig>,
569    mesh: &'a Mesh,
570    openvmm_path: &'a ResolvedArtifact,
571    uses_pipette_as_init: bool,
572    enable_serial: bool,
573}
574
575struct SerialData {
576    emulated_serial_config: [Option<Resource<SerialBackendHandle>>; 4],
577    serial_tasks: Vec<Task<anyhow::Result<()>>>,
578    linux_direct_serial_agent: Option<LinuxDirectSerialAgent>,
579}
580
581enum VideoDevice {
582    Vga(RomFileLocation),
583    Synth(DeviceVtl, Resource<VmbusDeviceHandleKind>),
584}
585
586impl PetriVmConfigSetupCore<'_> {
587    fn configure_serial(&self, logger: &PetriLogSource) -> anyhow::Result<SerialData> {
588        let mut serial_tasks = Vec::new();
589
590        let serial0_log_file = logger.log_file(match self.firmware {
591            Firmware::LinuxDirect { .. } | Firmware::OpenhclLinuxDirect { .. } => "linux",
592            Firmware::Pcat { .. } | Firmware::OpenhclPcat { .. } => "pcat",
593            Firmware::Uefi { .. } | Firmware::OpenhclUefi { .. } => "uefi",
594        })?;
595
596        let (serial0_host, serial0) = self
597            .create_serial_stream()
598            .context("failed to create serial0 stream")?;
599        let (serial0_read, serial0_write) = serial0_host.split();
600        let serial0_task = self.driver.spawn(
601            "serial0-console",
602            crate::log_task(serial0_log_file, serial0_read, "serial0-console"),
603        );
604        serial_tasks.push(serial0_task);
605
606        let serial2 = if self.firmware.is_openhcl() {
607            let (serial2_host, serial2) = self
608                .create_serial_stream()
609                .context("failed to create serial2 stream")?;
610            let serial2_task = self.driver.spawn(
611                "serial2-openhcl",
612                crate::log_task(logger.log_file("openhcl")?, serial2_host, "serial2-openhcl"),
613            );
614            serial_tasks.push(serial2_task);
615            serial2
616        } else {
617            None
618        };
619
620        if self.firmware.is_linux_direct() && !self.uses_pipette_as_init {
621            // Non-pipette-as-init Linux direct: create serial1 and a serial
622            // agent so we can send shell commands to launch pipette.
623            let (serial1_host, serial1) = self.create_serial_stream()?;
624            let (serial1_read, _serial1_write) = serial1_host.split();
625            let linux_direct_serial_agent =
626                LinuxDirectSerialAgent::new(serial1_read, serial0_write);
627            Ok(SerialData {
628                emulated_serial_config: [serial0, serial1, serial2, None],
629                serial_tasks,
630                linux_direct_serial_agent: Some(linux_direct_serial_agent),
631            })
632        } else {
633            Ok(SerialData {
634                emulated_serial_config: [serial0, None, serial2, None],
635                serial_tasks,
636                linux_direct_serial_agent: None,
637            })
638        }
639    }
640
641    fn create_serial_stream(
642        &self,
643    ) -> anyhow::Result<(
644        PolledSocket<UnixStream>,
645        Option<Resource<SerialBackendHandle>>,
646    )> {
647        let (host_side, guest_side) = UnixStream::pair()?;
648        let host_side = PolledSocket::new(self.driver, host_side)?;
649        let serial = OpenSocketSerialConfig::from(guest_side).into_resource();
650        Ok((host_side, Some(serial)))
651    }
652
653    fn load_firmware(&self) -> anyhow::Result<LoadMode> {
654        // The test kernel has both CONFIG_VIRTIO_VSOCK=y and
655        // CONFIG_HYPERV_VSOCKETS=y built in. The kernel only allows one G2H
656        // vsock transport, and virtio_vsock_init runs first, claiming the
657        // slot. This causes hv_sock registration to fail with -EBUSY,
658        // breaking pipette's AF_VSOCK connection. Blacklist virtio_vsock_init
659        // so that hv_sock can register as the G2H transport.
660        const VIRTIO_VSOCK_BLACKLIST: &str = "initcall_blacklist=virtio_vsock_init";
661
662        Ok(match (self.arch, &self.firmware) {
663            (arch, Firmware::LinuxDirect { kernel, initrd }) => {
664                let console = match arch {
665                    MachineArch::X86_64 => "console=ttyS0",
666                    MachineArch::Aarch64 => "console=ttyAMA0 earlycon",
667                };
668                let kernel = File::open(kernel.clone())
669                    .context("Failed to open kernel")?
670                    .into();
671                let initrd = File::open(initrd.clone())
672                    .context("Failed to open initrd")?
673                    .into();
674
675                let init = if self.uses_pipette_as_init {
676                    "/pipette"
677                } else {
678                    "/bin/sh"
679                };
680
681                let serial_args = if self.enable_serial {
682                    format!("{console} debug ")
683                } else {
684                    String::new()
685                };
686
687                let cmdline =
688                    format!("{serial_args}panic=-1 rdinit={init} {VIRTIO_VSOCK_BLACKLIST}");
689
690                LoadMode::Linux {
691                    kernel,
692                    initrd: Some(initrd),
693                    cmdline,
694                    custom_dsdt: None,
695                    enable_serial: self.enable_serial,
696                    boot_mode: openvmm_defs::config::LinuxDirectBootMode::Acpi,
697                }
698            }
699            (
700                MachineArch::X86_64,
701                Firmware::Pcat {
702                    bios_firmware: firmware,
703                    guest: _,         // load_boot_disk
704                    svga_firmware: _, // config_video
705                    ide_controllers: _,
706                },
707            ) => {
708                let firmware = openvmm_pcat_locator::find_pcat_bios(firmware.get())
709                    .context("Failed to load packaged PCAT binary")?;
710                LoadMode::Pcat {
711                    firmware,
712                    boot_order: DEFAULT_PCAT_BOOT_ORDER,
713                }
714            }
715            (
716                _,
717                Firmware::Uefi {
718                    uefi_firmware: firmware,
719                    guest: _, // load_boot_disk
720                    uefi_config:
721                        UefiConfig {
722                            secure_boot_enabled: _,  // new
723                            secure_boot_template: _, // new
724                            disable_frontpage,
725                            default_boot_always_attempt,
726                            enable_vpci_boot,
727                        },
728                },
729            ) => {
730                let firmware = File::open(firmware.clone())
731                    .context("Failed to open uefi firmware file")?
732                    .into();
733                LoadMode::Uefi {
734                    firmware,
735                    enable_debugging: false,
736                    enable_memory_protections: false,
737                    disable_frontpage: *disable_frontpage,
738                    enable_tpm: self.tpm_config.is_some(),
739                    enable_battery: false,
740                    enable_serial: true,
741                    enable_vpci_boot: *enable_vpci_boot,
742                    uefi_console_mode: Some(openvmm_defs::config::UefiConsoleMode::Com1),
743                    default_boot_always_attempt: *default_boot_always_attempt,
744                    bios_guid: Guid::new_random(),
745                }
746            }
747            (
748                MachineArch::X86_64,
749                Firmware::OpenhclLinuxDirect {
750                    igvm_path,
751                    openhcl_config,
752                }
753                | Firmware::OpenhclUefi {
754                    igvm_path,
755                    guest: _,       // load_boot_disk
756                    isolation: _,   // new via Firmware::isolation
757                    uefi_config: _, // config_openhcl_vmbus_devices
758                    openhcl_config,
759                },
760            ) => {
761                let OpenHclConfig {
762                    vmbus_redirect: _, // config_openhcl_vmbus_devices
763                    custom_command_line: _,
764                    log_levels: _,
765                    vtl2_base_address_type,
766                    vtl2_settings: _, // run_core
767                } = openhcl_config;
768
769                let mut cmdline = Some(openhcl_config.command_line());
770
771                append_cmdline(&mut cmdline, "panic=-1 reboot=triple");
772
773                let isolated = match self.firmware {
774                    Firmware::OpenhclLinuxDirect { .. } => {
775                        // Set UNDERHILL_SERIAL_WAIT_FOR_RTS=1 so that we don't pull serial data
776                        // until the guest is ready. Otherwise, Linux will drop the input serial
777                        // data on the floor during boot.
778                        append_cmdline(
779                            &mut cmdline,
780                            format!(
781                                "UNDERHILL_SERIAL_WAIT_FOR_RTS=1 UNDERHILL_CMDLINE_APPEND=\"rdinit=/bin/sh {VIRTIO_VSOCK_BLACKLIST}\""
782                            ),
783                        );
784                        false
785                    }
786                    Firmware::OpenhclUefi { isolation, .. } if isolation.is_some() => true,
787                    _ => false,
788                };
789
790                // For certain configurations, we need to override the override
791                // in new_underhill_vm.
792                //
793                // TODO: remove this (and OpenHCL override) once host changes
794                // are saturated.
795                if let Firmware::OpenhclUefi {
796                    uefi_config:
797                        UefiConfig {
798                            default_boot_always_attempt,
799                            secure_boot_enabled,
800                            ..
801                        },
802                    ..
803                } = self.firmware
804                {
805                    if !isolated
806                        && !secure_boot_enabled
807                        && self.tpm_config.is_none()
808                        && !default_boot_always_attempt
809                    {
810                        append_cmdline(&mut cmdline, "HCL_DEFAULT_BOOT_ALWAYS_ATTEMPT=0");
811                    }
812                }
813
814                let vtl2_base_address = vtl2_base_address_type.unwrap_or_else(|| {
815                    if isolated {
816                        // Isolated VMs must load at the location specified by
817                        // the file, as they do not support relocation.
818                        Vtl2BaseAddressType::File
819                    } else {
820                        // By default, utilize IGVM relocation and tell OpenVMM
821                        // to place VTL2 at 512MB. This tests both relocation
822                        // support in OpenVMM, and relocation support within
823                        // OpenHCL.
824                        Vtl2BaseAddressType::Absolute(512 * SIZE_1_MB)
825                    }
826                });
827
828                let file = File::open(igvm_path.clone())
829                    .context("failed to open openhcl firmware file")?
830                    .into();
831                LoadMode::Igvm {
832                    file,
833                    cmdline: cmdline.unwrap_or_default(),
834                    vtl2_base_address,
835                    com_serial: Some(SerialInformation {
836                        io_port: ComPort::Com3.io_port(),
837                        irq: ComPort::Com3.irq().into(),
838                    }),
839                }
840            }
841            (a, f) => anyhow::bail!("Unsupported firmware {f:?} for arch {a:?}"),
842        })
843    }
844
845    fn config_openhcl_vmbus_devices(
846        &self,
847        serial: &mut [Option<Resource<SerialBackendHandle>>],
848        devices: &mut impl Extend<(DeviceVtl, Resource<VmbusDeviceHandleKind>)>,
849        firmware_event_send: &mesh::Sender<FirmwareEvent>,
850        framebuffer: bool,
851    ) -> anyhow::Result<(
852        get_resources::ged::GuestEmulationDeviceHandle,
853        mesh::Sender<get_resources::ged::GuestEmulationRequest>,
854    )> {
855        let serial0 = serial[0].take();
856        devices.extend([(
857            DeviceVtl::Vtl2,
858            VmbusSerialDeviceHandle {
859                port: VmbusSerialPort::Com1,
860                backend: serial0.unwrap_or_else(|| DisconnectedSerialBackendHandle.into_resource()),
861            }
862            .into_resource(),
863        )]);
864        let serial1 = serial[1].take();
865        devices.extend([(
866            DeviceVtl::Vtl2,
867            VmbusSerialDeviceHandle {
868                port: VmbusSerialPort::Com2,
869                backend: serial1.unwrap_or_else(|| DisconnectedSerialBackendHandle.into_resource()),
870            }
871            .into_resource(),
872        )]);
873
874        let crash = spawn_dump_handler(self.driver, self.logger).into_resource();
875        devices.extend([(DeviceVtl::Vtl2, crash)]);
876
877        let (guest_request_send, guest_request_recv) = mesh::channel();
878
879        let (
880            UefiConfig {
881                secure_boot_enabled,
882                secure_boot_template,
883                disable_frontpage,
884                default_boot_always_attempt,
885                enable_vpci_boot,
886            },
887            OpenHclConfig { vmbus_redirect, .. },
888        ) = match self.firmware {
889            Firmware::OpenhclUefi {
890                uefi_config,
891                openhcl_config,
892                ..
893            } => (uefi_config, openhcl_config),
894            Firmware::OpenhclLinuxDirect { openhcl_config, .. } => {
895                (&UefiConfig::default(), openhcl_config)
896            }
897            _ => anyhow::bail!("not a supported openhcl firmware config"),
898        };
899
900        let test_gsp_by_id = self
901            .vmgs
902            .disk()
903            .is_some_and(|x| matches!(x.encryption_policy, GuestStateEncryptionPolicy::GspById(_)));
904
905        // Save the GED handle to add later after configuration is complete.
906        let ged = get_resources::ged::GuestEmulationDeviceHandle {
907            firmware: get_resources::ged::GuestFirmwareConfig::Uefi {
908                firmware_debug: false,
909                disable_frontpage: *disable_frontpage,
910                enable_vpci_boot: *enable_vpci_boot,
911                console_mode: get_resources::ged::UefiConsoleMode::COM1,
912                default_boot_always_attempt: *default_boot_always_attempt,
913            },
914            com1: true,
915            com2: true,
916            serial_tx_only: false,
917            vmbus_redirection: *vmbus_redirect,
918            vtl2_settings: None, // Will be added at startup to allow tests to modify
919            vmgs: memdiff_vmgs(self.vmgs)?,
920            framebuffer: framebuffer.then(|| SharedFramebufferHandle.into_resource()),
921            guest_request_recv,
922            enable_tpm: self.tpm_config.is_some(),
923            firmware_event_send: Some(firmware_event_send.clone()),
924            secure_boot_enabled: *secure_boot_enabled,
925            secure_boot_template: match secure_boot_template {
926                Some(SecureBootTemplate::MicrosoftWindows) => {
927                    get_resources::ged::GuestSecureBootTemplateType::MicrosoftWindows
928                }
929                Some(SecureBootTemplate::MicrosoftUefiCertificateAuthority) => {
930                    get_resources::ged::GuestSecureBootTemplateType::MicrosoftUefiCertificateAuthority
931                }
932                None => get_resources::ged::GuestSecureBootTemplateType::None,
933            },
934            enable_battery: false,
935            no_persistent_secrets: self.tpm_config.as_ref().is_some_and(|c| c.no_persistent_secrets),
936            igvm_attest_test_config: None,
937            test_gsp_by_id,
938            efi_diagnostics_log_level: Default::default(), // TODO: make configurable
939            hv_sint_enabled: false,
940        };
941
942        Ok((ged, guest_request_send))
943    }
944
945    fn config_video(
946        &self,
947    ) -> anyhow::Result<Option<(VideoDevice, Framebuffer, FramebufferAccess)>> {
948        if self.firmware.isolation().is_some() {
949            return Ok(None);
950        }
951
952        let video_dev = match self.firmware {
953            Firmware::Pcat { svga_firmware, .. } | Firmware::OpenhclPcat { svga_firmware, .. } => {
954                Some(VideoDevice::Vga(
955                    openvmm_pcat_locator::find_svga_bios(svga_firmware.get())
956                        .context("Failed to load VGA BIOS")?,
957                ))
958            }
959            Firmware::Uefi { .. } | Firmware::OpenhclUefi { .. } => Some(VideoDevice::Synth(
960                DeviceVtl::Vtl0,
961                SynthVideoHandle {
962                    framebuffer: SharedFramebufferHandle.into_resource(),
963                }
964                .into_resource(),
965            )),
966            Firmware::OpenhclLinuxDirect { .. } | Firmware::LinuxDirect { .. } => None,
967        };
968
969        Ok(if let Some(vdev) = video_dev {
970            let vram =
971                alloc_shared_memory(FRAMEBUFFER_SIZE, "vram").context("allocating framebuffer")?;
972            let (fb, fba) = framebuffer::framebuffer(vram, FRAMEBUFFER_SIZE, 0)
973                .context("creating framebuffer")?;
974            Some((vdev, fb, fba))
975        } else {
976            None
977        })
978    }
979
980    async fn config_tpm(&self) -> anyhow::Result<Option<ChipsetDeviceHandle>> {
981        if !self.firmware.is_openhcl()
982            && let Some(TpmConfig {
983                no_persistent_secrets,
984            }) = self.tpm_config
985        {
986            let register_layout = match self.arch {
987                MachineArch::X86_64 => TpmRegisterLayout::IoPort,
988                MachineArch::Aarch64 => TpmRegisterLayout::Mmio,
989            };
990
991            let (ppi_store, nvram_store) = if self.vmgs.disk().is_none() || *no_persistent_secrets {
992                (
993                    EphemeralNonVolatileStoreHandle.into_resource(),
994                    EphemeralNonVolatileStoreHandle.into_resource(),
995                )
996            } else {
997                (
998                    VmgsFileHandle::new(vmgs_format::FileId::TPM_PPI, true).into_resource(),
999                    VmgsFileHandle::new(vmgs_format::FileId::TPM_NVRAM, true).into_resource(),
1000                )
1001            };
1002
1003            Ok(Some(ChipsetDeviceHandle {
1004                name: "tpm".to_string(),
1005                resource: chipset_device_worker_defs::RemoteChipsetDeviceHandle {
1006                    device: TpmDeviceHandle {
1007                        ppi_store,
1008                        nvram_store,
1009                        refresh_tpm_seeds: false,
1010                        ak_cert_type: tpm_resources::TpmAkCertTypeResource::None,
1011                        register_layout,
1012                        guest_secret_key: None,
1013                        logger: None,
1014                        is_confidential_vm: self.firmware.isolation().is_some(),
1015                        // TODO: generate an actual BIOS GUID and put it here
1016                        bios_guid: Guid::ZERO,
1017                        nvram_size: None,
1018                    }
1019                    .into_resource(),
1020                    worker_host: self.make_device_worker("tpm").await?,
1021                }
1022                .into_resource(),
1023            }))
1024        } else {
1025            Ok(None)
1026        }
1027    }
1028
1029    async fn make_device_worker(&self, name: &str) -> anyhow::Result<mesh_worker::WorkerHost> {
1030        let (host, runner) = mesh_worker::worker_host();
1031        self.mesh
1032            .launch_host(
1033                mesh_process::ProcessConfig::new(name).process_name(self.openvmm_path),
1034                openvmm_defs::entrypoint::MeshHostParams { runner },
1035            )
1036            .await?;
1037        Ok(host)
1038    }
1039}
1040
1041fn spawn_dump_handler(driver: &DefaultDriver, logger: &PetriLogSource) -> GuestCrashDeviceHandle {
1042    let (send, mut recv) = mesh::channel();
1043    let handle = GuestCrashDeviceHandle {
1044        request_dump: send,
1045        max_dump_size: 256 * 1024 * 1024,
1046    };
1047    driver
1048        .spawn("openhcl-dump-handler", {
1049            let logger = logger.clone();
1050            let driver = driver.clone();
1051            async move {
1052                while let Some(rpc) = recv.next().await {
1053                    rpc.handle_failable_sync(|done| {
1054                        let (file, path) = logger.create_attachment("openhcl.core")?.into_parts();
1055                        driver
1056                            .spawn("crash-waiter", async move {
1057                                let filename = path.file_name().unwrap().to_str().unwrap();
1058                                if done.await.is_ok() {
1059                                    tracing::warn!(filename, "openhcl crash dump complete");
1060                                } else {
1061                                    tracing::error!(
1062                                        filename,
1063                                        "openhcl crash dump incomplete, may be corrupted"
1064                                    );
1065                                }
1066                            })
1067                            .detach();
1068                        anyhow::Ok(file)
1069                    })
1070                }
1071            }
1072        })
1073        .detach();
1074    handle
1075}
1076
1077/// Convert the generic IDE configuration to OpenVMM IDE disks.
1078fn ide_controllers_to_openvmm(
1079    ide_controllers: Option<&[[Option<Drive>; 2]; 2]>,
1080) -> anyhow::Result<Vec<IdeDeviceConfig>> {
1081    let mut ide_disks = Vec::new();
1082
1083    if let Some(ide_controllers) = ide_controllers {
1084        for (controller_number, controller) in ide_controllers.iter().enumerate() {
1085            for (controller_location, drive) in controller.iter().enumerate() {
1086                if let Some(drive) = drive {
1087                    if let Some(disk) = &drive.disk {
1088                        let disk = petri_disk_to_openvmm(disk)?;
1089                        let guest_media = if drive.is_dvd {
1090                            GuestMedia::Dvd(
1091                                SimpleScsiDvdHandle {
1092                                    media: Some(disk),
1093                                    requests: None,
1094                                }
1095                                .into_resource(),
1096                            )
1097                        } else {
1098                            GuestMedia::Disk {
1099                                disk_type: disk,
1100                                read_only: false,
1101                                disk_parameters: None,
1102                            }
1103                        };
1104
1105                        ide_disks.push(IdeDeviceConfig {
1106                            path: ide_resources::IdePath {
1107                                channel: controller_number as u8,
1108                                drive: controller_location as u8,
1109                            },
1110                            guest_media,
1111                        });
1112                    }
1113                }
1114            }
1115        }
1116    }
1117
1118    Ok(ide_disks)
1119}
1120
1121/// Convert the generic VMBUS storage configuration to OpenVMM VMBUS and VPCI devices.
1122fn vmbus_storage_controllers_to_openvmm(
1123    vmbus_storage_controllers: &HashMap<Guid, VmbusStorageController>,
1124) -> anyhow::Result<(
1125    Vec<(DeviceVtl, Resource<VmbusDeviceHandleKind>)>,
1126    Vec<VpciDeviceConfig>,
1127)> {
1128    let mut vmbus_devices = Vec::new();
1129    let mut vpci_devices = Vec::new();
1130
1131    // Add VMBus storage
1132    for (instance_id, controller) in vmbus_storage_controllers {
1133        let vtl = match controller.target_vtl {
1134            crate::Vtl::Vtl0 => DeviceVtl::Vtl0,
1135            crate::Vtl::Vtl1 => DeviceVtl::Vtl1,
1136            crate::Vtl::Vtl2 => DeviceVtl::Vtl2,
1137        };
1138        match controller.controller_type {
1139            VmbusStorageType::Scsi => {
1140                let mut devices = Vec::new();
1141                for (lun, Drive { disk, is_dvd }) in &controller.drives {
1142                    if !*is_dvd && let Some(disk) = disk {
1143                        devices.push(ScsiDeviceAndPath {
1144                            path: ScsiPath {
1145                                path: 0,
1146                                target: 0,
1147                                lun: (*lun).try_into().expect("invalid scsi lun"),
1148                            },
1149                            device: SimpleScsiDiskHandle {
1150                                disk: petri_disk_to_openvmm(disk)?,
1151                                read_only: false,
1152                                parameters: Default::default(),
1153                            }
1154                            .into_resource(),
1155                        });
1156                    } else {
1157                        todo!("dvd ({}) or empty ({})", *is_dvd, disk.is_none())
1158                    }
1159                }
1160
1161                vmbus_devices.push((
1162                    vtl,
1163                    ScsiControllerHandle {
1164                        instance_id: *instance_id,
1165                        max_sub_channel_count: 1,
1166                        io_queue_depth: None,
1167                        devices,
1168                        requests: None,
1169                        poll_mode_queue_depth: None,
1170                    }
1171                    .into_resource(),
1172                ));
1173            }
1174            VmbusStorageType::Nvme => {
1175                let mut namespaces = Vec::new();
1176                for (nsid, Drive { disk, is_dvd }) in &controller.drives {
1177                    if !*is_dvd && let Some(disk) = disk {
1178                        namespaces.push(NamespaceDefinition {
1179                            nsid: *nsid,
1180                            read_only: false,
1181                            disk: petri_disk_to_openvmm(disk)?,
1182                        });
1183                    } else {
1184                        todo!("dvd ({}) or empty ({})", *is_dvd, disk.is_none())
1185                    }
1186                }
1187
1188                vpci_devices.push(VpciDeviceConfig {
1189                    vtl,
1190                    instance_id: *instance_id,
1191                    resource: NvmeControllerHandle {
1192                        subsystem_id: *instance_id,
1193                        max_io_queues: 64,
1194                        msix_count: 64,
1195                        namespaces,
1196                        requests: None,
1197                    }
1198                    .into_resource(),
1199                });
1200            }
1201            VmbusStorageType::VirtioBlk => {
1202                // Each virtio-blk drive needs a unique VPCI instance ID.
1203                // Use a fixed template GUID with data1 set to the LUN.
1204                const VIRTIO_BLK_INSTANCE_ID_TEMPLATE: Guid = Guid {
1205                    data1: 0,
1206                    data2: 0x1234,
1207                    data3: 0x5678,
1208                    data4: [0xab, 0xcd, 0xef, 0x01, 0x23, 0x45, 0x67, 0x89],
1209                };
1210                for (lun, Drive { disk, is_dvd }) in &controller.drives {
1211                    if *is_dvd {
1212                        anyhow::bail!("dvd not supported with virtio-blk");
1213                    }
1214                    let Some(disk) = disk else {
1215                        anyhow::bail!("empty drive not supported with virtio-blk");
1216                    };
1217                    let mut drive_id = VIRTIO_BLK_INSTANCE_ID_TEMPLATE;
1218                    drive_id.data1 = *lun;
1219                    vpci_devices.push(VpciDeviceConfig {
1220                        vtl,
1221                        instance_id: drive_id,
1222                        resource: VirtioPciDeviceHandle(
1223                            VirtioBlkHandle {
1224                                disk: petri_disk_to_openvmm(disk)?,
1225                                read_only: false,
1226                            }
1227                            .into_resource(),
1228                        )
1229                        .into_resource(),
1230                    });
1231                }
1232            }
1233        }
1234    }
1235
1236    Ok((vmbus_devices, vpci_devices))
1237}