Skip to content

Commit 792ff49

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 1553fdf commit 792ff49

File tree

7 files changed

+223
-49
lines changed

7 files changed

+223
-49
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: 137 additions & 35 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,81 @@ 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+
.unwrap_or_else(|_| {
3167+
panic!(
3168+
"should have detected earlier that FDs {:?} can't be added: preserved: {:?}",
3169+
fds.clone().into_iter().collect::<Vec<_>>().len(),
3170+
self.preserved_fds
3171+
)
3172+
});
31393173

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

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

31513219
#[cfg(feature = "tdx")]
@@ -3188,18 +3256,10 @@ impl Clone for VmConfig {
31883256
tpm: self.tpm.clone(),
31893257
preserved_fds: self
31903258
.preserved_fds
3191-
.as_ref()
3259+
.iter()
31923260
// 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-
}),
3261+
.map(|fd| fd.dup())
3262+
.collect(),
32033263
landlock_rules: self.landlock_rules.clone(),
32043264
#[cfg(feature = "ivshmem")]
32053265
ivshmem: self.ivshmem.clone(),
@@ -3210,12 +3270,13 @@ impl Clone for VmConfig {
32103270

32113271
impl Drop for VmConfig {
32123272
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-
}
3273+
debug!("Closing preserved FDs: fds={:?}", self.preserved_fds);
3274+
3275+
// There is no .drain() for BTreeSet yet
3276+
let fds = mem::take(&mut self.preserved_fds);
3277+
for fd in fds {
3278+
// SAFETY: FFI call with valid FDs
3279+
unsafe { libc::close(fd.as_raw_fd()) };
32193280
}
32203281
}
32213282
}
@@ -3994,7 +4055,7 @@ mod tests {
39944055
pci_segments: None,
39954056
platform: None,
39964057
tpm: None,
3997-
preserved_fds: None,
4058+
preserved_fds: BTreeSet::new(),
39984059
net: Some(vec![
39994060
NetConfig {
40004061
id: Some("net0".to_owned()),
@@ -4207,7 +4268,7 @@ mod tests {
42074268
pci_segments: None,
42084269
platform: None,
42094270
tpm: None,
4210-
preserved_fds: None,
4271+
preserved_fds: BTreeSet::new(),
42114272
landlock_enable: false,
42124273
landlock_rules: None,
42134274
#[cfg(feature = "ivshmem")]
@@ -4834,12 +4895,53 @@ mod tests {
48344895
let fd1 = unsafe { libc::dup(File::open("/dev/null").unwrap().as_raw_fd()) };
48354896
// SAFETY: Safe as the file was just opened
48364897
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-
}
4898+
still_valid_config
4899+
.add_preserved_fds([fd1, fd2], false)
4900+
.unwrap();
48414901
let _still_valid_config = still_valid_config.clone();
48424902
}
4903+
4904+
#[test]
4905+
fn test_add_preserved_fds_corner_cases() {
4906+
let mut config = valid_vmconfig();
4907+
4908+
config.can_add_preserved_fds([3], false).unwrap();
4909+
config.add_preserved_fds([3], false).unwrap();
4910+
4911+
config.can_add_preserved_fds([3], false).unwrap_err();
4912+
4913+
config.can_add_preserved_fds([3], true).unwrap();
4914+
config.add_preserved_fds([3], true).unwrap();
4915+
4916+
config.can_add_preserved_fds([3], true).unwrap_err();
4917+
}
4918+
4919+
#[test]
4920+
#[should_panic]
4921+
fn test_add_preserved_fds_corner_cases_panic_cold_cold() {
4922+
let mut config = valid_vmconfig();
4923+
4924+
config.add_preserved_fds([3], false).unwrap();
4925+
4926+
// panic here
4927+
config
4928+
.add_preserved_fds([3], false)
4929+
.expect_err("should fail as the device is already hot");
4930+
}
4931+
4932+
#[test]
4933+
#[should_panic]
4934+
fn test_add_preserved_fds_corner_cases_panic_hot_hot() {
4935+
let mut config = valid_vmconfig();
4936+
4937+
config.add_preserved_fds([3], true).unwrap();
4938+
4939+
// panic here
4940+
config
4941+
.add_preserved_fds([3], true)
4942+
.expect_err("should fail as the device is already hot");
4943+
}
4944+
48434945
#[test]
48444946
fn test_landlock_parsing() -> Result<()> {
48454947
// 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(VmError::ConfigValidation)?;
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)