vmm, ch-remote: Allow VFIO fd substitution at restore

A VFIO device restored onto a different host has a device path and
iommufd that are meaningless there, and an FD backed device cannot
serialize a live descriptor into the snapshot at all. Restoring one
therefore needs fresh descriptors supplied with the request.

RestoreConfig gains vfio_fds, pairing each device id with a cdev FD,
and iommufd_fd for the backing iommufd. Both arrive over SCM_RIGHTS on
the restore request. vm_restore swaps each named device's stale path
or FD for the received one and installs the iommufd 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 an FD backed device
names no replacement.

ch-remote gains the vfio_fds and iommufd_fd options and forwards the
descriptors through the SCM_RIGHTS pool.

Signed-off-by: Saravanan D <saravanand@crusoe.ai>
This commit is contained in:
Saravanan D
2026-07-04 08:11:29 +00:00
committed by Bo Chen
parent c3c4281069
commit 2214dceb07
4 changed files with 358 additions and 6 deletions

View File

@@ -933,14 +933,22 @@ fn snapshot_config(url: &str) -> String {
fn restore_config(config: &str) -> Result<(String, Vec<i32>), Error> {
let mut restore_config = RestoreConfig::parse(config).map_err(Error::Restore)?;
// RestoreConfig is modified on purpose to take out the file descriptors.
// These fds are passed to the server side process via SCM_RIGHTS
let fds = match &mut restore_config.net_fds {
// These fds are passed to the server side process via SCM_RIGHTS, in the
// order net_fds, then vfio_fds, then the iommufd FD, matching the
// server's split.
let mut fds: Vec<i32> = match &mut restore_config.net_fds {
Some(net_fds) => net_fds
.iter_mut()
.flat_map(|net| net.fds.take().unwrap_or_default())
.collect(),
None => Vec::new(),
};
if let Some(vfio_fds) = restore_config.vfio_fds.as_mut() {
fds.extend(vfio_fds.iter_mut().filter_map(|v| v.fd.take()));
}
if let Some(iommufd_fd) = restore_config.iommufd_fd.take() {
fds.push(iommufd_fd);
}
let restore_config = serde_json::to_string(&restore_config).unwrap();
Ok((restore_config, fds))

View File

@@ -35,9 +35,11 @@
//! [special HTTP library]: https://github.com/firecracker-microvm/micro-http
use std::fs::File;
use std::os::fd::IntoRawFd;
use std::result;
use std::sync::mpsc::Sender;
use log::error;
use micro_http::{Body, Method, Request, Response, StatusCode, Version};
use vmm_sys_util::eventfd::EventFd;
@@ -120,7 +122,7 @@ mod fds_helper {
use std::slice::from_ref;
use super::{ConfigWithFDs, ConfigWithVariableFDs};
use crate::config::RestoredNetConfig;
use crate::config::{RestoredNetConfig, RestoredVfioConfig};
use crate::vm_config::{DeviceConfig, NetConfig};
impl ConfigWithFDs for NetConfig {
@@ -174,6 +176,26 @@ mod fds_helper {
self.num_fds
}
}
impl ConfigWithFDs for RestoredVfioConfig {
fn id(&self) -> Option<&str> {
Some(self.id.as_str())
}
fn fds_from_http_body(&self) -> Option<&[RawFd]> {
self.fd.as_ref().map(from_ref)
}
fn set_fds(&mut self, fds: Option<Vec<RawFd>>) {
self.fd = fds.and_then(|mut v| v.pop());
}
}
impl ConfigWithVariableFDs for RestoredVfioConfig {
fn expected_num_fds(&self) -> usize {
1
}
}
}
fn attach_fds_to_cfg_inner<T: ConfigWithFDs>(
@@ -561,10 +583,37 @@ impl PutHandler for VmRestore {
if let Some(body) = body {
let mut restore_cfg: RestoreConfig = serde_json::from_slice(body.raw())?;
let net_total: usize = restore_cfg
.net_fds
.iter()
.flatten()
.map(|c| c.num_fds)
.sum();
let vfio_total = restore_cfg.vfio_fds.as_ref().map_or(0, |c| c.len());
let iommufd_total = usize::from(restore_cfg.vfio_fds.is_some());
let expected = net_total + vfio_total + iommufd_total;
if files.len() != expected {
error!(
"Expected {expected} FDs in VmRestore request, received {}",
files.len()
);
return Err(HttpError::BadRequest);
}
// Split in the order net_fds, then vfio_fds, then the iommufd FD.
let mut files = files;
if let Some(cfgs) = restore_cfg.net_fds.as_mut() {
let net_files: Vec<File> = files.drain(..net_total).collect();
let mut cfgs = cfgs.iter_mut().collect::<Vec<&mut _>>();
let cfgs = cfgs.as_mut_slice();
attach_fds_to_cfgs(files, cfgs)?;
attach_fds_to_cfgs(net_files, cfgs.as_mut_slice())?;
}
if let Some(cfgs) = restore_cfg.vfio_fds.as_mut() {
let vfio_files: Vec<File> = files.drain(..vfio_total).collect();
let mut cfgs = cfgs.iter_mut().collect::<Vec<&mut _>>();
attach_fds_to_cfgs(vfio_files, cfgs.as_mut_slice())?;
}
if restore_cfg.vfio_fds.is_some() {
restore_cfg.iommufd_fd = Some(files.remove(0).into_raw_fd());
}
self.send(api_notifier, api_sender, restore_cfg)

View File

@@ -390,6 +390,12 @@ pub enum ValidationError {
/// Number of FDs passed during Restore are incorrect to the NetConfig
#[error("Number of Net FDs passed for '{0}' during Restore: {1}. Expected: {2}")]
RestoreNetFdCountMismatch(String, usize, usize),
/// vfio_fds entry does not match any DeviceConfig.id in the snapshot
#[error("VFIO device id '{0}' in 'vfio_fds' does not match any device in the snapshot")]
RestoreUnknownVfioId(String),
/// A device saved with an FD has no replacement FD for the restore
#[error("VFIO device '{0}' was FD backed and needs a new fd in 'vfio_fds'")]
RestoreMissingVfioFd(String),
/// Prefault cannot be combined with on-demand restore
#[error("'prefault' cannot be combined with 'memory_restore_mode=ondemand'")]
InvalidRestorePrefaultWithOnDemand,
@@ -2802,6 +2808,26 @@ impl FromStr for MemoryRestoreMode {
}
}
#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize, Default)]
pub struct RestoredVfioConfig {
pub id: String,
// FDs are not serialized and any deserialized value is invalid; see NetConfig::fds.
#[serde(default, deserialize_with = "deserialize_restored_fd")]
pub fd: Option<i32>,
}
fn deserialize_restored_fd<'de, D>(d: D) -> result::Result<Option<i32>, D::Error>
where
D: serde::Deserializer<'de>,
{
let invalid_fd: Option<i32> = Option::deserialize(d)?;
if invalid_fd.is_some() {
Ok(Some(-1))
} else {
Ok(None)
}
}
#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize, Default)]
pub struct RestoreConfig {
pub source_url: PathBuf,
@@ -2812,18 +2838,29 @@ pub struct RestoreConfig {
#[serde(default)]
pub net_fds: Option<Vec<RestoredNetConfig>>,
#[serde(default)]
pub vfio_fds: Option<Vec<RestoredVfioConfig>>,
// FDs are not serialized and any deserialized value is invalid; see NetConfig::fds.
#[serde(default, deserialize_with = "deserialize_restored_fd")]
pub iommufd_fd: Option<i32>,
#[serde(default)]
pub resume: bool,
}
impl RestoreConfig {
pub const SYNTAX: &'static str = "Restore from a VM snapshot. \
\nRestore parameters \"source_url=<source_url>,prefault=on|off,memory_restore_mode=copy|ondemand,\
net_fds=<list_of_net_ids_with_their_associated_fds>,resume=true|false\" \
net_fds=<list_of_net_ids_with_their_associated_fds>,\
vfio_fds=<list_of_vfio_ids_with_their_associated_fd>,iommufd_fd=<fd>,resume=true|false\" \
\n`source_url` should be a valid URL (e.g file:///foo/bar or tcp://192.168.1.10/foo) \
\n`prefault` controls eager prefaulting for the copy-based restore path (disabled by default) \
\n`memory_restore_mode=copy` preserves the existing eager read-copy restore behavior, while `memory_restore_mode=ondemand` enables lazy demand paging and fails restore if userfaultfd support is unavailable \
\n`net_fds` is a list of net ids with new file descriptors. \
Only net devices backed by FDs directly are needed as input.\
\n`vfio_fds` is a list of VFIO device ids each paired with a new cdev file descriptor, \
e.g. vfio_fds=[vfio0@5,vfio1@6]. Use this to restore a VFIO device onto a different \
sysfs path or host. Requires `iommufd_fd`.\
\n`iommufd_fd` is a new iommufd file descriptor for the restored VM. \
The one saved in the snapshot does not survive serialization.\
\n `resume` controls whether the VM will be directly resumed after restore ";
pub fn parse(restore: &str) -> Result<Self> {
@@ -2833,6 +2870,8 @@ impl RestoreConfig {
.add("prefault")
.add("memory_restore_mode")
.add("net_fds")
.add("vfio_fds")
.add("iommufd_fd")
.add("resume");
parser.parse(restore).map_err(Error::ParseRestore)?;
@@ -2861,6 +2900,20 @@ impl RestoreConfig {
})
.collect()
});
let vfio_fds = parser
.convert::<TupleList<String, u64>>("vfio_fds")
.map_err(Error::ParseRestore)?
.map(|v| {
v.0.iter()
.map(|Tuple(id, fd)| RestoredVfioConfig {
id: id.clone(),
fd: Some(*fd as i32),
})
.collect()
});
let iommufd_fd = parser
.convert::<i32>("iommufd_fd")
.map_err(Error::ParseRestore)?;
let resume = parser
.convert::<Toggle>("resume")
.map_err(Error::ParseRestore)?
@@ -2872,6 +2925,8 @@ impl RestoreConfig {
prefault,
memory_restore_mode,
net_fds,
vfio_fds,
iommufd_fd,
resume,
})
}
@@ -2923,6 +2978,54 @@ impl RestoreConfig {
warn!("Ignoring unused 'net_fds' for VM restore.");
}
let vfio_fds = self.vfio_fds.as_deref().unwrap_or_default();
if !vfio_fds.is_empty() {
// Substituted devices become FD backed, which requires an
// externally supplied iommufd FD and the iommufd backend.
if self.iommufd_fd.is_none() {
return Err(ValidationError::VfioFdRequiresIommufdFd);
}
if !vm_config.platform.as_ref().is_some_and(|p| p.iommufd) {
return Err(ValidationError::IommufdFdRequiresIommufd);
}
let mut seen = HashSet::new();
for v in vfio_fds {
if !seen.insert(v.id.as_str()) {
return Err(ValidationError::IdentifierNotUnique(v.id.clone()));
}
}
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(ValidationError::RestoreUnknownVfioId(v.id.clone()));
}
}
}
// A device saved with an FD cannot reuse it, the snapshot carries no
// live descriptor. Each one needs a replacement in vfio_fds.
let substituted: HashSet<&str> = vfio_fds.iter().map(|v| v.id.as_str()).collect();
for d in vm_config.devices.iter().flatten() {
if d.fd.is_some()
&& !d
.pci_common
.id
.as_deref()
.is_some_and(|id| substituted.contains(id))
{
return Err(ValidationError::RestoreMissingVfioFd(
d.pci_common.id.clone().unwrap_or_default(),
));
}
}
Ok(())
}
}
@@ -5090,6 +5193,8 @@ id=\"{id}\",pci_segment={pci_segment},queue_sizes={queue_sizes}"
prefault: false,
memory_restore_mode: MemoryRestoreMode::Copy,
net_fds: None,
vfio_fds: None,
iommufd_fd: None,
resume: false,
}
);
@@ -5113,6 +5218,8 @@ id=\"{id}\",pci_segment={pci_segment},queue_sizes={queue_sizes}"
fds: Some(vec![5, 6, 7, 8]),
}
]),
vfio_fds: None,
iommufd_fd: None,
resume: false,
}
);
@@ -5123,6 +5230,8 @@ id=\"{id}\",pci_segment={pci_segment},queue_sizes={queue_sizes}"
prefault: false,
memory_restore_mode: MemoryRestoreMode::OnDemand,
net_fds: None,
vfio_fds: None,
iommufd_fd: None,
resume: false,
}
);
@@ -5133,9 +5242,34 @@ id=\"{id}\",pci_segment={pci_segment},queue_sizes={queue_sizes}"
prefault: false,
memory_restore_mode: MemoryRestoreMode::Copy,
net_fds: None,
vfio_fds: None,
iommufd_fd: None,
resume: true,
}
);
assert_eq!(
RestoreConfig::parse(
"source_url=/path/to/snapshot,vfio_fds=[vfio0@5,vfio1@6],iommufd_fd=7"
)?,
RestoreConfig {
source_url: PathBuf::from("/path/to/snapshot"),
prefault: false,
memory_restore_mode: MemoryRestoreMode::Copy,
net_fds: None,
vfio_fds: Some(vec![
RestoredVfioConfig {
id: "vfio0".to_string(),
fd: Some(5),
},
RestoredVfioConfig {
id: "vfio1".to_string(),
fd: Some(6),
},
]),
iommufd_fd: Some(7),
resume: false,
}
);
// Parsing should fail as source_url is a required field
RestoreConfig::parse("prefault=off").unwrap_err();
RestoreConfig::parse("source_url=/path/to/snapshot,memory_restore_mode=bogus").unwrap_err();
@@ -5245,6 +5379,8 @@ id=\"{id}\",pci_segment={pci_segment},queue_sizes={queue_sizes}"
fds: Some(vec![7, 8]),
},
]),
vfio_fds: None,
iommufd_fd: None,
resume: false,
};
valid_config.validate(&snapshot_vm_config).unwrap();
@@ -5310,6 +5446,8 @@ id=\"{id}\",pci_segment={pci_segment},queue_sizes={queue_sizes}"
prefault: false,
memory_restore_mode: MemoryRestoreMode::Copy,
net_fds: None,
vfio_fds: None,
iommufd_fd: None,
resume: false,
};
snapshot_vm_config.net = Some(vec![NetConfig {
@@ -5327,6 +5465,8 @@ id=\"{id}\",pci_segment={pci_segment},queue_sizes={queue_sizes}"
prefault: true,
memory_restore_mode: MemoryRestoreMode::OnDemand,
net_fds: None,
vfio_fds: None,
iommufd_fd: None,
resume: false,
};
assert_eq!(
@@ -5335,6 +5475,135 @@ id=\"{id}\",pci_segment={pci_segment},queue_sizes={queue_sizes}"
);
}
#[test]
fn test_restore_config_vfio_fds_validation() {
// interested in only VmConfig.devices and platform, so set rest to
// default values
let mut snapshot_vm_config = VmConfig {
cpus: CpusConfig::default(),
memory: MemoryConfig::default(),
payload: None,
rate_limit_groups: None,
disks: None,
rng: RngConfig::default(),
generic_vhost_user: None,
balloon: None,
fs: None,
pmem: None,
serial: SerialConfig::default(),
console: ConsoleConfig::default(),
#[cfg(target_arch = "x86_64")]
debug_console: DebugConsoleConfig::default(),
devices: Some(vec![DeviceConfig {
pci_common: PciDeviceCommonConfig {
id: Some("vfio0".to_owned()),
..Default::default()
},
path: Some(PathBuf::from("/sys/bus/pci/devices/0000:01:00.0")),
fd: None,
x_nv_gpudirect_clique: None,
x_exclude_mmap_bars: Vec::new(),
}]),
user_devices: None,
vdpa: None,
vsock: None,
#[cfg(feature = "pvmemcontrol")]
pvmemcontrol: None,
pvpanic: false,
iommu: false,
numa: None,
watchdog: false,
rtc: None,
#[cfg(feature = "guest_debug")]
gdb: false,
pci_segments: None,
platform: Some(PlatformConfig {
iommufd: true,
..platform_fixture()
}),
tpm: None,
preserved_fds: None,
net: None,
landlock_enable: false,
landlock_rules: None,
#[cfg(feature = "ivshmem")]
ivshmem: None,
};
let valid_config = RestoreConfig {
source_url: PathBuf::from("/path/to/snapshot"),
prefault: false,
memory_restore_mode: MemoryRestoreMode::Copy,
net_fds: None,
vfio_fds: Some(vec![RestoredVfioConfig {
id: "vfio0".to_string(),
fd: Some(5),
}]),
iommufd_fd: Some(6),
resume: false,
};
valid_config.validate(&snapshot_vm_config).unwrap();
let missing_iommufd = RestoreConfig {
iommufd_fd: None,
..valid_config.clone()
};
assert_eq!(
missing_iommufd.validate(&snapshot_vm_config),
Err(ValidationError::VfioFdRequiresIommufdFd)
);
let unknown_id = RestoreConfig {
vfio_fds: Some(vec![RestoredVfioConfig {
id: "missing".to_string(),
fd: Some(5),
}]),
..valid_config.clone()
};
assert_eq!(
unknown_id.validate(&snapshot_vm_config),
Err(ValidationError::RestoreUnknownVfioId("missing".to_string()))
);
let duplicate_id = RestoreConfig {
vfio_fds: Some(vec![
RestoredVfioConfig {
id: "vfio0".to_string(),
fd: Some(5),
},
RestoredVfioConfig {
id: "vfio0".to_string(),
fd: Some(6),
},
]),
..valid_config.clone()
};
assert_eq!(
duplicate_id.validate(&snapshot_vm_config),
Err(ValidationError::IdentifierNotUnique("vfio0".to_string()))
);
// A device saved with an FD must get a replacement FD.
snapshot_vm_config.devices.as_mut().unwrap()[0].path = None;
snapshot_vm_config.devices.as_mut().unwrap()[0].fd = Some(-1);
let no_substitution = RestoreConfig {
vfio_fds: None,
iommufd_fd: None,
..valid_config.clone()
};
assert_eq!(
no_substitution.validate(&snapshot_vm_config),
Err(ValidationError::RestoreMissingVfioFd("vfio0".to_string()))
);
// The iommufd backend must be enabled in the snapshot config.
snapshot_vm_config.platform = Some(platform_fixture());
assert_eq!(
valid_config.validate(&snapshot_vm_config),
Err(ValidationError::IommufdFdRequiresIommufd)
);
}
fn platform_fixture() -> PlatformConfig {
PlatformConfig {
num_pci_segments: MAX_NUM_PCI_SEGMENTS,

View File

@@ -2374,6 +2374,32 @@ impl RequestHandler for Vmm {
}
}
// Swap each VFIO device's saved path or stale FD for the cdev
// FD received with the restore request, and install the fresh
// iommufd FD backing them. The one saved in the snapshot is
// stale, and validate() has confirmed both accompany vfio_fds.
if let Some(restored_vfios) = restore_cfg.vfio_fds {
let mut config = vm_config.lock().unwrap();
if let Some(vm_device_configs) = config.devices.as_mut() {
for v in restored_vfios.iter() {
for device_config in vm_device_configs.iter_mut() {
if device_config.pci_common.id.as_ref() == Some(&v.id) {
device_config.path = None;
device_config.fd = v.fd;
}
}
}
}
let iommufd_fd = restore_cfg
.iommufd_fd
.expect("restore validated an iommufd FD accompanies vfio_fds");
config
.platform
.as_mut()
.expect("restore validated iommufd=on, so a platform exists")
.iommufd_fd = Some(iommufd_fd);
}
self.vm_restore(
source_url,
vm_config,