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