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