Skip to main content

openvmm_entry/
lib.rs

1// Copyright (c) Microsoft Corporation.
2// Licensed under the MIT License.
3
4//! This module implements the interactive control process and the entry point
5//! for the worker process.
6
7#![expect(missing_docs)]
8#![forbid(unsafe_code)]
9
10mod cli_args;
11mod crash_dump;
12mod kvp;
13mod meshworker;
14mod pidfile;
15mod repl;
16mod serial_io;
17mod storage_builder;
18mod tracing_init;
19mod ttrpc;
20mod usb;
21mod vm_controller;
22
23// `pub` so that the missing_docs warning fires for options without
24// documentation.
25pub use cli_args::Options;
26use console_relay::ConsoleLaunchOptions;
27
28use crate::cli_args::SecureBootTemplateCli;
29use anyhow::Context;
30use anyhow::bail;
31use chipset_resources::acpi_power_button::PowerButtonRpc;
32use chipset_resources::battery::HostBatteryUpdate;
33use cli_args::DiskCliKind;
34use cli_args::EfiDiagnosticsLogLevelCli;
35use cli_args::EndpointConfigCli;
36use cli_args::IgvmPersonalityCli;
37use cli_args::NicConfigCli;
38use cli_args::ProvisionCondition;
39use cli_args::SerialConfigCli;
40use cli_args::TpmVersionCli;
41use cli_args::UefiConsoleModeCli;
42use cli_args::VirtioBusCli;
43use crash_dump::spawn_dump_handler;
44use cxl::test::CxlTestDeviceHandle;
45use disk_backend_resources::DelayDiskHandle;
46use disk_backend_resources::DiskLayerDescription;
47use disk_backend_resources::layer::DiskLayerHandle;
48use disk_backend_resources::layer::RamDiskLayerHandle;
49use disk_backend_resources::layer::SqliteAutoCacheDiskLayerHandle;
50use disk_backend_resources::layer::SqliteDiskLayerHandle;
51use floppy_resources::FloppyDiskConfig;
52use framebuffer::FRAMEBUFFER_SIZE;
53use framebuffer::FramebufferAccess;
54use futures::AsyncReadExt;
55use futures::AsyncWrite;
56use futures::StreamExt;
57use futures::executor::block_on;
58use futures::io::AllowStdIo;
59use gdma_resources::GdmaDeviceHandle;
60use gdma_resources::VportDefinition;
61use guid::Guid;
62use input_core::MultiplexedInputHandle;
63use inspect::InspectMut;
64use mesh::CancelContext;
65use mesh::CellUpdater;
66use mesh::rpc::RpcSend;
67use meshworker::VmmMesh;
68use net_backend_resources::mac_address::MacAddress;
69use nvme_resources::NvmeControllerRequest;
70use openvmm_defs::config::Config;
71use openvmm_defs::config::DEFAULT_ACPI_POWER_BUTTON_IRQ_X64;
72use openvmm_defs::config::DEFAULT_ACPI_POWER_BUTTON_SPI_AARCH64;
73use openvmm_defs::config::DEFAULT_PCAT_BOOT_ORDER;
74use openvmm_defs::config::DeviceVtl;
75use openvmm_defs::config::HypervisorConfig;
76use openvmm_defs::config::IgvmPersonality;
77use openvmm_defs::config::LateMapVtl0MemoryPolicy;
78use openvmm_defs::config::LoadMode;
79use openvmm_defs::config::MemoryConfig;
80use openvmm_defs::config::NumaDistance;
81use openvmm_defs::config::NumaNode;
82use openvmm_defs::config::NumaTopology;
83use openvmm_defs::config::PcieDeviceConfig;
84use openvmm_defs::config::PcieMmioRangeConfig;
85use openvmm_defs::config::PciePortConfig;
86use openvmm_defs::config::PcieRootComplexConfig;
87use openvmm_defs::config::PcieSwitchConfig;
88use openvmm_defs::config::ProcessorTopologyConfig;
89use openvmm_defs::config::RootComplexCxlConfig;
90use openvmm_defs::config::UartId;
91use openvmm_defs::config::VirtioBus;
92use openvmm_defs::config::VmbusConfig;
93use openvmm_defs::config::VpAssignment;
94use openvmm_defs::config::VpciDeviceConfig;
95use openvmm_defs::config::Vtl2BaseAddressType;
96use openvmm_defs::config::Vtl2Config;
97use openvmm_defs::rpc::VmRpc;
98use openvmm_defs::worker::VM_WORKER;
99use openvmm_defs::worker::VmWorkerParameters;
100use openvmm_helpers::config::supports_acpi_power_button;
101use openvmm_helpers::disk::OpenDiskOptions;
102use openvmm_helpers::disk::create_disk_type;
103use openvmm_helpers::disk::open_disk_type;
104use pal_async::DefaultDriver;
105use pal_async::DefaultPool;
106use pal_async::socket::PolledSocket;
107use pal_async::task::Spawn;
108use pal_async::task::Task;
109use serial_16550_resources::ComPort;
110use serial_core::resources::DisconnectedSerialBackendHandle;
111use serial_core::resources::NullSerialBackendHandle;
112use sparse_mmap::alloc_shared_memory;
113use std::cell::RefCell;
114use std::collections::BTreeMap;
115use std::collections::HashSet;
116use std::fmt::Write as _;
117use std::io;
118#[cfg(unix)]
119use std::io::IsTerminal;
120use std::io::Write;
121use std::net::TcpListener;
122use std::path::Path;
123use std::path::PathBuf;
124use std::sync::Arc;
125use std::thread;
126use std::time::Duration;
127use storvsp_resources::ScsiControllerRequest;
128use tpm_resources::TpmDeviceHandle;
129use tpm_resources::TpmRegisterLayout;
130use tpm_resources::TpmVersion;
131use uidevices_resources::SynthKeyboardHandle;
132use uidevices_resources::SynthMouseHandle;
133use uidevices_resources::SynthVideoHandle;
134use video_core::SharedFramebufferHandle;
135use virtio_resources::VirtioPciDeviceHandle;
136use vm_manifest_builder::BaseChipsetType;
137use vm_manifest_builder::MachineArch;
138use vm_manifest_builder::VmChipsetResult;
139use vm_manifest_builder::VmManifestBuilder;
140use vm_resource::IntoResource;
141use vm_resource::Resource;
142use vm_resource::kind::DiskHandleKind;
143use vm_resource::kind::DiskLayerHandleKind;
144use vm_resource::kind::NetEndpointHandleKind;
145use vm_resource::kind::NonVolatileStoreKind;
146use vm_resource::kind::VirtioDeviceHandle;
147use vm_resource::kind::VmbusDeviceHandleKind;
148use vmbus_serial_resources::VmbusSerialDeviceHandle;
149use vmbus_serial_resources::VmbusSerialPort;
150use vmcore::non_volatile_store::resources::EphemeralNonVolatileStoreHandle;
151use vmgs_resources::GuestStateEncryptionPolicy;
152use vmgs_resources::VmgsDisk;
153use vmgs_resources::VmgsFileHandle;
154use vmgs_resources::VmgsResource;
155use vmotherboard::ChipsetDeviceHandle;
156use vnc_worker_defs::VncParameters;
157
158pub fn openvmm_main() {
159    // Save the current state of the terminal so we can restore it back to
160    // normal before exiting.
161    #[cfg(unix)]
162    let orig_termios = io::stderr().is_terminal().then(term::get_termios);
163
164    let mut pidfile_guard: Option<pidfile::Pidfile> = None;
165    let exit_code = match do_main(&mut pidfile_guard) {
166        Ok(code) => code,
167        Err(err) => {
168            eprintln!("fatal error: {:?}", err);
169            1
170        }
171    };
172
173    // Restore the terminal to its initial state.
174    #[cfg(unix)]
175    if let Some(orig_termios) = orig_termios {
176        term::set_termios(orig_termios);
177    }
178
179    // Clean up the pidfile before terminating, since
180    // pal::process::terminate skips destructors.
181    drop(pidfile_guard);
182
183    // Terminate the process immediately without graceful shutdown of DLLs or
184    // C++ destructors or anything like that. This is all unnecessary and saves
185    // time on Windows.
186    //
187    // Do flush stdout, though, since there may be buffered data.
188    let _ = io::stdout().flush();
189    pal::process::terminate(exit_code);
190}
191
192#[derive(Default)]
193struct VmResources {
194    console_in: Option<Box<dyn AsyncWrite + Send + Unpin>>,
195    /// Keeps the dedicated serial reactor alive while serial I/O objects exist.
196    serial_driver: Option<DefaultDriver>,
197    framebuffer_access: Option<FramebufferAccess>,
198    shutdown_ic: Option<mesh::Sender<hyperv_ic_resources::shutdown::ShutdownRpc>>,
199    acpi_power_button: Option<mesh::Sender<PowerButtonRpc>>,
200    kvp_ic: Option<mesh::Sender<hyperv_ic_resources::kvp::KvpConnectRpc>>,
201    scsi_rpc: Option<mesh::Sender<ScsiControllerRequest>>,
202    nvme_vtl2_rpc: Option<mesh::Sender<NvmeControllerRequest>>,
203    consomme_rpc: Option<mesh::Sender<net_backend_resources::consomme::ConsommeRequest>>,
204    ged_rpc: Option<mesh::Sender<get_resources::ged::GuestEmulationRequest>>,
205    vtl2_settings: Option<vtl2_settings_proto::Vtl2Settings>,
206    /// Receives dirty rectangles from the synthetic video device for the VNC worker.
207    dirty_rect_recv: Option<mesh::Receiver<Vec<video_core::DirtyRect>>>,
208    #[cfg(windows)]
209    switch_ports: Vec<vmswitch::kernel::SwitchPort>,
210}
211
212struct ConsoleState<'a> {
213    device: &'a str,
214    input: Box<dyn AsyncWrite + Unpin + Send>,
215}
216
217/// Builds controller configurations from the USB command-line options. The
218/// controller resolver assigns omitted connectors and validates connectors.
219fn usb_controller_configs(
220    pcie_ports: &[cli_args::UsbXhciCli],
221    devices: &[cli_args::UsbDeviceCli],
222) -> anyhow::Result<Vec<(String, usb_config_resources::UsbControllerConfig)>> {
223    let mut controllers: Vec<_> = pcie_ports
224        .iter()
225        .map(|pcie_port| usb::UsbControllerSpec {
226            port_name: pcie_port.port_name.clone(),
227            devices: Vec::new(),
228            winusb_all: false,
229        })
230        .collect();
231    let default_pcie_port = if let [pcie_port] = pcie_ports {
232        Some(pcie_port.port_name.as_str())
233    } else {
234        None
235    };
236    for device in devices {
237        let pcie_port_name = device
238            .controller_pcie_port()
239            .or(default_pcie_port)
240            .context("on=PCIE_PORT is required unless exactly one xHCI controller is declared")?;
241        let controller = controllers
242            .iter_mut()
243            .find(|controller| controller.port_name == pcie_port_name)
244            .with_context(|| {
245                format!(
246                    "USB device references undeclared controller on PCIe port {pcie_port_name:?}"
247                )
248            })?;
249        match device {
250            cli_args::UsbDeviceCli::Device { location, backend } => controller.devices.push((
251                location.connector.map(cli_args::UsbConnectorCli::number),
252                backend.clone(),
253            )),
254            cli_args::UsbDeviceCli::WinUsbAll { .. } => {
255                anyhow::ensure!(
256                    !controller.winusb_all,
257                    "winusb-all may be selected only once per VM"
258                );
259                controller.winusb_all = true;
260            }
261        }
262    }
263    usb::controller_configs(controllers)
264}
265
266/// Build a flat list of switches with their parent port assignments.
267///
268/// This function converts hierarchical CLI switch definitions into a flat list
269/// where each switch specifies its parent port directly.
270fn build_switch_list(all_switches: &[cli_args::GenericPcieSwitchCli]) -> Vec<PcieSwitchConfig> {
271    all_switches
272        .iter()
273        .map(|switch_cli| PcieSwitchConfig {
274            name: switch_cli.name.clone(),
275            parent_port: switch_cli.port_name.clone(),
276            ports: (0..switch_cli.num_downstream_ports)
277                .map(|i| PciePortConfig {
278                    name: format!("{}-downstream-{}", switch_cli.name, i),
279                    devfn: None,
280                    hotplug: switch_cli.hotplug,
281                    acs_capabilities_supported: switch_cli.acs_capabilities_supported,
282                    cxl: false,
283                    pasid: switch_cli.pasid,
284                })
285                .collect(),
286        })
287        .collect()
288}
289
290fn base_chipset_type(opt: &Options) -> BaseChipsetType {
291    if let Some(igvm) = &opt.igvm {
292        match igvm.personality {
293            IgvmPersonalityCli::Openhcl => BaseChipsetType::HclHost,
294            IgvmPersonalityCli::Uefi => BaseChipsetType::HypervGen2Uefi,
295            IgvmPersonalityCli::LinuxDirect if opt.isolation.is_some() => {
296                BaseChipsetType::EnlightenedLinuxDirect
297            }
298            IgvmPersonalityCli::LinuxDirect if opt.hv => BaseChipsetType::HyperVGen2LinuxDirect,
299            IgvmPersonalityCli::LinuxDirect => BaseChipsetType::UnenlightenedLinuxDirect,
300        }
301    } else if matches!(opt.isolation, Some(cli_args::IsolationCli::Snp)) {
302        BaseChipsetType::EnlightenedLinuxDirect
303    } else if opt.pcat {
304        BaseChipsetType::HypervGen1
305    } else if opt.uefi.is_some() {
306        BaseChipsetType::HypervGen2Uefi
307    } else if opt.hv {
308        BaseChipsetType::HyperVGen2LinuxDirect
309    } else {
310        BaseChipsetType::UnenlightenedLinuxDirect
311    }
312}
313
314/// Build the loader's [`SmbiosConfig`](openvmm_defs::config::SmbiosConfig) from
315/// the parsed `--smbios` arguments.
316///
317/// Multiple `--smbios` arguments are merged (erroring on a field set twice).
318/// String overrides left unset fall through to the loader's default identity.
319/// The system UUID defaults to the all-zero GUID unless overridden with
320/// `uuid=GUID`; `uuid=random` requests a freshly generated per-VM GUID.
321fn smbios_config_from_cli(
322    args: &[cli_args::SmbiosCli],
323) -> anyhow::Result<openvmm_defs::config::SmbiosConfig> {
324    let mut merged = cli_args::SmbiosCli::default();
325    for arg in args {
326        merged.merge(arg.clone())?;
327    }
328    let cli_args::SmbiosCli {
329        bios:
330            cli_args::SmbiosBiosCli {
331                vendor: bios_vendor,
332                version: bios_version,
333                release_date: bios_release_date,
334                release: bios_release,
335            },
336        system:
337            cli_args::SmbiosSystemCli {
338                manufacturer: system_manufacturer,
339                product_name: system_product,
340                version: system_version,
341                serial_number: system_serial,
342                sku_number: system_sku,
343                family: system_family,
344                uuid: system_uuid,
345            },
346    } = merged;
347    Ok(openvmm_defs::config::SmbiosConfig {
348        bios: openvmm_defs::config::SmbiosBiosOverrides {
349            vendor: bios_vendor,
350            version: bios_version,
351            release_date: bios_release_date,
352            release: bios_release.map(|r| (r.0, r.1)),
353        },
354        system: openvmm_defs::config::SmbiosSystemOverrides {
355            manufacturer: system_manufacturer,
356            product_name: system_product,
357            version: system_version,
358            serial_number: system_serial,
359            sku_number: system_sku,
360            family: system_family,
361            uuid: match system_uuid {
362                None => Guid::ZERO,
363                Some(cli_args::SmbiosUuid::Random) => Guid::new_random(),
364                Some(cli_args::SmbiosUuid::Fixed(guid)) => guid,
365            },
366        },
367    })
368}
369
370async fn vm_config_from_command_line(
371    spawner: impl Spawn,
372    mesh: &VmmMesh,
373    opt: &Options,
374) -> anyhow::Result<(Config, VmResources)> {
375    opt.validate_isolation_options()?;
376    opt.validate_igvm_options()?;
377
378    let (_, serial_driver) = DefaultPool::spawn_on_thread("serial");
379    let uefi = opt.effective_uefi()?;
380    let default_uefi = cli_args::UefiCli::default();
381    let uefi_options = uefi.as_ref().unwrap_or(&default_uefi);
382
383    let openhcl_vtl = if opt.vtl2 {
384        DeviceVtl::Vtl2
385    } else {
386        DeviceVtl::Vtl0
387    };
388
389    let console_state: RefCell<Option<ConsoleState<'_>>> = RefCell::new(None);
390    let setup_serial = |name: &str, cli_cfg, device| -> anyhow::Result<_> {
391        Ok(match cli_cfg {
392            SerialConfigCli::Console => {
393                if let Some(console_state) = console_state.borrow().as_ref() {
394                    bail!("console already set by {}", console_state.device);
395                }
396                let (config, serial) = serial_io::anonymous_serial_pair(&serial_driver)?;
397                let (serial_read, serial_write) = AsyncReadExt::split(serial);
398                *console_state.borrow_mut() = Some(ConsoleState {
399                    device,
400                    input: Box::new(serial_write),
401                });
402                thread::Builder::new()
403                    .name(name.to_owned())
404                    .spawn(move || {
405                        let _ = block_on(futures::io::copy(
406                            serial_read,
407                            &mut AllowStdIo::new(term::raw_stdout()),
408                        ));
409                    })
410                    .unwrap();
411                Some(config)
412            }
413            SerialConfigCli::Stderr => {
414                let (config, serial) = serial_io::anonymous_serial_pair(&serial_driver)?;
415                thread::Builder::new()
416                    .name(name.to_owned())
417                    .spawn(move || {
418                        let _ = block_on(futures::io::copy(
419                            serial,
420                            &mut AllowStdIo::new(term::raw_stderr()),
421                        ));
422                    })
423                    .unwrap();
424                Some(config)
425            }
426            SerialConfigCli::File(path) => Some(serial_io::file_serial(&path, false)?),
427            SerialConfigCli::None => None,
428            SerialConfigCli::Null => Some(NullSerialBackendHandle.into_resource()),
429            SerialConfigCli::Pipe(path) => {
430                Some(serial_io::bind_serial(&path).context("failed to bind serial")?)
431            }
432            SerialConfigCli::Tcp(addr) => {
433                Some(serial_io::bind_tcp_serial(&addr).context("failed to bind serial")?)
434            }
435            SerialConfigCli::NewConsole(app, window_title) => {
436                let path = console_relay::random_console_path();
437                let config =
438                    serial_io::bind_serial(&path).context("failed to bind console serial")?;
439                let window_title =
440                    window_title.unwrap_or_else(|| name.to_uppercase() + " [OpenVMM]");
441
442                console_relay::launch_console(
443                    app.or_else(openvmm_terminal_app).as_deref(),
444                    &path,
445                    ConsoleLaunchOptions {
446                        window_title: Some(window_title),
447                    },
448                )
449                .context("failed to launch console")?;
450
451                Some(config)
452            }
453        })
454    };
455
456    let mut vmbus_devices = Vec::new();
457
458    let com_debugger_mode = [
459        opt.com1.as_ref().is_some_and(|c| c.debugger_mode),
460        opt.com2.as_ref().is_some_and(|c| c.debugger_mode),
461        opt.com3.as_ref().is_some_and(|c| c.debugger_mode),
462        opt.com4.as_ref().is_some_and(|c| c.debugger_mode),
463    ];
464
465    let serial0_cfg = setup_serial(
466        "com1",
467        opt.com1
468            .clone()
469            .map_or(SerialConfigCli::Console, |c| c.backend),
470        if cfg!(guest_arch = "x86_64") {
471            "ttyS0"
472        } else {
473            "ttyAMA0"
474        },
475    )?;
476    let serial1_cfg = setup_serial(
477        "com2",
478        opt.com2
479            .clone()
480            .map_or(SerialConfigCli::None, |c| c.backend),
481        if cfg!(guest_arch = "x86_64") {
482            "ttyS1"
483        } else {
484            "ttyAMA1"
485        },
486    )?;
487    let serial2_cfg = setup_serial(
488        "com3",
489        opt.com3
490            .clone()
491            .map_or(SerialConfigCli::None, |c| c.backend),
492        if cfg!(guest_arch = "x86_64") {
493            "ttyS2"
494        } else {
495            "ttyAMA2"
496        },
497    )?;
498    let serial3_cfg = setup_serial(
499        "com4",
500        opt.com4
501            .clone()
502            .map_or(SerialConfigCli::None, |c| c.backend),
503        if cfg!(guest_arch = "x86_64") {
504            "ttyS3"
505        } else {
506            "ttyAMA3"
507        },
508    )?;
509    let with_vmbus_com1_serial = if let Some(vmbus_com1_cfg) = setup_serial(
510        "vmbus_com1",
511        opt.vmbus_com1_serial
512            .clone()
513            .unwrap_or(SerialConfigCli::None),
514        "vmbus_com1",
515    )? {
516        vmbus_devices.push((
517            openhcl_vtl,
518            VmbusSerialDeviceHandle {
519                port: VmbusSerialPort::Com1,
520                backend: vmbus_com1_cfg,
521            }
522            .into_resource(),
523        ));
524        true
525    } else {
526        false
527    };
528    let with_vmbus_com2_serial = if let Some(vmbus_com2_cfg) = setup_serial(
529        "vmbus_com2",
530        opt.vmbus_com2_serial
531            .clone()
532            .unwrap_or(SerialConfigCli::None),
533        "vmbus_com2",
534    )? {
535        vmbus_devices.push((
536            openhcl_vtl,
537            VmbusSerialDeviceHandle {
538                port: VmbusSerialPort::Com2,
539                backend: vmbus_com2_cfg,
540            }
541            .into_resource(),
542        ));
543        true
544    } else {
545        false
546    };
547    let debugcon_cfg = setup_serial(
548        "debugcon",
549        opt.debugcon
550            .clone()
551            .map(|cfg| cfg.serial)
552            .unwrap_or(SerialConfigCli::None),
553        "debugcon",
554    )?;
555
556    let virtio_console_backend = if let Some(serial_cfg) = opt.virtio_console.clone() {
557        setup_serial("virtio-console", serial_cfg, "hvc0")?
558    } else {
559        None
560    };
561
562    let mut resources = VmResources::default();
563    let mut console_str = "";
564    if let Some(ConsoleState { device, input }) = console_state.into_inner() {
565        resources.console_in = Some(input);
566        console_str = device;
567    }
568
569    if opt.shared_memory {
570        tracing::warn!("--shared-memory/-M flag has no effect and will be removed");
571    }
572    if opt.deprecated_prefetch {
573        tracing::warn!("--prefetch is deprecated; use --memory prefetch=on");
574    }
575    if opt.deprecated_private_memory {
576        tracing::warn!("--private-memory is deprecated; use --memory shared=off");
577    }
578    if opt.deprecated_thp {
579        tracing::warn!("--thp is deprecated; use --memory shared=off,thp=on");
580    }
581    if opt.deprecated_memory_backing_file.is_some() {
582        tracing::warn!("--memory-backing-file is deprecated; use --memory file=<path>");
583    }
584
585    opt.validate_memory_options()?;
586
587    const MAX_PROCESSOR_COUNT: u32 = 1024;
588
589    if opt.processors == 0 || opt.processors > MAX_PROCESSOR_COUNT {
590        bail!("invalid proc count: {}", opt.processors);
591    }
592
593    // Total SCSI channel count should not exceed the processor count
594    // (at most, one channel per VP).
595    if opt.scsi_sub_channels > (MAX_PROCESSOR_COUNT - 1) as u16 {
596        bail!(
597            "invalid SCSI sub-channel count: requested {}, max {}",
598            opt.scsi_sub_channels,
599            MAX_PROCESSOR_COUNT - 1
600        );
601    }
602
603    let with_get = opt.get || (opt.vtl2 && !opt.no_get);
604
605    let mut storage = storage_builder::StorageBuilder::new(with_get.then_some(openhcl_vtl));
606
607    // Register named controllers first, so that --disk on=<name>
608    // references can be resolved.
609    for ctrl in &opt.nvme_pci {
610        let transport = match &ctrl.transport {
611            cli_args::NvmeControllerTransport::Pcie(port) => {
612                storage_builder::NvmeControllerTransport::Pcie(port.clone())
613            }
614            cli_args::NvmeControllerTransport::Vpci(guid) => {
615                let guid = guid.unwrap_or_else(|| storage_builder::deterministic_guid(&ctrl.id));
616                storage_builder::NvmeControllerTransport::Vpci(guid)
617            }
618        };
619        storage.add_nvme_controller(ctrl.id.clone(), ctrl.vtl, transport, None)?;
620    }
621
622    for ctrl in &opt.vmbus_scsi {
623        let instance_id = storage_builder::deterministic_guid(&ctrl.id);
624        storage.add_scsi_controller(ctrl.id.clone(), ctrl.vtl, instance_id, ctrl.sub_channels)?;
625    }
626
627    for ctrl in &opt.openhcl_controller {
628        let controller_type = match ctrl.controller_type {
629            cli_args::OpenhclControllerType::Scsi => storage_builder::OpenhclControllerType::Scsi,
630            cli_args::OpenhclControllerType::Nvme => storage_builder::OpenhclControllerType::Nvme,
631        };
632        let instance_id = ctrl
633            .guid
634            .unwrap_or_else(|| storage_builder::deterministic_guid(&ctrl.id));
635        storage.add_openhcl_controller(ctrl.id.clone(), controller_type, instance_id)?;
636    }
637
638    for &cli_args::DiskCli {
639        vtl,
640        ref kind,
641        read_only,
642        is_dvd,
643        underhill,
644        ref pcie_port,
645        ref serial,
646        ref controller,
647        nsid,
648        lun,
649        ref relay,
650    } in &opt.disk
651    {
652        if serial.is_some() {
653            anyhow::bail!("`serial` is only supported by `--virtio-blk`");
654        }
655        if controller.is_none() && underhill.is_none() && relay.is_none() {
656            tracing::warn!(
657                "--disk without `on` is deprecated; \
658                 use --vmbus-scsi and --disk on=<name> instead"
659            );
660        }
661
662        let relay_target = relay
663            .as_ref()
664            .map(|(name, loc)| storage_builder::RelayTarget {
665                controller: name.clone(),
666                location: *loc,
667            });
668
669        let target = if let Some(name) = controller {
670            if pcie_port.is_some() {
671                anyhow::bail!("`on` is incompatible with `pcie_port` on `--disk`");
672            }
673            storage_builder::DiskLocation::Named {
674                controller: name.clone(),
675                nsid,
676                lun,
677            }
678        } else if pcie_port.is_some() {
679            anyhow::bail!("`--disk` is incompatible with `pcie_port` without `controller`");
680        } else {
681            if opt.no_vmbus {
682                anyhow::bail!(
683                    "`--disk` without `on=` attaches to the default VMBus SCSI controller and \
684                     cannot be used with `--no-vmbus`; use `on=<name>` to attach to a named controller"
685                );
686            }
687            storage_builder::DiskLocation::Scsi(None)
688        };
689
690        storage
691            .add(
692                vtl,
693                underhill,
694                relay_target,
695                target,
696                kind,
697                is_dvd,
698                read_only,
699            )
700            .await?;
701    }
702
703    for &cli_args::IdeDiskCli {
704        ref kind,
705        read_only,
706        channel,
707        device,
708        is_dvd,
709    } in &opt.ide
710    {
711        storage
712            .add(
713                DeviceVtl::Vtl0,
714                None,
715                None,
716                storage_builder::DiskLocation::Ide(channel, device),
717                kind,
718                is_dvd,
719                read_only,
720            )
721            .await?;
722    }
723
724    if !opt.nvme.is_empty() {
725        tracing::warn!("--nvme is deprecated; use --nvme-pci and --disk on=<name> instead");
726
727        // Pre-register implicit PCIe controllers for unique port names.
728        let mut registered_ports = std::collections::BTreeSet::new();
729        for disk in &opt.nvme {
730            if let Some(port) = &disk.pcie_port {
731                if registered_ports.insert(port.clone()) {
732                    storage.add_nvme_controller(
733                        port.clone(),
734                        DeviceVtl::Vtl0,
735                        storage_builder::NvmeControllerTransport::Pcie(port.clone()),
736                        None,
737                    ).with_context(|| format!(
738                        "legacy --nvme flag conflicts with an explicit controller named '{port}'; \
739                         use --nvme-pci and --disk on=<name> instead"
740                    ))?;
741                }
742            }
743        }
744    }
745
746    for &cli_args::DiskCli {
747        vtl,
748        ref kind,
749        read_only,
750        is_dvd,
751        underhill,
752        ref pcie_port,
753        ref serial,
754        controller: _,
755        nsid: _,
756        lun: _,
757        relay: _,
758    } in &opt.nvme
759    {
760        if serial.is_some() {
761            anyhow::bail!("`serial` is only supported by `--virtio-blk`");
762        }
763        let target = if let Some(port) = pcie_port {
764            storage_builder::DiskLocation::Named {
765                controller: port.clone(),
766                nsid: None,
767                lun: None,
768            }
769        } else {
770            storage_builder::DiskLocation::Nvme(None)
771        };
772        storage
773            .add(vtl, underhill, None, target, kind, is_dvd, read_only)
774            .await?;
775    }
776
777    for &cli_args::DiskCli {
778        vtl,
779        ref kind,
780        read_only,
781        is_dvd,
782        ref underhill,
783        ref pcie_port,
784        ref serial,
785        controller: _,
786        nsid: _,
787        lun: _,
788        relay: _,
789    } in &opt.virtio_blk
790    {
791        if underhill.is_some() {
792            anyhow::bail!("underhill not supported with virtio-blk");
793        }
794        storage
795            .add(
796                vtl,
797                None,
798                None,
799                storage_builder::DiskLocation::VirtioBlk {
800                    pcie_port: pcie_port.clone(),
801                    serial: serial.clone(),
802                },
803                kind,
804                is_dvd,
805                read_only,
806            )
807            .await?;
808    }
809
810    let mut floppy_disks = Vec::new();
811    for disk in &opt.floppy {
812        let &cli_args::FloppyDiskCli {
813            ref kind,
814            read_only,
815        } = disk;
816        floppy_disks.push(FloppyDiskConfig {
817            disk_type: disk_open(kind, read_only).await?,
818            read_only,
819        });
820    }
821
822    let mut vpci_mana_nics = [(); 3].map(|()| None);
823    let mut pcie_mana_nics = BTreeMap::<String, GdmaDeviceHandle>::new();
824    let mut underhill_nics = Vec::new();
825    let mut vpci_devices = Vec::new();
826
827    let mut nic_index = 0;
828    for cli_cfg in &opt.net {
829        if cli_cfg.pcie_port.is_some() {
830            anyhow::bail!("`--net` does not support PCIe");
831        }
832        let vport = parse_endpoint(cli_cfg, &mut nic_index, &mut resources)?;
833        if cli_cfg.underhill {
834            if !opt.no_alias_map {
835                anyhow::bail!("must specify --no-alias-map to offer NICs to VTL2");
836            }
837            let mana = vpci_mana_nics[openhcl_vtl as usize].get_or_insert_with(|| {
838                let vpci_instance_id = Guid::new_random();
839                underhill_nics.push(vtl2_settings_proto::NicDeviceLegacy {
840                    instance_id: vpci_instance_id.to_string(),
841                    subordinate_instance_id: None,
842                    max_sub_channels: None,
843                });
844                (vpci_instance_id, GdmaDeviceHandle { vports: Vec::new() })
845            });
846            mana.1.vports.push(VportDefinition {
847                mac_address: vport.mac_address,
848                endpoint: vport.endpoint,
849            });
850        } else {
851            vmbus_devices.push(vport.into_netvsp_handle());
852        }
853    }
854
855    if opt.nic {
856        let nic_config = parse_endpoint(
857            &NicConfigCli {
858                vtl: DeviceVtl::Vtl0,
859                endpoint: EndpointConfigCli::Consomme {
860                    cidr: None,
861                    host_fwd: Vec::new(),
862                },
863                mac_address: None,
864                max_queues: None,
865                enable_rx_gso: false,
866                underhill: false,
867                pcie_port: None,
868            },
869            &mut nic_index,
870            &mut resources,
871        )?;
872        vmbus_devices.push(nic_config.into_netvsp_handle());
873    }
874
875    // Build initial PCIe devices list from CLI options. Storage devices
876    // (e.g., NVMe controllers on PCIe ports) are added later by storage_builder.
877    let mut pcie_devices = Vec::new();
878    for (pcie_port_name, config) in usb_controller_configs(&opt.usb_xhci, &opt.usb_device)? {
879        pcie_devices.push(PcieDeviceConfig {
880            port_name: pcie_port_name,
881            resource: config.into_resource(),
882        });
883    }
884    for (index, cli_cfg) in opt.pcie_remote.iter().enumerate() {
885        tracing::info!(
886            port_name = %cli_cfg.port_name,
887            socket_addr = ?cli_cfg.socket_addr,
888            "instantiating PCIe remote device"
889        );
890
891        // Generate a deterministic instance ID based on index
892        const PCIE_REMOTE_BASE_INSTANCE_ID: Guid =
893            guid::guid!("28ed784d-c059-429f-9d9a-46bea02562c0");
894        let instance_id = Guid {
895            data1: index as u32,
896            ..PCIE_REMOTE_BASE_INSTANCE_ID
897        };
898
899        pcie_devices.push(PcieDeviceConfig {
900            port_name: cli_cfg.port_name.clone(),
901            resource: pcie_remote_resources::PcieRemoteHandle {
902                instance_id,
903                socket_addr: cli_cfg.socket_addr.clone(),
904                hu: cli_cfg.hu,
905                controller: cli_cfg.controller,
906            }
907            .into_resource(),
908        });
909    }
910
911    #[cfg(windows)]
912    let mut kernel_vmnics = Vec::new();
913    #[cfg(windows)]
914    for (index, switch_id) in opt.kernel_vmnic.iter().enumerate() {
915        // Pick a random MAC address.
916        let mut mac_address = [0x00, 0x15, 0x5D, 0, 0, 0];
917        getrandom::fill(&mut mac_address[3..]).expect("rng failure");
918
919        // Pick a fixed instance ID based on the index.
920        const BASE_INSTANCE_ID: Guid = guid::guid!("00000000-435d-11ee-9f59-00155d5016fc");
921        let instance_id = Guid {
922            data1: index as u32,
923            ..BASE_INSTANCE_ID
924        };
925
926        let switch_id = if switch_id == "default" {
927            None
928        } else {
929            Some(switch_id.as_str())
930        };
931        let (port_id, port) = new_switch_port(switch_id)?;
932        resources.switch_ports.push(port);
933
934        kernel_vmnics.push(openvmm_defs::config::KernelVmNicConfig {
935            instance_id,
936            mac_address: mac_address.into(),
937            switch_port_id: port_id,
938        });
939    }
940
941    for vport in &opt.mana {
942        let vport = parse_endpoint(vport, &mut nic_index, &mut resources)?;
943        let vport_array = match (vport.vtl as usize, vport.pcie_port) {
944            (vtl, None) => {
945                &mut vpci_mana_nics[vtl]
946                    .get_or_insert_with(|| {
947                        (Guid::new_random(), GdmaDeviceHandle { vports: Vec::new() })
948                    })
949                    .1
950                    .vports
951            }
952            (0, Some(pcie_port)) => {
953                &mut pcie_mana_nics
954                    .entry(pcie_port)
955                    .or_insert(GdmaDeviceHandle { vports: Vec::new() })
956                    .vports
957            }
958            _ => anyhow::bail!("PCIe NICs only supported to VTL0"),
959        };
960        vport_array.push(VportDefinition {
961            mac_address: vport.mac_address,
962            endpoint: vport.endpoint,
963        });
964    }
965
966    vpci_devices.extend(
967        vpci_mana_nics
968            .into_iter()
969            .enumerate()
970            .filter_map(|(vtl, nic)| {
971                nic.map(|(instance_id, handle)| VpciDeviceConfig {
972                    vtl: match vtl {
973                        0 => DeviceVtl::Vtl0,
974                        1 => DeviceVtl::Vtl1,
975                        2 => DeviceVtl::Vtl2,
976                        _ => unreachable!(),
977                    },
978                    instance_id,
979                    resource: handle.into_resource(),
980                    vnode: None,
981                })
982            }),
983    );
984
985    pcie_devices.extend(
986        pcie_mana_nics
987            .into_iter()
988            .map(|(pcie_port, handle)| PcieDeviceConfig {
989                port_name: pcie_port,
990                resource: handle.into_resource(),
991            }),
992    );
993
994    for cxl_test in &opt.cxl_test {
995        pcie_devices.push(PcieDeviceConfig {
996            port_name: cxl_test.pcie_port.clone(),
997            resource: CxlTestDeviceHandle {
998                hdm_size_bytes: cxl_test.hdm_size,
999            }
1000            .into_resource(),
1001        });
1002    }
1003
1004    #[cfg(guest_arch = "aarch64")]
1005    let arch = MachineArch::Aarch64;
1006    #[cfg(guest_arch = "x86_64")]
1007    let arch = MachineArch::X86_64;
1008
1009    #[cfg(guest_arch = "x86_64")]
1010    anyhow::ensure!(
1011        opt.amd_iommu.is_empty() || opt.intel_vtd.is_empty(),
1012        "--amd-iommu and --intel-vtd cannot both be used in the same VM"
1013    );
1014
1015    #[cfg(guest_arch = "x86_64")]
1016    let mut amd_iommu_names: HashSet<&str> = opt.amd_iommu.iter().map(|s| s.as_str()).collect();
1017    #[cfg(guest_arch = "x86_64")]
1018    let mut vtd_names: HashSet<&str> = opt.intel_vtd.iter().map(|s| s.as_str()).collect();
1019
1020    // Map each `--smmu` entry to its root complex, rejecting duplicate `rc=`
1021    // entries up front. Entries are removed as they are matched to a root
1022    // complex below; any left over refer to unknown root complexes.
1023    #[cfg(guest_arch = "aarch64")]
1024    let mut smmu_names: std::collections::HashMap<&str, &cli_args::SmmuCli> = {
1025        let mut map = std::collections::HashMap::new();
1026        for s in &opt.smmu {
1027            if map.insert(s.rc_name.as_str(), s).is_some() {
1028                anyhow::bail!(
1029                    "--smmu specified multiple times for root complex '{}'",
1030                    s.rc_name
1031                );
1032            }
1033        }
1034        map
1035    };
1036
1037    let mut pcie_root_complexes = Vec::new();
1038    let mut legacy_pci_config_io_for_pcie = false;
1039    for (i, rc_cli) in opt.pcie_root_complex.iter().enumerate() {
1040        let ports: Vec<PciePortConfig> = opt
1041            .pcie_root_port
1042            .iter()
1043            .filter(|port_cli| port_cli.root_complex_name == rc_cli.name)
1044            .map(|port_cli| PciePortConfig {
1045                name: port_cli.name.clone(),
1046                devfn: port_cli.devfn,
1047                hotplug: port_cli.hotplug,
1048                acs_capabilities_supported: port_cli.acs_capabilities_supported,
1049                cxl: port_cli.cxl,
1050                pasid: port_cli.pasid,
1051            })
1052            .collect();
1053
1054        const ONE_MB: u64 = 1024 * 1024;
1055        // Keep all PCI windows 1MB-granular to match layout and downstream placement rules.
1056        let low_mmio_size = (rc_cli.low_mmio as u64).next_multiple_of(ONE_MB);
1057        let high_mmio_size = rc_cli
1058            .high_mmio
1059            .checked_next_multiple_of(ONE_MB)
1060            .context("high mmio rounding error")?;
1061
1062        // Count CXL-capable ports under the root bus. If the root bus has CXL root ports, it needs CHBCR.
1063        let cxl_port_count = ports.iter().filter(|port| port.cxl).count() as u64;
1064
1065        let cxl = if cxl_port_count != 0 {
1066            Some(RootComplexCxlConfig {
1067                hdm_size: rc_cli.hdm,
1068                hdm_window_restrictions: rc_cli.hdm_window_restrictions.bits(),
1069            })
1070        } else {
1071            None
1072        };
1073        let legacy_pci_config_io = arch == MachineArch::X86_64
1074            && !opt.pcat
1075            && rc_cli.segment == 0
1076            && rc_cli.start_bus == 0
1077            && rc_cli.end_bus == u8::MAX;
1078        legacy_pci_config_io_for_pcie |= legacy_pci_config_io;
1079        pcie_root_complexes.push(PcieRootComplexConfig {
1080            index: i as u32,
1081            name: rc_cli.name.clone(),
1082            segment: rc_cli.segment,
1083            start_bus: rc_cli.start_bus,
1084            end_bus: rc_cli.end_bus,
1085            legacy_pci_config_io,
1086            low_mmio: if let Some(base) = rc_cli.low_mmio_base {
1087                PcieMmioRangeConfig::Fixed(
1088                    memory_range::MemoryRange::try_new(base..base.wrapping_add(low_mmio_size))
1089                        .context("invalid low MMIO range")?,
1090                )
1091            } else {
1092                PcieMmioRangeConfig::Dynamic {
1093                    size: low_mmio_size,
1094                }
1095            },
1096            high_mmio: if let Some(base) = rc_cli.high_mmio_base {
1097                PcieMmioRangeConfig::Fixed(
1098                    memory_range::MemoryRange::try_new(base..base.wrapping_add(high_mmio_size))
1099                        .context("invalid high MMIO range")?,
1100                )
1101            } else {
1102                PcieMmioRangeConfig::Dynamic {
1103                    size: high_mmio_size,
1104                }
1105            },
1106            cxl,
1107            ports,
1108            #[cfg(guest_arch = "aarch64")]
1109            iommu: smmu_names.remove(rc_cli.name.as_str()).map(|s| {
1110                openvmm_defs::config::PcieIommuConfig::Smmu {
1111                    accel: s.accel,
1112                    oas: match s.oas {
1113                        cli_args::SmmuOasCli::Auto => openvmm_defs::config::SmmuOas::Auto,
1114                        cli_args::SmmuOasCli::Fixed(bits) => {
1115                            openvmm_defs::config::SmmuOas::Fixed(bits)
1116                        }
1117                    },
1118                    ssidsize: match s.ssidsize {
1119                        cli_args::SmmuSsidSizeCli::Auto => openvmm_defs::config::SmmuSsidSize::Auto,
1120                        cli_args::SmmuSsidSizeCli::Fixed(bits) => {
1121                            openvmm_defs::config::SmmuSsidSize::Fixed(bits)
1122                        }
1123                    },
1124                }
1125            }),
1126            #[cfg(guest_arch = "x86_64")]
1127            iommu: if amd_iommu_names.remove(rc_cli.name.as_str()) {
1128                Some(openvmm_defs::config::PcieIommuConfig::AmdVi)
1129            } else if vtd_names.remove(rc_cli.name.as_str()) {
1130                Some(openvmm_defs::config::PcieIommuConfig::IntelVtd)
1131            } else {
1132                None
1133            },
1134            vnode: rc_cli.vnode,
1135            preserve_bars: rc_cli.preserve_bars,
1136        });
1137    }
1138
1139    #[cfg(guest_arch = "aarch64")]
1140    if let Some(name) = smmu_names.into_keys().next() {
1141        anyhow::bail!("--smmu refers to unknown root complex '{name}'");
1142    }
1143    #[cfg(guest_arch = "x86_64")]
1144    if let Some(name) = amd_iommu_names.into_iter().next() {
1145        anyhow::bail!("--amd-iommu refers to unknown root complex '{name}'");
1146    }
1147    #[cfg(guest_arch = "x86_64")]
1148    if let Some(name) = vtd_names.into_iter().next() {
1149        anyhow::bail!("--intel-vtd refers to unknown root complex '{name}'");
1150    }
1151
1152    let pcie_switches = build_switch_list(&opt.pcie_switch);
1153    let pcie_generic_initiators = opt
1154        .pcie_generic_initiator
1155        .iter()
1156        .map(|gi| openvmm_defs::config::PcieGenericInitiatorConfig {
1157            port_name: gi.port_name.clone(),
1158            node: gi.node,
1159        })
1160        .collect();
1161    #[cfg(target_os = "linux")]
1162    let vfio_pcie_devices: Vec<PcieDeviceConfig> = {
1163        use std::collections::HashMap;
1164        use vm_resource::IntoResource;
1165
1166        // Process --iommu flags: open /dev/iommu for each declared context.
1167        let mut iommu_map: HashMap<String, std::fs::File> = HashMap::new();
1168        for iommu_cli in &opt.iommu {
1169            anyhow::ensure!(
1170                !iommu_map.contains_key(&iommu_cli.id),
1171                "duplicate --iommu id={}",
1172                iommu_cli.id
1173            );
1174            let file = std::fs::OpenOptions::new()
1175                .read(true)
1176                .write(true)
1177                .open("/dev/iommu")
1178                .context("failed to open /dev/iommu (is iommufd available?)")?;
1179            iommu_map.insert(iommu_cli.id.clone(), file);
1180        }
1181
1182        opt.vfio
1183            .iter()
1184            .map(|cli_cfg| {
1185                let sysfs_path = Path::new("/sys/bus/pci/devices").join(&cli_cfg.pci_id);
1186
1187                if let Some(iommu_id) = &cli_cfg.iommu {
1188                    // cdev + iommufd path
1189                    let iommufd = iommu_map.get(iommu_id).with_context(|| {
1190                        format!(
1191                            "--vfio device {} references iommu={iommu_id}, \
1192                             but no --iommu id={iommu_id} was specified",
1193                            cli_cfg.pci_id
1194                        )
1195                    })?;
1196                    // Clone the iommufd fd so the per-iommu manager can own it.
1197                    // The first device for a given iommu ID uses the cloned fd
1198                    // to create the IoasManager; subsequent devices reuse the
1199                    // existing manager and the cloned fd is dropped.
1200                    let iommufd = iommufd.try_clone().with_context(|| {
1201                        format!("failed to dup iommufd fd for iommu={iommu_id}")
1202                    })?;
1203
1204                    // Open the cdev device node.
1205                    let vfio_dev_dir = sysfs_path.join("vfio-dev");
1206                    let entry = std::fs::read_dir(&vfio_dev_dir)
1207                        .with_context(|| {
1208                            format!(
1209                                "failed to read {}: is {} bound to vfio-pci?",
1210                                vfio_dev_dir.display(),
1211                                cli_cfg.pci_id
1212                            )
1213                        })?
1214                        .next()
1215                        .context("no vfio-dev entry found")?
1216                        .context("failed to read vfio-dev entry")?;
1217                    let dev_path = Path::new("/dev/vfio/devices").join(entry.file_name());
1218                    let cdev = std::fs::OpenOptions::new()
1219                        .read(true)
1220                        .write(true)
1221                        .open(&dev_path)
1222                        .with_context(|| format!("failed to open {}", dev_path.display()))?;
1223
1224                    Ok(PcieDeviceConfig {
1225                        port_name: cli_cfg.port_name.clone(),
1226                        resource: vfio_assigned_device_resources::VfioCdevDeviceHandle {
1227                            pci_id: cli_cfg.pci_id.clone(),
1228                            cdev,
1229                            iommufd,
1230                            iommu_id: iommu_id.clone(),
1231                            bar_addresses: cli_cfg.bar_addresses,
1232                        }
1233                        .into_resource(),
1234                    })
1235                } else {
1236                    // Legacy group/container path
1237                    let iommu_group_link = std::fs::read_link(sysfs_path.join("iommu_group"))
1238                        .with_context(|| {
1239                            format!("failed to read IOMMU group for {}", cli_cfg.pci_id)
1240                        })?;
1241                    let group_id: u64 = iommu_group_link
1242                        .file_name()
1243                        .and_then(|s| s.to_str())
1244                        .context("invalid iommu_group symlink")?
1245                        .parse()
1246                        .context("failed to parse IOMMU group ID")?;
1247                    let group = std::fs::OpenOptions::new()
1248                        .read(true)
1249                        .write(true)
1250                        .open(format!("/dev/vfio/{group_id}"))
1251                        .with_context(|| format!("failed to open /dev/vfio/{group_id}"))?;
1252
1253                    Ok(PcieDeviceConfig {
1254                        port_name: cli_cfg.port_name.clone(),
1255                        resource: vfio_assigned_device_resources::VfioDeviceHandle {
1256                            pci_id: cli_cfg.pci_id.clone(),
1257                            group,
1258                            bar_addresses: cli_cfg.bar_addresses,
1259                        }
1260                        .into_resource(),
1261                    })
1262                }
1263            })
1264            .collect::<anyhow::Result<Vec<_>>>()?
1265    };
1266
1267    #[cfg(windows)]
1268    let vpci_resources: Vec<_> = opt
1269        .device
1270        .iter()
1271        .map(|path| -> anyhow::Result<_> {
1272            Ok(virt_whp::device::DeviceHandle(
1273                whp::VpciResource::new(
1274                    None,
1275                    Default::default(),
1276                    &whp::VpciResourceDescriptor::Sriov(path, 0, 0),
1277                )
1278                .with_context(|| format!("opening PCI device {}", path))?,
1279            ))
1280        })
1281        .collect::<Result<_, _>>()?;
1282
1283    // Create a vmbusproxy handle if needed by any devices.
1284    #[cfg(windows)]
1285    let vmbusproxy_handle = if !kernel_vmnics.is_empty() {
1286        Some(vmbus_proxy::ProxyHandle::new().context("failed to open vmbusproxy handle")?)
1287    } else {
1288        None
1289    };
1290
1291    let framebuffer = if opt.gfx || opt.vtl2_gfx || opt.vnc.vnc || opt.pcat {
1292        let vram = alloc_shared_memory(FRAMEBUFFER_SIZE, "vram")?;
1293        let (fb, fba) =
1294            framebuffer::framebuffer(vram, FRAMEBUFFER_SIZE, 0).context("creating framebuffer")?;
1295        resources.framebuffer_access = Some(fba);
1296        Some(fb)
1297    } else {
1298        None
1299    };
1300
1301    let load_mode;
1302    let with_hv;
1303
1304    let any_serial_configured = serial0_cfg.is_some()
1305        || serial1_cfg.is_some()
1306        || serial2_cfg.is_some()
1307        || serial3_cfg.is_some();
1308
1309    let has_com3 = serial2_cfg.is_some();
1310
1311    let mut chipset = VmManifestBuilder::new(base_chipset_type(opt), arch);
1312
1313    if legacy_pci_config_io_for_pcie {
1314        chipset = chipset.with_legacy_pci_config_io_for_pcie();
1315    }
1316
1317    if framebuffer.is_some() {
1318        chipset = chipset.with_framebuffer();
1319    }
1320    if opt.guest_watchdog {
1321        chipset = chipset.with_guest_watchdog();
1322    }
1323    if any_serial_configured {
1324        chipset = chipset.with_serial([serial0_cfg, serial1_cfg, serial2_cfg, serial3_cfg]);
1325    }
1326    chipset = chipset.with_serial_debugger_mode(com_debugger_mode);
1327    if opt.battery {
1328        let (tx, rx) = mesh::channel();
1329        tx.send(HostBatteryUpdate::default_present());
1330        chipset = chipset.with_battery(rx);
1331    }
1332    if opt.no_vmbus {
1333        chipset = chipset.without_vmbus();
1334    }
1335    if let Some(cfg) = &opt.debugcon {
1336        chipset = chipset.with_debugcon(
1337            debugcon_cfg.unwrap_or_else(|| DisconnectedSerialBackendHandle.into_resource()),
1338            cfg.port,
1339        );
1340    }
1341
1342    let (base_template, custom_uefi_json) = {
1343        #[cfg(guest_arch = "aarch64")]
1344        use firmware_uefi_resources::aarch64_secure_boot_templates as secure_boot_templates;
1345        #[cfg(guest_arch = "x86_64")]
1346        use firmware_uefi_resources::x64_secure_boot_templates as secure_boot_templates;
1347        let base_template = opt.secure_boot_template.map(|template| match template {
1348            SecureBootTemplateCli::Windows => secure_boot_templates::microsoft_windows(),
1349            SecureBootTemplateCli::UefiCa => secure_boot_templates::microsoft_uefi_ca(),
1350        });
1351
1352        // TODO: fallback to VMGS read if no command line flag was given
1353
1354        let custom_uefi_json = match &opt.custom_uefi_json {
1355            Some(file) => Some(
1356                fs_err::read(file)
1357                    .context("opening custom uefi json file")?
1358                    .into(),
1359            ),
1360            None => None,
1361        };
1362
1363        (base_template, custom_uefi_json)
1364    };
1365
1366    if (uefi.is_some() && opt.igvm.is_none())
1367        || opt
1368            .igvm
1369            .as_ref()
1370            .is_some_and(|igvm| igvm.personality == IgvmPersonalityCli::Uefi)
1371    {
1372        let log_level = match uefi_options.diagnostics.unwrap_or_default() {
1373            EfiDiagnosticsLogLevelCli::Default => firmware_uefi_resources::LogLevel::make_default(),
1374            EfiDiagnosticsLogLevelCli::Info => firmware_uefi_resources::LogLevel::make_info(),
1375            EfiDiagnosticsLogLevelCli::Full => firmware_uefi_resources::LogLevel::make_full(),
1376        };
1377        let nvram_storage =
1378            non_volatile_store_resource(opt.vmgs.is_some(), vmgs_format::FileId::BIOS_NVRAM);
1379        chipset = chipset.with_uefi(vm_manifest_builder::UefiManifest::new(
1380            arch,
1381            base_template,
1382            custom_uefi_json,
1383            opt.secure_boot,
1384            log_level,
1385            None,
1386            nvram_storage,
1387            None,
1388        ));
1389    }
1390
1391    // Build the SMBIOS config once, up front, so that UEFI and Linux direct
1392    // boot share a single source for the VM's BIOS GUID / system UUID. The TPM
1393    // also keys off this GUID.
1394    let smbios = Box::new(smbios_config_from_cli(&opt.smbios)?);
1395    let bios_guid = smbios.system.uuid;
1396
1397    // Capture the SMBIOS config for the OpenHCL/GED path before `smbios` is
1398    // potentially moved into a non-VTL2 LoadMode below. The GED forwards only
1399    // the system identity to the paravisor and fails closed on BIOS overrides
1400    // it cannot honor, so it is delivered as the shared `SmbiosConfig`.
1401    let ged_smbios = (*smbios).clone();
1402
1403    let tpm_version = opt.tpm.map(|cli_ver| match cli_ver {
1404        TpmVersionCli::V138 => TpmVersion::V138,
1405        TpmVersionCli::V185 => TpmVersion::V185,
1406    });
1407
1408    if let Some(igvm) = &opt.igvm {
1409        let cli_args::UefiCli {
1410            firmware,
1411            debug: _,
1412            enable_memory_protections: _,
1413            force_dma_bounce: _,
1414            force_firmware_version,
1415            disable_frontpage: _,
1416            console: _,
1417            diagnostics: _,
1418            default_boot_always_attempt: _,
1419        } = uefi_options;
1420
1421        anyhow::ensure!(
1422            firmware.is_none(),
1423            "--uefi firmware is not supported with --igvm"
1424        );
1425        anyhow::ensure!(
1426            !force_firmware_version,
1427            "--uefi force_firmware_version is not supported with --igvm"
1428        );
1429        let file = fs_err::File::open(&igvm.firmware)
1430            .context("failed to open igvm file")?
1431            .into();
1432        let cmdline = opt.cmdline.join(" ");
1433        with_hv = match igvm.personality {
1434            IgvmPersonalityCli::Openhcl | IgvmPersonalityCli::Uefi => true,
1435            IgvmPersonalityCli::LinuxDirect => opt.hv,
1436        };
1437
1438        load_mode = LoadMode::Igvm {
1439            file,
1440            cmdline,
1441            vtl2_base_address: if opt.vtl2 {
1442                opt.igvm_vtl2_relocation_type
1443            } else {
1444                Vtl2BaseAddressType::File
1445            },
1446            com_serial: has_com3.then_some(UartId::Com(ComPort::Com3)),
1447            personality: match igvm.personality {
1448                IgvmPersonalityCli::Openhcl => IgvmPersonality::Openhcl,
1449                IgvmPersonalityCli::Uefi => IgvmPersonality::Uefi,
1450                IgvmPersonalityCli::LinuxDirect => IgvmPersonality::LinuxDirect,
1451            },
1452        };
1453
1454        // An IGVM launch carries no SMBIOS field of its own; the identity is
1455        // only delivered over the GET/GED channel, which is absent here. Reject
1456        // overrides that would otherwise be silently dropped.
1457        let smbios_requested = !opt.smbios.is_empty();
1458        let smbios_delivered_via_get = with_get && with_hv;
1459        if smbios_requested && !smbios_delivered_via_get {
1460            anyhow::bail!(
1461                "--smbios is not supported for IGVM launches without an OpenHCL GET channel"
1462            );
1463        }
1464    } else if opt.pcat {
1465        // Emit a nice error early instead of complaining about missing firmware.
1466        if arch != MachineArch::X86_64 {
1467            anyhow::bail!("pcat not supported on this architecture");
1468        }
1469        with_hv = true;
1470
1471        let firmware = openvmm_pcat_locator::find_pcat_bios(opt.pcat_firmware.as_deref())?;
1472        load_mode = LoadMode::Pcat {
1473            firmware,
1474            boot_order: opt
1475                .pcat_boot_order
1476                .map(|x| x.0)
1477                .unwrap_or(DEFAULT_PCAT_BOOT_ORDER),
1478            hibernation_enabled: opt.hibernation,
1479            smbios,
1480        };
1481    } else if let Some(uefi_options) = &uefi {
1482        use openvmm_defs::config::UefiConsoleMode;
1483
1484        let cli_args::UefiCli {
1485            firmware,
1486            debug,
1487            enable_memory_protections,
1488            force_dma_bounce,
1489            force_firmware_version,
1490            disable_frontpage,
1491            console,
1492            diagnostics: _,
1493            default_boot_always_attempt,
1494        } = uefi_options;
1495
1496        if opt.no_hv && cfg!(guest_arch = "x86_64") {
1497            anyhow::bail!("--no-hv is not supported on x86_64");
1498        }
1499
1500        with_hv = !opt.no_hv;
1501
1502        let default_firmware = cli_args::default_uefi_firmware();
1503        let firmware = fs_err::File::open(
1504            firmware
1505                .as_ref()
1506                .or(default_firmware.as_ref())
1507                .context("must provide uefi firmware when booting with uefi")?,
1508        )
1509        .context("failed to open uefi firmware")?;
1510
1511        // TODO: It would be better to default memory protections to on, but currently Linux does not boot via UEFI due to what
1512        //       appears to be a GRUB memory protection fault. Memory protections are therefore only enabled if configured.
1513        load_mode = LoadMode::Uefi {
1514            firmware: firmware.into(),
1515            enable_debugging: *debug,
1516            enable_memory_protections: *enable_memory_protections,
1517            disable_frontpage: *disable_frontpage,
1518            tpm_version,
1519            enable_battery: opt.battery,
1520            enable_serial: any_serial_configured,
1521            enable_vpci_boot: false,
1522            uefi_console_mode: console.map(|m| match m {
1523                UefiConsoleModeCli::Default => UefiConsoleMode::Default,
1524                UefiConsoleModeCli::Com1 => UefiConsoleMode::Com1,
1525                UefiConsoleModeCli::Com2 => UefiConsoleMode::Com2,
1526                UefiConsoleModeCli::None => UefiConsoleMode::None,
1527            }),
1528            default_boot_always_attempt: *default_boot_always_attempt,
1529            smbios,
1530            enable_vmbus: !opt.no_vmbus,
1531            force_dma_bounce: *force_dma_bounce,
1532            enable_hv: !opt.no_hv,
1533            hibernation_enabled: opt.hibernation,
1534            force_firmware_version: *force_firmware_version,
1535        };
1536    } else {
1537        // Linux Direct
1538        let mut cmdline = "panic=-1 debug".to_string();
1539
1540        with_hv = opt.hv;
1541        if with_hv && opt.pcie_root_complex.is_empty() {
1542            cmdline += " pci=off";
1543        }
1544
1545        if !console_str.is_empty() {
1546            let _ = write!(&mut cmdline, " console={}", console_str);
1547        }
1548
1549        if opt.gfx {
1550            cmdline += " console=tty";
1551        }
1552        for extra in &opt.cmdline {
1553            let _ = write!(&mut cmdline, " {}", extra);
1554        }
1555
1556        let kernel = fs_err::File::open(
1557            (opt.kernel.0)
1558                .as_ref()
1559                .context("must provide kernel when booting with linux direct")?,
1560        )
1561        .context("failed to open kernel")?;
1562        let initrd = (opt.initrd.0)
1563            .as_ref()
1564            .map(fs_err::File::open)
1565            .transpose()
1566            .context("failed to open initrd")?;
1567
1568        load_mode = LoadMode::Linux {
1569            kernel: kernel.into(),
1570            initrd: initrd.map(Into::into),
1571            cmdline,
1572            enable_serial: any_serial_configured,
1573            isolation: if matches!(opt.isolation, Some(cli_args::IsolationCli::Snp)) {
1574                openvmm_defs::config::LinuxIsolationConfig::Snp {
1575                    restricted_injection: opt.snp_restricted_injection,
1576                }
1577            } else {
1578                openvmm_defs::config::LinuxIsolationConfig::None
1579            },
1580            boot_mode: if opt.device_tree {
1581                openvmm_defs::config::LinuxDirectBootMode::DeviceTree
1582            } else {
1583                openvmm_defs::config::LinuxDirectBootMode::Acpi
1584            },
1585            smbios,
1586        };
1587    }
1588
1589    if supports_acpi_power_button(&load_mode) {
1590        let (send, recv) = mesh::channel();
1591        let irq = match arch {
1592            MachineArch::X86_64 => DEFAULT_ACPI_POWER_BUTTON_IRQ_X64,
1593            MachineArch::Aarch64 => DEFAULT_ACPI_POWER_BUTTON_SPI_AARCH64,
1594        };
1595        chipset = chipset.with_acpi_power_button(irq, recv);
1596        resources.acpi_power_button = Some(send);
1597    }
1598
1599    let layout_config = chipset.layout_config();
1600    let VmChipsetResult {
1601        chipset,
1602        mut chipset_devices,
1603        pci_chipset_devices,
1604        isa_dma_controller,
1605        capabilities,
1606    } = chipset
1607        .build()
1608        .context("failed to build chipset configuration")?;
1609
1610    let mut vmgs = Some(if let Some(vmgs) = &opt.vmgs {
1611        let encryption_policy = if opt.test_gsp_by_id {
1612            GuestStateEncryptionPolicy::GspById(true)
1613        } else {
1614            GuestStateEncryptionPolicy::None(true)
1615        };
1616        open_vmgs(&vmgs.kind, vmgs.provision, encryption_policy)
1617            .await
1618            .context("failed to open vmgs disk")?
1619    } else {
1620        VmgsResource::Ephemeral
1621    });
1622
1623    if with_get && with_hv {
1624        let has_vtl0_nvme = storage.has_vtl0_nvme();
1625        let vtl2_settings = vtl2_settings_proto::Vtl2Settings {
1626            version: vtl2_settings_proto::vtl2_settings_base::Version::V1.into(),
1627            fixed: Some(Default::default()),
1628            dynamic: Some(vtl2_settings_proto::Vtl2SettingsDynamic {
1629                storage_controllers: storage.build_openhcl_settings(opt.vmbus_redirect),
1630                nic_devices: underhill_nics,
1631            }),
1632            namespace_settings: Vec::default(),
1633        };
1634
1635        // Cache the VTL2 settings for later modification via the interactive console.
1636        resources.vtl2_settings = Some(vtl2_settings.clone());
1637
1638        let (send, guest_request_recv) = mesh::channel();
1639        resources.ged_rpc = Some(send);
1640
1641        let vmgs = vmgs.take().unwrap();
1642
1643        vmbus_devices.extend([
1644            (
1645                openhcl_vtl,
1646                get_resources::gel::GuestEmulationLogHandle.into_resource(),
1647            ),
1648            (
1649                openhcl_vtl,
1650                get_resources::ged::GuestEmulationDeviceHandle {
1651                    firmware: if opt.pcat {
1652                        get_resources::ged::GuestFirmwareConfig::Pcat {
1653                            boot_order: opt
1654                                .pcat_boot_order
1655                                .map_or(DEFAULT_PCAT_BOOT_ORDER, |x| x.0)
1656                                .map(|x| match x {
1657                                    openvmm_defs::config::PcatBootDevice::Floppy => {
1658                                        get_resources::ged::PcatBootDevice::Floppy
1659                                    }
1660                                    openvmm_defs::config::PcatBootDevice::HardDrive => {
1661                                        get_resources::ged::PcatBootDevice::HardDrive
1662                                    }
1663                                    openvmm_defs::config::PcatBootDevice::Optical => {
1664                                        get_resources::ged::PcatBootDevice::Optical
1665                                    }
1666                                    openvmm_defs::config::PcatBootDevice::Network => {
1667                                        get_resources::ged::PcatBootDevice::Network
1668                                    }
1669                                }),
1670                        }
1671                    } else {
1672                        use get_resources::ged::UefiConsoleMode;
1673
1674                        get_resources::ged::GuestFirmwareConfig::Uefi {
1675                            enable_vpci_boot: has_vtl0_nvme,
1676                            firmware_debug: uefi_options.debug,
1677                            enable_memory_protections: uefi_options.enable_memory_protections,
1678                            disable_frontpage: uefi_options.disable_frontpage,
1679                            console_mode: match uefi_options.console.unwrap_or(UefiConsoleModeCli::Default) {
1680                                UefiConsoleModeCli::Default => UefiConsoleMode::Default,
1681                                UefiConsoleModeCli::Com1 => UefiConsoleMode::COM1,
1682                                UefiConsoleModeCli::Com2 => UefiConsoleMode::COM2,
1683                                UefiConsoleModeCli::None => UefiConsoleMode::None,
1684                            },
1685                            default_boot_always_attempt: uefi_options.default_boot_always_attempt,
1686                        }
1687                    },
1688                    com1: with_vmbus_com1_serial,
1689                    com2: with_vmbus_com2_serial,
1690                    serial_tx_only: opt.serial_tx_only,
1691                    vtl2_settings: Some(prost::Message::encode_to_vec(&vtl2_settings)),
1692                    vmbus_redirection: opt.vmbus_redirect,
1693                    vmgs,
1694                    framebuffer: opt
1695                        .vtl2_gfx
1696                        .then(|| SharedFramebufferHandle.into_resource()),
1697                    guest_request_recv,
1698                    tpm_version: tpm_version.map(|v| match v {
1699                        TpmVersion::V138 => get_resources::ged::GedTpmVersion::V138,
1700                        TpmVersion::V185 => get_resources::ged::GedTpmVersion::V185,
1701                    }),
1702                    firmware_event_send: None,
1703                    ipmi_sel_event_send: None,
1704                    secure_boot_enabled: opt.secure_boot,
1705                    secure_boot_template: match opt.secure_boot_template {
1706                        Some(SecureBootTemplateCli::Windows) => {
1707                            get_resources::ged::GuestSecureBootTemplateType::MicrosoftWindows
1708                        },
1709                        Some(SecureBootTemplateCli::UefiCa) => {
1710                            get_resources::ged::GuestSecureBootTemplateType::MicrosoftUefiCertificateAuthority
1711                        }
1712                        None => {
1713                            get_resources::ged::GuestSecureBootTemplateType::None
1714                        },
1715                    },
1716                    enable_battery: opt.battery,
1717                    enable_ipmi: false,
1718                    enable_hibernation: opt.hibernation,
1719                    no_persistent_secrets: true,
1720                    igvm_attest_test_config: None,
1721                    test_gsp_by_id: opt.test_gsp_by_id,
1722                    efi_diagnostics_log_level: {
1723                        match uefi_options.diagnostics.unwrap_or_default() {
1724                            EfiDiagnosticsLogLevelCli::Default => get_resources::ged::EfiDiagnosticsLogLevelType::Default,
1725                            EfiDiagnosticsLogLevelCli::Info => get_resources::ged::EfiDiagnosticsLogLevelType::Info,
1726                            EfiDiagnosticsLogLevelCli::Full => get_resources::ged::EfiDiagnosticsLogLevelType::Full,
1727                        }
1728                    },
1729                    force_dma_bounce_enabled: uefi_options.force_dma_bounce,
1730                    smbios: ged_smbios,
1731                }
1732                .into_resource(),
1733            ),
1734        ]);
1735    }
1736
1737    if let Some(tpm_version) = tpm_version
1738        && !opt.vtl2
1739    {
1740        let register_layout = if cfg!(guest_arch = "x86_64") {
1741            TpmRegisterLayout::IoPort
1742        } else {
1743            TpmRegisterLayout::Mmio
1744        };
1745
1746        let ppi_store =
1747            non_volatile_store_resource(opt.vmgs.is_some(), vmgs_format::FileId::TPM_PPI);
1748        let nvram_store = non_volatile_store_resource(
1749            opt.vmgs.is_some(),
1750            tpm_vmgs::tpm_nvram_file_id(tpm_version),
1751        );
1752
1753        chipset_devices.push(ChipsetDeviceHandle {
1754            name: "tpm".to_string(),
1755            resource: chipset_device_worker_defs::RemoteChipsetDeviceHandle {
1756                device: TpmDeviceHandle {
1757                    version: tpm_version,
1758                    ppi_store,
1759                    nvram_store,
1760                    nvram_size: None,
1761                    refresh_tpm_seeds: false,
1762                    ak_cert_type: tpm_resources::TpmAkCertTypeResource::None,
1763                    register_layout,
1764                    guest_secret_key: None,
1765                    logger: None,
1766                    is_confidential_vm: false,
1767                    bios_guid,
1768                }
1769                .into_resource(),
1770                worker_host: mesh.make_host("tpm", None).await?,
1771            }
1772            .into_resource(),
1773        });
1774    }
1775
1776    let vga_firmware = if opt.pcat {
1777        Some(openvmm_pcat_locator::find_svga_bios(
1778            opt.vga_firmware.as_deref(),
1779        )?)
1780    } else {
1781        None
1782    };
1783
1784    if opt.gfx {
1785        // Channel for the video device to report dirty rectangles to the VNC worker.
1786        let (dirt_send, dirt_recv) = mesh::channel();
1787        resources.dirty_rect_recv = Some(dirt_recv);
1788
1789        vmbus_devices.extend([
1790            (
1791                DeviceVtl::Vtl0,
1792                SynthVideoHandle {
1793                    framebuffer: SharedFramebufferHandle.into_resource(),
1794                    dirt_send: Some(dirt_send),
1795                }
1796                .into_resource(),
1797            ),
1798            (
1799                DeviceVtl::Vtl0,
1800                SynthKeyboardHandle {
1801                    source: MultiplexedInputHandle {
1802                        // Save 0 for PS/2
1803                        elevation: 1,
1804                    }
1805                    .into_resource(),
1806                }
1807                .into_resource(),
1808            ),
1809            (
1810                DeviceVtl::Vtl0,
1811                SynthMouseHandle {
1812                    source: MultiplexedInputHandle {
1813                        // Save 0 for PS/2
1814                        elevation: 1,
1815                    }
1816                    .into_resource(),
1817                }
1818                .into_resource(),
1819            ),
1820        ]);
1821    }
1822
1823    let vsock_listener = |path: Option<&str>| -> anyhow::Result<_> {
1824        if let Some(path) = path {
1825            cleanup_socket(path.as_ref());
1826            let listener = unix_socket::UnixListener::bind(path)
1827                .with_context(|| format!("failed to bind to hybrid vsock path: {}", path))?;
1828            Ok(Some(listener))
1829        } else {
1830            Ok(None)
1831        }
1832    };
1833
1834    let vtl0_vsock_listener = vsock_listener(opt.vmbus_vsock_path.as_deref())?;
1835    let vtl2_vsock_listener = vsock_listener(opt.vmbus_vtl2_vsock_path.as_deref())?;
1836
1837    if let Some(path) = &opt.openhcl_dump_path {
1838        let (resource, task) = spawn_dump_handler(&spawner, path.clone(), None);
1839        task.detach();
1840        vmbus_devices.push((openhcl_vtl, resource));
1841    }
1842
1843    #[cfg(guest_arch = "aarch64")]
1844    let topology_arch = openvmm_defs::config::ArchTopologyConfig::Aarch64(
1845        openvmm_defs::config::Aarch64TopologyConfig {
1846            // TODO: allow this to be configured from the command line
1847            gic_config: None,
1848            pmu_gsiv: openvmm_defs::config::PmuGsivConfig::Platform,
1849            gic_msi: match opt.gic_msi {
1850                cli_args::GicMsiCli::Auto => openvmm_defs::config::GicMsiConfig::Auto,
1851                cli_args::GicMsiCli::Its => openvmm_defs::config::GicMsiConfig::Its,
1852                cli_args::GicMsiCli::V2m => {
1853                    openvmm_defs::config::GicMsiConfig::V2m { spi_count: None }
1854                }
1855            },
1856        },
1857    );
1858    #[cfg(guest_arch = "x86_64")]
1859    let topology_arch =
1860        openvmm_defs::config::ArchTopologyConfig::X86(openvmm_defs::config::X86TopologyConfig {
1861            apic_id_offset: opt.apic_id_offset,
1862            x2apic: opt.x2apic,
1863        });
1864
1865    let with_isolation = if let Some(isolation) = &opt.isolation {
1866        match isolation {
1867            cli_args::IsolationCli::Vbs => {
1868                // TODO: For now, VBS isolation is only supported with VTL2.
1869                if !opt.vtl2 {
1870                    anyhow::bail!("VBS isolation is only currently supported with vtl2");
1871                }
1872
1873                // TODO: Alias map support is not yet implemented with isolation.
1874                if !opt.no_alias_map {
1875                    anyhow::bail!("alias map not supported with isolation");
1876                }
1877
1878                Some(openvmm_defs::config::IsolationType::Vbs)
1879            }
1880            cli_args::IsolationCli::Snp => Some(openvmm_defs::config::IsolationType::Snp {
1881                // SNP host data is currently only configurable via TTRPC.
1882                host_data: None,
1883            }),
1884        }
1885    } else {
1886        None
1887    };
1888
1889    if with_hv && !opt.no_vmbus {
1890        let (shutdown_send, shutdown_recv) = mesh::channel();
1891        resources.shutdown_ic = Some(shutdown_send);
1892        let (kvp_send, kvp_recv) = mesh::channel();
1893        resources.kvp_ic = Some(kvp_send);
1894        vmbus_devices.extend(
1895            [
1896                hyperv_ic_resources::shutdown::ShutdownIcHandle {
1897                    recv: shutdown_recv,
1898                }
1899                .into_resource(),
1900                hyperv_ic_resources::kvp::KvpIcHandle { recv: kvp_recv }.into_resource(),
1901                hyperv_ic_resources::timesync::TimesyncIcHandle.into_resource(),
1902            ]
1903            .map(|r| (DeviceVtl::Vtl0, r)),
1904        );
1905    }
1906
1907    if let Some(hive_path) = &opt.imc {
1908        let file = fs_err::File::open(hive_path).context("failed to open imc hive")?;
1909        vmbus_devices.push((
1910            DeviceVtl::Vtl0,
1911            vmbfs_resources::VmbfsImcDeviceHandle { file: file.into() }.into_resource(),
1912        ));
1913    }
1914
1915    let mut virtio_devices = Vec::new();
1916    let mut add_virtio_device =
1917        |bus, resource: Resource<VirtioDeviceHandle>, pcie_devices: &mut Vec<_>| match bus {
1918            VirtioBusCli::Auto => {
1919                // Use VPCI when possible (currently only on Windows and macOS due
1920                // to KVM backend limitations).
1921                if with_hv && (cfg!(windows) || cfg!(target_os = "macos")) {
1922                    vpci_devices.push(VpciDeviceConfig {
1923                        vtl: DeviceVtl::Vtl0,
1924                        instance_id: Guid::new_random(),
1925                        resource: VirtioPciDeviceHandle(resource).into_resource(),
1926                        vnode: None,
1927                    });
1928                } else {
1929                    virtio_devices.push((VirtioBus::Pci, resource));
1930                }
1931            }
1932            VirtioBusCli::Mmio => virtio_devices.push((VirtioBus::Mmio, resource)),
1933            VirtioBusCli::Pci => virtio_devices.push((VirtioBus::Pci, resource)),
1934            VirtioBusCli::Pcie(port_name) => pcie_devices.push(PcieDeviceConfig {
1935                port_name,
1936                resource: VirtioPciDeviceHandle(resource).into_resource(),
1937            }),
1938            VirtioBusCli::Vpci => vpci_devices.push(VpciDeviceConfig {
1939                vtl: DeviceVtl::Vtl0,
1940                instance_id: Guid::new_random(),
1941                resource: VirtioPciDeviceHandle(resource).into_resource(),
1942                vnode: None,
1943            }),
1944        };
1945
1946    for cli_cfg in &opt.virtio_net {
1947        if cli_cfg.underhill {
1948            anyhow::bail!("use --net uh:[...] to add underhill NICs")
1949        }
1950        let vport = parse_endpoint(cli_cfg, &mut nic_index, &mut resources)?;
1951        let resource = virtio_resources::net::VirtioNetHandle {
1952            max_queues: vport.max_queues,
1953            enable_rx_gso: vport.enable_rx_gso,
1954            mac_address: vport.mac_address,
1955            endpoint: vport.endpoint,
1956        }
1957        .into_resource();
1958        if let Some(pcie_port) = &cli_cfg.pcie_port {
1959            pcie_devices.push(PcieDeviceConfig {
1960                port_name: pcie_port.clone(),
1961                resource: VirtioPciDeviceHandle(resource).into_resource(),
1962            });
1963        } else {
1964            add_virtio_device(VirtioBusCli::Auto, resource, &mut pcie_devices);
1965        }
1966    }
1967
1968    for args in &opt.virtio_fs {
1969        let resource: Resource<VirtioDeviceHandle> = virtio_resources::fs::VirtioFsHandle {
1970            tag: args.tag.clone(),
1971            fs: virtio_resources::fs::VirtioFsBackend::HostFs {
1972                root_path: args.path.clone(),
1973                mount_options: args.options.clone(),
1974            },
1975        }
1976        .into_resource();
1977        if let Some(pcie_port) = &args.pcie_port {
1978            pcie_devices.push(PcieDeviceConfig {
1979                port_name: pcie_port.clone(),
1980                resource: VirtioPciDeviceHandle(resource).into_resource(),
1981            });
1982        } else {
1983            add_virtio_device(opt.virtio_fs_bus.clone(), resource, &mut pcie_devices);
1984        }
1985    }
1986
1987    for args in &opt.virtio_fs_shmem {
1988        let resource: Resource<VirtioDeviceHandle> = virtio_resources::fs::VirtioFsHandle {
1989            tag: args.tag.clone(),
1990            fs: virtio_resources::fs::VirtioFsBackend::SectionFs {
1991                root_path: args.path.clone(),
1992            },
1993        }
1994        .into_resource();
1995        if let Some(pcie_port) = &args.pcie_port {
1996            pcie_devices.push(PcieDeviceConfig {
1997                port_name: pcie_port.clone(),
1998                resource: VirtioPciDeviceHandle(resource).into_resource(),
1999            });
2000        } else {
2001            add_virtio_device(opt.virtio_fs_bus.clone(), resource, &mut pcie_devices);
2002        }
2003    }
2004
2005    for args in &opt.virtio_9p {
2006        let resource: Resource<VirtioDeviceHandle> = virtio_resources::p9::VirtioPlan9Handle {
2007            tag: args.tag.clone(),
2008            root_path: args.path.clone(),
2009            debug: opt.virtio_9p_debug,
2010        }
2011        .into_resource();
2012        if let Some(pcie_port) = &args.pcie_port {
2013            pcie_devices.push(PcieDeviceConfig {
2014                port_name: pcie_port.clone(),
2015                resource: VirtioPciDeviceHandle(resource).into_resource(),
2016            });
2017        } else {
2018            add_virtio_device(VirtioBusCli::Auto, resource, &mut pcie_devices);
2019        }
2020    }
2021
2022    if let Some(pmem_args) = &opt.virtio_pmem {
2023        let resource: Resource<VirtioDeviceHandle> = virtio_resources::pmem::VirtioPmemHandle {
2024            path: pmem_args.path.clone(),
2025            read_only: false,
2026        }
2027        .into_resource();
2028        if let Some(pcie_port) = &pmem_args.pcie_port {
2029            pcie_devices.push(PcieDeviceConfig {
2030                port_name: pcie_port.clone(),
2031                resource: VirtioPciDeviceHandle(resource).into_resource(),
2032            });
2033        } else {
2034            add_virtio_device(VirtioBusCli::Auto, resource, &mut pcie_devices);
2035        }
2036    }
2037
2038    if opt.virtio_rng {
2039        let resource: Resource<VirtioDeviceHandle> =
2040            virtio_resources::rng::VirtioRngHandle.into_resource();
2041        if let Some(pcie_port) = &opt.virtio_rng_pcie_port {
2042            pcie_devices.push(PcieDeviceConfig {
2043                port_name: pcie_port.clone(),
2044                resource: VirtioPciDeviceHandle(resource).into_resource(),
2045            });
2046        } else {
2047            add_virtio_device(opt.virtio_rng_bus.clone(), resource, &mut pcie_devices);
2048        }
2049    }
2050
2051    if let Some(rtc) = &opt.virtio_rtc {
2052        let resource: Resource<VirtioDeviceHandle> =
2053            virtio_resources::rtc::VirtioRtcHandle.into_resource();
2054        add_virtio_device(rtc.bus.clone(), resource, &mut pcie_devices);
2055    }
2056
2057    if let Some(backend) = virtio_console_backend {
2058        let resource: Resource<VirtioDeviceHandle> =
2059            virtio_resources::console::VirtioConsoleHandle { backend }.into_resource();
2060        if let Some(pcie_port) = &opt.virtio_console_pcie_port {
2061            pcie_devices.push(PcieDeviceConfig {
2062                port_name: pcie_port.clone(),
2063                resource: VirtioPciDeviceHandle(resource).into_resource(),
2064            });
2065        } else {
2066            add_virtio_device(VirtioBusCli::Auto, resource, &mut pcie_devices);
2067        }
2068    }
2069
2070    // Handle --vhost-user arguments.
2071    #[cfg(target_os = "linux")]
2072    for vhost_cli in &opt.vhost_user {
2073        let stream =
2074            unix_socket::UnixStream::connect(&vhost_cli.socket_path).with_context(|| {
2075                format!(
2076                    "failed to connect to vhost-user socket: {}",
2077                    vhost_cli.socket_path
2078                )
2079            })?;
2080
2081        use crate::cli_args::VhostUserDeviceTypeCli;
2082        let resource: Resource<VirtioDeviceHandle> = match vhost_cli.device_type {
2083            VhostUserDeviceTypeCli::Fs {
2084                ref tag,
2085                num_queues,
2086                queue_size,
2087            } => virtio_resources::vhost_user::VhostUserFsHandle {
2088                socket: stream.into(),
2089                tag: tag.clone(),
2090                num_queues,
2091                queue_size,
2092            }
2093            .into_resource(),
2094            VhostUserDeviceTypeCli::Blk {
2095                num_queues,
2096                queue_size,
2097            } => virtio_resources::vhost_user::VhostUserBlkHandle {
2098                socket: stream.into(),
2099                num_queues,
2100                queue_size,
2101            }
2102            .into_resource(),
2103            VhostUserDeviceTypeCli::Other {
2104                device_id,
2105                ref queue_sizes,
2106            } => virtio_resources::vhost_user::VhostUserGenericHandle {
2107                socket: stream.into(),
2108                device_id,
2109                queue_sizes: queue_sizes.clone(),
2110            }
2111            .into_resource(),
2112        };
2113        if let Some(pcie_port) = &vhost_cli.pcie_port {
2114            pcie_devices.push(PcieDeviceConfig {
2115                port_name: pcie_port.clone(),
2116                resource: VirtioPciDeviceHandle(resource).into_resource(),
2117            });
2118        } else {
2119            add_virtio_device(VirtioBusCli::Auto, resource, &mut pcie_devices);
2120        }
2121    }
2122
2123    let virtio_vsock_bus = opt.virtio_vsock_bus.clone().unwrap_or(VirtioBusCli::Auto);
2124
2125    if let Some(vsock_path) = &opt.virtio_vsock_path {
2126        let listener = vsock_listener(Some(vsock_path))?.unwrap();
2127        let resource: Resource<VirtioDeviceHandle> = virtio_resources::vsock::VirtioVsockHandle {
2128            // The guest CID does not matter since the UDS relay does not use it. It just needs
2129            // to be some non-reserved value for the guest to use.
2130            guest_cid: 0x3,
2131            base_path: vsock_path.clone(),
2132            listener,
2133        }
2134        .into_resource();
2135        add_virtio_device(virtio_vsock_bus.clone(), resource, &mut pcie_devices);
2136    }
2137
2138    #[cfg(target_os = "linux")]
2139    if let Some(guest_cid) = opt.virtio_vsock_vhost_cid {
2140        let vhost = std::fs::OpenOptions::new()
2141            .read(true)
2142            .write(true)
2143            .open("/dev/vhost-vsock")
2144            .context("failed to open /dev/vhost-vsock")?
2145            .into();
2146        let resource =
2147            virtio_resources::vsock::VirtioVsockVhostHandle { vhost, guest_cid }.into_resource();
2148        add_virtio_device(virtio_vsock_bus, resource, &mut pcie_devices);
2149    }
2150
2151    #[cfg(target_os = "linux")]
2152    pcie_devices.extend(vfio_pcie_devices);
2153
2154    let mut cfg = Config {
2155        chipset,
2156        load_mode,
2157        floppy_disks,
2158        pcie_root_complexes,
2159        pcie_ecam_below_4gb: opt.pcie_ecam_below_4gb,
2160        #[cfg(target_os = "linux")]
2161        pcie_devices,
2162        #[cfg(not(target_os = "linux"))]
2163        pcie_devices,
2164        pcie_switches,
2165        pcie_generic_initiators,
2166        vpci_devices,
2167        ide_disks: Vec::new(),
2168        numa: {
2169            if let Some(ref nodes) = opt.numa {
2170                // --numa mode: each --numa flag defines a node.
2171                NumaTopology {
2172                    nodes: nodes
2173                        .iter()
2174                        .map(|n| {
2175                            let vps = match &n.vps {
2176                                Some(vps) if vps.0.is_empty() => VpAssignment::Empty,
2177                                Some(vps) => {
2178                                    VpAssignment::Explicit(vps.expand_below(opt.processors)?)
2179                                }
2180                                None => VpAssignment::FromTopology,
2181                            };
2182                            Ok(NumaNode {
2183                                mem: Some(MemoryConfig {
2184                                    mem_size: n
2185                                        .memory
2186                                        .size
2187                                        .expect("NUMA memory size was validated")
2188                                        .0,
2189                                    prefetch_memory: n.memory.prefetch,
2190                                    private_memory: n.memory.shared == Some(false),
2191                                    transparent_hugepages: n
2192                                        .memory
2193                                        .transparent_hugepages
2194                                        .unwrap_or(!n.memory.hugepages),
2195                                    hugepages: n.memory.hugepages,
2196                                    hugepage_size: n.memory.hugepage_size.map(|m| m.0),
2197                                    host_numa_node: n.host_numa_node,
2198                                }),
2199                                vps,
2200                            })
2201                        })
2202                        .collect::<anyhow::Result<Vec<_>>>()?,
2203                    distances: opt
2204                        .numa_distance
2205                        .as_deref()
2206                        .unwrap_or(&[])
2207                        .iter()
2208                        .map(|d| NumaDistance {
2209                            src: d.src,
2210                            dst: d.dst,
2211                            distance: d.distance,
2212                        })
2213                        .collect(),
2214                }
2215            } else {
2216                // Single-node default from --memory.
2217                NumaTopology {
2218                    nodes: vec![NumaNode {
2219                        mem: Some(MemoryConfig {
2220                            mem_size: opt.memory_size(),
2221                            prefetch_memory: opt.prefetch_memory(),
2222                            private_memory: opt.private_memory(),
2223                            transparent_hugepages: opt.transparent_hugepages(),
2224                            hugepages: opt.memory.hugepages,
2225                            hugepage_size: opt.memory.hugepage_size.map(|m| m.0),
2226                            host_numa_node: None,
2227                        }),
2228                        vps: VpAssignment::FromTopology,
2229                    }],
2230                    distances: vec![],
2231                }
2232            }
2233        },
2234        processor_topology: ProcessorTopologyConfig {
2235            proc_count: opt.processors,
2236            vps_per_socket: opt.vps_per_socket,
2237            enable_smt: match opt.smt {
2238                cli_args::SmtConfigCli::Auto => None,
2239                cli_args::SmtConfigCli::Force => Some(true),
2240                cli_args::SmtConfigCli::Off => Some(false),
2241            },
2242            arch: Some(topology_arch),
2243        },
2244        hypervisor: HypervisorConfig {
2245            with_hv,
2246            with_vtl2: opt.vtl2.then_some(Vtl2Config {
2247                vtl0_alias_map: !opt.no_alias_map,
2248                late_map_vtl0_memory: match opt.late_map_vtl0_policy {
2249                    cli_args::Vtl0LateMapPolicyCli::Off => None,
2250                    cli_args::Vtl0LateMapPolicyCli::Log => Some(LateMapVtl0MemoryPolicy::Log),
2251                    cli_args::Vtl0LateMapPolicyCli::Halt => Some(LateMapVtl0MemoryPolicy::Halt),
2252                    cli_args::Vtl0LateMapPolicyCli::Exception => {
2253                        Some(LateMapVtl0MemoryPolicy::InjectException)
2254                    }
2255                },
2256            }),
2257            with_isolation,
2258            nested_virt: opt.nested_virt,
2259        },
2260        #[cfg(windows)]
2261        kernel_vmnics,
2262        input: mesh::Receiver::new(),
2263        framebuffer,
2264        vga_firmware,
2265        vtl2_gfx: opt.vtl2_gfx,
2266        virtio_devices,
2267        vmbus: (with_hv && !opt.no_vmbus).then_some(VmbusConfig {
2268            vsock_listener: vtl0_vsock_listener,
2269            vsock_path: opt.vmbus_vsock_path.clone(),
2270            vtl2_redirect: opt.vmbus_redirect,
2271            vmbus_max_version: opt.vmbus_max_version,
2272            #[cfg(windows)]
2273            vmbusproxy_handle,
2274        }),
2275        vtl2_vmbus: (with_hv && opt.vtl2).then_some(VmbusConfig {
2276            vsock_listener: vtl2_vsock_listener,
2277            vsock_path: opt.vmbus_vtl2_vsock_path.clone(),
2278            ..Default::default()
2279        }),
2280        vmbus_devices,
2281        chipset_devices,
2282        pci_chipset_devices,
2283        isa_dma_controller,
2284        chipset_capabilities: capabilities,
2285        layout: layout_config,
2286        #[cfg(windows)]
2287        vpci_resources,
2288        vmgs,
2289        firmware_event_send: None,
2290        debugger_rpc: None,
2291        rtc_delta_milliseconds: 0,
2292    };
2293
2294    storage.build_config(&mut cfg, &mut resources, opt.scsi_sub_channels)?;
2295    let mut pcie_port_names = HashSet::new();
2296    for device in &cfg.pcie_devices {
2297        anyhow::ensure!(
2298            pcie_port_names.insert(&device.port_name),
2299            "multiple devices use PCIe port '{}'",
2300            device.port_name
2301        );
2302    }
2303    resources.serial_driver = Some(serial_driver);
2304    validate_snp_config(&cfg)?;
2305    Ok((cfg, resources))
2306}
2307
2308fn validate_snp_config(cfg: &Config) -> anyhow::Result<()> {
2309    if !matches!(
2310        cfg.hypervisor.with_isolation,
2311        Some(openvmm_defs::config::IsolationType::Snp { .. })
2312    ) {
2313        return Ok(());
2314    }
2315
2316    if !matches!(
2317        cfg.load_mode,
2318        LoadMode::Linux { .. } | LoadMode::Igvm { .. }
2319    ) {
2320        anyhow::bail!("SNP isolation currently only supports Linux direct or IGVM boot");
2321    }
2322    if cfg.hypervisor.with_vtl2.is_some() {
2323        anyhow::bail!("SNP isolation currently does not support VTL2");
2324    }
2325    if cfg.vmbus.is_some() || cfg.vtl2_vmbus.is_some() || !cfg.vmbus_devices.is_empty() {
2326        anyhow::bail!("SNP isolation currently does not support VMBus devices");
2327    }
2328
2329    let only_supported_chipset_devices = cfg.chipset_devices.iter().all(|device| {
2330        matches!(
2331            device.resource.id(),
2332            "serial_16550"
2333                | "pic"
2334                | "pit"
2335                | "generic-ioapic"
2336                | "hyperv_power_management"
2337                | "missing-dev"
2338        )
2339    });
2340    let only_virtio_pcie_devices = cfg
2341        .pcie_devices
2342        .iter()
2343        .all(|device| device.resource.id() == "virtio");
2344    if !cfg.floppy_disks.is_empty()
2345        || !cfg.ide_disks.is_empty()
2346        || !cfg.virtio_devices.is_empty()
2347        || !only_virtio_pcie_devices
2348        || !cfg.vpci_devices.is_empty()
2349        || !only_supported_chipset_devices
2350        || !cfg.pci_chipset_devices.is_empty()
2351    {
2352        anyhow::bail!("SNP isolation currently only supports virtio devices");
2353    }
2354    if cfg.framebuffer.is_some() || cfg.vga_firmware.is_some() || cfg.debugger_rpc.is_some() {
2355        anyhow::bail!("SNP isolation currently does not support this VM configuration");
2356    }
2357
2358    Ok(())
2359}
2360
2361/// Gets the terminal to use for externally launched console windows.
2362pub(crate) fn openvmm_terminal_app() -> Option<PathBuf> {
2363    std::env::var_os("OPENVMM_TERM")
2364        .or_else(|| std::env::var_os("HVLITE_TERM"))
2365        .map(Into::into)
2366}
2367
2368// Tries to remove `path` if it is confirmed to be a Unix socket.
2369fn cleanup_socket(path: &Path) {
2370    #[cfg(windows)]
2371    let is_socket = pal::windows::fs::is_unix_socket(path).unwrap_or(false);
2372    #[cfg(not(windows))]
2373    let is_socket = path
2374        .metadata()
2375        .is_ok_and(|meta| std::os::unix::fs::FileTypeExt::is_socket(&meta.file_type()));
2376
2377    if is_socket {
2378        let _ = std::fs::remove_file(path);
2379    }
2380}
2381
2382#[cfg(windows)]
2383fn new_switch_port(
2384    switch_id: Option<&str>,
2385) -> anyhow::Result<(
2386    openvmm_defs::config::SwitchPortId,
2387    vmswitch::kernel::SwitchPort,
2388)> {
2389    let id = vmswitch::kernel::SwitchPortId {
2390        switch: match switch_id {
2391            Some(s) => s.parse().context("invalid switch id")?,
2392            None => vmswitch::hcn::DEFAULT_SWITCH,
2393        },
2394        port: Guid::new_random(),
2395    };
2396    let _ = vmswitch::hcn::Network::open(&id.switch)
2397        .with_context(|| format!("could not find switch {}", id.switch))?;
2398
2399    let port = vmswitch::kernel::SwitchPort::new(&id).context("failed to create switch port")?;
2400
2401    let id = openvmm_defs::config::SwitchPortId {
2402        switch: id.switch,
2403        port: id.port,
2404    };
2405    Ok((id, port))
2406}
2407
2408fn parse_endpoint(
2409    cli_cfg: &NicConfigCli,
2410    index: &mut usize,
2411    resources: &mut VmResources,
2412) -> anyhow::Result<NicConfig> {
2413    let _ = resources;
2414    let endpoint = match &cli_cfg.endpoint {
2415        EndpointConfigCli::Consomme { cidr, host_fwd } => {
2416            let ports = host_fwd
2417                .iter()
2418                .map(|fwd| {
2419                    use net_backend_resources::consomme::HostPortProtocol;
2420                    net_backend_resources::consomme::HostPortConfig {
2421                        protocol: match fwd.protocol {
2422                            cli_args::HostPortProtocolCli::Tcp => HostPortProtocol::Tcp,
2423                            cli_args::HostPortProtocolCli::Udp => HostPortProtocol::Udp,
2424                        },
2425                        host_address: fwd
2426                            .host_address
2427                            .map(net_backend_resources::consomme::HostIpAddress::from),
2428                        host_port: net_backend_resources::consomme::HostPort::Fixed(fwd.host_port),
2429                        guest_port: fwd.guest_port,
2430                    }
2431                })
2432                .collect();
2433            // Only wire the bind/unbind RPC channel to the first consomme
2434            // endpoint. Additional consomme NICs work normally but cannot be
2435            // targeted by runtime bind/unbind commands.
2436            let recv = if resources.consomme_rpc.is_none() {
2437                let (send, recv) = mesh::channel();
2438                resources.consomme_rpc = Some(send);
2439                Some(recv)
2440            } else {
2441                None
2442            };
2443            net_backend_resources::consomme::ConsommeHandle {
2444                cidr: cidr.clone(),
2445                params: Default::default(),
2446                ports,
2447                internal_dns: true,
2448                recv,
2449            }
2450            .into_resource()
2451        }
2452        EndpointConfigCli::None => net_backend_resources::null::NullHandle.into_resource(),
2453        EndpointConfigCli::Dio { id } => {
2454            #[cfg(windows)]
2455            {
2456                let (port_id, port) = new_switch_port(id.as_deref())?;
2457                resources.switch_ports.push(port);
2458                net_backend_resources::dio::WindowsDirectIoHandle {
2459                    switch_port_id: net_backend_resources::dio::SwitchPortId {
2460                        switch: port_id.switch,
2461                        port: port_id.port,
2462                    },
2463                }
2464                .into_resource()
2465            }
2466
2467            #[cfg(not(windows))]
2468            {
2469                let _ = id;
2470                bail!("cannot use dio on non-windows platforms")
2471            }
2472        }
2473        EndpointConfigCli::Tap { name } => {
2474            #[cfg(target_os = "linux")]
2475            {
2476                let fd = net_tap::tap::open_tap(name)
2477                    .with_context(|| format!("failed to open TAP device '{name}'"))?;
2478                net_backend_resources::tap::TapHandle { fd }.into_resource()
2479            }
2480
2481            #[cfg(not(target_os = "linux"))]
2482            {
2483                let _ = name;
2484                bail!("TAP backend is only supported on Linux")
2485            }
2486        }
2487    };
2488
2489    let mac_address = cli_cfg.mac_address.unwrap_or_else(|| {
2490        let mut mac_address = [0x00, 0x15, 0x5D, 0, 0, 0];
2491        getrandom::fill(&mut mac_address[3..]).expect("rng failure");
2492        mac_address.into()
2493    });
2494
2495    // Pick a fixed instance ID based on the index.
2496    const BASE_INSTANCE_ID: Guid = guid::guid!("00000000-da43-11ed-936a-00155d6db52f");
2497    let instance_id = Guid {
2498        data1: *index as u32,
2499        ..BASE_INSTANCE_ID
2500    };
2501    *index += 1;
2502
2503    Ok(NicConfig {
2504        vtl: cli_cfg.vtl,
2505        instance_id,
2506        endpoint,
2507        mac_address,
2508        max_queues: cli_cfg.max_queues,
2509        enable_rx_gso: cli_cfg.enable_rx_gso,
2510        pcie_port: cli_cfg.pcie_port.clone(),
2511    })
2512}
2513
2514#[derive(Debug)]
2515struct NicConfig {
2516    vtl: DeviceVtl,
2517    instance_id: Guid,
2518    mac_address: MacAddress,
2519    endpoint: Resource<NetEndpointHandleKind>,
2520    max_queues: Option<u16>,
2521    enable_rx_gso: bool,
2522    pcie_port: Option<String>,
2523}
2524
2525impl NicConfig {
2526    fn into_netvsp_handle(self) -> (DeviceVtl, Resource<VmbusDeviceHandleKind>) {
2527        (
2528            self.vtl,
2529            netvsp_resources::NetvspHandle {
2530                instance_id: self.instance_id,
2531                mac_address: self.mac_address,
2532                endpoint: self.endpoint,
2533                max_queues: self.max_queues,
2534            }
2535            .into_resource(),
2536        )
2537    }
2538}
2539
2540fn non_volatile_store_resource(
2541    with_vmgs: bool,
2542    file_id: vmgs_format::FileId,
2543) -> Resource<NonVolatileStoreKind> {
2544    if with_vmgs {
2545        VmgsFileHandle::new(file_id, true).into_resource()
2546    } else {
2547        EphemeralNonVolatileStoreHandle.into_resource()
2548    }
2549}
2550
2551async fn open_vmgs(
2552    kind: &DiskCliKind,
2553    provision: ProvisionCondition,
2554    encryption_policy: GuestStateEncryptionPolicy,
2555) -> anyhow::Result<VmgsResource> {
2556    let disk = VmgsDisk {
2557        disk: disk_open(kind, false).await?,
2558        encryption_policy,
2559    };
2560    Ok(match provision {
2561        ProvisionCondition::OnEmpty => VmgsResource::Disk(disk),
2562        ProvisionCondition::OnFailure => VmgsResource::ReprovisionOnFailure(disk),
2563        ProvisionCondition::Single => VmgsResource::ReprovisionSingle(disk),
2564    })
2565}
2566
2567enum LayerOrDisk {
2568    Layer(DiskLayerDescription),
2569    Disk(Resource<DiskHandleKind>),
2570}
2571
2572async fn disk_open(
2573    disk_cli: &DiskCliKind,
2574    read_only: bool,
2575) -> anyhow::Result<Resource<DiskHandleKind>> {
2576    let mut layers = Vec::new();
2577    disk_open_inner(disk_cli, read_only, &mut layers).await?;
2578    if layers.len() == 1 && matches!(layers[0], LayerOrDisk::Disk(_)) {
2579        let LayerOrDisk::Disk(disk) = layers.pop().unwrap() else {
2580            unreachable!()
2581        };
2582        Ok(disk)
2583    } else {
2584        Ok(Resource::new(disk_backend_resources::LayeredDiskHandle {
2585            layers: layers
2586                .into_iter()
2587                .map(|layer| match layer {
2588                    LayerOrDisk::Layer(layer) => layer,
2589                    LayerOrDisk::Disk(disk) => DiskLayerDescription {
2590                        layer: DiskLayerHandle(disk).into_resource(),
2591                        read_cache: false,
2592                        write_through: false,
2593                    },
2594                })
2595                .collect(),
2596        }))
2597    }
2598}
2599
2600fn disk_open_inner<'a>(
2601    disk_cli: &'a DiskCliKind,
2602    read_only: bool,
2603    layers: &'a mut Vec<LayerOrDisk>,
2604) -> futures::future::BoxFuture<'a, anyhow::Result<()>> {
2605    Box::pin(async move {
2606        fn layer<T: IntoResource<DiskLayerHandleKind>>(layer: T) -> LayerOrDisk {
2607            LayerOrDisk::Layer(layer.into_resource().into())
2608        }
2609        fn disk<T: IntoResource<DiskHandleKind>>(disk: T) -> LayerOrDisk {
2610            LayerOrDisk::Disk(disk.into_resource())
2611        }
2612        match disk_cli {
2613            &DiskCliKind::Memory(len) => {
2614                layers.push(layer(RamDiskLayerHandle {
2615                    len: Some(len),
2616                    sector_size: None,
2617                }));
2618            }
2619            DiskCliKind::File {
2620                path,
2621                create_with_len,
2622                direct,
2623            } => layers.push(LayerOrDisk::Disk(if let Some(size) = create_with_len {
2624                create_disk_type(
2625                    path,
2626                    *size,
2627                    OpenDiskOptions {
2628                        read_only: false,
2629                        direct: *direct,
2630                    },
2631                )
2632                .with_context(|| format!("failed to create {}", path.display()))?
2633            } else {
2634                open_disk_type(
2635                    path,
2636                    OpenDiskOptions {
2637                        read_only,
2638                        direct: *direct,
2639                    },
2640                )
2641                .await
2642                .with_context(|| format!("failed to open {}", path.display()))?
2643            })),
2644            DiskCliKind::Blob { kind, url } => {
2645                layers.push(disk(disk_backend_resources::BlobDiskHandle {
2646                    url: url.to_owned(),
2647                    format: match kind {
2648                        cli_args::BlobKind::Flat => disk_backend_resources::BlobDiskFormat::Flat,
2649                        cli_args::BlobKind::Vhd1 => {
2650                            disk_backend_resources::BlobDiskFormat::FixedVhd1
2651                        }
2652                    },
2653                }))
2654            }
2655            DiskCliKind::MemoryDiff(inner) => {
2656                layers.push(layer(RamDiskLayerHandle {
2657                    len: None,
2658                    sector_size: None,
2659                }));
2660                disk_open_inner(inner, true, layers).await?;
2661            }
2662            DiskCliKind::PersistentReservationsWrapper(inner) => {
2663                layers.push(disk(disk_backend_resources::DiskWithReservationsHandle(
2664                    disk_open(inner, read_only).await?,
2665                )))
2666            }
2667            DiskCliKind::DelayDiskWrapper {
2668                delay_ms,
2669                disk: inner,
2670            } => layers.push(disk(DelayDiskHandle {
2671                delay: CellUpdater::new(Duration::from_millis(*delay_ms)).cell(),
2672                disk: disk_open(inner, read_only).await?,
2673            })),
2674            DiskCliKind::Crypt {
2675                disk: inner,
2676                cipher,
2677                key_file,
2678            } => layers.push(disk(disk_crypt_resources::DiskCryptHandle {
2679                disk: disk_open(inner, read_only).await?,
2680                cipher: match cipher {
2681                    cli_args::DiskCipher::XtsAes256 => disk_crypt_resources::Cipher::XtsAes256,
2682                },
2683                key: fs_err::read(key_file).context("failed to read key file")?,
2684            })),
2685            DiskCliKind::Sqlite {
2686                path,
2687                create_with_len,
2688            } => {
2689                // FUTURE: this code should be responsible for opening
2690                // file-handle(s) itself, and passing them into sqlite via a custom
2691                // vfs. For now though - simply check if the file exists or not, and
2692                // perform early validation of filesystem-level create options.
2693                match (create_with_len.is_some(), path.exists()) {
2694                    (true, true) => anyhow::bail!(
2695                        "cannot create new sqlite disk at {} - file already exists",
2696                        path.display()
2697                    ),
2698                    (false, false) => anyhow::bail!(
2699                        "cannot open sqlite disk at {} - file not found",
2700                        path.display()
2701                    ),
2702                    _ => {}
2703                }
2704
2705                layers.push(layer(SqliteDiskLayerHandle {
2706                    dbhd_path: path.display().to_string(),
2707                    format_dbhd: create_with_len.map(|len| {
2708                        disk_backend_resources::layer::SqliteDiskLayerFormatParams {
2709                            logically_read_only: false,
2710                            len: Some(len),
2711                        }
2712                    }),
2713                }));
2714            }
2715            DiskCliKind::SqliteDiff { path, create, disk } => {
2716                // FUTURE: this code should be responsible for opening
2717                // file-handle(s) itself, and passing them into sqlite via a custom
2718                // vfs. For now though - simply check if the file exists or not, and
2719                // perform early validation of filesystem-level create options.
2720                match (create, path.exists()) {
2721                    (true, true) => anyhow::bail!(
2722                        "cannot create new sqlite disk at {} - file already exists",
2723                        path.display()
2724                    ),
2725                    (false, false) => anyhow::bail!(
2726                        "cannot open sqlite disk at {} - file not found",
2727                        path.display()
2728                    ),
2729                    _ => {}
2730                }
2731
2732                layers.push(layer(SqliteDiskLayerHandle {
2733                    dbhd_path: path.display().to_string(),
2734                    format_dbhd: create.then_some(
2735                        disk_backend_resources::layer::SqliteDiskLayerFormatParams {
2736                            logically_read_only: false,
2737                            len: None,
2738                        },
2739                    ),
2740                }));
2741                disk_open_inner(disk, true, layers).await?;
2742            }
2743            DiskCliKind::AutoCacheSqlite {
2744                cache_path,
2745                key,
2746                disk,
2747            } => {
2748                layers.push(LayerOrDisk::Layer(DiskLayerDescription {
2749                    read_cache: true,
2750                    write_through: false,
2751                    layer: SqliteAutoCacheDiskLayerHandle {
2752                        cache_path: cache_path.clone(),
2753                        cache_key: key.clone(),
2754                    }
2755                    .into_resource(),
2756                }));
2757                disk_open_inner(disk, read_only, layers).await?;
2758            }
2759        }
2760        Ok(())
2761    })
2762}
2763
2764/// Get the system page size.
2765pub(crate) fn system_page_size() -> u32 {
2766    sparse_mmap::SparseMapping::page_size() as u32
2767}
2768
2769/// The guest architecture string, derived from the compile-time `guest_arch` cfg.
2770pub(crate) const GUEST_ARCH: &str = if cfg!(guest_arch = "x86_64") {
2771    "x86_64"
2772} else {
2773    "aarch64"
2774};
2775
2776/// Open a snapshot directory and validate it against the current VM config.
2777/// Returns the shared memory fd (from memory.bin) and the saved device state.
2778fn prepare_snapshot_restore(
2779    snapshot_dir: &Path,
2780    opt: &Options,
2781) -> anyhow::Result<(
2782    openvmm_defs::worker::SharedMemoryFd,
2783    mesh::payload::message::ProtobufMessage,
2784)> {
2785    let (manifest, state_bytes) = openvmm_helpers::snapshot::read_snapshot(snapshot_dir)?;
2786
2787    // Validate manifest against current VM config.
2788    openvmm_helpers::snapshot::validate_manifest(
2789        &manifest,
2790        GUEST_ARCH,
2791        opt.memory_size(),
2792        opt.processors,
2793        system_page_size(),
2794    )?;
2795
2796    // Open memory.bin (existing file, no create, no resize).
2797    let memory_file = fs_err::OpenOptions::new()
2798        .read(true)
2799        .write(true)
2800        .open(snapshot_dir.join("memory.bin"))?;
2801
2802    // Validate file size matches expected memory size.
2803    let file_size = memory_file.metadata()?.len();
2804    if file_size != manifest.memory_size_bytes {
2805        anyhow::bail!(
2806            "memory.bin size ({file_size} bytes) doesn't match manifest ({} bytes)",
2807            manifest.memory_size_bytes,
2808        );
2809    }
2810
2811    let shared_memory_fd =
2812        openvmm_helpers::shared_memory::file_to_shared_memory_fd(memory_file.into())?;
2813
2814    // Reconstruct ProtobufMessage from the saved state bytes.
2815    // The save side wrote mesh::payload::encode(ProtobufMessage), so we decode
2816    // back to ProtobufMessage.
2817    let state_msg: mesh::payload::message::ProtobufMessage = mesh::payload::decode(&state_bytes)
2818        .context("failed to decode saved state from snapshot")?;
2819
2820    Ok((shared_memory_fd, state_msg))
2821}
2822
2823fn do_main(pidfile_guard: &mut Option<pidfile::Pidfile>) -> anyhow::Result<i32> {
2824    #[cfg(windows)]
2825    pal::windows::disable_hard_error_dialog();
2826
2827    let _tracing_guard = tracing_init::enable_tracing()?;
2828
2829    // Try to run as a worker host.
2830    // On success the worker runs to completion and then exits the process (does
2831    // not return). Any worker host setup errors are return and bubbled up.
2832    meshworker::run_vmm_mesh_host()?;
2833
2834    let opt = cli_args::parse_options();
2835
2836    // Print the version number. This comes after argument parsing to not interfere
2837    // with --version and --help.
2838    tracing::info!(version = openvmm_build_info::get().version());
2839
2840    if let Some(path) = &opt.write_saved_state_proto {
2841        mesh::payload::protofile::DescriptorWriter::new(vmcore::save_restore::saved_state_roots())
2842            .write_to_path(path)
2843            .context("failed to write protobuf descriptors")?;
2844        return Ok(0);
2845    }
2846
2847    if let Some(ref path) = opt.pidfile {
2848        *pidfile_guard = Some(pidfile::Pidfile::new(path).context("failed to create pidfile")?);
2849    }
2850
2851    if let Some(path) = opt.relay_console_path {
2852        let console_title = opt.relay_console_title.unwrap_or_default();
2853        return console_relay::relay_console(&path, console_title.as_str()).map(|()| 0);
2854    }
2855
2856    #[cfg(any(feature = "grpc", feature = "ttrpc"))]
2857    {
2858        let rpc = opt
2859            .rpc
2860            .as_ref()
2861            .map(|rpc| {
2862                let transport = match rpc.transport {
2863                    cli_args::RpcTransportCli::Auto => ttrpc::RpcTransport::Auto,
2864                    cli_args::RpcTransportCli::Ttrpc => ttrpc::RpcTransport::Ttrpc,
2865                    cli_args::RpcTransportCli::Grpc => ttrpc::RpcTransport::Grpc,
2866                };
2867                (rpc.path.as_path(), transport)
2868            })
2869            .or_else(|| {
2870                opt.ttrpc
2871                    .as_deref()
2872                    .map(|p| (p, ttrpc::RpcTransport::Ttrpc))
2873            })
2874            .or_else(|| opt.grpc.as_deref().map(|p| (p, ttrpc::RpcTransport::Grpc)));
2875
2876        if let Some((path, transport)) = rpc {
2877            return block_on(async {
2878                let _ = std::fs::remove_file(path);
2879                let listener =
2880                    unix_socket::UnixListener::bind(path).context("failed to bind to socket")?;
2881
2882                // This is a local launch
2883                let (exit_code_send, exit_code_recv) = mesh::oneshot();
2884                let mut handle =
2885                    mesh_worker::launch_local_worker::<ttrpc::TtrpcWorker>(ttrpc::Parameters {
2886                        listener,
2887                        transport,
2888                        exit_code: exit_code_send,
2889                    })
2890                    .await?;
2891
2892                tracing::info!(%transport, path = %path.display(), "listening");
2893
2894                // Signal the parent process that the server is ready.
2895                pal::close_stdout().context("failed to close stdout")?;
2896
2897                handle.join().await?;
2898
2899                exit_code_recv
2900                    .await
2901                    .context("RPC worker did not report an exit code")
2902            });
2903        }
2904    }
2905
2906    DefaultPool::run_with(async |driver| run_control(&driver, opt).await)
2907}
2908
2909fn new_hvsock_service_id(port: u32) -> Guid {
2910    // This GUID is an embedding of the AF_VSOCK port into an
2911    // AF_HYPERV service ID.
2912    Guid {
2913        data1: port,
2914        .."00000000-facb-11e6-bd58-64006a7986d3".parse().unwrap()
2915    }
2916}
2917
2918async fn run_control(driver: &DefaultDriver, opt: Options) -> anyhow::Result<i32> {
2919    let mut mesh = Some(VmmMesh::new(&driver, opt.single_process)?);
2920    let result = run_control_inner(driver, &mut mesh, opt).await;
2921    // If setup failed before the mesh was handed to the controller, shut it
2922    // down so the child host process exits cleanly without noisy logs.
2923    if let Some(mesh) = mesh {
2924        mesh.shutdown().await;
2925    }
2926    result
2927}
2928
2929async fn run_control_inner(
2930    driver: &DefaultDriver,
2931    mesh_slot: &mut Option<VmmMesh>,
2932    opt: Options,
2933) -> anyhow::Result<i32> {
2934    let mesh = mesh_slot.as_ref().unwrap();
2935    let (mut vm_config, mut resources) = vm_config_from_command_line(driver, mesh, &opt).await?;
2936
2937    let mut vnc_worker = None;
2938    if opt.gfx || opt.vnc.vnc {
2939        // Parse the listen address. Try as a full SocketAddr (host:port) first;
2940        // fall back to a bare IP, using the configured port.
2941        let addr: std::net::SocketAddr = if let Ok(sa) =
2942            opt.vnc.vnc_listen.parse::<std::net::SocketAddr>()
2943        {
2944            sa
2945        } else {
2946            let ip: std::net::IpAddr = opt.vnc.vnc_listen.parse().with_context(|| {
2947                format!(
2948                    "invalid VNC listen address: {} (expected IP address or socket address like [::1]:5900)",
2949                    opt.vnc.vnc_listen
2950                )
2951            })?;
2952            std::net::SocketAddr::new(ip, opt.vnc.vnc_port)
2953        };
2954
2955        let socket = socket2::Socket::new(
2956            if addr.is_ipv6() {
2957                socket2::Domain::IPV6
2958            } else {
2959                socket2::Domain::IPV4
2960            },
2961            socket2::Type::STREAM,
2962            None,
2963        )
2964        .with_context(|| format!("creating VNC socket for {}", addr))?;
2965
2966        if addr.is_ipv6() {
2967            if let Err(e) = socket.set_only_v6(false) {
2968                tracing::warn!(
2969                    error = %e,
2970                    "failed to enable dual-stack on IPv6 VNC socket, IPv4 clients may not be able to connect"
2971                );
2972            }
2973        }
2974        socket.set_reuse_address(true)?;
2975        socket
2976            .bind(&addr.into())
2977            .with_context(|| format!("binding VNC socket to {}", addr))?;
2978        socket
2979            .listen(128)
2980            .with_context(|| format!("listening on VNC socket {}", addr))?;
2981        let listener: TcpListener = socket.into();
2982
2983        if !addr.ip().is_loopback() {
2984            tracing::warn!(
2985                address = %addr,
2986                "VNC server listening on non-localhost address without authentication"
2987            );
2988        }
2989
2990        let input_send = vm_config.input.sender();
2991        let framebuffer = resources
2992            .framebuffer_access
2993            .take()
2994            .expect("synth video enabled");
2995
2996        let vnc_host = mesh
2997            .make_host("vnc", None)
2998            .await
2999            .context("spawning vnc process failed")?;
3000
3001        vnc_worker = Some(
3002            vnc_host
3003                .launch_worker(
3004                    vnc_worker_defs::VNC_WORKER_TCP,
3005                    VncParameters {
3006                        listener,
3007                        framebuffer,
3008                        input_send,
3009                        dirty_recv: resources.dirty_rect_recv.take(),
3010                        max_clients: opt.vnc.vnc_max_clients,
3011                        evict_oldest: opt.vnc.vnc_evict_oldest,
3012                    },
3013                )
3014                .await?,
3015        )
3016    }
3017
3018    // spin up the debug worker
3019    let gdb_worker = if let Some(port) = opt.gdb {
3020        let listener = TcpListener::bind(format!("127.0.0.1:{}", port))
3021            .with_context(|| format!("binding to gdb port {}", port))?;
3022
3023        let (req_tx, req_rx) = mesh::channel();
3024        vm_config.debugger_rpc = Some(req_rx);
3025
3026        let gdb_host = mesh
3027            .make_host("gdb", None)
3028            .await
3029            .context("spawning gdbstub process failed")?;
3030
3031        Some(
3032            gdb_host
3033                .launch_worker(
3034                    debug_worker_defs::DEBUGGER_WORKER,
3035                    debug_worker_defs::DebuggerParameters {
3036                        listener,
3037                        req_chan: req_tx,
3038                        vp_count: vm_config.processor_topology.proc_count,
3039                        target_arch: if cfg!(guest_arch = "x86_64") {
3040                            debug_worker_defs::TargetArch::X86_64
3041                        } else {
3042                            debug_worker_defs::TargetArch::Aarch64
3043                        },
3044                    },
3045                )
3046                .await
3047                .context("failed to launch gdbstub worker")?,
3048        )
3049    } else {
3050        None
3051    };
3052
3053    // spin up the VM
3054    let (vm_rpc, rpc_recv) = mesh::channel();
3055    let (notify_send, notify_recv) = mesh::channel();
3056    let vm_worker = {
3057        let vm_host = mesh.make_host("vm", opt.log_file.clone()).await?;
3058
3059        let (shared_memory, saved_state) = if let Some(snapshot_dir) = &opt.restore_snapshot {
3060            let (fd, state_msg) = prepare_snapshot_restore(snapshot_dir, &opt)?;
3061            (Some(fd), Some(state_msg))
3062        } else {
3063            let shared_memory = opt
3064                .memory_backing_file()
3065                .map(|path| {
3066                    openvmm_helpers::shared_memory::open_memory_backing_file(
3067                        path,
3068                        opt.memory_size(),
3069                    )
3070                })
3071                .transpose()?;
3072            (shared_memory, None)
3073        };
3074
3075        let params = VmWorkerParameters {
3076            hypervisor: match &opt.hypervisor {
3077                Some(name) => openvmm_helpers::hypervisor::hypervisor_resource(name)?,
3078                None => openvmm_helpers::hypervisor::choose_hypervisor()?,
3079            },
3080            cfg: vm_config,
3081            saved_state,
3082            shared_memory,
3083            rpc: rpc_recv,
3084            notify: notify_send,
3085        };
3086        vm_host
3087            .launch_worker(VM_WORKER, params)
3088            .await
3089            .context("failed to launch vm worker")?
3090    };
3091
3092    if opt.restore_snapshot.is_some() {
3093        tracing::info!("restoring VM from snapshot");
3094    }
3095
3096    if !opt.paused {
3097        vm_rpc.call(VmRpc::Resume, ()).await?;
3098    }
3099
3100    let paravisor_diag = Arc::new(diag_client::DiagClient::from_dialer(
3101        driver.clone(),
3102        DiagDialer {
3103            driver: driver.clone(),
3104            vm_rpc: vm_rpc.clone(),
3105            openhcl_vtl: if opt.vtl2 {
3106                DeviceVtl::Vtl2
3107            } else {
3108                DeviceVtl::Vtl0
3109            },
3110        },
3111    ));
3112
3113    let diag_inspector = DiagInspector::new(driver.clone(), paravisor_diag.clone());
3114
3115    // Create channels between the REPL and VmController.
3116    let (vm_controller_send, vm_controller_recv) = mesh::channel();
3117    let (vm_controller_event_send, vm_controller_event_recv) = mesh::channel();
3118
3119    let has_vtl2 = resources.vtl2_settings.is_some();
3120    let serial_driver = resources
3121        .serial_driver
3122        .take()
3123        .expect("serial driver must outlive serial resources");
3124
3125    // Build the VmController with exclusive resources.
3126    let controller = vm_controller::VmController {
3127        mesh: mesh_slot.take().unwrap(),
3128        vm_worker,
3129        vnc_worker,
3130        gdb_worker,
3131        diag_inspector: Some(diag_inspector),
3132        vtl2_settings: resources.vtl2_settings,
3133        ged_rpc: resources.ged_rpc.clone(),
3134        vm_rpc: vm_rpc.clone(),
3135        paravisor_diag: Some(paravisor_diag),
3136        igvm_path: opt.igvm.as_ref().map(|igvm| igvm.firmware.clone()),
3137        memory_backing_file: opt.memory_backing_file().cloned(),
3138        memory: opt.memory_size(),
3139        processors: opt.processors,
3140        log_file: opt.log_file.clone(),
3141        crash_dump_path: opt.crash_dump_path.clone(),
3142        guest_power_actions: vm_controller::GuestPowerActions {
3143            shutdown: opt.guest_shutdown_action,
3144            reset: opt.guest_reset_action,
3145            crash: opt.guest_crash_action,
3146            watchdog: opt.guest_watchdog_action,
3147        },
3148    };
3149
3150    // Spawn the VmController as a task.
3151    let controller_task = driver.spawn(
3152        "vm-controller",
3153        controller.run(vm_controller_recv, vm_controller_event_send, notify_recv),
3154    );
3155
3156    // Run the REPL with shareable resources.
3157    let repl_result = repl::run_repl(
3158        driver,
3159        repl::ReplResources {
3160            vm_rpc,
3161            vm_controller: vm_controller_send,
3162            vm_controller_events: vm_controller_event_recv,
3163            scsi_rpc: resources.scsi_rpc,
3164            nvme_vtl2_rpc: resources.nvme_vtl2_rpc,
3165            consomme_rpc: resources.consomme_rpc,
3166            shutdown_ic: resources.shutdown_ic,
3167            acpi_power_button: resources.acpi_power_button,
3168            kvp_ic: resources.kvp_ic,
3169            console_in: resources.console_in,
3170            has_vtl2,
3171        },
3172    )
3173    .await;
3174
3175    // Wait for the controller task to finish (it stops the VM worker and
3176    // shuts down the mesh).
3177    controller_task.await;
3178    drop(serial_driver);
3179
3180    // run_repl returns the exit status: the code the guest drove via an opt-in
3181    // exit (VmControllerEvent::ExitRequested), or 0 when the VM stopped normally.
3182    repl_result
3183}
3184
3185struct DiagDialer {
3186    driver: DefaultDriver,
3187    vm_rpc: mesh::Sender<VmRpc>,
3188    openhcl_vtl: DeviceVtl,
3189}
3190
3191impl mesh_rpc::client::Dial for DiagDialer {
3192    type Stream = PolledSocket<unix_socket::UnixStream>;
3193
3194    async fn dial(&mut self) -> io::Result<Self::Stream> {
3195        let service_id = new_hvsock_service_id(1);
3196        let socket = self
3197            .vm_rpc
3198            .call_failable(
3199                VmRpc::ConnectHvsock,
3200                (
3201                    CancelContext::new().with_timeout(Duration::from_secs(2)),
3202                    service_id,
3203                    self.openhcl_vtl,
3204                ),
3205            )
3206            .await
3207            .map_err(io::Error::other)?;
3208
3209        PolledSocket::new(&self.driver, socket)
3210    }
3211}
3212
3213/// An object that implements [`InspectMut`] by sending an inspect request over
3214/// TTRPC to the guest (typically the paravisor running in VTL2), then stitching
3215/// the response back into the inspect tree.
3216///
3217/// This also caches the TTRPC connection to the guest so that only the first
3218/// inspect request has to wait for the connection to be established.
3219pub(crate) struct DiagInspector(DiagInspectorInner);
3220
3221enum DiagInspectorInner {
3222    NotStarted(DefaultDriver, Arc<diag_client::DiagClient>),
3223    Started {
3224        send: mesh::Sender<inspect::Deferred>,
3225        _task: Task<()>,
3226    },
3227    Invalid,
3228}
3229
3230impl DiagInspector {
3231    pub fn new(driver: DefaultDriver, diag_client: Arc<diag_client::DiagClient>) -> Self {
3232        Self(DiagInspectorInner::NotStarted(driver, diag_client))
3233    }
3234
3235    fn start(&mut self) -> &mesh::Sender<inspect::Deferred> {
3236        loop {
3237            match self.0 {
3238                DiagInspectorInner::NotStarted { .. } => {
3239                    let DiagInspectorInner::NotStarted(driver, client) =
3240                        std::mem::replace(&mut self.0, DiagInspectorInner::Invalid)
3241                    else {
3242                        unreachable!()
3243                    };
3244                    let (send, recv) = mesh::channel();
3245                    let task = driver.clone().spawn("diag-inspect", async move {
3246                        Self::run(&client, recv).await
3247                    });
3248
3249                    self.0 = DiagInspectorInner::Started { send, _task: task };
3250                }
3251                DiagInspectorInner::Started { ref send, .. } => break send,
3252                DiagInspectorInner::Invalid => unreachable!(),
3253            }
3254        }
3255    }
3256
3257    async fn run(
3258        diag_client: &diag_client::DiagClient,
3259        mut recv: mesh::Receiver<inspect::Deferred>,
3260    ) {
3261        while let Some(deferred) = recv.next().await {
3262            let info = deferred.external_request();
3263            let result = match info.request_type {
3264                inspect::ExternalRequestType::Inspect { depth } => {
3265                    if depth == 0 {
3266                        Ok(inspect::Node::Unevaluated)
3267                    } else {
3268                        // TODO: Support taking timeouts from the command line
3269                        diag_client
3270                            .inspect(info.path, Some(depth - 1), Some(Duration::from_secs(1)))
3271                            .await
3272                    }
3273                }
3274                inspect::ExternalRequestType::Update { value } => {
3275                    (diag_client.update(info.path, value).await).map(inspect::Node::Value)
3276                }
3277            };
3278            deferred.complete_external(
3279                result.unwrap_or_else(|err| {
3280                    inspect::Node::Failed(inspect::Error::Mesh(format!("{err:#}")))
3281                }),
3282                inspect::SensitivityLevel::Unspecified,
3283            )
3284        }
3285    }
3286}
3287
3288impl InspectMut for DiagInspector {
3289    fn inspect_mut(&mut self, req: inspect::Request<'_>) {
3290        self.start().send(req.defer());
3291    }
3292}
3293
3294#[cfg(test)]
3295mod tests {
3296    use super::*;
3297    use clap::Parser;
3298    use std::fs::File;
3299    use test_with_tracing::test;
3300    use vm_resource::ResourceId;
3301
3302    /// The same boot flags give the same ACPI power button with and without
3303    /// `--restore-snapshot`.
3304    #[test]
3305    fn restore_builds_the_acpi_power_button_of_its_boot_command_line() {
3306        DefaultPool::run_with(async |driver| {
3307            let temp_dir = tempfile::tempdir().unwrap();
3308            let file = |name: &str| {
3309                let path = temp_dir.path().join(name);
3310                File::create(&path).unwrap();
3311                path.to_str().unwrap().to_owned()
3312            };
3313            let kernel = file("kernel");
3314            let initrd = file("initrd");
3315            let uefi = format!("--uefi=firmware={}", file("firmware"));
3316            // The config build does not read the snapshot directory.
3317            let snapshot = temp_dir.path().join("snapshot");
3318            let linux = ["--kernel", kernel.as_str(), "--initrd", initrd.as_str()];
3319
3320            for (boot, has_button) in [
3321                (linux.to_vec(), true),
3322                ([&linux[..], &["--device-tree"]].concat(), false),
3323                (vec![uefi.as_str()], true),
3324            ] {
3325                for restore in [false, true] {
3326                    let mut args = vec!["openvmm", "--single-process"];
3327                    args.extend(&boot);
3328                    if restore {
3329                        args.extend(["--restore-snapshot", snapshot.to_str().unwrap()]);
3330                    }
3331                    let opt = Options::try_parse_from(&args).unwrap();
3332                    let mesh = VmmMesh::new(&driver, true).unwrap();
3333                    let (config, _) = vm_config_from_command_line(driver.clone(), &mesh, &opt)
3334                        .await
3335                        .unwrap();
3336                    mesh.shutdown().await;
3337
3338                    let built = config
3339                        .chipset_devices
3340                        .iter()
3341                        .any(|device| device.name == "acpi-power-button");
3342                    assert_eq!(built, has_button, "{args:?}");
3343                }
3344            }
3345        });
3346    }
3347
3348    #[test]
3349    fn selects_non_volatile_store_backing() {
3350        for file_id in [
3351            vmgs_format::FileId::BIOS_NVRAM,
3352            vmgs_format::FileId::TPM_PPI,
3353            tpm_vmgs::tpm_nvram_file_id(TpmVersion::V138),
3354            tpm_vmgs::tpm_nvram_file_id(TpmVersion::V185),
3355        ] {
3356            assert_eq!(
3357                non_volatile_store_resource(true, file_id).id(),
3358                <VmgsFileHandle as ResourceId<NonVolatileStoreKind>>::ID
3359            );
3360            assert_eq!(
3361                non_volatile_store_resource(false, file_id).id(),
3362                <EphemeralNonVolatileStoreHandle as ResourceId<NonVolatileStoreKind>>::ID
3363            );
3364        }
3365    }
3366
3367    #[test]
3368    fn opens_vmgs_with_provisioning_and_encryption_policy() {
3369        block_on(async {
3370            for provision in [
3371                ProvisionCondition::OnEmpty,
3372                ProvisionCondition::OnFailure,
3373                ProvisionCondition::Single,
3374            ] {
3375                for encryption_policy in [
3376                    GuestStateEncryptionPolicy::None(true),
3377                    GuestStateEncryptionPolicy::GspById(true),
3378                ] {
3379                    let vmgs = open_vmgs(
3380                        &DiskCliKind::Memory(vmgs_format::VMGS_DEFAULT_CAPACITY),
3381                        provision,
3382                        encryption_policy,
3383                    )
3384                    .await
3385                    .unwrap();
3386                    let disk = match (provision, vmgs) {
3387                        (ProvisionCondition::OnEmpty, VmgsResource::Disk(disk))
3388                        | (
3389                            ProvisionCondition::OnFailure,
3390                            VmgsResource::ReprovisionOnFailure(disk),
3391                        )
3392                        | (ProvisionCondition::Single, VmgsResource::ReprovisionSingle(disk)) => {
3393                            disk
3394                        }
3395                        _ => panic!("incorrect VMGS provisioning"),
3396                    };
3397                    assert!(matches!(
3398                        (encryption_policy, disk.encryption_policy),
3399                        (
3400                            GuestStateEncryptionPolicy::None(true),
3401                            GuestStateEncryptionPolicy::None(true)
3402                        ) | (
3403                            GuestStateEncryptionPolicy::GspById(true),
3404                            GuestStateEncryptionPolicy::GspById(true)
3405                        )
3406                    ));
3407                }
3408            }
3409        });
3410    }
3411
3412    /// Declaring controllers alone must not create implicit device attachments.
3413    #[test]
3414    fn usb_cli_empty_controllers() {
3415        let opt = Options::try_parse_from(["openvmm", "--usb-xhci", "usb0", "--usb-xhci", "usb1"])
3416            .unwrap();
3417        let controllers = usb_controller_configs(&opt.usb_xhci, &opt.usb_device).unwrap();
3418        assert_eq!(controllers.len(), 2);
3419        assert!(
3420            controllers
3421                .iter()
3422                .all(|(_, config)| config.devices.is_empty())
3423        );
3424    }
3425
3426    /// A sole controller supplies omitted on values; omitted connectors are left to the resolver.
3427    #[test]
3428    fn usb_cli_infers_single_controller() {
3429        let opt = Options::try_parse_from([
3430            "openvmm",
3431            "--usb-xhci",
3432            "usb0",
3433            "--usb-device",
3434            "mock-hid",
3435            "--usb-device",
3436            "mock-loopback",
3437            "--usb-device",
3438            "mock-storage",
3439            "--usb-device",
3440            "mock-mutt",
3441            "--usb-device",
3442            "mock-supermutt",
3443            "--usb-device",
3444            r"winusb,instance_id=USB\A",
3445            "--usb-device",
3446            "winusb-all",
3447        ])
3448        .unwrap();
3449        let controllers = usb_controller_configs(&opt.usb_xhci, &opt.usb_device).unwrap();
3450        assert_eq!(controllers[0].0, "usb0");
3451        assert_eq!(controllers[0].1.devices.len(), 7);
3452        for config in controllers[0].1.devices.iter().take(6) {
3453            let usb_config_resources::UsbDeviceConfig::Device { connector, .. } = config else {
3454                panic!("expected a device attachment");
3455            };
3456            assert_eq!(*connector, None);
3457        }
3458        assert!(matches!(
3459            controllers[0].1.devices[6],
3460            usb_config_resources::UsbDeviceConfig::Discover(_)
3461        ));
3462    }
3463
3464    /// An omitted on value is ambiguous when more than one controller is declared.
3465    #[test]
3466    fn usb_cli_requires_explicit_controller_when_ambiguous() {
3467        for selection in [
3468            "mock-hid",
3469            "mock-hid,connector=1",
3470            r"winusb,instance_id=USB\A",
3471            "winusb-all",
3472        ] {
3473            let opt = Options::try_parse_from([
3474                "openvmm",
3475                "--usb-xhci",
3476                "usb0",
3477                "--usb-xhci",
3478                "usb1",
3479                "--usb-device",
3480                selection,
3481            ])
3482            .unwrap();
3483            let error = usb_controller_configs(&opt.usb_xhci, &opt.usb_device).unwrap_err();
3484            assert!(error.to_string().contains("on=PCIE_PORT is required"));
3485        }
3486    }
3487
3488    /// Normalization preserves one-based connector numbers and explicit mock overrides.
3489    #[test]
3490    fn usb_cli_preserves_one_based_connectors_and_mock_overrides() {
3491        let opt = Options::try_parse_from([
3492            "openvmm",
3493            "--usb-xhci",
3494            "usb0",
3495            "--usb-xhci",
3496            "usb1",
3497            "--usb-device",
3498            "mock-hid,on=usb0,connector=1,vid=0x1234,pid=65535,serial=HID001",
3499            "--usb-device",
3500            "mock-storage,on=usb1,connector=15,blocks=4096",
3501        ])
3502        .unwrap();
3503        let controllers = usb_controller_configs(&opt.usb_xhci, &opt.usb_device).unwrap();
3504        let usb_config_resources::UsbDeviceConfig::Device { connector, device } =
3505            &controllers[0].1.devices[0]
3506        else {
3507            panic!("expected fixed HID attachment");
3508        };
3509        assert_eq!(*connector, Some(1));
3510        assert_eq!(device.id(), "mock_hid_config");
3511        let usb_config_resources::UsbDeviceConfig::Device { connector, .. } =
3512            &controllers[1].1.devices[0]
3513        else {
3514            panic!("expected fixed storage attachment");
3515        };
3516        assert_eq!(*connector, Some(15));
3517        let cli_args::UsbDeviceCli::Device {
3518            backend: usb::UsbBackend::Hid(handle),
3519            ..
3520        } = &opt.usb_device[0]
3521        else {
3522            panic!("expected HID");
3523        };
3524        assert_eq!(
3525            (handle.identity.vendor_id, handle.identity.product_id),
3526            (Some(0x1234), Some(65535))
3527        );
3528        assert_eq!(handle.identity.serial_number.as_deref(), Some("HID001"));
3529        let cli_args::UsbDeviceCli::Device {
3530            backend: usb::UsbBackend::Storage(handle),
3531            ..
3532        } = &opt.usb_device[1]
3533        else {
3534            panic!("expected storage");
3535        };
3536        assert_eq!(handle.block_count, Some(4096));
3537    }
3538
3539    /// Explicit host IDs are preserved and automatic discovery remains a separate request.
3540    #[test]
3541    fn usb_cli_preserves_winusb_ids_and_automatic_selection() {
3542        let instance_id = r"USB\MS_COMP_WINUSB\A&B C";
3543        let selection = format!("winusb,on=usb0,connector=3,instance_id={instance_id}");
3544        let opt = Options::try_parse_from([
3545            "openvmm",
3546            "--usb-xhci",
3547            "usb0",
3548            "--usb-device",
3549            &selection,
3550            "--usb-device",
3551            "winusb-all,on=usb0",
3552        ])
3553        .unwrap();
3554        let controllers = usb_controller_configs(&opt.usb_xhci, &opt.usb_device).unwrap();
3555        let cli_args::UsbDeviceCli::Device {
3556            backend: usb::UsbBackend::WinUsb(handle),
3557            ..
3558        } = &opt.usb_device[0]
3559        else {
3560            panic!("expected WinUSB");
3561        };
3562        assert_eq!(handle.instance_id, instance_id);
3563        let usb_config_resources::UsbDeviceConfig::Device { connector, device } =
3564            &controllers[0].1.devices[0]
3565        else {
3566            panic!("expected fixed WinUSB attachment");
3567        };
3568        assert_eq!(*connector, Some(3));
3569        assert_eq!(device.id(), "winusb_config");
3570        let usb_config_resources::UsbDeviceConfig::Discover(discovery) =
3571            &controllers[0].1.devices[1]
3572        else {
3573            panic!("expected WinUSB discovery");
3574        };
3575        assert_eq!(discovery.id(), "winusb_all_config");
3576    }
3577
3578    /// Reject malformed backend options and missing, duplicate, or unknown controllers.
3579    #[test]
3580    fn usb_cli_rejects_bad_syntax_and_unknown_controllers() {
3581        for selection in [
3582            "usb0:hid",
3583            "winusb",
3584            "mock-hid,on=usb0,connector=0",
3585            "mock-hid,on=usb0,connector=256",
3586            "mock-hid,on=usb0,connector=1,vid=0x10000",
3587            "mock-hid,on=usb0,connector=1,pid=bad",
3588            "mock-hid,on=usb0,connector=1,unknown=1",
3589            "mock-hid,on=usb0,connector=1,instance_id=USB",
3590            "mock-hid,on=usb0,connector=1,blocks=1",
3591            "mock-supermutt,on=usb0,connector=1,vid=1",
3592            "mock-storage,on=usb0,connector=1,blocks=0",
3593            "winusb,on=usb0,connector=1,instance_id=USB,vid=1",
3594            "winusb-all,on=usb0,connector=1",
3595        ] {
3596            assert!(
3597                Options::try_parse_from([
3598                    "openvmm",
3599                    "--usb-xhci",
3600                    "usb0",
3601                    "--usb-device",
3602                    selection
3603                ])
3604                .is_err()
3605            );
3606        }
3607        assert!(
3608            Options::try_parse_from(["openvmm", "--usb-device", "mock-hid,on=usb0,connector=1"])
3609                .is_err()
3610        );
3611        assert!(Options::try_parse_from(["openvmm", "--usb-xhci", "usb0,unknown=1"]).is_err());
3612        let opt = Options::try_parse_from([
3613            "openvmm",
3614            "--usb-xhci",
3615            "usb0",
3616            "--usb-device",
3617            "mock-hid,on=missing,connector=1",
3618        ])
3619        .unwrap();
3620        assert!(usb_controller_configs(&opt.usb_xhci, &opt.usb_device).is_err());
3621        let opt = Options::try_parse_from(["openvmm", "--usb-xhci", "usb0", "--usb-xhci", "usb0"])
3622            .unwrap();
3623        assert!(usb_controller_configs(&opt.usb_xhci, &opt.usb_device).is_err());
3624    }
3625
3626    /// Backend defaults, capacity limits, and connector conflicts must not be
3627    /// baked into the shared CLI.
3628    #[test]
3629    fn usb_cli_leaves_defaults_and_implementation_limits_to_resolvers() {
3630        let opt = Options::try_parse_from([
3631            "openvmm",
3632            "--usb-xhci",
3633            "usb0",
3634            "--usb-device",
3635            "mock-storage,on=usb0,connector=16,blocks=524289",
3636            "--usb-device",
3637            "mock-storage,on=usb0,connector=1",
3638            "--usb-device",
3639            "mock-hid,on=usb0,connector=1",
3640        ])
3641        .unwrap();
3642        assert!(usb_controller_configs(&opt.usb_xhci, &opt.usb_device).is_ok());
3643        let cli_args::UsbDeviceCli::Device {
3644            backend: usb::UsbBackend::Storage(config),
3645            ..
3646        } = &opt.usb_device[1]
3647        else {
3648            panic!("expected storage");
3649        };
3650        assert_eq!(config.block_count, None);
3651        assert_eq!(config.identity, Default::default());
3652    }
3653
3654    /// Discovery excludes explicit host IDs across controllers regardless of option order.
3655    /// Resource resolution is async even when the test only decodes configuration.
3656    #[pal_async::async_test]
3657    async fn usb_cli_excludes_explicit_winusb_across_controllers() {
3658        use usb_config_resources::UsbDeviceDiscoveryConfigKind;
3659        use usb_config_resources::WinUsbAllConfig;
3660        use vm_resource::CanResolveTo;
3661        use vm_resource::ResolveResource;
3662        use vm_resource::ResourceResolver;
3663
3664        /// Discovery settings decoded for assertions without host enumeration.
3665        struct CapturedDiscoveryConfig {
3666            /// The actual configuration emitted by the CLI.
3667            config: WinUsbAllConfig,
3668        }
3669        /// Reads an opaque discovery resource without constructing USB devices.
3670        struct CaptureDiscoveryConfig;
3671
3672        impl CanResolveTo<CapturedDiscoveryConfig> for UsbDeviceDiscoveryConfigKind {
3673            type Input<'a> = ();
3674        }
3675
3676        impl ResolveResource<UsbDeviceDiscoveryConfigKind, WinUsbAllConfig> for CaptureDiscoveryConfig {
3677            type Output = CapturedDiscoveryConfig;
3678            type Error = std::convert::Infallible;
3679
3680            fn resolve(&self, config: WinUsbAllConfig, _: ()) -> Result<Self::Output, Self::Error> {
3681                Ok(CapturedDiscoveryConfig { config })
3682            }
3683        }
3684
3685        let mut resolver = ResourceResolver::new();
3686        resolver.add_resolver(CaptureDiscoveryConfig);
3687        for selections in [
3688            [
3689                "winusb-all,on=usb0",
3690                r"winusb,on=usb1,instance_id=usb\z",
3691                r"winusb,on=usb1,instance_id=USB\a",
3692            ],
3693            [
3694                r"winusb,on=usb1,instance_id=usb\z",
3695                r"winusb,on=usb1,instance_id=USB\a",
3696                "winusb-all,on=usb0",
3697            ],
3698        ] {
3699            let mut opt =
3700                Options::try_parse_from(["openvmm", "--usb-xhci", "usb0", "--usb-xhci", "usb1"])
3701                    .unwrap();
3702            opt.usb_device = selections
3703                .into_iter()
3704                .map(|selection| selection.parse().unwrap())
3705                .collect();
3706            let mut controllers = usb_controller_configs(&opt.usb_xhci, &opt.usb_device).unwrap();
3707            assert_eq!(controllers[1].1.devices.len(), 2);
3708            let usb_config_resources::UsbDeviceConfig::Discover(discovery) =
3709                controllers[0].1.devices.pop().unwrap()
3710            else {
3711                panic!("expected discovery on usb0");
3712            };
3713            let captured: CapturedDiscoveryConfig = resolver.resolve(discovery, ()).await.unwrap();
3714            assert_eq!(captured.config.excluded_instance_ids, [r"USB\A", r"USB\Z"]);
3715        }
3716    }
3717
3718    /// Reject duplicate host IDs and multiple automatic discovery requests across the VM.
3719    #[test]
3720    fn usb_cli_rejects_overlapping_host_selections() {
3721        for (first, second) in [
3722            (
3723                "winusb,on=usb0,connector=1,instance_id=USB\\A",
3724                "winusb,on=usb1,connector=1,instance_id=usb\\a",
3725            ),
3726            ("winusb-all,on=usb0", "winusb-all,on=usb1"),
3727        ] {
3728            let opt = Options::try_parse_from([
3729                "openvmm",
3730                "--usb-xhci",
3731                "usb0",
3732                "--usb-xhci",
3733                "usb1",
3734                "--usb-device",
3735                first,
3736                "--usb-device",
3737                second,
3738            ])
3739            .unwrap();
3740            assert!(usb_controller_configs(&opt.usb_xhci, &opt.usb_device).is_err());
3741        }
3742    }
3743
3744    #[test]
3745    fn maps_igvm_personalities_to_chipsets() {
3746        for (args, expected) in [
3747            (
3748                vec!["openvmm", "--igvm", "firmware=guest.igvm,personality=uefi"],
3749                BaseChipsetType::HypervGen2Uefi,
3750            ),
3751            (
3752                vec![
3753                    "openvmm",
3754                    "--igvm",
3755                    "firmware=guest.igvm,personality=linux-direct",
3756                ],
3757                BaseChipsetType::UnenlightenedLinuxDirect,
3758            ),
3759            (
3760                vec![
3761                    "openvmm",
3762                    "--igvm",
3763                    "firmware=guest.igvm,personality=linux-direct",
3764                    "--hv",
3765                ],
3766                BaseChipsetType::HyperVGen2LinuxDirect,
3767            ),
3768            (
3769                vec![
3770                    "openvmm",
3771                    "--igvm",
3772                    "firmware=guest.igvm,personality=linux-direct",
3773                    "--isolation",
3774                    "snp",
3775                ],
3776                BaseChipsetType::EnlightenedLinuxDirect,
3777            ),
3778            (
3779                vec![
3780                    "openvmm",
3781                    "--igvm",
3782                    "firmware=guest.igvm,personality=openhcl",
3783                    "--hv",
3784                    "--vtl2",
3785                ],
3786                BaseChipsetType::HclHost,
3787            ),
3788        ] {
3789            let opt = Options::try_parse_from(args).unwrap();
3790            opt.validate_igvm_options().unwrap();
3791            assert!(
3792                std::mem::discriminant(&base_chipset_type(&opt))
3793                    == std::mem::discriminant(&expected)
3794            );
3795        }
3796    }
3797
3798    #[test]
3799    fn builds_openhcl_igvm_config_without_host_uefi() {
3800        DefaultPool::run_with(async |driver| {
3801            let temp_dir = tempfile::tempdir().unwrap();
3802            let igvm_path = temp_dir.path().join("guest.igvm");
3803            File::create(&igvm_path).unwrap();
3804            let igvm_options = format!("firmware={},personality=openhcl", igvm_path.display());
3805            let mesh = VmmMesh::new(&driver, true).unwrap();
3806
3807            for (extra, expected_alias, expected_policy) in [
3808                (vec![], true, Some(LateMapVtl0MemoryPolicy::Halt)),
3809                (
3810                    vec!["--uefi", "console=com1,disable_frontpage"],
3811                    true,
3812                    Some(LateMapVtl0MemoryPolicy::Halt),
3813                ),
3814                (
3815                    vec!["--net", "uh:consomme", "--no-alias-map"],
3816                    false,
3817                    Some(LateMapVtl0MemoryPolicy::Halt),
3818                ),
3819                (
3820                    vec!["--isolation", "vbs", "--no-alias-map"],
3821                    false,
3822                    Some(LateMapVtl0MemoryPolicy::Halt),
3823                ),
3824                (
3825                    vec!["--no-alias-map", "--late-map-vtl0-policy", "exception"],
3826                    false,
3827                    Some(LateMapVtl0MemoryPolicy::InjectException),
3828                ),
3829                (
3830                    vec!["--late-map-vtl0-policy", "log"],
3831                    true,
3832                    Some(LateMapVtl0MemoryPolicy::Log),
3833                ),
3834                (
3835                    vec!["--igvm-vtl2-relocation-type", "vtl2=filesize"],
3836                    true,
3837                    Some(LateMapVtl0MemoryPolicy::Halt),
3838                ),
3839                (
3840                    vec![
3841                        "--igvm-vtl2-relocation-type",
3842                        "vtl2=filesize",
3843                        "--late-map-vtl0-policy",
3844                        "off",
3845                    ],
3846                    true,
3847                    None,
3848                ),
3849            ] {
3850                let mut args = vec![
3851                    "openvmm",
3852                    "--igvm",
3853                    &igvm_options,
3854                    "--hv",
3855                    "--vtl2",
3856                    "--single-process",
3857                ];
3858                args.extend(extra);
3859                let opt = Options::try_parse_from(args).unwrap();
3860                let (config, resources) = vm_config_from_command_line(driver.clone(), &mesh, &opt)
3861                    .await
3862                    .unwrap();
3863
3864                assert!(matches!(config.load_mode, LoadMode::Igvm { .. }));
3865                assert!(config.hypervisor.with_hv);
3866                let vtl2 = config.hypervisor.with_vtl2.as_ref().unwrap();
3867                assert_eq!(vtl2.vtl0_alias_map, expected_alias);
3868                assert_eq!(vtl2.late_map_vtl0_memory, expected_policy);
3869                assert!(config.vmbus.is_some());
3870                assert!(config.vtl2_vmbus.is_some());
3871                assert!(resources.ged_rpc.is_some());
3872                assert!(resources.acpi_power_button.is_none());
3873                for id in ["ged", "gel"] {
3874                    assert!(
3875                        config.vmbus_devices.iter().any(|(vtl, resource)| {
3876                            *vtl == DeviceVtl::Vtl2 && resource.id() == id
3877                        })
3878                    );
3879                }
3880                assert!(
3881                    config
3882                        .chipset_devices
3883                        .iter()
3884                        .all(|device| { device.resource.id() != "hyperv_firmware_uefi" })
3885                );
3886            }
3887            for (extra, expected_error) in [
3888                (
3889                    ["--net", "uh:consomme"],
3890                    "must specify --no-alias-map to offer NICs to VTL2",
3891                ),
3892                (
3893                    ["--isolation", "vbs"],
3894                    "alias map not supported with isolation",
3895                ),
3896            ] {
3897                let mut args = vec![
3898                    "openvmm",
3899                    "--igvm",
3900                    &igvm_options,
3901                    "--hv",
3902                    "--vtl2",
3903                    "--single-process",
3904                ];
3905                args.extend(extra);
3906                let opt = Options::try_parse_from(args).unwrap();
3907                let err = vm_config_from_command_line(driver.clone(), &mesh, &opt)
3908                    .await
3909                    .err()
3910                    .unwrap();
3911                assert_eq!(err.to_string(), expected_error);
3912            }
3913            mesh.shutdown().await;
3914        });
3915    }
3916
3917    #[test]
3918    fn maps_virtio_vsock_to_named_pcie_port() {
3919        DefaultPool::run_with(async |driver| {
3920            let temp_dir = tempfile::tempdir().unwrap();
3921            let kernel_path = temp_dir.path().join("kernel");
3922            File::create(&kernel_path).unwrap();
3923            let initrd_path = temp_dir.path().join("initrd");
3924            File::create(&initrd_path).unwrap();
3925            let socket_path = temp_dir.path().join("vsock");
3926            let opt = Options::try_parse_from([
3927                "openvmm",
3928                "--kernel",
3929                kernel_path.to_str().unwrap(),
3930                "--initrd",
3931                initrd_path.to_str().unwrap(),
3932                "--virtio-vsock-path",
3933                socket_path.to_str().unwrap(),
3934                "--virtio-vsock-bus",
3935                "pcie:custom",
3936                "--single-process",
3937            ])
3938            .unwrap();
3939            let mesh = VmmMesh::new(&driver, true).unwrap();
3940
3941            let (config, _resources) = vm_config_from_command_line(driver, &mesh, &opt)
3942                .await
3943                .unwrap();
3944
3945            assert_eq!(config.pcie_devices.len(), 1);
3946            assert_eq!(config.pcie_devices[0].port_name, "custom");
3947            mesh.shutdown().await;
3948        });
3949    }
3950
3951    #[test]
3952    fn maps_virtio_fs_and_rng_to_named_pcie_ports() {
3953        DefaultPool::run_with(async |driver| {
3954            let temp_dir = tempfile::tempdir().unwrap();
3955            let kernel_path = temp_dir.path().join("kernel");
3956            File::create(&kernel_path).unwrap();
3957            let initrd_path = temp_dir.path().join("initrd");
3958            File::create(&initrd_path).unwrap();
3959            let root_path = temp_dir.path().to_str().unwrap();
3960            let opt = Options::try_parse_from([
3961                "openvmm",
3962                "--kernel",
3963                kernel_path.to_str().unwrap(),
3964                "--initrd",
3965                initrd_path.to_str().unwrap(),
3966                "--virtio-fs",
3967                &format!("fs,{root_path}"),
3968                "--virtio-fs-bus",
3969                "pcie:fs",
3970                "--virtio-rng",
3971                "--virtio-rng-bus",
3972                "pcie:rng",
3973                "--single-process",
3974            ])
3975            .unwrap();
3976            let mesh = VmmMesh::new(&driver, true).unwrap();
3977
3978            let (config, _resources) = vm_config_from_command_line(driver, &mesh, &opt)
3979                .await
3980                .unwrap();
3981
3982            let port_names: Vec<_> = config
3983                .pcie_devices
3984                .iter()
3985                .map(|device| device.port_name.as_str())
3986                .collect();
3987            assert_eq!(port_names, ["fs", "rng"]);
3988            mesh.shutdown().await;
3989        });
3990    }
3991
3992    #[test]
3993    fn rejects_duplicate_pcie_port_assignments() {
3994        DefaultPool::run_with(async |driver| {
3995            let temp_dir = tempfile::tempdir().unwrap();
3996            let kernel_path = temp_dir.path().join("kernel");
3997            File::create(&kernel_path).unwrap();
3998            let initrd_path = temp_dir.path().join("initrd");
3999            File::create(&initrd_path).unwrap();
4000            let root_path = temp_dir.path().to_str().unwrap();
4001            let opt = Options::try_parse_from([
4002                "openvmm",
4003                "--kernel",
4004                kernel_path.to_str().unwrap(),
4005                "--initrd",
4006                initrd_path.to_str().unwrap(),
4007                "--virtio-fs",
4008                &format!("pcie_port=custom:fs,{root_path}"),
4009                "--virtio-rng",
4010                "--virtio-rng-bus",
4011                "pcie:custom",
4012                "--single-process",
4013            ])
4014            .unwrap();
4015            let mesh = VmmMesh::new(&driver, true).unwrap();
4016
4017            let error = vm_config_from_command_line(driver, &mesh, &opt)
4018                .await
4019                .err()
4020                .unwrap();
4021
4022            assert_eq!(error.to_string(), "multiple devices use PCIe port 'custom'");
4023            mesh.shutdown().await;
4024        });
4025    }
4026
4027    #[test]
4028    fn rejects_duplicate_pcie_port_assignment_from_storage() {
4029        DefaultPool::run_with(async |driver| {
4030            let temp_dir = tempfile::tempdir().unwrap();
4031            let kernel_path = temp_dir.path().join("kernel");
4032            File::create(&kernel_path).unwrap();
4033            let initrd_path = temp_dir.path().join("initrd");
4034            File::create(&initrd_path).unwrap();
4035            let opt = Options::try_parse_from([
4036                "openvmm",
4037                "--kernel",
4038                kernel_path.to_str().unwrap(),
4039                "--initrd",
4040                initrd_path.to_str().unwrap(),
4041                "--nvme-pci",
4042                "id=nvme0,pcie_port=custom",
4043                "--virtio-rng",
4044                "--virtio-rng-bus",
4045                "pcie:custom",
4046                "--single-process",
4047            ])
4048            .unwrap();
4049            let mesh = VmmMesh::new(&driver, true).unwrap();
4050
4051            let error = vm_config_from_command_line(driver, &mesh, &opt)
4052                .await
4053                .err()
4054                .unwrap();
4055
4056            assert_eq!(error.to_string(), "multiple devices use PCIe port 'custom'");
4057            mesh.shutdown().await;
4058        });
4059    }
4060}