diff --git a/cloud-hypervisor/src/bin/ch-remote.rs b/cloud-hypervisor/src/bin/ch-remote.rs index 7df71e9ad..f0ca60105 100644 --- a/cloud-hypervisor/src/bin/ch-remote.rs +++ b/cloud-hypervisor/src/bin/ch-remote.rs @@ -531,18 +531,19 @@ fn rest_api_do_command(matches: &ArgMatches, socket: &mut UnixStream) -> ApiResu .map_err(Error::HttpApiClient) } Some("receive-migration") => { - let receive_migration_data = receive_migration_data( + let (receive_migration_data, fds) = receive_migration_data( matches .subcommand_matches("receive-migration") .unwrap() .get_one::("receive_migration_config") .unwrap(), )?; - simple_api_command( + simple_api_command_with_fds( socket, "PUT", "receive-migration", Some(&receive_migration_data), + &fds, ) .map_err(Error::HttpApiClient) } @@ -750,7 +751,7 @@ fn dbus_api_do_command(matches: &ArgMatches, proxy: &DBusApi1ProxyBlocking<'_>) proxy.api_vm_send_migration(&send_migration_data) } Some("receive-migration") => { - let receive_migration_data = receive_migration_data( + let (receive_migration_data, _fds) = receive_migration_data( matches .subcommand_matches("receive-migration") .unwrap() @@ -962,10 +963,21 @@ fn coredump_config(destination_url: &str) -> String { serde_json::to_string(&coredump_config).unwrap() } -fn receive_migration_data(config: &str) -> Result { - let receive_migration_data = +fn receive_migration_data(config: &str) -> Result<(String, Vec), Error> { + let mut data = api::VmReceiveMigrationData::parse(config).map_err(Error::ReceiveMigrationConfig)?; - Ok(serde_json::to_string(&receive_migration_data).unwrap()) + + // The FDs are passed to the server side process via SCM_RIGHTS, in the + // order vfio_fds then the iommufd FD, matching the server's split. + let mut fds: Vec = match data.vfio_fds.as_mut() { + Some(vfio_fds) => vfio_fds.iter_mut().filter_map(|v| v.fd.take()).collect(), + None => Vec::new(), + }; + if let Some(iommufd_fd) = data.iommufd_fd.take() { + fds.push(iommufd_fd); + } + + Ok((serde_json::to_string(&data).unwrap(), fds)) } fn send_migration_data(config: &str) -> Result { diff --git a/vmm/src/api/http/http_endpoint.rs b/vmm/src/api/http/http_endpoint.rs index 23c690d2d..4978e08bb 100644 --- a/vmm/src/api/http/http_endpoint.rs +++ b/vmm/src/api/http/http_endpoint.rs @@ -52,8 +52,8 @@ use crate::api::{ AddDisk, ApiAction, ApiError, ApiRequest, DeviceConfig, NetConfig, VmAddDevice, VmAddFs, VmAddGenericVhostUser, VmAddNet, VmAddPmem, VmAddUserDevice, VmAddVdpa, VmAddVsock, VmBoot, VmConfig, VmCounters, VmDelete, VmNmi, VmPause, VmPowerButton, VmReboot, VmReceiveMigration, - VmRemoveDevice, VmResize, VmResizeDisk, VmResizeZone, VmRestore, VmResume, VmSendMigration, - VmShutdown, VmSnapshot, + VmReceiveMigrationData, VmRemoveDevice, VmResize, VmResizeDisk, VmResizeZone, VmRestore, + VmResume, VmSendMigration, VmShutdown, VmSnapshot, }; use crate::config::RestoreConfig; use crate::cpu::Error as CpuError; @@ -484,7 +484,6 @@ vm_action_put_handler_body!(VmRemoveDevice); vm_action_put_handler_body!(VmResizeDisk); vm_action_put_handler_body!(VmResizeZone); vm_action_put_handler_body!(VmSnapshot); -vm_action_put_handler_body!(VmReceiveMigration); vm_action_put_handler_body!(VmSendMigration); #[cfg(all(target_arch = "x86_64", feature = "guest_debug"))] @@ -626,6 +625,54 @@ impl PutHandler for VmRestore { impl GetHandler for VmRestore {} +// Custom handler so the SCM_RIGHTS file pool can be split the same way +// VmRestore does, cdev FDs for the vfio_fds entries then the iommufd FD. +impl PutHandler for VmReceiveMigration { + fn handle_request( + &'static self, + api_notifier: EventFd, + api_sender: Sender, + body: &Option, + files: Vec, + ) -> result::Result, HttpError> { + if let Some(body) = body { + let mut data: VmReceiveMigrationData = serde_json::from_slice(body.raw())?; + + let vfio_total = data.vfio_fds.as_ref().map_or(0, |c| c.len()); + let expected = if data.vfio_fds.is_some() { + vfio_total + 1 + } else { + 0 + }; + if files.len() != expected { + error!( + "Expected {expected} FDs in VmReceiveMigration request, received {}", + files.len() + ); + return Err(HttpError::BadRequest); + } + + // Split in the order vfio_fds, then the iommufd FD. + let mut files = files; + if let Some(cfgs) = data.vfio_fds.as_mut() { + let vfio_files: Vec = files.drain(..vfio_total).collect(); + let mut cfgs = cfgs.iter_mut().collect::>(); + attach_fds_to_cfgs(vfio_files, cfgs.as_mut_slice())?; + } + if data.vfio_fds.is_some() { + data.iommufd_fd = Some(files.remove(0).into_raw_fd()); + } + + self.send(api_notifier, api_sender, data) + .map_err(HttpError::ApiError) + } else { + Err(HttpError::BadRequest) + } + } +} + +impl GetHandler for VmReceiveMigration {} + // Common handler for boot, shutdown and reboot pub struct VmActionHandler { action: &'static dyn HttpVmAction, diff --git a/vmm/src/api/mod.rs b/vmm/src/api/mod.rs index a3f114bc1..a62e9dca2 100644 --- a/vmm/src/api/mod.rs +++ b/vmm/src/api/mod.rs @@ -33,6 +33,7 @@ pub mod dbus; pub mod http; +use std::collections::HashSet; use std::io; use std::num::{NonZeroU32, NonZeroU64}; use std::path::PathBuf; @@ -42,7 +43,7 @@ use std::time::Duration; use log::info; use micro_http::Body; -use option_parser::{OptionParser, OptionParserError, Toggle}; +use option_parser::{OptionParser, OptionParserError, Toggle, Tuple, TupleList}; use serde::{Deserialize, Serialize}; use thiserror::Error; use vm_migration::MigratableError; @@ -53,7 +54,7 @@ use vmm_sys_util::eventfd::EventFd; pub use self::dbus::start_dbus_thread; pub use self::http::{start_http_fd_thread, start_http_path_thread}; use crate::Error as VmmError; -use crate::config::RestoreConfig; +use crate::config::{RestoreConfig, RestoredVfioConfig, deserialize_restored_fd}; use crate::device_tree::DeviceTree; use crate::migration::transport::{ MAX_MIGRATION_CONNECTIONS, TcpAddressParseError, tcp_address_to_server_name, @@ -311,6 +312,13 @@ pub struct VmReceiveMigrationData { /// Memory transfer mode. #[serde(default)] pub memory_mode: MigrationMode, + /// Optional VFIO device id to cdev FD pairs, used to substitute each + /// device's saved path or stale FD in the received VmConfig. + #[serde(default)] + pub vfio_fds: Option>, + // FDs are not serialized and any deserialized value is invalid; see NetConfig::fds. + #[serde(default, deserialize_with = "deserialize_restored_fd")] + pub iommufd_fd: Option, } #[derive(Debug, Error)] @@ -324,11 +332,17 @@ pub enum VmReceiveMigrationConfigError { impl VmReceiveMigrationData { pub const SYNTAX: &'static str = "VM receive migration parameters \ - \"\" or \"receiver_url=[,tls_dir=][,memory_mode=precopy|postcopy]\""; + \"\" or \"receiver_url=[,tls_dir=][,memory_mode=precopy|postcopy]\ + [,vfio_fds=][,iommufd_fd=]\""; pub fn parse(migration: &str) -> Result { let mut parser = OptionParser::new(); - parser.add("receiver_url").add("tls_dir").add("memory_mode"); + parser + .add("receiver_url") + .add("tls_dir") + .add("memory_mode") + .add("vfio_fds") + .add("iommufd_fd"); parser .parse(migration) .map_err(VmReceiveMigrationConfigError::ParseError)?; @@ -346,11 +360,27 @@ impl VmReceiveMigrationData { .convert::("memory_mode") .map_err(VmReceiveMigrationConfigError::ParseError)? .unwrap_or_default(); + let vfio_fds = parser + .convert::>("vfio_fds") + .map_err(VmReceiveMigrationConfigError::ParseError)? + .map(|v| { + v.0.iter() + .map(|Tuple(id, fd)| RestoredVfioConfig { + id: id.clone(), + fd: Some(*fd as i32), + }) + .collect() + }); + let iommufd_fd = parser + .convert::("iommufd_fd") + .map_err(VmReceiveMigrationConfigError::ParseError)?; let data = Self { receiver_url, tls_dir, memory_mode, + vfio_fds, + iommufd_fd, }; data.validate()?; @@ -391,6 +421,75 @@ impl VmReceiveMigrationData { Ok(()) } + + pub fn validate_vfio_fds( + &self, + vm_config: &VmConfig, + ) -> Result<(), VmReceiveMigrationConfigError> { + let vfio_fds = self.vfio_fds.as_deref().unwrap_or_default(); + + // A migrated VFIO device cannot reuse its source handle. Its fd is + // invalid across the migration and its path names the source host's + // topology, so every device needs a replacement in vfio_fds. This + // holds even when no vfio_fds are supplied at all. + let substituted: HashSet<&str> = vfio_fds.iter().map(|v| v.id.as_str()).collect(); + for d in vm_config.devices.iter().flatten() { + if !d + .pci_common + .id + .as_deref() + .is_some_and(|id| substituted.contains(id)) + { + return Err(VmReceiveMigrationConfigError::ValidationError(format!( + "VFIO device '{}' has no replacement in vfio_fds, its source path or fd is not usable on the destination", + d.pci_common.id.as_deref().unwrap_or_default() + ))); + } + } + + if vfio_fds.is_empty() { + return Ok(()); + } + + // The supplied vfio_fds must be usable against the received VmConfig. + if self.iommufd_fd.is_none() { + return Err(VmReceiveMigrationConfigError::ValidationError( + "vfio_fds requires iommufd_fd".to_string(), + )); + } + if !vm_config.platform.as_ref().is_some_and(|p| p.iommufd) { + return Err(VmReceiveMigrationConfigError::ValidationError( + "vfio_fds requires platform iommufd=on in the received VmConfig".to_string(), + )); + } + + let mut seen = HashSet::new(); + for v in vfio_fds { + if !seen.insert(v.id.as_str()) { + return Err(VmReceiveMigrationConfigError::ValidationError(format!( + "duplicate vfio_fds id '{}'", + v.id + ))); + } + } + + let known_ids: HashSet<&str> = vm_config + .devices + .iter() + .flatten() + .filter_map(|d| d.pci_common.id.as_deref()) + .collect(); + for v in vfio_fds { + if !known_ids.contains(v.id.as_str()) { + return Err(VmReceiveMigrationConfigError::ValidationError(format!( + "vfio_fds id '{}' does not match any device in the received VmConfig", + v.id + ))); + } + } + + Ok(()) + } } #[derive(Copy, Clone, Default, Deserialize, Serialize, Debug, PartialEq, Eq)] @@ -1988,6 +2087,8 @@ mod unit_tests { receiver_url: "tcp:192.168.1.1:8080".to_string(), tls_dir: None, memory_mode: MigrationMode::Precopy, + vfio_fds: None, + iommufd_fd: None, } ); @@ -2015,6 +2116,8 @@ mod unit_tests { receiver_url: "tcp:192.168.1.1:8080".to_string(), tls_dir: Some(tls_dir_path), memory_mode: MigrationMode::Precopy, + vfio_fds: None, + iommufd_fd: None, } ); @@ -2038,6 +2141,8 @@ mod unit_tests { receiver_url: "tcp:127.0.0.1:1234".to_string(), tls_dir: None, memory_mode: MigrationMode::Precopy, + vfio_fds: None, + iommufd_fd: None, } ); @@ -2051,11 +2156,33 @@ mod unit_tests { receiver_url: "unix:/tmp/sock".to_string(), tls_dir: None, memory_mode: MigrationMode::Postcopy, + vfio_fds: None, + iommufd_fd: None, } ); // Missing receiver_url in keyed form must fail. VmReceiveMigrationData::parse("memory_mode=postcopy").unwrap_err(); + + // vfio_fds without iommufd_fd parses fine now, the pairing is checked + // later against the received VmConfig by validate_vfio_fds. + let data = + VmReceiveMigrationData::parse("receiver_url=tcp:127.0.0.1:1234,vfio_fds=[vfio0@5]") + .unwrap(); + assert!(data.iommufd_fd.is_none()); + + // vfio_fds entries with the iommufd FD. + let data = VmReceiveMigrationData::parse( + "receiver_url=tcp:127.0.0.1:1234,vfio_fds=[vfio0@5,vfio1@7],iommufd_fd=9", + ) + .unwrap(); + let fds = data.vfio_fds.expect("vfio_fds populated"); + assert_eq!(fds.len(), 2); + assert_eq!(fds[0].id, "vfio0"); + assert_eq!(fds[0].fd, Some(5)); + assert_eq!(fds[1].id, "vfio1"); + assert_eq!(fds[1].fd, Some(7)); + assert_eq!(data.iommufd_fd, Some(9)); } #[test] diff --git a/vmm/src/config.rs b/vmm/src/config.rs index 662fa3569..bc6d1ef7e 100644 --- a/vmm/src/config.rs +++ b/vmm/src/config.rs @@ -2816,7 +2816,7 @@ pub struct RestoredVfioConfig { pub fd: Option, } -fn deserialize_restored_fd<'de, D>(d: D) -> result::Result, D::Error> +pub(crate) fn deserialize_restored_fd<'de, D>(d: D) -> result::Result, D::Error> where D: serde::Deserializer<'de>, { diff --git a/vmm/src/lib.rs b/vmm/src/lib.rs index c53fdd983..047e204c9 100644 --- a/vmm/src/lib.rs +++ b/vmm/src/lib.rs @@ -962,13 +962,13 @@ impl Vmm { ))) }; - let mode = receive_data_migration.memory_mode; let mut configure_vm = |socket: &mut SocketStream, memory_files: HashMap| -> result::Result { let shared_backing = !memory_files.is_empty(); - let memory_manager = self.vm_receive_config(req, socket, memory_files, mode)?; + let memory_manager = + self.vm_receive_config(req, socket, memory_files, receive_data_migration)?; let guest_memory = memory_manager.lock().unwrap().guest_memory(); // Create the additional-connection receiver even in the single-connection case. // At this point the receiver does not know whether the sender will use extra TCP @@ -1148,11 +1148,13 @@ impl Vmm { req: &Request, socket: &mut T, existing_memory_files: HashMap, - mode: MigrationMode, + receive_data_migration: &VmReceiveMigrationData, ) -> result::Result>, MigratableError> where T: Read, { + let mode = receive_data_migration.memory_mode; + // Read in config data along with memory manager data let mut data: Vec = Vec::new(); data.resize_with(req.length() as usize, Default::default); @@ -1164,6 +1166,14 @@ impl Vmm { .context("Error deserialising config") .map_err(MigratableError::MigrateReceive)?; + // Mirrors the vm_restore handling of RestoreConfig.vfio_fds. The + // received VmConfig carries the source's device paths or stale FDs, + // neither of which is usable on this host. + receive_data_migration + .validate_vfio_fds(&vm_migration_config.vm_config.lock().unwrap()) + .map_err(|e| MigratableError::MigrateReceive(e.into()))?; + apply_vfio_fds_to_vm_config(receive_data_migration, &vm_migration_config.vm_config); + // Eager prefault populates memory before UFFD is registered, so those // pages never fault and are never served. Reject postcopy+prefault // rather than serve stale data. @@ -2193,6 +2203,44 @@ fn apply_landlock(vm_config: &mut VmConfig) -> result::Result<(), LandlockError> Ok(()) } +// For each matched DeviceConfig.id, swap the saved path or stale FD for the +// cdev FD received with the request, and install the fresh iommufd FD backing +// them. The values in the migrated VmConfig are stale. validate_vfio_fds has +// already confirmed the ids match and the iommufd FD is present. +fn apply_vfio_fds_to_vm_config( + receive_data_migration: &VmReceiveMigrationData, + vm_config: &Arc>, +) { + let vfio_fds = receive_data_migration + .vfio_fds + .as_deref() + .unwrap_or_default(); + if vfio_fds.is_empty() { + return; + } + + let mut config = vm_config.lock().unwrap(); + if let Some(devices) = config.devices.as_mut() { + for v in vfio_fds { + for device in devices.iter_mut() { + if device.pci_common.id.as_deref() == Some(v.id.as_str()) { + device.path = None; + device.fd = v.fd; + } + } + } + } + + let iommufd_fd = receive_data_migration + .iommufd_fd + .expect("receive-migration validated an iommufd FD accompanies vfio_fds"); + config + .platform + .as_mut() + .expect("receive-migration validated iommufd=on, so a platform exists") + .iommufd_fd = Some(iommufd_fd); +} + impl RequestHandler for Vmm { fn vm_create(&mut self, config: Box) -> result::Result<(), VmError> { match &self.vm { @@ -3299,12 +3347,13 @@ mod unit_tests { use arch::CpuProfile; use super::*; + use crate::config::RestoredVfioConfig; #[cfg(target_arch = "x86_64")] use crate::vm_config::DebugConsoleConfig; use crate::vm_config::{ CommonConsoleConfig, ConsoleConfig, ConsoleOutputMode, CoreScheduling, CpuFeatures, - CpusConfig, HotplugMethod, MemoryConfig, PayloadConfig, PciDeviceCommonConfig, RngConfig, - SerialConfig, + CpusConfig, DeviceConfig, HotplugMethod, MemoryConfig, PayloadConfig, + PciDeviceCommonConfig, PlatformConfig, RngConfig, SerialConfig, }; fn create_dummy_vmm() -> Vmm { @@ -3843,4 +3892,140 @@ mod unit_tests { vsock_config ); } + + fn vm_config_with_vfio_devices(ids: &[&str], iommufd: bool) -> Arc> { + let mut config = *create_dummy_vm_config(); + let platform = if iommufd { "iommufd=on" } else { "iommufd=off" }; + config.platform = Some(PlatformConfig::parse(platform).unwrap()); + config.devices = Some( + ids.iter() + .map(|id| DeviceConfig { + pci_common: PciDeviceCommonConfig { + id: Some((*id).to_owned()), + ..Default::default() + }, + path: Some(PathBuf::from(format!("/sys/bus/pci/devices/{id}"))), + fd: None, + x_nv_gpudirect_clique: None, + x_exclude_mmap_bars: Vec::new(), + }) + .collect(), + ); + Arc::new(Mutex::new(config)) + } + + fn receive_data( + vfio_fds: Option>, + iommufd_fd: Option, + ) -> VmReceiveMigrationData { + VmReceiveMigrationData { + receiver_url: "tcp:127.0.0.1:4321".to_string(), + tls_dir: None, + memory_mode: MigrationMode::default(), + vfio_fds, + iommufd_fd, + } + } + + fn vfio_fd(id: &str, fd: i32) -> RestoredVfioConfig { + RestoredVfioConfig { + id: id.to_owned(), + fd: Some(fd), + } + } + + #[test] + fn test_apply_vfio_fds_empty_is_noop() { + let vm_config = vm_config_with_vfio_devices(&["vfio0"], true); + apply_vfio_fds_to_vm_config(&receive_data(None, None), &vm_config); + let config = vm_config.lock().unwrap(); + let device = &config.devices.as_ref().unwrap()[0]; + assert!(device.path.is_some()); + assert!(device.fd.is_none()); + } + + #[test] + fn test_apply_vfio_fds_swaps_path_for_fd() { + let vm_config = vm_config_with_vfio_devices(&["vfio0", "vfio1"], true); + let data = receive_data(Some(vec![vfio_fd("vfio1", 5)]), Some(6)); + apply_vfio_fds_to_vm_config(&data, &vm_config); + let config = vm_config.lock().unwrap(); + let devices = config.devices.as_ref().unwrap(); + assert!(devices[0].path.is_some()); + assert!(devices[0].fd.is_none()); + assert!(devices[1].path.is_none()); + assert_eq!(devices[1].fd, Some(5)); + assert_eq!(config.platform.as_ref().unwrap().iommufd_fd, Some(6)); + } + + #[test] + fn test_validate_vfio_fds_requires_iommufd_fd() { + let vm_config = vm_config_with_vfio_devices(&["vfio0"], true); + let data = receive_data(Some(vec![vfio_fd("vfio0", 5)]), None); + data.validate_vfio_fds(&vm_config.lock().unwrap()) + .unwrap_err(); + } + + #[test] + fn test_validate_vfio_fds_unknown_id_fails() { + // vfio0 is covered so coverage passes, the extra bogus id is what the + // id match must reject. + let vm_config = vm_config_with_vfio_devices(&["vfio0"], true); + let data = receive_data( + Some(vec![vfio_fd("vfio0", 5), vfio_fd("missing", 6)]), + Some(7), + ); + data.validate_vfio_fds(&vm_config.lock().unwrap()) + .unwrap_err(); + } + + #[test] + fn test_validate_vfio_fds_duplicate_id_fails() { + let vm_config = vm_config_with_vfio_devices(&["vfio0"], true); + let data = receive_data( + Some(vec![vfio_fd("vfio0", 5), vfio_fd("vfio0", 6)]), + Some(7), + ); + data.validate_vfio_fds(&vm_config.lock().unwrap()) + .unwrap_err(); + } + + #[test] + fn test_validate_vfio_fds_requires_iommufd_backend() { + let vm_config = vm_config_with_vfio_devices(&["vfio0"], false); + let data = receive_data(Some(vec![vfio_fd("vfio0", 5)]), Some(6)); + data.validate_vfio_fds(&vm_config.lock().unwrap()) + .unwrap_err(); + } + + #[test] + fn test_validate_vfio_fds_stale_fd_needs_replacement() { + let vm_config = vm_config_with_vfio_devices(&["vfio0"], true); + { + let mut config = vm_config.lock().unwrap(); + let device = &mut config.devices.as_mut().unwrap()[0]; + device.path = None; + device.fd = Some(-1); + } + receive_data(None, None) + .validate_vfio_fds(&vm_config.lock().unwrap()) + .unwrap_err(); + } + + #[test] + fn test_validate_vfio_fds_path_based_needs_replacement() { + // A path based device carries its source path, which is unusable on + // the destination, so it must be replaced in vfio_fds as well. + let vm_config = vm_config_with_vfio_devices(&["vfio0"], true); + receive_data(None, None) + .validate_vfio_fds(&vm_config.lock().unwrap()) + .unwrap_err(); + } + + #[test] + fn test_validate_vfio_fds_all_substituted_ok() { + let vm_config = vm_config_with_vfio_devices(&["vfio0"], true); + let data = receive_data(Some(vec![vfio_fd("vfio0", 5)]), Some(6)); + data.validate_vfio_fds(&vm_config.lock().unwrap()).unwrap(); + } }