diff --git a/src/devices/src/virtio/fs/device.rs b/src/devices/src/virtio/fs/device.rs index a8dd53d6b..eb8cf7196 100644 --- a/src/devices/src/virtio/fs/device.rs +++ b/src/devices/src/virtio/fs/device.rs @@ -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}; @@ -58,6 +59,7 @@ pub struct Fs { shm_region: Option, backend: FsBackend, session_options: u64, + max_backend_state_bytes: usize, worker_thread: Option>, worker_stopfd: EventFd, exit_code: Arc, @@ -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, @@ -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, @@ -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); } @@ -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(), @@ -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() @@ -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> { + 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()); + } } diff --git a/src/devices/src/virtio/fs/state.rs b/src/devices/src/virtio/fs/state.rs index 3a30a12bb..c2aa2af85 100644 --- a/src/devices/src/virtio/fs/state.rs +++ b/src/devices/src/virtio/fs/state.rs @@ -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 @@ -28,11 +28,12 @@ pub(super) struct FsDeviceState { //-------------------------------------------------------------------------------------------------- impl FsDeviceState { - pub(super) fn encode(&self) -> io::Result> { - 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> { + 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()); @@ -42,18 +43,17 @@ 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 { - 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 { + 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 { @@ -61,12 +61,14 @@ impl FsDeviceState { } 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(), }) } } @@ -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 { + 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) } @@ -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()); } } diff --git a/src/devices/src/virtio/fs_state_limits.rs b/src/devices/src/virtio/fs_state_limits.rs new file mode 100644 index 000000000..717797504 --- /dev/null +++ b/src/devices/src/virtio/fs_state_limits.rs @@ -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, + } + } +} diff --git a/src/devices/src/virtio/mod.rs b/src/devices/src/virtio/mod.rs index 3581a3afe..e2f925703 100644 --- a/src/devices/src/virtio/mod.rs +++ b/src/devices/src/virtio/mod.rs @@ -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")] @@ -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"))] diff --git a/src/krun/src/api/builder.rs b/src/krun/src/api/builder.rs index 7515ab528..61702dff9 100644 --- a/src/krun/src/api/builder.rs +++ b/src/krun/src/api/builder.rs @@ -90,6 +90,8 @@ pub struct VmBuilder { #[cfg_attr(feature = "tee", allow(dead_code))] #[cfg(not(feature = "tee"))] fs: FsBuilder, + #[cfg(not(feature = "tee"))] + device_state_limits: devices::virtio::DeviceStateLimits, console: ConsoleBuilder, exec: ExecBuilder, #[cfg(feature = "net")] @@ -119,6 +121,8 @@ impl VmBuilder { kernel: KernelBuilder::new(), #[cfg(not(feature = "tee"))] fs: FsBuilder::new(), + #[cfg(not(feature = "tee"))] + device_state_limits: devices::virtio::DeviceStateLimits::default(), console: ConsoleBuilder::new(), exec: ExecBuilder::new(), #[cfg(feature = "net")] @@ -211,6 +215,46 @@ impl VmBuilder { self } + /// Set the largest backend state, in bytes, that each virtio-fs device captures or restores. + /// + /// Applies to every filesystem device, including custom backends. Defaults to + /// [`DEFAULT_MAX_FS_BACKEND_STATE_BYTES`](crate::DEFAULT_MAX_FS_BACKEND_STATE_BYTES). The + /// limit must fit a `u32`; larger values make state capture and restore fail. + /// With `blk` enabled, use the built VM's `device_state_codec()` so encoding + /// and decoding automatically use this budget too. Standalone + /// `VirtioDeviceState::encode()` and `decode()` still use the default budget. + #[cfg(not(feature = "tee"))] + pub fn fs_state_limit(mut self, bytes: usize) -> Self { + self.device_state_limits = self.device_state_limits.with_fs_state_limit(bytes); + self + } + + /// Use limits shared with standalone device-state codecs. + /// + /// The built VM exposes a codec using these limits through `device_state_codec()` + /// when the `blk` feature is enabled. Later calls to `fs_state_limit()` override + /// the filesystem budget in this value. + /// + /// ```no_run + /// # #[cfg(all(feature = "blk", not(feature = "tee")))] + /// # fn example() -> Result<(), Box> { + /// use msb_krun::{DeviceStateCodec, DeviceStateLimits, VmBuilder}; + /// + /// let limits = DeviceStateLimits::default().with_fs_state_limit(64 << 20); + /// let standalone = DeviceStateCodec::new(limits); + /// let vm = VmBuilder::new().device_state_limits(limits).build()?; + /// let codec = vm.device_state_codec(); + /// // Use codec.encode(&state) and codec.decode(&bytes) for this VM. + /// // standalone uses the same budget for imports before VM creation. + /// # Ok(()) + /// # } + /// ``` + #[cfg(not(feature = "tee"))] + pub fn device_state_limits(mut self, limits: devices::virtio::DeviceStateLimits) -> Self { + self.device_state_limits = limits; + self + } + /// Configure network devices. /// /// Can be called multiple times to add multiple devices. @@ -644,6 +688,10 @@ impl VmBuilder { // Apply filesystem configuration #[cfg(not(feature = "tee"))] apply_fs_configs(&mut vmr, self.fs.configs); + #[cfg(not(feature = "tee"))] + { + vmr.device_state_limits = self.device_state_limits; + } // Apply console configuration if let Some(output) = self.console.output { @@ -1859,4 +1907,58 @@ mod tests { assert_eq!(vmr.custom_fs[0].fs_id, "share"); assert_eq!(vmr.custom_fs[0].shm_size, Some(64 << 20)); } + + #[test] + #[cfg(all(feature = "blk", not(feature = "tee")))] + fn fs_state_limit_is_used_by_the_vm_codec() { + use devices::virtio::{DeviceStateLimits, VirtioMmioState, VIRTIO_MMIO_STATE_VERSION}; + use vmm::device_state::{DeviceStateCodec, VirtioDeviceState}; + + let limits = DeviceStateLimits::default().with_fs_state_limit(8 << 20); + let state = VirtioDeviceState { + pause_generation: 1, + device_id: "fs0".into(), + transport: VirtioMmioState { + version: VIRTIO_MMIO_STATE_VERSION, + device_type: 26, + features_select: 0, + acked_features_select: 0, + queue_select: 0, + device_status: 0, + config_generation: 0, + shm_region_select: 0, + interrupt_status: 0, + irq_line: None, + acked_features: 0, + queues: vec![], + }, + device_state: vec![7; 5 << 20], + }; + let standalone = DeviceStateCodec::new(limits); + let encoded = standalone.encode(&state).unwrap(); + let default_vm = VmBuilder::new().build().unwrap(); + assert!(default_vm.device_state_codec().decode(&encoded).is_err()); + assert!(default_vm.device_state_codec().encode(&state).is_err()); + + for builder in [ + VmBuilder::new().fs_state_limit(8 << 20), + VmBuilder::new().device_state_limits(limits), + ] { + let vm = builder.build().unwrap(); + let codec = vm.device_state_codec(); + assert_eq!(codec.decode(&encoded).unwrap(), state); + assert_eq!( + standalone.decode(&codec.encode(&state).unwrap()).unwrap(), + state + ); + assert!(encoded.len() <= codec.max_state_bytes(26)); + assert_eq!(codec.limits(), limits); + } + let vm = VmBuilder::new() + .device_state_limits(limits) + .fs_state_limit(1024) + .build() + .unwrap(); + assert!(vm.device_state_codec().decode(&encoded).is_err()); + } } diff --git a/src/krun/src/api/mod.rs b/src/krun/src/api/mod.rs index 7b657066c..d5b27dcc5 100644 --- a/src/krun/src/api/mod.rs +++ b/src/krun/src/api/mod.rs @@ -61,6 +61,10 @@ pub use builders::{ pub use devices::virtio::block::{ImageType as BlockImageFormat, SyncMode as BlockSyncMode}; #[cfg(feature = "blk")] pub use devices::virtio::{BlockBackendSpec, BlockLayerSpec, PreparedBlockBackend}; +#[cfg(not(feature = "tee"))] +pub use devices::virtio::{ + DeviceStateLimits, DEFAULT_MAX_FS_BACKEND_STATE_BYTES, FS_DEVICE_STATE_HEADER_BYTES, +}; pub use error::{BuildError, ConfigError, Error, Result, RuntimeError}; pub use exit_handle::ExitHandle; pub use metrics::{ @@ -75,7 +79,9 @@ pub use vm::{ VmMemoryState, VmPauseGeneration, }; #[cfg(all(feature = "blk", not(feature = "tee")))] -pub use vmm::device_state::{BlockDeviceState, VirtioDeviceState}; +pub use vmm::device_state::{ + max_virtio_device_state_bytes, BlockDeviceState, DeviceStateCodec, VirtioDeviceState, +}; #[cfg(not(feature = "tee"))] pub use vmm::execution_state::{ ExecutionArchitecture, ExecutionBackend, ExecutionState, VcpuExecutionState, diff --git a/src/krun/src/api/vm.rs b/src/krun/src/api/vm.rs index 242c831b2..acae3bda4 100644 --- a/src/krun/src/api/vm.rs +++ b/src/krun/src/api/vm.rs @@ -303,6 +303,16 @@ pub struct VmMemoryState { //-------------------------------------------------------------------------------------------------- impl Vm { + /// Return a codec using the limits configured on this VM's builder. + /// + /// Use this for states returned by `capture_virtio_device_state`, and for + /// decoding states before `restore_virtio_device_state`. It is available + /// before VM startup and can be retained independently of the VM. + #[cfg(all(feature = "blk", not(feature = "tee")))] + pub fn device_state_codec(&self) -> vmm::device_state::DeviceStateCodec { + vmm::device_state::DeviceStateCodec::new(self.vmr.device_state_limits) + } + /// Create a new Vm instance. #[allow(clippy::too_many_arguments)] pub(crate) fn new( diff --git a/src/krun/src/lib.rs b/src/krun/src/lib.rs index 9cce729c0..7ac3b143b 100644 --- a/src/krun/src/lib.rs +++ b/src/krun/src/lib.rs @@ -231,12 +231,18 @@ pub use api::vm::{ VmGenerationState, VmGenerationWaitOutcome, VmMemoryRestoreSource, VmMemoryRestoreTarget, VmMemoryState, VmPauseGeneration, }; +#[cfg(all(feature = "blk", not(feature = "tee")))] +pub use api::{ + max_virtio_device_state_bytes, BlockDeviceState, DeviceStateCodec, VirtioDeviceState, +}; #[cfg(feature = "blk")] pub use api::{ BlockBackendSpec, BlockImageFormat, BlockLayerSpec, BlockSyncMode, PreparedBlockBackend, }; -#[cfg(all(feature = "blk", not(feature = "tee")))] -pub use api::{BlockDeviceState, VirtioDeviceState}; +#[cfg(not(feature = "tee"))] +pub use api::{ + DeviceStateLimits, DEFAULT_MAX_FS_BACKEND_STATE_BYTES, FS_DEVICE_STATE_HEADER_BYTES, +}; #[cfg(not(feature = "tee"))] pub use api::{ ExecutionArchitecture, ExecutionBackend, ExecutionState, FullCaptureReason, GuestMemoryRange, diff --git a/src/vmm/src/builder.rs b/src/vmm/src/builder.rs index 4388ffc87..3ed9d9b18 100644 --- a/src/vmm/src/builder.rs +++ b/src/vmm/src/builder.rs @@ -1998,6 +1998,7 @@ pub fn build_microvm_paused( attach_fs_devices( &mut vmm, &vm_resources.fs, + vm_resources.device_state_limits.fs_state_limit(), &mut _shm_manager, #[cfg(not(feature = "tee"))] export_table, @@ -2011,6 +2012,7 @@ pub fn build_microvm_paused( attach_custom_fs_devices( &mut vmm, &vm_resources.custom_fs, + vm_resources.device_state_limits.fs_state_limit(), &mut _shm_manager, vm_resources.fs.len(), intc.clone(), @@ -4296,9 +4298,11 @@ fn attach_mmio_device( } #[cfg(not(feature = "tee"))] +#[allow(clippy::too_many_arguments)] fn attach_fs_devices( vmm: &mut Vmm, fs_devs: &[FsDeviceConfig], + backend_state_limit: usize, shm_manager: &mut ShmManager, #[cfg(not(feature = "tee"))] export_table: Option, intc: IrqChip, @@ -4318,6 +4322,10 @@ fn attach_fs_devices( .unwrap(), )); + fs.lock() + .unwrap() + .set_max_backend_state_bytes(backend_state_limit); + let id = format!("{}{}", String::from(fs.lock().unwrap().id()), i); if let Some(shm_region) = shm_manager.fs_region(i) { @@ -4347,9 +4355,11 @@ fn attach_fs_devices( } #[cfg(not(any(feature = "tee", feature = "aws-nitro")))] +#[allow(clippy::too_many_arguments)] fn attach_custom_fs_devices( vmm: &mut Vmm, custom_fs_devs: &[CustomFsDeviceConfig], + backend_state_limit: usize, shm_manager: &mut ShmManager, index_offset: usize, intc: IrqChip, @@ -4368,6 +4378,10 @@ fn attach_custom_fs_devices( .unwrap(), )); + fs.lock() + .unwrap() + .set_max_backend_state_bytes(backend_state_limit); + let id = format!( "{}{}", String::from(fs.lock().unwrap().id()), diff --git a/src/vmm/src/device_state.rs b/src/vmm/src/device_state.rs index 10e923aa5..c30aad695 100644 --- a/src/vmm/src/device_state.rs +++ b/src/vmm/src/device_state.rs @@ -8,7 +8,8 @@ use std::fmt::{Display, Formatter}; #[cfg(feature = "blk")] use devices::virtio::{ - BlockState, CacheType, QueueState, VirtioMmioState, BLOCK_STATE_VERSION, QUEUE_STATE_VERSION, + BlockState, CacheType, DeviceStateLimits, QueueState, VirtioMmioState, BLOCK_STATE_VERSION, + DEFAULT_MAX_FS_BACKEND_STATE_BYTES, FS_DEVICE_STATE_HEADER_BYTES, QUEUE_STATE_VERSION, VIRTIO_MMIO_STATE_VERSION, }; @@ -33,10 +34,6 @@ const MAX_DEVICE_STATE_BYTES: usize = 1024 * 1024; #[cfg(feature = "blk")] const MAX_DEVICE_SPECIFIC_STATE_BYTES: usize = 64 * 1024; #[cfg(feature = "blk")] -const MAX_FS_DEVICE_STATE_BYTES: usize = 8 * 1024 * 1024; -#[cfg(feature = "blk")] -const MAX_FS_DEVICE_SPECIFIC_STATE_BYTES: usize = 8 * 1024 * 1024; -#[cfg(feature = "blk")] const MAX_DEVICE_STRING_BYTES: usize = 4096; #[cfg(feature = "blk")] const MAX_DISK_ID_BYTES: usize = 256; @@ -76,6 +73,13 @@ pub struct VirtioDeviceState { pub device_state: Vec, } +/// A reusable codec carrying the same limits as VM device capture and restore. +#[cfg(feature = "blk")] +#[derive(Clone, Copy, Debug, Default)] +pub struct DeviceStateCodec { + limits: DeviceStateLimits, +} + /// Framing and compatibility errors for virtio-block state artifacts. #[cfg(feature = "blk")] #[derive(Clone, Copy, Debug, Eq, PartialEq)] @@ -261,20 +265,55 @@ impl BlockDeviceState { } } +#[cfg(feature = "blk")] +impl DeviceStateCodec { + /// Create a standalone codec, for example when importing state before a VM exists. + pub fn new(limits: DeviceStateLimits) -> Self { + Self { limits } + } + + /// Return the limits used by this codec, for reuse when building a destination VM. + pub fn limits(self) -> DeviceStateLimits { + self.limits + } + + /// Encode a device state using this codec's configured budget. + pub fn encode(&self, state: &VirtioDeviceState) -> Result> { + state.encode_with_fs_state_limit(self.limits.fs_state_limit()) + } + + /// Decode a device state using this codec's configured budget. + pub fn decode(&self, bytes: &[u8]) -> Result { + VirtioDeviceState::decode_with_fs_state_limit(bytes, self.limits.fs_state_limit()) + } + + /// Largest admitted encoded object for a device type, including its envelope. + pub fn max_state_bytes(&self, device_type: u32) -> usize { + max_virtio_device_state_bytes(device_type, self.limits.fs_state_limit()) + } +} + #[cfg(feature = "blk")] impl VirtioDeviceState { - /// Encodes this typed state into deterministic, bounded bytes. + /// Encodes this typed state into deterministic, bounded bytes, allowing virtio-fs backend + /// state up to the default budget. pub fn encode(&self) -> Result> { + self.encode_with_fs_state_limit(DEFAULT_MAX_FS_BACKEND_STATE_BYTES) + } + + /// Encodes this typed state, allowing virtio-fs backend state up to `fs_state_limit` bytes. + pub fn encode_with_fs_state_limit(&self, fs_state_limit: usize) -> Result> { let mut writer = Writer { bytes: Vec::new() }; writer.bytes(VIRTIO_DEVICE_STATE_MAGIC); writer.u16(VIRTIO_DEVICE_STATE_SCHEMA); writer.u64(self.pause_generation); writer.string(&self.device_id, MAX_DEVICE_STRING_BYTES)?; encode_transport(&mut writer, &self.transport)?; - let max_device_state = max_device_state_bytes(self.transport.device_type); + let max_device_state = + max_virtio_device_state_bytes(self.transport.device_type, fs_state_limit); writer.sized_bytes( &self.device_state, - max_device_specific_state_bytes(self.transport.device_type), + max_device_specific_state_bytes(self.transport.device_type, fs_state_limit), )?; if writer.bytes.len() > max_device_state { return Err(Error::InvalidLength); @@ -282,9 +321,16 @@ impl VirtioDeviceState { Ok(writer.bytes) } - /// Decodes and validates one generic virtio state artifact. + /// Decodes and validates one generic virtio state artifact, allowing virtio-fs backend state + /// up to the default budget. pub fn decode(bytes: &[u8]) -> Result { - if bytes.len() > MAX_FS_DEVICE_STATE_BYTES { + Self::decode_with_fs_state_limit(bytes, DEFAULT_MAX_FS_BACKEND_STATE_BYTES) + } + + /// Decodes and validates one generic virtio state artifact, allowing virtio-fs backend state + /// up to `fs_state_limit` bytes. + pub fn decode_with_fs_state_limit(bytes: &[u8], fs_state_limit: usize) -> Result { + if bytes.len() > max_virtio_device_state_bytes(TYPE_FS, fs_state_limit) { return Err(Error::InvalidLength); } let mut reader = Reader { bytes, offset: 0 }; @@ -296,11 +342,14 @@ impl VirtioDeviceState { let pause_generation = reader.u64()?; let device_id = reader.string(MAX_DEVICE_STRING_BYTES)?; let transport = decode_transport(&mut reader)?; - if bytes.len() > max_device_state_bytes(transport.device_type) { + if bytes.len() > max_virtio_device_state_bytes(transport.device_type, fs_state_limit) { return Err(Error::InvalidLength); } let device_state = reader - .sized_bytes(max_device_specific_state_bytes(transport.device_type))? + .sized_bytes(max_device_specific_state_bytes( + transport.device_type, + fs_state_limit, + ))? .to_vec(); if reader.offset != bytes.len() { return Err(Error::TrailingBytes); @@ -469,19 +518,22 @@ impl<'a> Reader<'a> { // Functions //-------------------------------------------------------------------------------------------------- +/// Largest encoded [`VirtioDeviceState`] for a device type. `fs_state_limit` is the virtio-fs +/// backend state budget and only affects virtio-fs devices. #[cfg(feature = "blk")] -fn max_device_state_bytes(device_type: u32) -> usize { +pub fn max_virtio_device_state_bytes(device_type: u32, fs_state_limit: usize) -> usize { if device_type == TYPE_FS { - MAX_FS_DEVICE_STATE_BYTES + max_device_specific_state_bytes(device_type, fs_state_limit) + .saturating_add(MAX_DEVICE_STATE_BYTES) } else { MAX_DEVICE_STATE_BYTES } } #[cfg(feature = "blk")] -fn max_device_specific_state_bytes(device_type: u32) -> usize { +fn max_device_specific_state_bytes(device_type: u32, fs_state_limit: usize) -> usize { if device_type == TYPE_FS { - MAX_FS_DEVICE_SPECIFIC_STATE_BYTES + FS_DEVICE_STATE_HEADER_BYTES.saturating_add(fs_state_limit) } else { MAX_DEVICE_SPECIFIC_STATE_BYTES } @@ -709,4 +761,73 @@ mod tests { MAX_DEVICE_SPECIFIC_STATE_BYTES + 1 ); } + + #[test] + fn fs_virtio_state_above_the_default_budget_needs_a_larger_limit() { + let mut state = generic_state(); + state.transport.device_type = TYPE_FS; + state.device_state = + vec![7; FS_DEVICE_STATE_HEADER_BYTES + DEFAULT_MAX_FS_BACKEND_STATE_BYTES + 1]; + let limit = DEFAULT_MAX_FS_BACKEND_STATE_BYTES * 2; + + assert_eq!(state.encode(), Err(Error::InvalidLength)); + let encoded = state.encode_with_fs_state_limit(limit).unwrap(); + assert_eq!( + VirtioDeviceState::decode(&encoded), + Err(Error::InvalidLength) + ); + assert_eq!( + VirtioDeviceState::decode_with_fs_state_limit(&encoded, limit).unwrap(), + state + ); + assert_eq!( + VirtioDeviceState::decode_with_fs_state_limit( + &encoded, + DEFAULT_MAX_FS_BACKEND_STATE_BYTES + ), + Err(Error::InvalidLength) + ); + } + + #[test] + fn fs_virtio_state_limits_are_derived_from_the_backend_budget() { + let limit = 10 * 1024 * 1024; + let specific = FS_DEVICE_STATE_HEADER_BYTES + limit; + assert_eq!( + max_virtio_device_state_bytes(TYPE_FS, limit), + specific + MAX_DEVICE_STATE_BYTES + ); + + let mut state = generic_state(); + state.transport.device_type = TYPE_FS; + state.device_state = vec![0; specific]; + let encoded = state.encode_with_fs_state_limit(limit).unwrap(); + assert!(encoded.len() <= max_virtio_device_state_bytes(TYPE_FS, limit)); + assert_eq!( + VirtioDeviceState::decode_with_fs_state_limit(&encoded, limit).unwrap(), + state + ); + + state.device_state.push(0); + assert_eq!( + state.encode_with_fs_state_limit(limit), + Err(Error::InvalidLength) + ); + } + + #[test] + fn non_fs_virtio_state_limits_ignore_the_fs_budget() { + let limit = DEFAULT_MAX_FS_BACKEND_STATE_BYTES * 4; + assert_eq!( + max_virtio_device_state_bytes(3, limit), + MAX_DEVICE_STATE_BYTES + ); + + let mut state = generic_state(); + state.device_state = vec![0; MAX_DEVICE_SPECIFIC_STATE_BYTES + 1]; + assert_eq!( + state.encode_with_fs_state_limit(limit), + Err(Error::InvalidLength) + ); + } } diff --git a/src/vmm/src/resources.rs b/src/vmm/src/resources.rs index 9323956e3..74d055155 100644 --- a/src/vmm/src/resources.rs +++ b/src/vmm/src/resources.rs @@ -325,6 +325,9 @@ pub struct VmResources { /// The fs device. #[cfg(not(feature = "tee"))] pub fs: Vec, + /// Limits shared by device capture, restore, and standalone state codecs. + #[cfg(not(feature = "tee"))] + pub device_state_limits: devices::virtio::DeviceStateLimits, /// Custom filesystem devices. #[cfg(not(any(feature = "tee", feature = "aws-nitro")))] pub custom_fs: Vec, @@ -422,6 +425,8 @@ impl Default for VmResources { initrd_bundle: None, #[cfg(not(feature = "tee"))] fs: Vec::new(), + #[cfg(not(feature = "tee"))] + device_state_limits: devices::virtio::DeviceStateLimits::default(), #[cfg(not(any(feature = "tee", feature = "aws-nitro")))] custom_fs: Vec::new(), vsock: VsockBuilder::default(),