Skip to content

Commit 7d80afa

Browse files
committed
feat(api): POST /uffd/wp endpoint for snapshot-side WP
Add a Firecracker API endpoint that lets an external tool (forkd-controller for the v0.4 live-fork path) arm UFFD-WP on guest memory. FC creates the userfaultfd, issues UFFDIO_REGISTER with WRITE_PROTECT mode against every guest_memory region, then connects to the caller-supplied UDS and ships the fd via SCM_RIGHTS along with a JSON descriptor of the regions. Why FC has to be the one creating the uffd: UFFDIO_REGISTER can only register VMAs in the same process that called userfaultfd(2). KVM runs inside FC's process; guest writes go through FC's EPT/VMA. A controller-side uffd registered against its own mmap would never see KVM writes. Modeling on FC's existing restore-side Uffd backend (which sends the fd from FC to the page-fault handler), this new endpoint flips the direction for the snapshot side. Request shape: PUT /uffd/wp {"socket": "/run/forkd-controller/.../wp.sock"} -> 204 No Content -> SCM_RIGHTS message on the socket carrying the uffd fd plus a JSON array of GuestRegionUffdMapping describing what was registered (matches the existing restore-side handshake). Patch covers five files because SnapshotType's pre-existing exhaustive matches force a touch in each: - src/vmm/src/vmm_config/wp_uffd.rs (new) - request params - src/vmm/src/persist.rs - setup_wp_uffd + error type - src/vmm/src/rpc_interface.rs - VmmAction variant + dispatch - src/firecracker/src/api_server/mod.rs - metric branch - src/firecracker/src/api_server/request/wp_uffd.rs (new) - route parser - src/firecracker/src/api_server/parsed_request.rs - routing - src/firecracker/src/api_server/request/mod.rs - mod declaration - src/vmm/Cargo.toml - userfaultfd "linux5_7" feature End-to-end smoke test (via a memfd-backed restore so the WP target VMA is shmem, not ext4): /snapshot/load -> 204 FC mmap: /memfd:forkd-wp-test (deleted), 512 MiB, MAP_SHARED /uffd/wp -> 204 Receiver got: 1 region (size=512MiB, page=4KiB), fd -> anon_inode:[userfaultfd] Note: UFFD_WP requires shmem-backed (or anon) VMAs, not ext4 file-backed. Hitting this endpoint against a snapshot restored with backend_type=File/backend_path=<ext4 file> returns EINVAL on UFFDIO_REGISTER — the kernel correctly refuses to WP a file-backed VMA. forkd-controller's v0.4 path goes through memfd (Phase 5b: MemoryBackend::MemfdShared), so this is fine for the intended caller.
1 parent c5dff4b commit 7d80afa

7 files changed

Lines changed: 96 additions & 2 deletions

File tree

src/firecracker/src/api_server/mod.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,12 @@ impl ApiServer {
166166
VmmAction::LoadSnapshot(_) => {
167167
Some((&METRICS.latencies_us.load_snapshot, "load snapshot"))
168168
}
169+
VmmAction::SetupWpUffd(_) => Some((
170+
// Reuse full-snapshot bucket — setup is one syscall + send,
171+
// doesn't warrant its own metric for v0.4 experimental.
172+
&METRICS.latencies_us.full_create_snapshot,
173+
"setup wp uffd",
174+
)),
169175
VmmAction::Pause => Some((&METRICS.latencies_us.pause_vm, "pause vm")),
170176
VmmAction::Resume => Some((&METRICS.latencies_us.resume_vm, "resume vm")),
171177
_ => None,

src/firecracker/src/api_server/parsed_request.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ use super::request::metrics::parse_put_metrics;
2525
use super::request::mmds::{parse_get_mmds, parse_patch_mmds, parse_put_mmds};
2626
use super::request::net::{parse_patch_net, parse_put_net};
2727
use super::request::snapshot::{parse_patch_vm_state, parse_put_snapshot};
28+
use super::request::wp_uffd::parse_put_uffd;
2829
use super::request::version::parse_get_version;
2930
use super::request::vsock::parse_put_vsock;
3031

@@ -97,6 +98,7 @@ impl TryFrom<&Request> for ParsedRequest {
9798
parse_put_net(body, path_tokens.next())
9899
}
99100
(Method::Put, "snapshot", Some(body)) => parse_put_snapshot(body, path_tokens.next()),
101+
(Method::Put, "uffd", Some(body)) => parse_put_uffd(body, path_tokens.next()),
100102
(Method::Put, "vsock", Some(body)) => parse_put_vsock(body),
101103
(Method::Put, "entropy", Some(body)) => parse_put_entropy(body),
102104
(Method::Put, _, None) => method_to_error(Method::Put),

src/firecracker/src/api_server/request/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ pub mod metrics;
1414
pub mod mmds;
1515
pub mod net;
1616
pub mod snapshot;
17+
pub mod wp_uffd;
1718
pub mod version;
1819
pub mod vsock;
1920
pub use micro_http::{Body, Method, StatusCode};

src/vmm/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ serde_json = "1.0.140"
3636
slab = "0.4.7"
3737
thiserror = "2.0.12"
3838
timerfd = "1.5.0"
39-
userfaultfd = "0.8.1"
39+
userfaultfd = { version = "0.8.1", features = ["linux5_7"] }
4040
utils = { path = "../utils" }
4141
vhost = { version = "0.13.0", features = ["vhost-user-frontend"] }
4242
vm-allocator = "0.1.0"

src/vmm/src/persist.rs

Lines changed: 63 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ use crate::vmm_config::machine_config::{HugePageConfig, MachineConfigError, Mach
3737
use crate::vmm_config::snapshot::{CreateSnapshotParams, LoadSnapshotParams, MemBackendType, SnapshotType};
3838
use crate::vstate::kvm::KvmState;
3939
use crate::vstate::memory;
40-
use crate::vstate::memory::{GuestMemoryState, GuestRegionMmap, MemoryError};
40+
use crate::vstate::memory::{GuestMemory, GuestMemoryRegion, GuestMemoryState, GuestRegionMmap, MemoryError};
4141
use crate::vstate::vcpu::{VcpuSendEventError, VcpuState};
4242
use crate::vstate::vm::VmState;
4343
use crate::{EventManager, Vmm, vstate};
@@ -485,6 +485,68 @@ pub enum GuestMemoryFromUffdError {
485485
Send(#[from] vmm_sys_util::errno::Error),
486486
}
487487

488+
/// Error from [`setup_wp_uffd`].
489+
#[derive(Debug, thiserror::Error, displaydoc::Display)]
490+
pub enum SetupWpUffdError {
491+
/// Failed to create userfaultfd: {0}
492+
Create(userfaultfd::Error),
493+
/// Failed to register guest memory region with userfaultfd: {0}
494+
Register(userfaultfd::Error),
495+
/// Failed to connect to listener UDS: {0}
496+
Connect(#[from] std::io::Error),
497+
/// Failed to send file descriptor: {0}
498+
Send(#[from] vmm_sys_util::errno::Error),
499+
/// Failed to serialize region descriptors: {0}
500+
Serialize(#[from] serde_json::Error),
501+
}
502+
503+
/// Set up a snapshot-side WP userfaultfd over the running VM's guest
504+
/// memory, then hand the fd to the listener at `socket_path` via
505+
/// SCM_RIGHTS.
506+
///
507+
/// Counterpart to [`guest_memory_from_uffd`] (the restore-side UFFD
508+
/// path): same handshake, but register mode is WP instead of MISSING,
509+
/// and we don't allocate new guest memory — we register over what FC
510+
/// is already running.
511+
pub fn setup_wp_uffd(
512+
vmm: &crate::Vmm,
513+
socket_path: &std::path::Path,
514+
) -> Result<(), SetupWpUffdError> {
515+
use userfaultfd::{FeatureFlags, RegisterMode, UffdBuilder};
516+
517+
let uffd = UffdBuilder::new()
518+
.require_features(FeatureFlags::PAGEFAULT_FLAG_WP)
519+
.close_on_exec(true)
520+
.non_blocking(true)
521+
.user_mode_only(false)
522+
.create()
523+
.map_err(SetupWpUffdError::Create)?;
524+
525+
let guest_memory = vmm.vm.guest_memory();
526+
let mut backend_mappings: Vec<GuestRegionUffdMapping> = Vec::new();
527+
let mut offset = 0u64;
528+
for region in guest_memory.iter() {
529+
let addr = region.as_ptr() as *mut libc::c_void;
530+
let size = region.size();
531+
uffd.register_with_mode(addr, size, RegisterMode::WRITE_PROTECT)
532+
.map_err(SetupWpUffdError::Register)?;
533+
#[allow(deprecated)]
534+
backend_mappings.push(GuestRegionUffdMapping {
535+
base_host_virt_addr: region.as_ptr() as u64,
536+
size,
537+
offset,
538+
page_size: 4096,
539+
page_size_kib: 4096,
540+
});
541+
offset += size as u64;
542+
}
543+
544+
let mappings_json = serde_json::to_string(&backend_mappings)?;
545+
let socket = UnixStream::connect(socket_path)?;
546+
socket.send_with_fd(mappings_json.as_bytes(), uffd.as_raw_fd())?;
547+
Ok(())
548+
}
549+
488550
fn guest_memory_from_uffd(
489551
mem_uds_path: &Path,
490552
mem_state: &GuestMemoryState,

src/vmm/src/rpc_interface.rs

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ use crate::vmm_config::net::{
3434
NetworkInterfaceConfig, NetworkInterfaceError, NetworkInterfaceUpdateConfig,
3535
};
3636
use crate::vmm_config::snapshot::{CreateSnapshotParams, LoadSnapshotParams, SnapshotType};
37+
use crate::vmm_config::wp_uffd::SetupWpUffdParams;
3738
use crate::vmm_config::vsock::{VsockConfigError, VsockDeviceConfig};
3839
use crate::vmm_config::{self, RateLimiterUpdate};
3940

@@ -53,6 +54,12 @@ pub enum VmmAction {
5354
/// Create a snapshot using as input the `CreateSnapshotParams`. This action can only be called
5455
/// after the microVM has booted and only when the microVM is in `Paused` state.
5556
CreateSnapshot(CreateSnapshotParams),
57+
/// Set up a write-protected userfaultfd over guest memory, register
58+
/// every region in WP mode, and send the resulting fd to a UDS the
59+
/// caller is listening on (via SCM_RIGHTS). Can only be called
60+
/// after the microVM has booted; does not require the VM to be
61+
/// paused.
62+
SetupWpUffd(SetupWpUffdParams),
5663
/// Get the balloon device configuration.
5764
GetBalloonConfig,
5865
/// Get the ballon device latest statistics.
@@ -142,6 +149,8 @@ pub enum VmmActionError {
142149
InternalVmm(#[from] VmmError),
143150
/// Load snapshot error: {0}
144151
LoadSnapshot(#[from] LoadSnapshotError),
152+
/// Setup WP uffd error: {0}
153+
SetupWpUffd(#[from] crate::persist::SetupWpUffdError),
145154
/// Logger error: {0}
146155
Logger(#[from] crate::logger::LoggerUpdateError),
147156
/// Machine config error: {0}
@@ -438,6 +447,7 @@ impl<'a> PrebootApiController<'a> {
438447
SetEntropyDevice(config) => self.set_entropy_device(config),
439448
// Operations not allowed pre-boot.
440449
CreateSnapshot(_)
450+
| SetupWpUffd(_)
441451
| FlushMetrics
442452
| Pause
443453
| Resume
@@ -627,6 +637,7 @@ impl RuntimeApiController {
627637
match request {
628638
// Supported operations allowed post-boot.
629639
CreateSnapshot(snapshot_create_cfg) => self.create_snapshot(&snapshot_create_cfg),
640+
SetupWpUffd(params) => self.setup_wp_uffd(&params),
630641
FlushMetrics => self.flush_metrics(),
631642
GetBalloonConfig => self
632643
.vmm
@@ -749,6 +760,17 @@ impl RuntimeApiController {
749760
.map_err(VmmActionError::InternalVmm)
750761
}
751762

763+
fn setup_wp_uffd(
764+
&mut self,
765+
params: &SetupWpUffdParams,
766+
) -> Result<VmmData, VmmActionError> {
767+
log_dev_preview_warning("Snapshot-side WP userfaultfd", None);
768+
let locked_vmm = self.vmm.lock().expect("Poisoned lock");
769+
crate::persist::setup_wp_uffd(&locked_vmm, &params.socket)
770+
.map_err(VmmActionError::SetupWpUffd)?;
771+
Ok(VmmData::Empty)
772+
}
773+
752774
fn create_snapshot(
753775
&mut self,
754776
create_params: &CreateSnapshotParams,

src/vmm/src/vmm_config/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ pub mod mmds;
3232
pub mod net;
3333
/// Wrapper for configuring microVM snapshots and the microVM state.
3434
pub mod snapshot;
35+
pub mod wp_uffd;
3536
/// Wrapper for configuring the vsock devices attached to the microVM.
3637
pub mod vsock;
3738

0 commit comments

Comments
 (0)