Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 55 additions & 3 deletions src/devices/src/virtio/fs/device.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ use vm_memory::{ByteValued, GuestMemoryMmap};

use super::super::{
ActivateResult, DeviceQueue, DeviceState, FsError, QueueConfig, VirtioDevice, VirtioShmRegion,
DEFAULT_MAX_FS_BACKEND_STATE_BYTES,
};
use super::dyn_filesystem::{DynFileSystem, DynFileSystemAdapter};
use super::filesystem::{FileSystem, FsOptions};
Expand Down Expand Up @@ -58,6 +59,7 @@ pub struct Fs {
shm_region: Option<VirtioShmRegion>,
backend: FsBackend,
session_options: u64,
max_backend_state_bytes: usize,
worker_thread: Option<JoinHandle<super::worker::FsWorkerState>>,
worker_stopfd: EventFd,
exit_code: Arc<AtomicI32>,
Expand Down Expand Up @@ -96,6 +98,7 @@ impl Fs {
filesystem: OnceLock::new(),
},
session_options: 0,
max_backend_state_bytes: DEFAULT_MAX_FS_BACKEND_STATE_BYTES,
worker_thread: None,
worker_stopfd: EventFd::new(EFD_NONBLOCK).map_err(FsError::EventFd)?,
exit_code,
Expand Down Expand Up @@ -124,6 +127,7 @@ impl Fs {
shm_region: None,
backend: FsBackend::Custom(backend),
session_options: 0,
max_backend_state_bytes: DEFAULT_MAX_FS_BACKEND_STATE_BYTES,
worker_thread: None,
worker_stopfd: EventFd::new(EFD_NONBLOCK).map_err(FsError::EventFd)?,
exit_code,
Expand All @@ -136,6 +140,11 @@ impl Fs {
defs::FS_DEV_ID
}

/// Sets the largest backend state this device captures or restores.
pub fn set_max_backend_state_bytes(&mut self, bytes: usize) {
self.max_backend_state_bytes = bytes;
}

pub fn set_shm_region(&mut self, shm_region: VirtioShmRegion) {
self.shm_region = Some(shm_region);
}
Expand Down Expand Up @@ -358,12 +367,13 @@ impl VirtioDevice for Fs {
session_options: self.session_options,
backend_state,
}
.encode()
.encode(self.max_backend_state_bytes)
.map_err(super::super::VirtioStateError::Device)
}

fn validate_device_state(&self, bytes: &[u8]) -> Result<(), super::super::VirtioStateError> {
let state = FsDeviceState::decode(bytes).map_err(super::super::VirtioStateError::Device)?;
let state = FsDeviceState::decode(bytes, self.max_backend_state_bytes)
.map_err(super::super::VirtioStateError::Device)?;
if FsOptions::from_bits(state.session_options).is_none() {
return Err(super::super::VirtioStateError::Incompatible(
"virtio-fs state contains unknown negotiated FUSE options".into(),
Expand All @@ -383,7 +393,8 @@ impl VirtioDevice for Fs {

fn restore_device_state(&mut self, bytes: &[u8]) -> Result<(), super::super::VirtioStateError> {
self.validate_device_state(bytes)?;
let state = FsDeviceState::decode(bytes).map_err(super::super::VirtioStateError::Device)?;
let state = FsDeviceState::decode(bytes, self.max_backend_state_bytes)
.map_err(super::super::VirtioStateError::Device)?;
match &self.backend {
FsBackend::Passthrough { filesystem, .. } => filesystem
.get()
Expand Down Expand Up @@ -455,4 +466,45 @@ mod tests {

std::fs::remove_dir(directory).unwrap();
}

struct StatefulBackend(usize);

impl DynFileSystem for StatefulBackend {
fn capture_state(&self) -> std::io::Result<Vec<u8>> {
Ok(vec![0x5a; self.0])
}

fn validate_state(&self, state: &[u8]) -> std::io::Result<()> {
assert_eq!(state.len(), self.0);
Ok(())
}

fn restore_state(&self, state: &[u8]) -> std::io::Result<()> {
self.validate_state(state)
}
}

#[test]
fn fs_state_budget_applies_to_capture_validate_and_restore() {
let backend_len = DEFAULT_MAX_FS_BACKEND_STATE_BYTES + 1;
let new_fs = || {
Fs::with_custom_backend(
"test-fs".into(),
Arc::new(StatefulBackend(backend_len)),
Arc::new(AtomicI32::new(0)),
)
.unwrap()
};

let mut fs = new_fs();
assert!(fs.capture_device_state().is_err());

fs.set_max_backend_state_bytes(backend_len);
let state = fs.capture_device_state().unwrap();
fs.restore_device_state(&state).unwrap();

let mut default_fs = new_fs();
assert!(default_fs.validate_device_state(&state).is_err());
assert!(default_fs.restore_device_state(&state).is_err());
}
}
103 changes: 82 additions & 21 deletions src/devices/src/virtio/fs/state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,14 @@

use std::io;

use super::super::FS_DEVICE_STATE_HEADER_BYTES;

//--------------------------------------------------------------------------------------------------
// Constants
//--------------------------------------------------------------------------------------------------

const MAGIC: &[u8; 8] = b"MSBKFS\0\0";
const VERSION: u16 = 1;
pub(super) const MAX_BACKEND_STATE_BYTES: usize = 4 * 1024 * 1024;
pub(super) const MAX_DEVICE_STATE_BYTES: usize = 8 * 1024 * 1024;

//--------------------------------------------------------------------------------------------------
// Types
Expand All @@ -28,11 +28,12 @@ pub(super) struct FsDeviceState {
//--------------------------------------------------------------------------------------------------

impl FsDeviceState {
pub(super) fn encode(&self) -> io::Result<Vec<u8>> {
if self.backend_state.len() > MAX_BACKEND_STATE_BYTES {
return Err(invalid_data("virtio-fs backend state exceeds 4 MiB"));
pub(super) fn encode(&self, max_backend_state_bytes: usize) -> io::Result<Vec<u8>> {
let max_device_state_bytes = max_device_state_bytes(max_backend_state_bytes)?;
if self.backend_state.len() > max_backend_state_bytes {
return Err(invalid_data("virtio-fs backend state exceeds its limit"));
}
let mut bytes = Vec::with_capacity(22 + self.backend_state.len());
let mut bytes = Vec::with_capacity(FS_DEVICE_STATE_HEADER_BYTES + self.backend_state.len());
bytes.extend_from_slice(MAGIC);
bytes.extend_from_slice(&VERSION.to_le_bytes());
bytes.extend_from_slice(&self.session_options.to_le_bytes());
Expand All @@ -42,31 +43,32 @@ impl FsDeviceState {
.to_le_bytes(),
);
bytes.extend_from_slice(&self.backend_state);
if bytes.len() > MAX_DEVICE_STATE_BYTES {
return Err(invalid_data("virtio-fs device state exceeds 8 MiB"));
if bytes.len() > max_device_state_bytes {
return Err(invalid_data("virtio-fs device state exceeds its limit"));
}
Ok(bytes)
}

pub(super) fn decode(bytes: &[u8]) -> io::Result<Self> {
const HEADER_LEN: usize = 22;
if bytes.len() > MAX_DEVICE_STATE_BYTES {
return Err(invalid_data("virtio-fs device state exceeds 8 MiB"));
pub(super) fn decode(bytes: &[u8], max_backend_state_bytes: usize) -> io::Result<Self> {
if bytes.len() > max_device_state_bytes(max_backend_state_bytes)? {
return Err(invalid_data("virtio-fs device state exceeds its limit"));
}
if bytes.len() < HEADER_LEN || &bytes[..MAGIC.len()] != MAGIC {
if bytes.len() < FS_DEVICE_STATE_HEADER_BYTES || &bytes[..MAGIC.len()] != MAGIC {
return Err(invalid_data("invalid virtio-fs state magic"));
}
if u16::from_le_bytes(bytes[8..10].try_into().unwrap()) != VERSION {
return Err(invalid_data("unsupported virtio-fs state version"));
}
let session_options = u64::from_le_bytes(bytes[10..18].try_into().unwrap());
let backend_len = u32::from_le_bytes(bytes[18..22].try_into().unwrap()) as usize;
if backend_len > MAX_BACKEND_STATE_BYTES || HEADER_LEN + backend_len != bytes.len() {
if backend_len > max_backend_state_bytes
|| FS_DEVICE_STATE_HEADER_BYTES + backend_len != bytes.len()
{
return Err(invalid_data("invalid virtio-fs backend state length"));
}
Ok(Self {
session_options,
backend_state: bytes[HEADER_LEN..].to_vec(),
backend_state: bytes[FS_DEVICE_STATE_HEADER_BYTES..].to_vec(),
})
}
}
Expand All @@ -75,6 +77,15 @@ impl FsDeviceState {
// Functions
//--------------------------------------------------------------------------------------------------

/// Largest encoded device state for a backend state budget; the budget must fit the u32 length
/// field.
fn max_device_state_bytes(max_backend_state_bytes: usize) -> io::Result<usize> {
u32::try_from(max_backend_state_bytes)
.ok()
.and_then(|_| FS_DEVICE_STATE_HEADER_BYTES.checked_add(max_backend_state_bytes))
.ok_or_else(|| invalid_data("virtio-fs backend state limit does not fit u32"))
}

fn invalid_data(message: &'static str) -> io::Error {
io::Error::new(io::ErrorKind::InvalidData, message)
}
Expand All @@ -87,25 +98,75 @@ fn invalid_data(message: &'static str) -> io::Error {
mod tests {
use super::*;

use super::super::super::DEFAULT_MAX_FS_BACKEND_STATE_BYTES;

const DEFAULT_LIMIT: usize = DEFAULT_MAX_FS_BACKEND_STATE_BYTES;

#[test]
fn state_round_trip_and_bounds() {
let encoded = FsDeviceState {
session_options: 0x1234,
backend_state: vec![1, 2, 3],
}
.encode()
.encode(DEFAULT_LIMIT)
.unwrap();
let decoded = FsDeviceState::decode(&encoded).unwrap();
let decoded = FsDeviceState::decode(&encoded, DEFAULT_LIMIT).unwrap();
assert_eq!(decoded.session_options, 0x1234);
assert_eq!(decoded.backend_state, vec![1, 2, 3]);
assert!(FsDeviceState {
session_options: 0,
backend_state: vec![0; MAX_BACKEND_STATE_BYTES + 1],
backend_state: vec![0; DEFAULT_LIMIT + 1],
}
.encode()
.encode(DEFAULT_LIMIT)
.is_err());
let mut oversized = vec![0; MAX_DEVICE_STATE_BYTES + 1];
let mut oversized = vec![0; FS_DEVICE_STATE_HEADER_BYTES + DEFAULT_LIMIT + 1];
oversized[..MAGIC.len()].copy_from_slice(MAGIC);
assert!(FsDeviceState::decode(&oversized).is_err());
assert!(FsDeviceState::decode(&oversized, DEFAULT_LIMIT).is_err());
}

#[test]
fn state_above_default_budget_needs_a_larger_budget() {
let limit = DEFAULT_LIMIT + 1024;
let state = FsDeviceState {
session_options: 7,
backend_state: vec![0xa5; DEFAULT_LIMIT + 1],
};
assert!(state.encode(DEFAULT_LIMIT).is_err());
let encoded = state.encode(limit).unwrap();
assert_eq!(
encoded.len(),
FS_DEVICE_STATE_HEADER_BYTES + DEFAULT_LIMIT + 1
);
assert!(FsDeviceState::decode(&encoded, DEFAULT_LIMIT).is_err());
let decoded = FsDeviceState::decode(&encoded, limit).unwrap();
assert_eq!(decoded.backend_state, state.backend_state);
}

#[test]
fn decode_bounds_the_whole_state_by_header_plus_budget() {
let limit = 64;
let mut at_limit = FsDeviceState {
session_options: 0,
backend_state: vec![0; limit],
}
.encode(limit)
.unwrap();
assert!(FsDeviceState::decode(&at_limit, limit).is_ok());
at_limit.push(0);
assert!(FsDeviceState::decode(&at_limit, limit).is_err());
}

#[test]
fn budgets_that_do_not_fit_the_length_field_are_rejected() {
let state = FsDeviceState {
session_options: 0,
backend_state: Vec::new(),
};
let encoded = state.encode(DEFAULT_LIMIT).unwrap();
if let Some(too_large) = (u32::MAX as usize).checked_add(1) {
assert!(state.encode(too_large).is_err());
assert!(FsDeviceState::decode(&encoded, too_large).is_err());
}
assert!(max_device_state_bytes(u32::MAX as usize - FS_DEVICE_STATE_HEADER_BYTES).is_ok());
}
}
55 changes: 55 additions & 0 deletions src/devices/src/virtio/fs_state_limits.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
// Copyright 2026 Microsandbox Authors. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

//! Size limits shared by the virtio-fs device and the typed device state codec.

//--------------------------------------------------------------------------------------------------
// Constants
//--------------------------------------------------------------------------------------------------

/// Default maximum size of the backend state carried by one virtio-fs device.
pub const DEFAULT_MAX_FS_BACKEND_STATE_BYTES: usize = 4 * 1024 * 1024;
/// Size of the fixed header preceding the backend state in a virtio-fs device state.
pub const FS_DEVICE_STATE_HEADER_BYTES: usize = 22;

//--------------------------------------------------------------------------------------------------
// Types
//--------------------------------------------------------------------------------------------------

/// Limits shared by VM device capture and standalone state codecs.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct DeviceStateLimits {
fs_state_limit: usize,
}

//--------------------------------------------------------------------------------------------------
// Methods
//--------------------------------------------------------------------------------------------------

impl DeviceStateLimits {
/// Set the maximum filesystem backend payload per device, in bytes.
///
/// Device headers and transport metadata receive additional bounded space.
/// The limit must fit a `u32`; larger values cannot be captured or restored.
pub fn with_fs_state_limit(mut self, bytes: usize) -> Self {
self.fs_state_limit = bytes;
self
}

/// Maximum filesystem backend payload per device, in bytes.
pub fn fs_state_limit(self) -> usize {
self.fs_state_limit
}
}

//--------------------------------------------------------------------------------------------------
// Trait Implementations
//--------------------------------------------------------------------------------------------------

impl Default for DeviceStateLimits {
fn default() -> Self {
Self {
fs_state_limit: DEFAULT_MAX_FS_BACKEND_STATE_BYTES,
}
}
}
4 changes: 4 additions & 0 deletions src/devices/src/virtio/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ pub mod device;
pub mod file_traits;
#[cfg(not(any(feature = "tee", feature = "aws-nitro")))]
pub mod fs;
// The virtio-fs state limits are compiled under every feature: the typed device
// state codec recognizes virtio-fs state even when the fs device is not built.
pub mod fs_state_limits;
#[cfg(feature = "gpu")]
pub mod gpu;
#[cfg(feature = "input")]
Expand Down Expand Up @@ -71,6 +74,7 @@ pub use self::cpu::*;
pub use self::device::*;
#[cfg(not(any(feature = "tee", feature = "aws-nitro")))]
pub use self::fs::*;
pub use self::fs_state_limits::*;
#[cfg(feature = "gpu")]
pub use self::gpu::*;
#[cfg(not(feature = "tee"))]
Expand Down
Loading
Loading