From 59a6563a51b640da9eb8315ec89c4bc5b198dc2c Mon Sep 17 00:00:00 2001 From: Saravanan D Date: Sat, 4 Jul 2026 08:29:46 +0000 Subject: [PATCH] vmm, ch-remote: Allow VFIO fd substitution at receive-migration The VmConfig that arrives over the migration stream carries the source's device paths and stale FDs, none of which are usable on the destination. A received VFIO device therefore needs fresh descriptors supplied with the request. VmReceiveMigrationData gains vfio_fds, pairing each device id with a cdev FD, and iommufd_fd for the backing iommufd, both arriving over SCM_RIGHTS. Once the received VmConfig is available, each named device's path or FD is swapped for the received one and the iommufd is installed before the VM is built, so the device comes up FD backed. The request is rejected when a substituted device lacks the iommufd backend, when an id is unknown or repeated, or when a device names no replacement in vfio_fds. These checks run against the migrated VmConfig once it has been received. ch-remote gains the vfio_fds and iommufd_fd options and forwards the descriptors over SCM_RIGHTS. The D-Bus transport cannot carry file descriptors and drops them. Signed-off-by: Saravanan D --- cloud-hypervisor/src/bin/ch-remote.rs | 24 +++- vmm/src/api/http/http_endpoint.rs | 53 ++++++- vmm/src/api/mod.rs | 135 +++++++++++++++++- vmm/src/config.rs | 2 +- vmm/src/lib.rs | 195 +++++++++++++++++++++++++- 5 files changed, 390 insertions(+), 19 deletions(-) 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(); + } }