From 4a713cadba438d71060ee94e87146b1531ff945a Mon Sep 17 00:00:00 2001 From: "Matheus Henrique(Aka: TheusHen)" Date: Wed, 8 Jul 2026 15:39:28 -0300 Subject: [PATCH 1/2] fix: add KSM hints for memfd-backed forks --- crates/forkd-cli/src/doctor.rs | 104 ++++++++++++++++++++++++++ crates/forkd-cli/src/main.rs | 2 +- crates/forkd-controller/src/api.rs | 2 +- crates/forkd-controller/src/http.rs | 9 +++ crates/forkd-vmm/src/memfd.rs | 110 ++++++++++++++++++++++++++++ docs/API.md | 2 +- docs/design/userfaultfd.md | 21 ++++++ sdk/mcp/forkd_mcp/server.py | 2 +- 8 files changed, 248 insertions(+), 4 deletions(-) diff --git a/crates/forkd-cli/src/doctor.rs b/crates/forkd-cli/src/doctor.rs index ca4cf3e..fb79d96 100644 --- a/crates/forkd-cli/src/doctor.rs +++ b/crates/forkd-cli/src/doctor.rs @@ -98,6 +98,7 @@ pub fn run(daemon_url: &str, daemon_token: Option) -> anyhow::Result<()> // unprivileged_userfaultfd / runs an older kernel. check_uffd_wp(), check_memfd_create(), + check_ksm(), check_hugepages(), ]; @@ -678,6 +679,77 @@ fn check_memfd_create() -> Check { } } +fn check_ksm() -> Check { + #[cfg(target_os = "linux")] + { + let run = match std::fs::read_to_string("/sys/kernel/mm/ksm/run") { + Ok(v) => v, + Err(e) => { + return Check::warn( + "ksm", + format!("read /sys/kernel/mm/ksm/run: {e}"), + "kernel samepage merging unavailable; KSM hints are ignored", + ) + } + }; + let pages_to_scan = match std::fs::read_to_string("/sys/kernel/mm/ksm/pages_to_scan") { + Ok(v) => v, + Err(e) => { + return Check::warn( + "ksm", + format!("read /sys/kernel/mm/ksm/pages_to_scan: {e}"), + "expected when CONFIG_KSM is enabled", + ) + } + }; + check_ksm_values(run.trim(), pages_to_scan.trim()) + } + #[cfg(not(target_os = "linux"))] + { + Check::skip("ksm", "not Linux") + } +} + +#[cfg(target_os = "linux")] +fn check_ksm_values(run: &str, pages_to_scan: &str) -> Check { + let run = match run.parse::() { + Ok(v) => v, + Err(_) => { + return Check::warn( + "ksm", + format!("run={run} (unparseable)"), + "expected /sys/kernel/mm/ksm/run to be 1", + ) + } + }; + let pages_to_scan = match pages_to_scan.parse::() { + Ok(v) => v, + Err(_) => { + return Check::warn( + "ksm", + format!("pages_to_scan={pages_to_scan} (unparseable)"), + "expected /sys/kernel/mm/ksm/pages_to_scan to be >0", + ) + } + }; + + if run != 1 { + return Check::warn( + "ksm", + format!("run={run}, pages_to_scan={pages_to_scan}"), + "enable KSM: echo 1 | sudo tee /sys/kernel/mm/ksm/run", + ); + } + if pages_to_scan == 0 { + return Check::warn( + "ksm", + "run=1 but pages_to_scan=0", + "raise the scan budget: echo 1000 | sudo tee /sys/kernel/mm/ksm/pages_to_scan", + ); + } + Check::pass("ksm", format!("run=1, pages_to_scan={pages_to_scan}")) +} + fn check_hugepages() -> Check { #[cfg(target_os = "linux")] { @@ -760,3 +832,35 @@ fn check_daemon(daemon_url: &str, token: Option<&str>) -> Check { ), } } + +#[cfg(all(test, target_os = "linux"))] +mod tests { + use super::*; + + #[test] + fn ksm_passes_when_enabled_and_scanning() { + let check = check_ksm_values("1", "1000"); + assert!(check.status == Status::Pass); + } + + #[test] + fn ksm_warns_when_disabled() { + let check = check_ksm_values("0", "1000"); + assert!(check.status == Status::Warn); + assert!(check.detail.contains("run=0")); + } + + #[test] + fn ksm_warns_on_unexpected_run_mode() { + let check = check_ksm_values("2", "1000"); + assert!(check.status == Status::Warn); + assert!(check.detail.contains("run=2")); + } + + #[test] + fn ksm_warns_when_scan_budget_is_zero() { + let check = check_ksm_values("1", "0"); + assert!(check.status == Status::Warn); + assert!(check.detail.contains("pages_to_scan=0")); + } +} diff --git a/crates/forkd-cli/src/main.rs b/crates/forkd-cli/src/main.rs index d078214..493ad97 100644 --- a/crates/forkd-cli/src/main.rs +++ b/crates/forkd-cli/src/main.rs @@ -68,7 +68,7 @@ enum Cmd { Snapshot { /// Name of the snapshot. Becomes `~/.local/share/forkd/snapshots//`. /// With `--from-sandbox`, leave unset to let the daemon generate - /// `branch--`. + /// `branch---`. #[arg(long)] tag: Option, /// Branch a running sandbox instead of booting a fresh parent VM. diff --git a/crates/forkd-controller/src/api.rs b/crates/forkd-controller/src/api.rs index 47f6c2a..9ba186c 100644 --- a/crates/forkd-controller/src/api.rs +++ b/crates/forkd-controller/src/api.rs @@ -103,7 +103,7 @@ pub enum SnapshotStatus { #[derive(Debug, Clone, Serialize, Deserialize)] pub struct BranchSandboxRequest { /// Optional tag for the new snapshot. When unset the controller - /// generates `branch--`. + /// generates `branch---`. #[serde(default)] pub tag: Option, /// Phase 1a measurement hook: take a Diff snapshot in addition to diff --git a/crates/forkd-controller/src/http.rs b/crates/forkd-controller/src/http.rs index e3099c4..a0d29ca 100644 --- a/crates/forkd-controller/src/http.rs +++ b/crates/forkd-controller/src/http.rs @@ -2093,6 +2093,15 @@ fn run_live_branch_setup( std::io::Error::last_os_error() ); } + if let Err(e) = + unsafe { forkd_vmm::memfd::advise_ksm_mergeable_mapping(region_ptr, region_size) } + { + tracing::warn!( + error = %e, + bytes = region_size, + "MADV_MERGEABLE failed for controller-side memfd mmap; continuing without KSM hint" + ); + } let mmap_guard = MmapGuard { ptr: region_ptr, size: region_size, diff --git a/crates/forkd-vmm/src/memfd.rs b/crates/forkd-vmm/src/memfd.rs index 7813183..52e11b7 100644 --- a/crates/forkd-vmm/src/memfd.rs +++ b/crates/forkd-vmm/src/memfd.rs @@ -53,9 +53,29 @@ const HUGE_PAGE_2MB: u64 = 2 * 1024 * 1024; pub struct MemfdRegion { #[cfg(target_os = "linux")] file: File, + #[cfg(target_os = "linux")] + _mergeable_mapping: Option, size_bytes: u64, } +#[cfg(target_os = "linux")] +#[derive(Debug)] +struct MergeableMapping { + addr: usize, + size: usize, +} + +#[cfg(target_os = "linux")] +impl Drop for MergeableMapping { + fn drop(&mut self) { + // SAFETY: `addr`/`size` describe the mmap created by + // create_mergeable_mapping and owned by this guard. + unsafe { + libc::munmap(self.addr as *mut libc::c_void, self.size); + } + } +} + impl MemfdRegion { /// Logical size of the region in bytes. pub fn size_bytes(&self) -> u64 { @@ -87,6 +107,30 @@ impl MemfdRegion { } } +/// Mark a live mmap as KSM-mergeable. +/// +/// `MADV_MERGEABLE` is VMA-scoped, so callers must pass the actual mapping +/// that should be scanned. This is best-effort: hosts with KSM disabled still +/// accept the hint, but merging only happens when `/sys/kernel/mm/ksm/run` is +/// enabled and the scanner is configured to inspect pages. +/// +/// # Safety +/// +/// `ptr` must be either null or the start of a live mapping that is valid for +/// `size` bytes in the current process. The mapping must outlive this call. +#[cfg(target_os = "linux")] +pub unsafe fn advise_ksm_mergeable_mapping(ptr: *mut libc::c_void, size: usize) -> io::Result<()> { + if ptr.is_null() || size == 0 { + return Ok(()); + } + let rc = unsafe { libc::madvise(ptr, size, libc::MADV_MERGEABLE) }; + if rc == 0 { + Ok(()) + } else { + Err(io::Error::last_os_error()) + } +} + /// Create a memfd, size it to the source file's length, and copy the /// source bytes in. /// @@ -181,12 +225,78 @@ pub fn create_and_populate(source: &Path, name: &str, use_hugepages: bool) -> Re ); } + let mergeable_mapping = if backed_by_hugepages { + None + } else { + create_mergeable_mapping(&memfd, size_bytes, name) + }; + Ok(MemfdRegion { file: memfd, + _mergeable_mapping: mergeable_mapping, size_bytes, }) } +#[cfg(target_os = "linux")] +fn create_mergeable_mapping(file: &File, size_bytes: u64, name: &str) -> Option { + use std::os::fd::AsRawFd; + + let size: usize = match size_bytes.try_into() { + Ok(0) => return None, + Ok(size) => size, + Err(_) => { + tracing::warn!( + bytes = size_bytes, + "memfd '{name}' too large to mmap for KSM hint on this platform" + ); + return None; + } + }; + + // SAFETY: file is an open memfd sized to at least `size`; MAP_SHARED + // keeps this VMA tied to the same backing pages Firecracker restores + // from. MAP_POPULATE installs PTEs up front so KSM can scan promptly + // instead of waiting for the controller to fault pages lazily. The guard + // below unmaps it when the MemfdRegion is dropped. + let ptr = unsafe { + libc::mmap( + std::ptr::null_mut(), + size, + libc::PROT_READ | libc::PROT_WRITE, + libc::MAP_SHARED | libc::MAP_POPULATE, + file.as_raw_fd(), + 0, + ) + }; + if ptr == libc::MAP_FAILED { + tracing::warn!( + error = %io::Error::last_os_error(), + bytes = size, + "mmap memfd '{name}' for KSM hint failed; continuing without MADV_MERGEABLE" + ); + return None; + } + + if let Err(e) = unsafe { advise_ksm_mergeable_mapping(ptr, size) } { + tracing::warn!( + error = %e, + bytes = size, + "MADV_MERGEABLE failed for memfd '{name}'; continuing without KSM hint" + ); + // SAFETY: ptr/size are the mapping created above in this function. + unsafe { + libc::munmap(ptr, size); + } + return None; + } + + Some(MergeableMapping { + addr: ptr as usize, + size, + }) +} + /// Populate a hugetlb-backed memfd from a source file via mmap + memcpy. /// /// HugeTLB files don't support write(). This method populates these pages diff --git a/docs/API.md b/docs/API.md index c0465c8..0698f01 100644 --- a/docs/API.md +++ b/docs/API.md @@ -255,7 +255,7 @@ Request: ``` - `tag` is optional. When unset the daemon generates - `branch--`. Must match + `branch---`. Must match `^[A-Za-z0-9_-]{1,64}$` (1–64 chars, ASCII alphanumeric plus `-`/`_`). - `mode` (v0.4+) is one of `"full"`, `"diff"`, `"live"`. Defaults to `"full"` when unset. `"live"` requires the source sandbox to have diff --git a/docs/design/userfaultfd.md b/docs/design/userfaultfd.md index dd06686..4ea878f 100644 --- a/docs/design/userfaultfd.md +++ b/docs/design/userfaultfd.md @@ -59,6 +59,27 @@ Source: Firecracker docs on snapshot-resume page-fault handling (v1.10.1), reviewed against `src/firecracker/examples/uffd/` sample handlers and the CodeSandbox blog post on UFFD page sharing. +### Answer to issue #257 + +Keep `MemoryBackend::File` as the default for normal spawn/fan-out. +The file-backed `MAP_PRIVATE` restore path is the memory-efficient +primitive: clean pages are shared by the kernel page cache across all +children, and only divergent pages are copied. A UFFD-backed spawn path +would serve first touches with `UFFDIO_COPY`, which turns shared clean +pages into per-child private copies and loses the main density benefit. + +The UFFD-backed work that did ship is for live BRANCH, not ordinary +spawn. That path uses `MemoryBackend::MemfdShared` plus +`WpBranch`: Firecracker maps the running source's RAM from a shared +memfd, registers write-protect userfaultfd on that mapping, and the +controller copies clean and dirtied pages while the source resumes. + +The older `MemoryBackend::Userfault { handler_sock }` shape and its +per-child handler socket are historical scaffolding. New work should not +anchor on that socket topology unless the design is reopened; a shared +handler/routing layer remains an open design question, not a committed +production direction. + ## The design we actually want: UFFD_WP-mediated live fork The MITOSIS / NFork approach. Sketch: diff --git a/sdk/mcp/forkd_mcp/server.py b/sdk/mcp/forkd_mcp/server.py index 9dd77b7..29a06f1 100644 --- a/sdk/mcp/forkd_mcp/server.py +++ b/sdk/mcp/forkd_mcp/server.py @@ -124,7 +124,7 @@ def branch_sandbox( Args: sandbox_id: Id of the source sandbox (see list_sandboxes). tag: Optional name for the new snapshot. When unset the - daemon generates `branch--`. + daemon generates `branch---`. mode: v0.4+ canonical mode selector. One of "full", "diff", "live". Prefer this over the legacy `diff` boolean. "live" requires the source to have been spawned with From e13965ed1f15718329120c65f825b8490dd822a7 Mon Sep 17 00:00:00 2001 From: "Matheus Henrique(Aka: TheusHen)" Date: Thu, 9 Jul 2026 00:19:38 -0300 Subject: [PATCH 2/2] test: add KSM memfd probe evidence --- bench/ksm-memfd/RESULTS.md | 51 ++++++++ bench/ksm-memfd/probe.py | 191 ++++++++++++++++++++++++++++ crates/forkd-controller/src/http.rs | 9 -- crates/forkd-vmm/src/memfd.rs | 110 ---------------- 4 files changed, 242 insertions(+), 119 deletions(-) create mode 100644 bench/ksm-memfd/RESULTS.md create mode 100644 bench/ksm-memfd/probe.py diff --git a/bench/ksm-memfd/RESULTS.md b/bench/ksm-memfd/RESULTS.md new file mode 100644 index 0000000..f1fc37e --- /dev/null +++ b/bench/ksm-memfd/RESULTS.md @@ -0,0 +1,51 @@ +# KSM memfd probe + +This probe checks whether `MADV_MERGEABLE` produces KSM sharing for +memfd-backed `MAP_SHARED` regions. + +Why this exists: issue #5 asks for directed KSM hints for fork families. +For the v0.4 live-fork path the tempting implementation is to mark the +controller's memfd mapping mergeable. The Linux KSM documentation says +KSM targets private anonymous pages, though, so this benchmark keeps the +evidence next to the code before forkd grows a misleading hint. + +## Environment + +- Run from Docker with `--privileged` +- Host KSM sysfs writable at `/sys/kernel/mm/ksm` +- Probe command: + +```bash +docker run --rm --privileged \ + -v "$PWD:/work" -w /work \ + python:3.12-slim \ + bash -lc 'python bench/ksm-memfd/probe.py --pages 4096 --timeout 30' +``` + +The probe saves and restores `run`, `pages_to_scan`, and +`sleep_millisecs`. Before each case it writes `run=2` to unmerge any +existing KSM pages, waits for `pages_sharing=0`, then enables KSM and +waits for scans. + +## Result + +```text +anonymous-private + before={'pages_shared': 0, 'pages_sharing': 0, 'pages_unshared': 0, 'pages_volatile': 0, 'full_scans': 0} + after ={'pages_shared': 2, 'pages_sharing': 510, 'pages_unshared': 0, 'pages_volatile': 5069, 'full_scans': 2} + delta ={'pages_shared': 2, 'pages_sharing': 510, 'pages_unshared': 0, 'pages_volatile': 5069, 'full_scans': 2} +memfd-map-shared-two-fds + before={'pages_shared': 0, 'pages_sharing': 0, 'pages_unshared': 0, 'pages_volatile': 0, 'full_scans': 0} + after ={'pages_shared': 0, 'pages_sharing': 0, 'pages_unshared': 0, 'pages_volatile': 0, 'full_scans': 0} + delta ={'pages_shared': 0, 'pages_sharing': 0, 'pages_unshared': 0, 'pages_volatile': 0, 'full_scans': 0} +``` + +The control case confirms KSM is active and can merge identical +`MADV_MERGEABLE` anonymous private pages. The memfd `MAP_SHARED` case +does not move `pages_shared` or `pages_sharing`, so forkd should not add a +controller-side memfd `MADV_MERGEABLE` mapping as if it fixed #5. + +The useful part of #5 that remains in this PR is the `forkd doctor` KSM +check. A future implementation should target a backing/mapping shape that +KSM actually scans, and should include this probe (or a forkd-specific +variant of it) in the evidence. diff --git a/bench/ksm-memfd/probe.py b/bench/ksm-memfd/probe.py new file mode 100644 index 0000000..11aa4ac --- /dev/null +++ b/bench/ksm-memfd/probe.py @@ -0,0 +1,191 @@ +#!/usr/bin/env python3 +""" +Probe whether KSM merges MADV_MERGEABLE memfd MAP_SHARED mappings. + +This is intentionally small and direct: it maps two same-sized regions, +fills both with identical page contents, marks the VMAs mergeable, then +waits for ksmd full scans and reports /sys/kernel/mm/ksm counters. + +Run on a Linux host where the caller can write /sys/kernel/mm/ksm: + + sudo python3 bench/ksm-memfd/probe.py +""" + +import argparse +import ctypes +import multiprocessing as mp +import os +import signal +import time + +libc = ctypes.CDLL(None, use_errno=True) + +MADV_MERGEABLE = 12 +MAP_PRIVATE = 0x02 +MAP_ANONYMOUS = 0x20 +MAP_SHARED = 0x01 +PROT_READ = 0x1 +PROT_WRITE = 0x2 +MAP_FAILED = ctypes.c_void_p(-1).value + +PAGE = os.sysconf("SC_PAGE_SIZE") +PATTERN = b"forkd-ksm-page".ljust(PAGE, b"x") +KSM = "/sys/kernel/mm/ksm" + +libc.mmap.restype = ctypes.c_void_p +libc.mmap.argtypes = [ + ctypes.c_void_p, + ctypes.c_size_t, + ctypes.c_int, + ctypes.c_int, + ctypes.c_int, + ctypes.c_long, +] +libc.madvise.argtypes = [ctypes.c_void_p, ctypes.c_size_t, ctypes.c_int] + + +def read_ksm(): + out = {} + for name in [ + "pages_shared", + "pages_sharing", + "pages_unshared", + "pages_volatile", + "full_scans", + ]: + with open(f"{KSM}/{name}", "r", encoding="utf-8") as f: + out[name] = int(f.read().strip()) + return out + + +def write_ksm(name, value): + with open(f"{KSM}/{name}", "w", encoding="utf-8") as f: + f.write(str(value)) + + +def configure_ksm(pages): + saved = {} + for name in ["run", "pages_to_scan", "sleep_millisecs"]: + with open(f"{KSM}/{name}", "r", encoding="utf-8") as f: + saved[name] = f.read().strip() + write_ksm("run", 2) + wait_for(lambda counters: counters["pages_sharing"] == 0, 10) + write_ksm("run", 1) + write_ksm("pages_to_scan", max(pages * 4, 10000)) + write_ksm("sleep_millisecs", 20) + return saved + + +def restore_ksm(saved): + for name, value in saved.items(): + try: + write_ksm(name, value) + except OSError: + pass + + +def mmap_region(size, flags, fd=-1): + ptr = libc.mmap(None, size, PROT_READ | PROT_WRITE, flags, fd, 0) + if ptr == MAP_FAILED: + err = ctypes.get_errno() + raise OSError(err, os.strerror(err)) + rc = libc.madvise(ctypes.c_void_p(ptr), size, MADV_MERGEABLE) + if rc != 0: + err = ctypes.get_errno() + raise OSError(err, f"madvise MADV_MERGEABLE: {os.strerror(err)}") + return ptr + + +def fill(ptr, pages): + for i in range(pages): + ctypes.memmove(ptr + i * PAGE, PATTERN, PAGE) + + +def child(kind, pages, ready): + size = pages * PAGE + if kind == "anonymous-private": + ptrs = [ + mmap_region(size, MAP_PRIVATE | MAP_ANONYMOUS), + mmap_region(size, MAP_PRIVATE | MAP_ANONYMOUS), + ] + elif kind == "memfd-map-shared-two-fds": + fds = [ + os.memfd_create("forkd-ksm-a", os.MFD_CLOEXEC), + os.memfd_create("forkd-ksm-b", os.MFD_CLOEXEC), + ] + for fd in fds: + os.ftruncate(fd, size) + ptrs = [mmap_region(size, MAP_SHARED, fd) for fd in fds] + else: + raise ValueError(kind) + + for ptr in ptrs: + fill(ptr, pages) + + ready.send(os.getpid()) + signal.pause() + + +def wait_for(predicate, timeout): + deadline = time.time() + timeout + last = None + while time.time() < deadline: + last = read_ksm() + if predicate(last): + return last + time.sleep(0.2) + return last or read_ksm() + + +def run_case(kind, pages, timeout): + write_ksm("run", 2) + wait_for(lambda counters: counters["pages_sharing"] == 0, timeout) + write_ksm("run", 1) + before = read_ksm() + parent, child_conn = mp.Pipe(duplex=False) + proc = mp.Process(target=child, args=(kind, pages, child_conn)) + proc.start() + parent.recv() + if not proc.is_alive(): + raise RuntimeError(f"{kind} child exited before KSM scan") + after = wait_for( + lambda counters: counters["full_scans"] >= before["full_scans"] + 2 + and counters["pages_sharing"] > before["pages_sharing"], + timeout, + ) + proc.terminate() + proc.join(timeout=5) + wait_for( + lambda counters: counters["pages_sharing"] <= before["pages_sharing"], + timeout, + ) + return before, after + + +def delta(before, after): + return {k: after[k] - before[k] for k in before} + + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument("--pages", type=int, default=4096) + parser.add_argument("--timeout", type=float, default=20.0) + args = parser.parse_args() + + if not os.path.exists(f"{KSM}/run"): + raise SystemExit("KSM sysfs is not available") + + saved = configure_ksm(args.pages) + try: + for kind in ["anonymous-private", "memfd-map-shared-two-fds"]: + before, after = run_case(kind, args.pages, args.timeout) + print(kind) + print(f" before={before}") + print(f" after ={after}") + print(f" delta ={delta(before, after)}") + finally: + restore_ksm(saved) + + +if __name__ == "__main__": + main() diff --git a/crates/forkd-controller/src/http.rs b/crates/forkd-controller/src/http.rs index a0d29ca..e3099c4 100644 --- a/crates/forkd-controller/src/http.rs +++ b/crates/forkd-controller/src/http.rs @@ -2093,15 +2093,6 @@ fn run_live_branch_setup( std::io::Error::last_os_error() ); } - if let Err(e) = - unsafe { forkd_vmm::memfd::advise_ksm_mergeable_mapping(region_ptr, region_size) } - { - tracing::warn!( - error = %e, - bytes = region_size, - "MADV_MERGEABLE failed for controller-side memfd mmap; continuing without KSM hint" - ); - } let mmap_guard = MmapGuard { ptr: region_ptr, size: region_size, diff --git a/crates/forkd-vmm/src/memfd.rs b/crates/forkd-vmm/src/memfd.rs index 52e11b7..7813183 100644 --- a/crates/forkd-vmm/src/memfd.rs +++ b/crates/forkd-vmm/src/memfd.rs @@ -53,29 +53,9 @@ const HUGE_PAGE_2MB: u64 = 2 * 1024 * 1024; pub struct MemfdRegion { #[cfg(target_os = "linux")] file: File, - #[cfg(target_os = "linux")] - _mergeable_mapping: Option, size_bytes: u64, } -#[cfg(target_os = "linux")] -#[derive(Debug)] -struct MergeableMapping { - addr: usize, - size: usize, -} - -#[cfg(target_os = "linux")] -impl Drop for MergeableMapping { - fn drop(&mut self) { - // SAFETY: `addr`/`size` describe the mmap created by - // create_mergeable_mapping and owned by this guard. - unsafe { - libc::munmap(self.addr as *mut libc::c_void, self.size); - } - } -} - impl MemfdRegion { /// Logical size of the region in bytes. pub fn size_bytes(&self) -> u64 { @@ -107,30 +87,6 @@ impl MemfdRegion { } } -/// Mark a live mmap as KSM-mergeable. -/// -/// `MADV_MERGEABLE` is VMA-scoped, so callers must pass the actual mapping -/// that should be scanned. This is best-effort: hosts with KSM disabled still -/// accept the hint, but merging only happens when `/sys/kernel/mm/ksm/run` is -/// enabled and the scanner is configured to inspect pages. -/// -/// # Safety -/// -/// `ptr` must be either null or the start of a live mapping that is valid for -/// `size` bytes in the current process. The mapping must outlive this call. -#[cfg(target_os = "linux")] -pub unsafe fn advise_ksm_mergeable_mapping(ptr: *mut libc::c_void, size: usize) -> io::Result<()> { - if ptr.is_null() || size == 0 { - return Ok(()); - } - let rc = unsafe { libc::madvise(ptr, size, libc::MADV_MERGEABLE) }; - if rc == 0 { - Ok(()) - } else { - Err(io::Error::last_os_error()) - } -} - /// Create a memfd, size it to the source file's length, and copy the /// source bytes in. /// @@ -225,78 +181,12 @@ pub fn create_and_populate(source: &Path, name: &str, use_hugepages: bool) -> Re ); } - let mergeable_mapping = if backed_by_hugepages { - None - } else { - create_mergeable_mapping(&memfd, size_bytes, name) - }; - Ok(MemfdRegion { file: memfd, - _mergeable_mapping: mergeable_mapping, size_bytes, }) } -#[cfg(target_os = "linux")] -fn create_mergeable_mapping(file: &File, size_bytes: u64, name: &str) -> Option { - use std::os::fd::AsRawFd; - - let size: usize = match size_bytes.try_into() { - Ok(0) => return None, - Ok(size) => size, - Err(_) => { - tracing::warn!( - bytes = size_bytes, - "memfd '{name}' too large to mmap for KSM hint on this platform" - ); - return None; - } - }; - - // SAFETY: file is an open memfd sized to at least `size`; MAP_SHARED - // keeps this VMA tied to the same backing pages Firecracker restores - // from. MAP_POPULATE installs PTEs up front so KSM can scan promptly - // instead of waiting for the controller to fault pages lazily. The guard - // below unmaps it when the MemfdRegion is dropped. - let ptr = unsafe { - libc::mmap( - std::ptr::null_mut(), - size, - libc::PROT_READ | libc::PROT_WRITE, - libc::MAP_SHARED | libc::MAP_POPULATE, - file.as_raw_fd(), - 0, - ) - }; - if ptr == libc::MAP_FAILED { - tracing::warn!( - error = %io::Error::last_os_error(), - bytes = size, - "mmap memfd '{name}' for KSM hint failed; continuing without MADV_MERGEABLE" - ); - return None; - } - - if let Err(e) = unsafe { advise_ksm_mergeable_mapping(ptr, size) } { - tracing::warn!( - error = %e, - bytes = size, - "MADV_MERGEABLE failed for memfd '{name}'; continuing without KSM hint" - ); - // SAFETY: ptr/size are the mapping created above in this function. - unsafe { - libc::munmap(ptr, size); - } - return None; - } - - Some(MergeableMapping { - addr: ptr as usize, - size, - }) -} - /// Populate a hugetlb-backed memfd from a source file via mmap + memcpy. /// /// HugeTLB files don't support write(). This method populates these pages