Add UFFD snapshot pager graduation#272
Conversation
Detach running UFFD-backed VMs from their snapshot memory pager after a
soak period instead of leaving them pinned for the life of the restore.
A new pager /sessions/{id}/complete endpoint populates the remaining
pages from the backing file and unregisters userfaultfd, so the VM keeps
running on resident memory with no pager dependency and no pause or
network interruption. This bounds the number of active pager sessions
and lets old pager versions drain to zero and exit.
A background controller (lib/uffdgraduate) drives graduations subject to
min_session_age, max_concurrent, and an optional max_active_sessions
ceiling, prioritising sessions on outdated pager versions. Disabled by
default and only active on the uffd backend. The detach is gated behind
a new hypervisor capability so the controller stays hypervisor-agnostic.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Sibling of the UFFD one-shot lifecycle test that detaches a running UFFD-backed VM from its pager and asserts the VM keeps running with its guest memory and disk intact, new writes still work, and a later standby/restore preserves memory. Leaves the existing test unchanged. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Overlapping the graduation test's full memory populate with the sibling UFFD lifecycle test's VMs saturated the CI runner and timed out guest-agent readiness. Drop t.Parallel so peak concurrent UFFD VM load matches the pre-existing single-test profile. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Main advanced the pager to 0.1.3 independently (CLOCK cache eviction), colliding with this branch's bump. Advance to 0.1.4 so the graduation pager change carries a distinct version. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
reviewed end-to-end — solid, careful work, and the populate-then-unregister core is correct by construction. a few things worth a look, one i'd treat as a fix before merge. should fix
concurrency
questions / confirm intent
test gaps
nits
|
Remove max_active_sessions: time-based weaning covers the rollout goals and the ceiling semantics were the confusing part of the feature. Graduation now clears all UFFD restore state (like standby/stop) so a graduated VM no longer references the source snapshot and its next standby writes a self-contained Full snapshot. Completion aborts when the caller times out or disconnects, keeping the session serving so the control plane binding stays accurate. Guard the wake pipe against use-after-close, back off failed graduations instead of retrying every scan, and error on a missing recorded pager version instead of guessing. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
Thanks for the careful review — every claim checked out against the code, and one of your questions turned out to be a bigger deal than framed. All addressed in ce5009b, point by point: Should fix — deferred memory path: adopted. Graduation now calls wakeW use-after-close: fixed with gctx timeout mid-populate: you were right to poke, and it was worse than "self-heals next scan." Standby in that window is safe ( overCap netting: resolved by deletion. We dropped Populate-sweep fault latency: intended, and your read of the mechanics is right — a guest thread faulting on a not-yet-swept page blocks until the address-ordered sweep reaches it; Test gaps: one correction — Nits: failure backoff added (fixed 5m — each retry is a full-image read, so once a scan was too hot); provider no-op paths now log warnings; the empty-stored-version fallback is now an error instead of guessing (guessing could 404 against the wrong pager and clear a binding for a live session). Pager VERSION bumped to 0.1.5. Ballooning interaction and |
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using high effort and found 4 potential issues.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit ce5009b. Configure here.
hiroTamada
left a comment
There was a problem hiding this comment.
re-reviewed the latest commit — the feedback is well addressed, approving.
- deferred-path fix looks right: graduation now calls
clearFirecrackerUFFDRestoreState, so a graduated VM dropsFirecrackerDeferredSnapshotMemoryPathand its next standby writes a self-contained Full snapshot instead of materializing from a source snapshot that may be gone. integration test now asserts the path is cleared. - wakeW use-after-close is guarded (wakeMu + set to -1 under lock in close(), read/write under lock in wake()); the new
-racetest covers it. - completion now aborts before unregister when the caller times out/disconnects, so the control-plane binding stays accurate and the attempt just retries.
- dropping the ceiling mode removes the confusing over-select-below-the-ceiling behavior; time-based weaning + backoff on failed graduations is clean.
verified lib/uffdgraduate and lib/uffdpager pass with -race. did not run the KVM-gated graduation lifecycle test locally.
approval is on code correctness/mergeability — it's off by default, so enabling in prod still rides on the soak plan validating the kernel/Firecracker assumptions (mid-run unregister tolerated, UFFDIO_COPY dirty-neutral, ballooned-page re-fault safe).
A successful completion delivers its result and then tears the session down, so the reply and done channels can be ready together and select picks randomly. Taking the done branch reported failure for a detach that actually happened, stranding the caller's session binding on a gone session until a retry 404-cleared it — or, when it was the last session on a draining pager version, permanently (the exited pager fails the instance health check). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

Summary
Running UFFD-backed VMs are pinned to their snapshot memory pager for the life of the restore. This adds a way to detach a running VM from its pager after it has soaked, so long-lived VMs stop depending on a pager (or on the snapshot backing their restore) and old pager versions can drain to zero and exit.
Detach happens without touching the VM: a new pager endpoint
POST /sessions/{id}/completepopulates every outstanding page from the backing file and then unregisters userfaultfd. The guest never pauses and its network is untouched; the VM ends up running on resident memory with no pager dependency.Why not migrate UFFD→UFFD or fall back to the file backend: the memory backend is fixed at the mmap when a VM is restored, so reaching the file backend requires a VMM restart, which drops every TCP connection. Graduation (finish the lazy load, then detach) is the only path that is non-interrupting.
What's here
lib/uffdpager):POST /sessions/{id}/complete+Supervisor.CompleteSessionVersion. Completion runs in the fault-loop goroutine (woken via a pipe), populates all pages (reusing the existing read/copy path), thenUFFDIO_UNREGISTERs the ranges. Unregister happens only after a full populate — otherwise the kernel zero-fills still-absent pages (corruption). On populate failure, or if the caller times out/disconnects (the request context is checked throughout the sweep), the session keeps serving faults and is not torn down, so the control plane's session binding stays accurate for a retry.Capabilities().UsesDetachableSnapshotMemoryPager(true for Firecracker) so the controller stays hypervisor-agnostic.GraduateSnapshotMemoryPagerperforms the detach under the instance lock and clears all UFFD restore state (same helper as standby/stop), including the deferred snapshot memory path — a graduated VM no longer references the source snapshot, and its next standby writes a self-contained Full snapshot.lib/uffdgraduate): scans for running pager-backed VMs and graduates every one past the soak, prioritising outdated pager versions, with a fixed 5m backoff after a failed attempt.hypervisor.firecracker_uffd_graduation):enabled(default false),min_session_age(10m),max_concurrent(1),scan_interval(1m),completion_timeout(10m). Wired inmain.govia the existing configure/start pattern (no wire regen).Behaviour
uffdbackend (a warn is logged if enabled without one).min_session_ageis graduated, oldest and outdated-version sessions first, paced bymax_concurrent. Steady-state pager sessions ≈ restore rate × soak. (An earlier revision also had amax_active_sessionsceiling; it was dropped — time-based weaning covers the rollout goals and the ceiling semantics were confusing.)Tradeoffs
EEXIST, skipping the copy but not the read) — hence the soak + concurrency pacing.DeleteSnapshothas no reference guard, so an operator deleting the source breaks that rollback either way). After the first post-graduation standby the VM has a self-contained snapshot and crash recovery returns.Test plan
go build ./...,go vet, and unit tests (incl.-racefor the pager) pass forlib/uffdgraduate,lib/uffdpager,lib/instances,lib/providers,cmd/api/config.max_concurrentthrottling, outdated-version priority, and disabled = no-op. Config Normalize/Validate covered.UFFDIO_UNREGISTERioctl value, the wake pipe, wake-vs-close teardown safety, and that an abandoned completion aborts before unregister.TestFCUFFDGraduationLifecycle(real Firecracker + pager, runs in the linuxtestCI job) covers: graduation of a live fork, guest memory/disk integrity across detach, new writes post-detach, idempotent re-graduation, cleared restore state (session, version, deferred memory path), and file-backed standby/restore afterwards.UFFDIO_COPYdirty-neutrality on the host kernel (post-graduation diff snapshot size — a size regression risk, not correctness).🤖 Generated with Claude Code
Note
High Risk
Touches live guest memory via userfaultfd completion on running VMs; incorrect ordering or metadata clearing could corrupt memory or strand sessions, though the design is heavily guarded and off by default.
Overview
Adds UFFD graduation: running Firecracker VMs restored with the UFFD snapshot memory backend can be detached from the pager without pausing or restarting the guest, so long-lived workloads stop depending on a pager session (and outdated pager versions can drain).
The pager gains
POST /sessions/{id}/completeandCompleteSessionVersion, which populate remaining guest pages from the backing file, thenUFFDIO_UNREGISTERonly after a full populate (aborted/timed-out attempts leave the session serving faults). The instance manager exposesGraduateSnapshotMemoryPager, which completes the session and clears all UFFD restore metadata (including deferred snapshot memory paths) like standby/stop. Firecracker advertisesUsesDetachableSnapshotMemoryPager.A new
lib/uffdgraduatebackground controller (wired fromhypervisor.firecracker_uffd_graduation, default off) periodically graduates soaked running pager-backed VMs with soak age, concurrency limits, outdated-version priority, and 5m failure backoff. Pager version bumps to 0.1.5; integration testTestFCUFFDGraduationLifecyclecovers the end-to-end detach path.Reviewed by Cursor Bugbot for commit 1333a27. Bugbot is set up for automated code reviews on this repo. Configure here.