Skip to content

feat(minvmd,minimald): quiesce data volume on stop and gate READY on mount - #705

Merged
norrietaylor merged 4 commits into
mainfrom
feat/vdb-unit2-quiesce
Jul 10, 2026
Merged

feat(minvmd,minimald): quiesce data volume on stop and gate READY on mount#705
norrietaylor merged 4 commits into
mainfrom
feat/vdb-unit2-quiesce

Conversation

@norrietaylor

@norrietaylor norrietaylor commented Jul 10, 2026

Copy link
Copy Markdown
Member

Since #672, session worktrees and the package cache persist on the per-VM /dev/vdb volume — user data with no host copy. This PR fixes the failure modes that made that persistence unsafe: stopping a VM corrupted-by-default, and a broken volume failed silently.

Failure modes fixed

1. Every minvmd stop was an unclean unmount — dirty journal, possible data loss.
stop sent SIGTERM straight to the VMM with nothing syncing or unmounting the volume. Every boot after a stop replayed the ext4 journal ("recovering journal" on every startup), and with sync_mode=relaxed any writes from the final seconds before the kill — a git commit in a session, a build artifact — could vanish entirely.
Now: stop asks the in-VM minimald to shut down over the bridge UDS before signalling: sessions drain, the volume gets syncfs + unmount (falling back to remount-ro, which closes the journal even with open fds), and only then does SIGTERM fire. A clean stop leaves a cleanly-unmounted filesystem; SIGTERM/SIGKILL escalation is unchanged when the guest doesn't answer.

2. A VM whose volume failed to mount reported READY anyway — a ghost VM that eats data.
A mount failure was only logged; the guest silently kept its state on the boot tmpfs and signalled READY. Sessions appeared to work, wrote everything to RAM, and lost it all at the next stop — with zero indication anything was wrong. An undersized or corrupt volume image produced a "healthy" VM indistinguishable from a real one.
Now: the guest never substitutes tmpfs. On any mount failure it emits a distinct MOUNT_FAILED\n<reason>\n beacon instead of READY, and minvmd boot/run fail with a non-zero exit and the guest's actual failure reason.

3. A failing volume offered no repair path and no safe disposition.
There was no distinction between "this image holds your sessions and won't mount" and "this blank image was just provisioned wrong" — and no repair attempt at all.
Now: a formatted volume that fails to mount gets one e2fsck -p repair + mount retry before failing closed (never a reformat once a superblock exists). On failure, the host keys off whether the image pre-existed the boot: a pre-existing image may hold session data, so it is left untouched and the error points at repair or MINVMD_DATA_VOLUME_PATH; a blank image freshly created this boot is removed so the next boot isn't misdiagnosed as data-at-risk.

Found along the way

Two lifetime-held write fds on the volume made every unmount/remount fail EBUSY, keeping the journal dirty even after a quiesce — the shutdown sequence only became verifiable once both were gone:

  • minimald's lcache read tracker held a locked alog/<n>.v1 open for the daemon's lifetime; the sessions manager now releases it during shutdown (Cache::release_read_tracker).
  • #690's minimald.lock lives in the provider dir, which in-guest sits on the volume; the vsock-mode instance lock now lives on the /run tmpfs (host-side placement unchanged — nothing outside the boot reads the guest's lock).

The quiesce EBUSY warning now lists the offending fd holders via a /proc scan, so the next accidental pin is diagnosable from the console log.

Verification

New gated e2e suite (MINVMD_E2E=1, verified on macOS/HVF), asserting against the raw image bytes — no host ext4 tooling needed:

  • clean stop → superblock marked cleanly unmounted (EXT4_VALID_FS set, INCOMPAT_RECOVER clear);
  • unformattable fresh volume (below the mkfs floor) → no vm-up, non-zero exit, blank image removed;
  • unmountable pre-existing volume (valid magic, corrupt fs) → no vm-up, non-zero exit, image preserved.

VM-booting tests (including the pre-existing boot_e2e) now also isolate HOME, so dirs-based path fallbacks can never escape onto a developer's real state dir.

Refs: #583, builds on #613's Shutdown RPC

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Added more detailed VM boot failure reporting, including MOUNT_FAILED reasons, and better run diagnostics via run.log.
    • Added state-volume quiescing on shutdown (sync + unmount/detach) and fail-closed mounting with automatic repair for existing ext4.
    • Improved cache read-tracker release during daemon shutdown/cleanup.
  • Bug Fixes
    • Prevented “ready” outcomes when storage mounting fails.
    • Ensured freshly provisioned failed volume images are discarded, while pre-existing images are preserved for investigation.
  • Tests
    • Added/extended e2e coverage for volume quiescing and mount-failure handling.

@coderabbitai

coderabbitai Bot commented Jul 10, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 8706f587-2a01-420a-a312-10af8eb4d5b5

📥 Commits

Reviewing files that changed from the base of the PR and between f2203e0 and 5e7b1cf.

📒 Files selected for processing (12)
  • crates/mctx/src/lib.rs
  • crates/minimald/src/guest.rs
  • crates/minimald/src/main.rs
  • crates/minimald/src/rpc.rs
  • crates/minimald/src/server.rs
  • crates/minimald/src/test_harness.rs
  • crates/minvmd/src/cmd/boot.rs
  • crates/minvmd/src/cmd/mod.rs
  • crates/minvmd/src/cmd/run.rs
  • crates/minvmd/src/cmd/stop.rs
  • crates/minvmd/src/rpc_client.rs
  • crates/minvmd/tests/volume_quiesce_e2e.rs
🚧 Files skipped from review as they are similar to previous changes (3)
  • crates/mctx/src/lib.rs
  • crates/minvmd/tests/volume_quiesce_e2e.rs
  • crates/minimald/src/guest.rs

📝 Walkthrough

Walkthrough

Changes

State volume lifecycle

Layer / File(s) Summary
Guest mounting and beacon emission
crates/minimald/src/guest.rs, crates/minimald/src/main.rs
READY and MOUNT_FAILED payloads are generalized, state-volume mounting becomes fail-closed with ext4 repair or formatting, and shutdown quiescing is added.
Boot beacon propagation
crates/minvmd/src/cmd/{mod,boot,run}.rs, crates/minvmd/tests/*
minvmd parses structured boot results, classifies mount failures by volume provenance, and terminates failed boots with corresponding errors.
Shutdown quiescing and cleanup
crates/minvmd/src/cmd/{stop,rpc_client}.rs, crates/minimald/src/{rpc,server,sessions}.rs, crates/{lcache,mctx}/src/lib.rs
The stop flow requests guest shutdown, minimald releases the cache read tracker, quiesces the state volume, and then proceeds with VMM termination.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant minvmd
  participant rpc_client
  participant minimald
  participant state_volume
  minvmd->>rpc_client: request guest shutdown
  rpc_client->>minimald: send Shutdown RPC
  minimald->>state_volume: release tracker and quiesce mount
  minvmd->>minvmd: terminate VMM
Loading

Possibly related issues

Possibly related PRs

Suggested labels: needs-human

Suggested reviewers: evanspearman

Poem

I thumped my paws on ext4’s floor,
“Mount cleanly, please—and journal no more!”
READY now sings, failures speak,
Trackers release when shutdowns seek.
The VMM rests beneath moonlight bright.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the two main changes: stopping quiesces the data volume and boot READY is gated on successful mounting.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch

Comment @coderabbitai help to get the list of available commands.

…mount

Unit 2 of the per-VM writable ext4 volume spec (R2.1-R2.5): a clean
`minvmd stop` now leaves the volume's ext4 journal closed, and a volume
that cannot be mounted fails the boot loudly instead of ghost-READY.

Guest (minimald):
- quiesce_state_volume: syncfs + plain umount, falling back to
  remount-ro (clears INCOMPAT_RECOVER despite open fds) + MNT_DETACH;
  called from the Shutdown RPC handler after session drain, before the
  ack, with a 10s ceiling (R2.1, R2.2).
- mount_state_volume fails closed: device absence is an error (minvmd
  attaches vdb on every boot), and a formatted volume that fails to
  mount gets one e2fsck -p repair + retry, never a reformat (R2.4).
- MOUNT_FAILED beacon: a mount failure emits MOUNT_FAILED\n<reason>\n
  instead of READY and parks; no tmpfs fallback path remains (R2.4,
  R2.5).
- The sessions manager releases the lcache read tracker on shutdown:
  its locked alog fd on the volume pinned the mount busy (EBUSY) and
  left the journal dirty on every stop. lcache/mctx grow a
  release_read_tracker() for this; the EBUSY warn now lists fd holders.

Host (minvmd):
- `stop` invokes the guest Shutdown RPC (force) over the bridge UDS
  before SIGTERM, via a new blocking rpc_client; failures warn and
  proceed with the unchanged SIGTERM->SIGKILL escalation (R2.3). The
  15s client timeout contains the guest's 10s quiesce budget.
- read_ready_beacon returns a typed BootBeacon; on MountFailed, boot/
  run fail with a pre-existence-aware message: a pre-existing image may
  hold session data and is kept, a freshly provisioned blank is removed
  so the next boot is not misclassified (R2.5).

e2e (gated MINVMD_E2E=1, verified on macOS/HVF): a clean stop leaves
EXT4_VALID_FS set and INCOMPAT_RECOVER clear on the image; an
unformattable fresh volume and an unmountable pre-existing volume both
fail boot loudly with the right image disposition. boot_e2e (and the
new tests) now also isolate HOME: dirs::state_dir() ignores
XDG_STATE_HOME on macOS, so the tests otherwise clobber the real state
dir.

Refs: #583

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@norrietaylor
norrietaylor force-pushed the feat/vdb-unit2-quiesce branch from 8f969a6 to bd23a5e Compare July 10, 2026 17:04
@norrietaylor

Copy link
Copy Markdown
Member Author

Rebased onto latest main (over #690's socket/path rework). Two things beyond mechanical conflict resolution:

  • #690's lifetime minimald.lock reintroduced the dirty-journal bug this PR fixes: in-guest, the provider dir lives on the data volume, so the lock's lifetime-held write fd made the quiesce unmount/remount-ro fail EBUSY on every clean stop (the new fd-holders diagnostic caught it: pid 1 fd /var/lib/minimal/providers/local-0/minimald.lock). Fixed by placing the in-guest (vsock-mode) instance lock on the /run tmpfs — nothing outside the boot reads the guest's lock, and boot-ephemeral coordination state doesn't belong on durable storage. Host-side (DM2) lock placement is unchanged.
  • The e2e tests adopted #690's conventions: short /tmp state dirs (the bridge UDS now lives inside the state dir and must fit sun_path), the MINVMD_BIN override, and the new providers/local-0/ locations for the volume image.

All proofs re-verified on macOS/HVF after the rebase: clean stop → EXT4_VALID_FS set / INCOMPAT_RECOVER clear; both mount-failure modes fail loudly with correct image disposition.

🤖 Generated with Claude Code

@norrietaylor
norrietaylor marked this pull request as ready for review July 10, 2026 18:21

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@crates/minvmd/src/cmd/mod.rs`:
- Around line 289-311: Track provisioning ownership from the volume
creation/provisioning function through the mount-failure path instead of relying
on the pre-spawn volume_preexisted check. Update the provisioning result and
callers to carry an atomic created_by_this_boot flag, then have
mount_failed_error remove the image only when that flag is true; preserve the
existing warning and pre-existing-image behavior otherwise.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: d56ead12-e9ac-41be-a112-ad8d48472d0c

📥 Commits

Reviewing files that changed from the base of the PR and between 20a9859 and f2203e0.

📒 Files selected for processing (15)
  • crates/lcache/src/lib.rs
  • crates/mctx/src/lib.rs
  • crates/minimald/src/guest.rs
  • crates/minimald/src/main.rs
  • crates/minimald/src/rpc.rs
  • crates/minimald/src/server.rs
  • crates/minimald/src/sessions.rs
  • crates/minvmd/src/cmd/boot.rs
  • crates/minvmd/src/cmd/mod.rs
  • crates/minvmd/src/cmd/run.rs
  • crates/minvmd/src/cmd/stop.rs
  • crates/minvmd/src/lib.rs
  • crates/minvmd/src/rpc_client.rs
  • crates/minvmd/tests/boot_e2e.rs
  • crates/minvmd/tests/volume_quiesce_e2e.rs

Comment thread crates/minvmd/src/cmd/mod.rs Outdated

@twitchyliquid64 twitchyliquid64 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"quiesce" lol

norrietaylor and others added 2 commits July 10, 2026 11:47
Review findings on the quiesce/loud-mount-failure work:

- stop: split the Shutdown RPC deadline into a 5 s connect/handshake
  budget (a wedged guest must not stall the recovery command — libkrun
  accepts the bridge connect regardless of guest health, so only a
  completed handshake proves a live daemon) and a 120 s RPC budget (the
  ack lands only after the unbounded session drain plus the guest's
  10 s quiesce; giving up mid-drain would SIGTERM with a dirty journal).
- boot/run: the image pre-existence probe treats a stat error as
  "pre-existing" so a transient failure can never route a data-bearing
  image to the delete branch; the fresh blank image is discarded on
  every failed-boot arm (beacon error, READY timeout) — not just
  MOUNT_FAILED — so a stranded blank can't masquerade as data-at-risk
  on the next boot; mount_failed_error is now message-only.
- the shutdown quiesce gates on Config.state_volume_mounted (set only
  when the guest actually mounted the volume) instead of the vsock
  transport, so a native --vsock daemon or a volume-less microVM never
  has its state dir synced-and-unmounted out from under it.
- the guest instance-lock relocation to /run keys on being the VM init
  (is_minimal_microvm) rather than --vsock, keeping non-root native
  --vsock daemons working; a non-vsock daemon whose state-volume mount
  fails exits with an error instead of parking behind a beacon nobody
  reads; a failed MOUNT_FAILED emit is now logged rather than silently
  degrading to a READY timeout.
- the data volume mounts via a strict (EBUSY-propagating) mount — the
  EBUSY-tolerant raw_mount is for idempotent pseudo-fs mounts only —
  and every mount_state_volume failure names its operation, so the
  MOUNT_FAILED reason can't misdirect (a pre-R1.7 rootfs EROFS no
  longer reads as a corrupt image needing fsck).
- run --detach: the supervisor's stderr lands in <state>/run.log and
  both failure messages point at it, instead of /dev/null swallowing
  the mount-failure diagnosis.
- volume_quiesce_e2e stops the VM on drop, so a mid-test panic no
  longer leaks a detached VM whose state dir the TempDir then unlinks.

Refs: #583

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@norrietaylor
norrietaylor merged commit d93df4c into main Jul 10, 2026
27 checks passed
@norrietaylor
norrietaylor deleted the feat/vdb-unit2-quiesce branch July 10, 2026 22:15
norrietaylor added a commit that referenced this pull request Jul 13, 2026
VM e2e harnesses are now selected by binary-name convention instead of a
hardcoded per-test list, so adding a test never requires a CI edit:

- Harnesses in crates/minvmd/tests/ end in `_e2e` (non-root) or
  `_root_e2e` (root, run under sudo for CAP_NET_ADMIN).
- The KVM lane's three enumerated steps collapse to two convention
  filtersets: `binary(/_e2e$/) and not binary(/_root_e2e$/)` and
  `binary(/_root_e2e$/)`. A new `*_e2e.rs` is picked up with no YAML.
- Rename krun_smoke -> krun_smoke_e2e and vsock_relay_e2e ->
  vsock_relay_root_e2e; delete the retired bridge_e2e stub.
- A unit test in minvmd (runs in core-tests, no libkrun) guards the
  suffix so a typo fails CI instead of silently dropping a harness.

This newly runs volume_quiesce_e2e (added in #705 but never wired into a
lane) in the KVM lane. The macOS lane tracks the rename only; its
filterset conversion is coupled to the nextest-archive-on-mac work.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HbXL6jxHTY2YuLPaMxc1yU
norrietaylor added a commit that referenced this pull request Jul 14, 2026
…s every lane (#732)

* test(minvmd,ci): auto-discover VM e2e harnesses by name suffix

VM e2e harnesses are now selected by binary-name convention instead of a
hardcoded per-test list, so adding a test never requires a CI edit:

- Harnesses in crates/minvmd/tests/ end in `_e2e` (non-root) or
  `_root_e2e` (root, run under sudo for CAP_NET_ADMIN).
- The KVM lane's three enumerated steps collapse to two convention
  filtersets: `binary(/_e2e$/) and not binary(/_root_e2e$/)` and
  `binary(/_root_e2e$/)`. A new `*_e2e.rs` is picked up with no YAML.
- Rename krun_smoke -> krun_smoke_e2e and vsock_relay_e2e ->
  vsock_relay_root_e2e; delete the retired bridge_e2e stub.
- A unit test in minvmd (runs in core-tests, no libkrun) guards the
  suffix so a typo fails CI instead of silently dropping a harness.

This newly runs volume_quiesce_e2e (added in #705 but never wired into a
lane) in the KVM lane. The macOS lane tracks the rename only; its
filterset conversion is coupled to the nextest-archive-on-mac work.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HbXL6jxHTY2YuLPaMxc1yU

* test(minimald,linux-native): fold the netns track into root e2e auto-discovery

The netns-integration job hardcoded `cargo test -p minimald --test netns`.
Rename netns.rs -> netns_root_e2e.rs and select it by the same convention
the KVM lane uses: the renamed `minimald-root-e2e` job runs
`cargo nextest run -p minimald -E 'binary(/_root_e2e$/)'`, so a new
crates/minimald/tests/*_root_e2e.rs runs with no workflow edit.

A minimald unit test guards the suffix; mesh_uc7 is allowlisted because
its networking-wg feature is off by default and no lane runs it.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HbXL6jxHTY2YuLPaMxc1yU

* ci(macos): auto-discover VM e2e harnesses from a nextest archive

The mac e2e job hardcoded two test names and ran them as direct binaries.
Build a nextest archive before the codesign (`nextest run --archive-file`
is offline, so the signature survives) and select harnesses by the `_e2e`
convention, so a new crates/minvmd/tests/*_e2e.rs runs on mac with no
workflow edit. This newly runs boot_e2e and volume_quiesce_e2e on mac.

The full-boot harnesses spawn the codesigned minvmd as a subprocess
(MINVMD_BIN). krun_smoke_e2e stays a separate kernel-less FFI-smoke step,
excluded from the archive run: it boots in-process through
krun_smoke_child, which cannot take the hypervisor entitlement from
within the archive.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HbXL6jxHTY2YuLPaMxc1yU

* docs: update specs for the e2e test renames and bridge_e2e removal

Reflect the auto-discovery migration in the frozen unit specs: krun_smoke
-> krun_smoke_e2e in R1.5, and note that bridge_e2e was removed
(superseded by minimald_session_e2e).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HbXL6jxHTY2YuLPaMxc1yU

* refactor(minvmd,ci): drop the dead host-side tap relay + its KVM wiring

minvmd's net::relay (open_tap/attach_to_switch/SwitchRelay) has had no
production caller since #581 moved the per-PTask tap+relay into the guest
minimald over the vsock shuttle; it was a frozen byte-for-byte fork of
minimald's net::switch that never received the ingress gate the live copy
grew. Its only caller was vsock_relay_root_e2e, which booted no VM and
re-proved the relay against a hand-made netns — coverage already provided,
with real PTask-to-PTask traffic, by minimald's netns_root_e2e UC6.

Delete net/relay.rs, its module wiring, and the test; correct the net.rs
module doc (the tap + relay run in the guest, not here). The KVM lane's
root step existed only for that test, so it is removed along with the
gvproxy materialize step and the vendor/gvproxy pin filter it needed; the
lane keeps a single auto-discovered harness step (a future minvmd
*_root_e2e would need its own sudo-wrapped step).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HbXL6jxHTY2YuLPaMxc1yU

* refactor(minimald,ci): name and describe the root e2e proofs functionally

Drop the internal use-case numbers (UC1/UC4/UC6/UC7) from the netns test
function names, its module doc, the native-lane comments, and the minimald
naming guard. Describe each proof by what it does — a no-network task
cannot reach the internet, a static ingress mapping exposes then removes a
task's port, two own-IP tasks reach each other over the gvproxy switch —
and state why they need root: they create tap devices and configure
network namespaces, which requires CAP_NET_ADMIN, so the unprivileged CI
runner grants it via sudo.

No behavior change; the harness is selected by binary name, not function
name, so the renames are self-contained.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HbXL6jxHTY2YuLPaMxc1yU

* refactor(minvmd,minimald,ci): rename harness e2e tests to _integration

The harness-driven nextest proofs are component/subsystem INTEGRATION
tests, not full-system e2e: each stands up one subsystem against real
resources (boot a microVM, round-trip the vsock bridge, drive the libkrun
FFI, check a real ext4 volume, exercise the tap/netns stack) rather than
driving the `minimal` CLI through the whole system. The only true
end-to-end proof is the CLI script (scripts/session-e2e.sh).

Rename the auto-discovery suffix _e2e / _root_e2e -> _integration /
_root_integration across the five harnesses, the KVM/native/mac lane
filtersets, and both naming guards; the guard docs now state when to
reach for an integration harness vs. a full-system e2e script. Rename the
minvmd-scoped lifecycle script (lifecycle-e2e.sh -> minvmd-lifecycle.sh)
and the native root job (minimald-root-e2e -> minimald-root-integration).

Also scrub the workflow YAMLs of internal plan jargon (UC/N/R references)
in favour of functional descriptions.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HbXL6jxHTY2YuLPaMxc1yU

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants