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