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