1#![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
23pub 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 #[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 #[cfg(unix)]
175 if let Some(orig_termios) = orig_termios {
176 term::set_termios(orig_termios);
177 }
178
179 drop(pidfile_guard);
182
183 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 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 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
217fn 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
266fn 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
314fn 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 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 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 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 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 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 let mut mac_address = [0x00, 0x15, 0x5D, 0, 0, 0];
917 getrandom::fill(&mut mac_address[3..]).expect("rng failure");
918
919 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 #[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 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 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 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 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 let iommufd = iommufd.try_clone().with_context(|| {
1201 format!("failed to dup iommufd fd for iommu={iommu_id}")
1202 })?;
1203
1204 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 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 #[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 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 let smbios = Box::new(smbios_config_from_cli(&opt.smbios)?);
1395 let bios_guid = smbios.system.uuid;
1396
1397 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 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 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 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 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 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 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 elevation: 1,
1804 }
1805 .into_resource(),
1806 }
1807 .into_resource(),
1808 ),
1809 (
1810 DeviceVtl::Vtl0,
1811 SynthMouseHandle {
1812 source: MultiplexedInputHandle {
1813 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 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 if !opt.vtl2 {
1870 anyhow::bail!("VBS isolation is only currently supported with vtl2");
1871 }
1872
1873 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 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 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 #[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 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 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 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
2361pub(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
2368fn 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 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 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 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 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
2764pub(crate) fn system_page_size() -> u32 {
2766 sparse_mmap::SparseMapping::page_size() as u32
2767}
2768
2769pub(crate) const GUEST_ARCH: &str = if cfg!(guest_arch = "x86_64") {
2771 "x86_64"
2772} else {
2773 "aarch64"
2774};
2775
2776fn 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 openvmm_helpers::snapshot::validate_manifest(
2789 &manifest,
2790 GUEST_ARCH,
2791 opt.memory_size(),
2792 opt.processors,
2793 system_page_size(),
2794 )?;
2795
2796 let memory_file = fs_err::OpenOptions::new()
2798 .read(true)
2799 .write(true)
2800 .open(snapshot_dir.join("memory.bin"))?;
2801
2802 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 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 meshworker::run_vmm_mesh_host()?;
2833
2834 let opt = cli_args::parse_options();
2835
2836 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 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 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 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 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 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 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 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 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 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 let controller_task = driver.spawn(
3152 "vm-controller",
3153 controller.run(vm_controller_recv, vm_controller_event_send, notify_recv),
3154 );
3155
3156 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 controller_task.await;
3178 drop(serial_driver);
3179
3180 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
3213pub(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 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 #[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 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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 struct CapturedDiscoveryConfig {
3666 config: WinUsbAllConfig,
3668 }
3669 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 #[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}