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
6 changes: 6 additions & 0 deletions src/firecracker/src/api_server/request/snapshot.rs
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@ fn parse_put_snapshot_load(body: &Body) -> Result<ParsedRequest, RequestError> {
vsock_override: snapshot_config.vsock_override,
clock_realtime: snapshot_config.clock_realtime,
shared: snapshot_config.shared,
uffd_base_file: snapshot_config.uffd_base_file,
};

// Construct the `ParsedRequest` object.
Expand Down Expand Up @@ -193,6 +194,7 @@ mod tests {
vsock_override: None,
clock_realtime: false,
shared: false,
uffd_base_file: None,
};
let mut parsed_request = parse_put_snapshot(&Body::new(body), Some("load")).unwrap();
assert!(
Expand Down Expand Up @@ -226,6 +228,7 @@ mod tests {
vsock_override: None,
clock_realtime: false,
shared: false,
uffd_base_file: None,
};
let mut parsed_request = parse_put_snapshot(&Body::new(body), Some("load")).unwrap();
assert!(
Expand Down Expand Up @@ -259,6 +262,7 @@ mod tests {
vsock_override: None,
clock_realtime: false,
shared: false,
uffd_base_file: None,
};
let mut parsed_request = parse_put_snapshot(&Body::new(body), Some("load")).unwrap();
assert!(
Expand Down Expand Up @@ -301,6 +305,7 @@ mod tests {
vsock_override: None,
clock_realtime: false,
shared: false,
uffd_base_file: None,
};
let mut parsed_request = parse_put_snapshot(&Body::new(body), Some("load")).unwrap();
assert!(
Expand Down Expand Up @@ -331,6 +336,7 @@ mod tests {
vsock_override: None,
clock_realtime: false,
shared: false,
uffd_base_file: None,
};
let parsed_request = parse_put_snapshot(&Body::new(body), Some("load")).unwrap();
assert_eq!(
Expand Down
10 changes: 10 additions & 0 deletions src/firecracker/swagger/firecracker.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -1721,6 +1721,16 @@ definitions:
memory MAP_SHARED so guest writes flush back to the backing memfile (the
page-readable source for post-copy live migration). No effect for the UFFD
backend. Optional; defaults to false (MAP_PRIVATE).
uffd_base_file:
type: string
description:
Unified-memory-substrate base backing (engrams ADR 0045 v2b). When set
with the Uffd memory backend, guest memory is created as MAP_PRIVATE of
this shmem file instead of anonymous, and the UFFD registration uses
MISSING|MINOR. The external handler resolves base-identical pages with
UFFDIO_CONTINUE (page-cache sharing across same-template microVMs) and
divergent pages with UFFDIO_COPY; guest writes COW natively. Invalid
with the File backend. Optional.


TokenBucket:
Expand Down
3 changes: 2 additions & 1 deletion src/vmm/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,8 @@ serde = { version = "1.0.228", features = ["derive", "rc"] }
serde_json = "1.0.149"
slab = "0.4.12"
thiserror = "2.0.18"
userfaultfd = "0.9.0"
# linux5_13: RegisterMode::MINOR for the ADR 0045 substrate (v2b) registration.
userfaultfd = { version = "0.9.0", features = ["linux5_13"] }
utils = { path = "../utils" }
uuid = "1.23.1"
vhost = { version = "0.15.0", features = ["vhost-user-frontend"] }
Expand Down
105 changes: 100 additions & 5 deletions src/vmm/src/persist.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ use std::sync::{Arc, Mutex};

use semver::Version;
use serde::{Deserialize, Serialize};
use userfaultfd::{FeatureFlags, Uffd, UffdBuilder};
use userfaultfd::{FeatureFlags, RegisterMode, Uffd, UffdBuilder};
use vmm_sys_util::sock_ctrl_msg::ScmSocket;

#[cfg(target_arch = "aarch64")]
Expand Down Expand Up @@ -455,6 +455,12 @@ pub fn restore_from_snapshot(
)
.into());
}
if params.uffd_base_file.is_some() {
return Err(RestoreFromSnapshotGuestMemoryError::Uffd(
GuestMemoryFromUffdError::BaseFileRequiresUffdBackend,
)
.into());
}
(
guest_memory_from_file(mem_backend_path, mem_state, track_dirty_pages, params.shared)
.map_err(RestoreFromSnapshotGuestMemoryError::File)?,
Expand All @@ -466,6 +472,7 @@ pub fn restore_from_snapshot(
mem_state,
track_dirty_pages,
vm_resources.machine_config.huge_pages,
params.uffd_base_file.as_deref(),
)
.map_err(RestoreFromSnapshotGuestMemoryError::Uffd)?,
};
Expand Down Expand Up @@ -545,16 +552,25 @@ pub enum GuestMemoryFromUffdError {
Connect(#[from] std::io::Error),
/// Failed to sends file descriptor: {0}
Send(#[from] vmm_sys_util::errno::Error),
/// `uffd_base_file` is only supported with the Uffd memory backend.
BaseFileRequiresUffdBackend,
/// `uffd_base_file` is not supported with hugetlbfs-backed snapshots (UFFD minor faults are shmem-only here).
BaseFileHugetlbfs,
/// Failed to open uffd_base_file: {0}
OpenBaseFile(std::io::Error),
/// Failed to map guest memory from uffd_base_file: {0}
BaseFileMap(MemoryError),
}

fn guest_memory_from_uffd(
mem_uds_path: &Path,
mem_state: &GuestMemoryState,
track_dirty_pages: bool,
huge_pages: HugePageConfig,
base_file: Option<&Path>,
) -> Result<(Vec<GuestRegionMmap>, Option<Uffd>), GuestMemoryFromUffdError> {
let (guest_memory, backend_mappings) =
create_guest_memory(mem_state, track_dirty_pages, huge_pages)?;
create_guest_memory(mem_state, track_dirty_pages, huge_pages, base_file)?;

let mut uffd_builder = UffdBuilder::new();

Expand All @@ -571,8 +587,17 @@ fn guest_memory_from_uffd(
.create()
.map_err(GuestMemoryFromUffdError::Create)?;

// ADR 0045 substrate v2b: with a base file backing, register MISSING|MINOR
// so the handler can resolve base-identical pages with UFFDIO_CONTINUE
// (shared page cache) and divergent pages with UFFDIO_COPY (private).
// Without it, the stock MISSING-only registration is byte-identical.
let register_mode = if base_file.is_some() {
RegisterMode::MISSING | RegisterMode::MINOR
} else {
RegisterMode::MISSING
};
for mem_region in guest_memory.iter() {
uffd.register(mem_region.as_ptr().cast(), mem_region.size() as _)
uffd.register_with_mode(mem_region.as_ptr().cast(), mem_region.size() as _, register_mode)
.map_err(GuestMemoryFromUffdError::Register)?;
}

Expand All @@ -585,8 +610,22 @@ fn create_guest_memory(
mem_state: &GuestMemoryState,
track_dirty_pages: bool,
huge_pages: HugePageConfig,
base_file: Option<&Path>,
) -> Result<(Vec<GuestRegionMmap>, Vec<GuestRegionUffdMapping>), GuestMemoryFromUffdError> {
let guest_memory = memory::anonymous(mem_state.regions(), track_dirty_pages, huge_pages)?;
let guest_memory = match base_file {
Some(path) => {
// ADR 0045 substrate v2b: MAP_PRIVATE of the shared base shm file.
// Read-only fd is enough — guest writes COW to private anon pages;
// only the external handler ever writes the base (by path).
if huge_pages.is_hugetlbfs() {
return Err(GuestMemoryFromUffdError::BaseFileHugetlbfs);
}
let file = std::fs::File::open(path).map_err(GuestMemoryFromUffdError::OpenBaseFile)?;
memory::snapshot_file(file, mem_state.regions(), track_dirty_pages, /* shared */ false)
.map_err(GuestMemoryFromUffdError::BaseFileMap)?
}
None => memory::anonymous(mem_state.regions(), track_dirty_pages, huge_pages)?,
};
let mut backend_mappings = Vec::with_capacity(guest_memory.len());
let mut offset = 0;
for mem_region in guest_memory.iter() {
Expand Down Expand Up @@ -791,14 +830,70 @@ mod tests {
};

let (_, uffd_regions) =
create_guest_memory(&mem_state, false, HugePageConfig::None).unwrap();
create_guest_memory(&mem_state, false, HugePageConfig::None, None).unwrap();

assert_eq!(uffd_regions.len(), 1);
assert_eq!(uffd_regions[0].size, 0x20000);
assert_eq!(uffd_regions[0].offset, 0);
assert_eq!(uffd_regions[0].page_size, HugePageConfig::None.page_size());
}

/// ADR 0045 substrate v2b: guest memory created over a base file maps it
/// MAP_PRIVATE — content reads through from the file (page-cache shared),
/// guest-side writes COW privately and never reach the file, and the
/// backend mappings carry the same sequential file offsets as the
/// anonymous path.
#[test]
fn test_create_guest_memory_with_base_file() {
use std::io::Write;

let region_size: usize = 0x20000;
let base = TempFile::new().unwrap();
let pattern: Vec<u8> = (0..region_size).map(|i| (i % 251) as u8).collect();
base.as_file().write_all(&pattern).unwrap();

let mem_state = GuestMemoryState {
regions: vec![GuestMemoryRegionState {
base_address: 0,
size: region_size,
region_type: GuestRegionType::Dram,
plugged: vec![true],
}],
};

let (guest_memory, uffd_regions) =
create_guest_memory(&mem_state, false, HugePageConfig::None, Some(base.as_path()))
.unwrap();
assert_eq!(uffd_regions.len(), 1);
assert_eq!(uffd_regions[0].size, region_size);
assert_eq!(uffd_regions[0].offset, 0);

// Content reads through from the base file (no uffd registered yet, so
// plain page-cache faults serve these).
let ptr = guest_memory[0].as_ptr();
// SAFETY: ptr covers region_size bytes of the mapping we just created.
let view = unsafe { std::slice::from_raw_parts_mut(ptr, region_size) };
assert_eq!(&view[..64], &pattern[..64]);
assert_eq!(view[0x10000], pattern[0x10000]);

// A write COWs privately: visible through the mapping, absent from the file.
view[0x10000] = 0xEE;
assert_eq!(view[0x10000], 0xEE);
let mut on_disk = vec![0u8; 4];
std::os::unix::fs::FileExt::read_exact_at(base.as_file(), &mut on_disk, 0x10000).unwrap();
assert_eq!(on_disk[0], pattern[0x10000], "guest write leaked into the base file");

// Hugetlbfs + base file is rejected.
let err = create_guest_memory(
&mem_state,
false,
HugePageConfig::Hugetlbfs2M,
Some(base.as_path()),
)
.unwrap_err();
assert!(matches!(err, GuestMemoryFromUffdError::BaseFileHugetlbfs));
}

#[test]
fn test_send_uffd_handshake() {
#[allow(deprecated)]
Expand Down
1 change: 1 addition & 0 deletions src/vmm/src/rpc_interface.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1341,6 +1341,7 @@ mod tests {
vsock_override: None,
clock_realtime: false,
shared: false,
uffd_base_file: None,
},
)));
check_unsupported(runtime_request(VmmAction::SetEntropyDevice(
Expand Down
15 changes: 15 additions & 0 deletions src/vmm/src/vmm_config/snapshot.rs
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,16 @@ pub struct LoadSnapshotParams {
/// memfile — the page-readable source for ADR 0045 post-copy. No effect for
/// the UFFD backend. Defaults to false (today's `MAP_PRIVATE` behavior).
pub shared: bool,
/// ADR 0045 unified memory substrate (v2b). When set with the Uffd memory
/// backend, guest memory is created as `MAP_PRIVATE` of this file (a
/// tmpfs/shmem per-template base) instead of anonymous, and the UFFD
/// registration uses `MISSING | MINOR` instead of MISSING-only. The
/// external handler then resolves base-identical pages with
/// `UFFDIO_CONTINUE` (shared page cache across same-template microVMs)
/// and session-divergent pages with `UFFDIO_COPY` (private); guest writes
/// COW natively. Invalid with the File backend. Requires a kernel with
/// UFFD minor faults on shmem (>= 5.13).
pub uffd_base_file: Option<PathBuf>,
}

/// Stores the configuration for loading a snapshot that is provided by the user.
Expand Down Expand Up @@ -136,6 +146,11 @@ pub struct LoadSnapshotConfig {
/// source). Top-level (not inside `mem_backend`). Defaults to false.
#[serde(default)]
pub shared: bool,
/// ADR 0045 substrate v2b: back Uffd-restored guest memory `MAP_PRIVATE`
/// by this shmem base file and register UFFD `MISSING | MINOR`. Top-level
/// (not inside `mem_backend`, which is `deny_unknown_fields`).
#[serde(skip_serializing_if = "Option::is_none")]
pub uffd_base_file: Option<PathBuf>,
}

/// Stores the configuration used for managing snapshot memory.
Expand Down
2 changes: 2 additions & 0 deletions src/vmm/tests/integration_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -318,6 +318,7 @@ fn verify_load_snapshot(snapshot_file: TempFile, memory_file: TempFile) {
vsock_override: None,
clock_realtime: false,
shared: false,
uffd_base_file: None,
}))
.unwrap();

Expand Down Expand Up @@ -405,6 +406,7 @@ fn verify_load_snap_disallowed_after_boot_resources(res: VmmAction, res_name: &s
vsock_override: None,
clock_realtime: false,
shared: false,
uffd_base_file: None,
});
let err = preboot_api_controller.handle_preboot_request(req);
assert!(
Expand Down
Loading