Skip to content

Commit 8f88c6e

Browse files
committed
vmm: preserved FDs: prevent usage of same FD multiple times
Allowing the same file descriptor (FD) to be preserved more than once can lead to severe issues. Consider the case where management software adds two virtio-net devices at runtime, both backed by the same externally provided FD. When the first device is removed during runtime, the FD for the second device becomes invalid [0]. To avoid misconfigurations, we now prevent multiple preservation attempts of the same FD. This however has some caveats: In the current model, external FDs are added to the list of preserved FDs after the corresponding device has been created successfully. Now assume the following: - VM boots - VM config has no preserved FDs - We add a device to the VM that uses an external FD for its Tap dev - The device is successfully initialized - VM config is extended with the preserved FDs - VM reboots - VM config already has preserved FDs - Same as above - CRASH Therefore, we need to know for every preserved FD whether it is "cold", i.e., only has been part of a former VmConfig without associated device, or "hot", i.e., it is currently also actively in use. This allows state transitions from cold->hot in which case we won't throw an error for a reused FD. Otherwise, we throw errors. This requires that on shutdown, all preserved FDs are marked as cold. [0] cloud-hypervisor#7371 Signed-off-by: Philipp Schuster <philipp.schuster@cyberus-technology.de> On-behalf-of: SAP philipp.schuster@sap.com
1 parent a16080d commit 8f88c6e

File tree

7 files changed

+221
-50
lines changed

7 files changed

+221
-50
lines changed

fuzz/fuzz_targets/http_api.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
// SPDX-License-Identifier: Apache-2.0
44

55
#![no_main]
6+
use std::collections::BTreeSet;
67
use std::os::unix::io::AsRawFd;
78
use std::path::PathBuf;
89
use std::sync::mpsc::{channel, Receiver};
@@ -192,7 +193,7 @@ impl RequestHandler for StubApiRequestHandler {
192193
pci_segments: None,
193194
platform: None,
194195
tpm: None,
195-
preserved_fds: None,
196+
preserved_fds: BTreeSet::new(),
196197
landlock_enable: false,
197198
landlock_rules: None,
198199
#[cfg(feature = "ivshmem")]

src/main.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -904,6 +904,7 @@ fn main() {
904904

905905
#[cfg(test)]
906906
mod unit_tests {
907+
use std::collections::BTreeSet;
907908
use std::path::PathBuf;
908909

909910
use vmm::config::VmParams;
@@ -1028,7 +1029,7 @@ mod unit_tests {
10281029
pci_segments: None,
10291030
platform: None,
10301031
tpm: None,
1031-
preserved_fds: None,
1032+
preserved_fds: BTreeSet::new(),
10321033
landlock_enable: false,
10331034
landlock_rules: None,
10341035
#[cfg(feature = "ivshmem")]

vmm/src/config.rs

Lines changed: 135 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,10 @@
66
use std::collections::{BTreeSet, HashMap};
77
#[cfg(feature = "ivshmem")]
88
use std::fs;
9-
use std::os::fd::RawFd;
9+
use std::os::fd::{AsRawFd, RawFd};
1010
use std::path::PathBuf;
11-
use std::result;
1211
use std::str::FromStr;
12+
use std::{mem, result};
1313

1414
use clap::ArgMatches;
1515
use option_parser::{
@@ -361,6 +361,14 @@ pub enum ValidationError {
361361
InvalidIvshmemPath,
362362
#[error("Payload configuration is not bootable")]
363363
PayloadError(#[from] PayloadConfigError),
364+
/// Multiple things in a VM can be backed up by externally provided FDs,
365+
/// such as memory zones, virtio-net devices, or virtio-blk devices (not
366+
/// yet implemented, but likely in future).
367+
///
368+
/// It is a hard configuration error to use the same FD for multiple
369+
/// things.
370+
#[error("Externally provided FD already in use! FD={0}")]
371+
ExternalFdAlreadyInUse(RawFd),
364372
}
365373

366374
type ValidationResult<T> = std::result::Result<T, ValidationError>;
@@ -3058,7 +3066,7 @@ impl VmConfig {
30583066
pci_segments,
30593067
platform,
30603068
tpm,
3061-
preserved_fds: None,
3069+
preserved_fds: BTreeSet::new(),
30623070
landlock_enable: vm_params.landlock_enable,
30633071
landlock_rules,
30643072
#[cfg(feature = "ivshmem")]
@@ -3131,21 +3139,78 @@ impl VmConfig {
31313139
removed
31323140
}
31333141

3142+
/// Adds the provided external FDs to the preserved FDs.
3143+
///
3144+
/// This is useful to keep external FDs valid across VM reboots, where no
3145+
/// device instance holds open FDs.
3146+
///
31343147
/// # Safety
31353148
/// To use this safely, the caller must guarantee that the input
3136-
/// fds are all valid.
3137-
pub unsafe fn add_preserved_fds(&mut self, mut fds: Vec<i32>) {
3138-
debug!("adding preserved FDs to VM list: {fds:?}");
3149+
/// FDs are all valid.
3150+
///
3151+
/// # Arguments
3152+
/// - `fds`: Iterator over the external FDs of some component
3153+
/// - `hot`: Whether this happens when the VM is running, i.e., whether
3154+
/// device instances also have open (dup'ed) FDs of this or just the VM
3155+
/// config.
3156+
///
3157+
/// # Panics
3158+
/// This panics when an external FD is used multiple times to force
3159+
/// programmers to do proper checking early.
3160+
pub fn add_preserved_fds(
3161+
&mut self,
3162+
fds: impl IntoIterator<Item = RawFd> + Clone,
3163+
hot: bool,
3164+
) -> ValidationResult<()> {
3165+
self.can_add_preserved_fds(fds.clone(), hot)
3166+
.expect(&format!(
3167+
"should have detected earlier that FDs {:?} can't be added: preserved: {:?}",
3168+
fds.clone().into_iter().collect::<Vec<_>>().len(),
3169+
self.preserved_fds
3170+
));
31393171

3140-
if fds.is_empty() {
3141-
return;
3172+
for fd in fds.into_iter() {
3173+
let pfd = self.preserved_fds.iter().find(|pfd| pfd.as_raw_fd() == fd);
3174+
match (pfd, hot) {
3175+
(None, false) => {
3176+
self.preserved_fds.insert(PreservedFd::Cold(fd));
3177+
}
3178+
(None, true) => {
3179+
self.preserved_fds.insert(PreservedFd::Hot(fd));
3180+
}
3181+
// We allow the transition from Cold to Hot, i.e., "in use"
3182+
(Some(PreservedFd::Cold(_)), true) => {
3183+
self.preserved_fds.remove(&PreservedFd::Cold(fd));
3184+
self.preserved_fds.insert(PreservedFd::Hot(fd));
3185+
}
3186+
// Invalid state transition, propagate error
3187+
(Some(_), _) => return Err(ValidationError::ExternalFdAlreadyInUse(fd)),
3188+
}
31423189
}
3190+
Ok(())
3191+
}
31433192

3144-
if let Some(preserved_fds) = &self.preserved_fds {
3145-
fds.append(&mut preserved_fds.clone());
3193+
/// Checks if any of the externally provided FDs is already preserved in the
3194+
/// list of preserved FDs.
3195+
///
3196+
/// This function is supposed to be called early in every code path that adds
3197+
/// configurations tide to such FDs.
3198+
pub fn can_add_preserved_fds(
3199+
&self,
3200+
external_fds: impl IntoIterator<Item = RawFd>,
3201+
hot: bool,
3202+
) -> ValidationResult<()> {
3203+
for fd in external_fds.into_iter() {
3204+
let pfd = self.preserved_fds.iter().find(|pfd| pfd.as_raw_fd() == fd);
3205+
return match (pfd, hot) {
3206+
(None, _) => Ok(()),
3207+
// We allow the transition from Cold to Hot, i.e., "in use now"
3208+
(Some(PreservedFd::Cold(_)), true) => Ok(()),
3209+
// Invalid state transition, propagate error
3210+
(Some(_), _) => Err(ValidationError::ExternalFdAlreadyInUse(fd)),
3211+
};
31463212
}
3147-
3148-
self.preserved_fds = Some(fds);
3213+
Ok(())
31493214
}
31503215

31513216
#[cfg(feature = "tdx")]
@@ -3188,18 +3253,10 @@ impl Clone for VmConfig {
31883253
tpm: self.tpm.clone(),
31893254
preserved_fds: self
31903255
.preserved_fds
3191-
.as_ref()
3256+
.iter()
31923257
// SAFETY: FFI call with valid FDs
3193-
.map(|fds| {
3194-
fds.iter()
3195-
.map(|fd| {
3196-
// SAFETY: Trivially safe.
3197-
let fd_duped = unsafe { libc::dup(*fd) };
3198-
warn!("Cloning VM config: duping preserved FD {fd} => {fd_duped}");
3199-
fd_duped
3200-
})
3201-
.collect()
3202-
}),
3258+
.map(|fd| fd.dup())
3259+
.collect(),
32033260
landlock_rules: self.landlock_rules.clone(),
32043261
#[cfg(feature = "ivshmem")]
32053262
ivshmem: self.ivshmem.clone(),
@@ -3210,12 +3267,13 @@ impl Clone for VmConfig {
32103267

32113268
impl Drop for VmConfig {
32123269
fn drop(&mut self) {
3213-
if let Some(mut fds) = self.preserved_fds.take() {
3214-
debug!("Closing preserved FDs from VM: fds={fds:?}");
3215-
for fd in fds.drain(..) {
3216-
// SAFETY: FFI call with valid FDs
3217-
unsafe { libc::close(fd) };
3218-
}
3270+
debug!("Closing preserved FDs: fds={:?}", self.preserved_fds);
3271+
3272+
// There is no .drain() for BTreeSet yet
3273+
let fds = mem::take(&mut self.preserved_fds);
3274+
for fd in fds {
3275+
// SAFETY: FFI call with valid FDs
3276+
unsafe { libc::close(fd.as_raw_fd()) };
32193277
}
32203278
}
32213279
}
@@ -3994,7 +4052,7 @@ mod tests {
39944052
pci_segments: None,
39954053
platform: None,
39964054
tpm: None,
3997-
preserved_fds: None,
4055+
preserved_fds: BTreeSet::new(),
39984056
net: Some(vec![
39994057
NetConfig {
40004058
id: Some("net0".to_owned()),
@@ -4207,7 +4265,7 @@ mod tests {
42074265
pci_segments: None,
42084266
platform: None,
42094267
tpm: None,
4210-
preserved_fds: None,
4268+
preserved_fds: BTreeSet::new(),
42114269
landlock_enable: false,
42124270
landlock_rules: None,
42134271
#[cfg(feature = "ivshmem")]
@@ -4829,17 +4887,58 @@ mod tests {
48294887
config_with_invalid_host_data.validate().unwrap_err();
48304888
}
48314889

4832-
let mut still_valid_config = valid_config;
4890+
let mut still_valid_config = valid_vmconfig();
48334891
// SAFETY: Safe as the file was just opened
48344892
let fd1 = unsafe { libc::dup(File::open("/dev/null").unwrap().as_raw_fd()) };
48354893
// SAFETY: Safe as the file was just opened
48364894
let fd2 = unsafe { libc::dup(File::open("/dev/null").unwrap().as_raw_fd()) };
4837-
// SAFETY: safe as both FDs are valid
4838-
unsafe {
4839-
still_valid_config.add_preserved_fds(vec![fd1, fd2]);
4840-
}
4895+
still_valid_config
4896+
.add_preserved_fds([fd1, fd2], false)
4897+
.unwrap();
48414898
let _still_valid_config = still_valid_config.clone();
48424899
}
4900+
4901+
#[test]
4902+
fn test_add_preserved_fds_corner_cases() {
4903+
let mut config = valid_vmconfig();
4904+
4905+
config.can_add_preserved_fds([3], false).unwrap();
4906+
config.add_preserved_fds([3], false).unwrap();
4907+
4908+
config.can_add_preserved_fds([3], false).unwrap_err();
4909+
4910+
config.can_add_preserved_fds([3], true).unwrap();
4911+
config.add_preserved_fds([3], true).unwrap();
4912+
4913+
config.can_add_preserved_fds([3], true).unwrap_err();
4914+
}
4915+
4916+
#[test]
4917+
#[should_panic]
4918+
fn test_add_preserved_fds_corner_cases_panic_cold_cold() {
4919+
let mut config = valid_vmconfig();
4920+
4921+
config.add_preserved_fds([3], false).unwrap();
4922+
4923+
// panic here
4924+
config
4925+
.add_preserved_fds([3], false)
4926+
.expect_err("should fail as the device is already hot");
4927+
}
4928+
4929+
#[test]
4930+
#[should_panic]
4931+
fn test_add_preserved_fds_corner_cases_panic_hot_hot() {
4932+
let mut config = valid_vmconfig();
4933+
4934+
config.add_preserved_fds([3], true).unwrap();
4935+
4936+
// panic here
4937+
config
4938+
.add_preserved_fds([3], true)
4939+
.expect_err("should fail as the device is already hot");
4940+
}
4941+
48434942
#[test]
48444943
fn test_landlock_parsing() -> Result<()> {
48454944
// should not be empty

vmm/src/device_manager.rs

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,7 @@ use crate::vm_config::{
124124
DeviceConfig, DiskConfig, FsConfig, NetConfig, PmemConfig, UserDeviceConfig, VdpaConfig,
125125
VhostMode, VmConfig, VsockConfig,
126126
};
127-
use crate::{DEVICE_MANAGER_SNAPSHOT_ID, GuestRegionMmap, PciDeviceInfo, device_node};
127+
use crate::{DEVICE_MANAGER_SNAPSHOT_ID, GuestRegionMmap, PciDeviceInfo, config, device_node};
128128

129129
#[cfg(any(target_arch = "aarch64", target_arch = "riscv64"))]
130130
const MMIO_LEN: u64 = 0x1000;
@@ -667,6 +667,9 @@ pub enum DeviceManagerError {
667667
/// Error adding fw_cfg to bus.
668668
#[error("Error adding fw_cfg to bus")]
669669
ErrorAddingFwCfgToBus(#[source] vm_device::BusError),
670+
671+
#[error("Multiple use of the same FD is misconfiguration")]
672+
ExternalFdMisconfiguration(#[source] config::ValidationError),
670673
}
671674

672675
pub type DeviceManagerResult<T> = result::Result<T, DeviceManagerError>;
@@ -2955,10 +2958,11 @@ impl DeviceManager {
29552958
)
29562959
.map_err(DeviceManagerError::CreateVirtioNet)?;
29572960

2958-
// SAFETY: 'fds' are valid because TAP devices are created successfully
2959-
unsafe {
2960-
self.config.lock().unwrap().add_preserved_fds(fds.clone());
2961-
}
2961+
self.config
2962+
.lock()
2963+
.unwrap()
2964+
.add_preserved_fds(fds.iter().cloned(), true)
2965+
.map_err(DeviceManagerError::ExternalFdMisconfiguration)?;
29622966

29632967
Arc::new(Mutex::new(net))
29642968
} else {
@@ -4542,8 +4546,8 @@ impl DeviceManager {
45424546

45434547
debug!("Closing preserved FDs from virtio-net device: id={id}, fds={fds:?}");
45444548
for fd in fds {
4545-
config.preserved_fds.as_mut().unwrap().retain(|x| *x != fd);
4546-
// SAFETY: Trivially safe. We know the FD is not referenced any longer.
4549+
config.preserved_fds.retain(|x| x.as_raw_fd() != fd);
4550+
// SAFETY: We are closing the only remaining instance of this FD.
45474551
unsafe {
45484552
libc::close(fd);
45494553
}

vmm/src/lib.rs

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2253,10 +2253,17 @@ impl RequestHandler for Vmm {
22532253

22542254
fn vm_add_net(&mut self, net_cfg: NetConfig) -> result::Result<Option<Vec<u8>>, VmError> {
22552255
self.vm_config.as_ref().ok_or(VmError::VmNotCreated)?;
2256-
22572256
{
22582257
// Validate the configuration change in a cloned configuration
22592258
let mut config = self.vm_config.as_ref().unwrap().lock().unwrap().clone();
2259+
2260+
// Special handling for externally provided FDs
2261+
if let Some(fds) = &net_cfg.fds {
2262+
config
2263+
.can_add_preserved_fds(fds.iter().cloned(), true)
2264+
.map_err(|a| VmError::ConfigValidation(a))?;
2265+
}
2266+
22602267
add_to_config(&mut config.net, net_cfg.clone());
22612268
config.validate().map_err(VmError::ConfigValidation)?;
22622269
}
@@ -2593,6 +2600,8 @@ const DEVICE_MANAGER_SNAPSHOT_ID: &str = "device-manager";
25932600

25942601
#[cfg(test)]
25952602
mod unit_tests {
2603+
use std::collections::BTreeSet;
2604+
25962605
use super::*;
25972606
#[cfg(target_arch = "x86_64")]
25982607
use crate::vm_config::DebugConsoleConfig;
@@ -2693,7 +2702,7 @@ mod unit_tests {
26932702
pci_segments: None,
26942703
platform: None,
26952704
tpm: None,
2696-
preserved_fds: None,
2705+
preserved_fds: BTreeSet::new(),
26972706
landlock_enable: false,
26982707
landlock_rules: None,
26992708
#[cfg(feature = "ivshmem")]

0 commit comments

Comments
 (0)