Skip to content

docs(spec-vm-ext4-volume): pkg cache on the in-VM ext4 volume (volume-aware) - #658

Merged
norrietaylor merged 8 commits into
mainfrom
spec/vm-ext4-volume-205fa69651149a87
Jul 8, 2026
Merged

docs(spec-vm-ext4-volume): pkg cache on the in-VM ext4 volume (volume-aware)#658
norrietaylor merged 8 commits into
mainfrom
spec/vm-ext4-volume-205fa69651149a87

Conversation

@gominimal-aw-bot

@gominimal-aw-bot gominimal-aw-bot Bot commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

Spec for #583: per-VM writable ext4 /dev/vdb volume, resolving the EXDEV hardlink constraint between the package cache and rootfs staging trees inside the VM guest.

What this spec covers

This spec replaces the per-boot /run tmpfs with a persistent per-VM ext4 volume so that:

  • The package cache, rootfs staging trees (tasks/, sandboxes/), and session workbenches all live on one filesystem (satisfying the hardlink constraint without any cross-device fallback).
  • Session worktrees survive clean VM restarts.
  • The RAM stop-gap (4096/2048 MiB) can be reduced once the cache leaves RAM.

Three demoable units

Unit 1 — Writable volume attachment (EXDEV fix)
krun_add_disk3 FFI binding, second disk attach in vm.rs, guest mount_state_volume step in enter_rootfs, minimal_state_dir/var/lib/minimal, rootfs ships /var/lib/minimal mountpoint + e2fsprogs.

Unit 2 — Shutdown quiesce + crash safety
Shutdown RPC extended with guest syncfs + unmount before VMM teardown; minvmd stop invokes RPC before SIGTERM; loud mount failure (no silent tmpfs fallback); no false READY on volume attach failure.

Unit 3 — Session persistence, self-heal, upload-on-resume, provider index
sessions/ exempt from boot-time reset; corrupt index.json self-heals from record.json; workspace upload to non-empty worktree has defined atomicity semantics; host-side ProviderIndex maps session id → volume image path.

Next step

Please review and merge the spec PR to advance the tracking issue from the spec phase into triage.

Merging this spec PR will close the spec sub-issue (Closes #<spec-sub-issue> is added to the PR body by sdd-pr-sanitize).

Related to #583

Generated by sdd-spec for issue #583 ·

Closes #657

Summary by CodeRabbit

  • Documentation
    • Added a design specification for a per-VM persistent writable ext4 volume to replace boot-lifetime /run tmpfs usage for cache, staging, and session workbenches.
    • Describes the two-disk layout (read-only base rootfs plus an ext4 data disk) and idempotent guest initialization.
    • Defines crash-safe shutdown expectations and clear failure behavior when the persistent disk can’t be mounted.
    • Specifies session persistence/self-healing and workspace upload-on-resume with strict and atomic swap semantics.

@coderabbitai

coderabbitai Bot commented Jul 7, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds a new specification document for a persistent writable ext4 volume in the VM, covering disk layout, provisioning, shutdown quiescing, READY gating, session persistence, and verification requirements.

Changes

Ext4 volume specification document

Layer / File(s) Summary
Problem statement and solution overview
docs/specs/08-spec-vm-ext4-volume/08-spec-vm-ext4-volume.md
Adds front-matter, the current tmpfs and hardlink constraints, the two-disk ext4 design, and the initial goals/user stories.
Unit breakdown: provisioning, shutdown, session persistence
docs/specs/08-spec-vm-ext4-volume/08-spec-vm-ext4-volume.md
Defines host volume provisioning, guest formatting/mount flow, shutdown quiescing, READY gating on mount success, session recovery, upload-on-resume behavior, and host-side session indexing.
Non-goals, design considerations, open questions, verification
docs/specs/08-spec-vm-ext4-volume/08-spec-vm-ext4-volume.md
Adds non-goals plus technical, security, and verification sections for the proposed volume model.

Estimated code review effort: 2 (Simple) | ~10 minutes

Possibly related issues

Possibly related PRs

  • gominimal/pkgs#365: Updates the rootfs image to include ext4 tooling and prepare the /var/lib/minimal mountpoint for the same volume model.

Suggested reviewers: norrietaylor

Poem

A bunny hopped to /dev/vdb,
Where ext4 roots may grow with glee.
No tmpfs fog, no session lost,
Just quiet syncs at lessened cost.
Hop, hop, safe state in sight —
Persistent burrows through the night. 🐇

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Linked Issues check ✅ Passed The spec covers the requested per-VM ext4 volume, /var/lib/minimal mount, crash-safety, persistence, and ProviderIndex behavior.
Out of Scope Changes check ✅ Passed No clear unrelated changes are introduced; the added material stays within the spec scope for #657/#583.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title is specific and accurately reflects the spec’s main theme of moving package cache state onto an in-VM ext4 volume.

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

@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: 3

🤖 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 `@docs/specs/08-spec-vm-ext4-volume/08-spec-vm-ext4-volume.md`:
- Around line 299-304: The current R2.5 mount-failure check is unreachable
because it depends on inspecting sessions/ after a failed /dev/vdb mount. Update
the spec language to make the guest emit MOUNT_FAILED, or exit, on any mount
error instead of conditionally checking mounted state, and clarify in the R2.5
text that minvmd run should surface the failure as a user-visible error while
the host determines fatality.
- Around line 377-383: Normalize the ProviderIndex schema in the spec text so
the key/value model is consistent: choose a single canonical representation for
session_id and state clearly whether the JSON map is keyed by session UUIDv7,
with session_id stored only in VolumeEntry, or whether session_id is duplicated
in both key and value. Update the ProviderIndex/VolumeEntry description to match
the insert, get_by_session, and remove behavior without conflicting terminology.
- Around line 368-373: The force=true upload flow in the spec’s worktree swap
path assumes rename(2) can atomically replace an existing non-empty worktree,
which it cannot. Update the behavior described around the staging-directory swap
to use a different atomic replacement mechanism or an indirection layer for the
live worktree, and ensure the atomicity requirement is still expressed in terms
of the worktree becoming either the old or new state after upload.
🪄 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: 85ae430a-3cf9-479e-a27f-f459f3746a96

📥 Commits

Reviewing files that changed from the base of the PR and between 29e9f6a and 061fd00.

📒 Files selected for processing (3)
  • .github/workflows/release.yml
  • docs/specs/08-spec-vm-ext4-volume/08-spec-vm-ext4-volume.md
  • scripts/stage-release.sh
💤 Files with no reviewable changes (2)
  • scripts/stage-release.sh
  • .github/workflows/release.yml

Comment thread docs/specs/08-spec-vm-ext4-volume/08-spec-vm-ext4-volume.md Outdated
Comment thread docs/specs/08-spec-vm-ext4-volume/08-spec-vm-ext4-volume.md
Comment thread docs/specs/08-spec-vm-ext4-volume/08-spec-vm-ext4-volume.md Outdated
@gominimal-aw-bot

This comment has been minimized.

The sdd-spec commit for this branch bundled a stale revert of the
libkrun macOS release job and its stage-release entry (both landed in
#655) alongside the spec file. This PR is spec-only; restore the two
files to match main so the release flow is unchanged.

Refs: #655
- R2.5: emit MOUNT_FAILED unconditionally on any /dev/vdb mount failure;
  drop the unreachable "inspect sessions/ after a failed mount" branch and
  move the fatal-vs-recoverable decision host-side.
- R3.3: replace non-empty worktree via renameat2(RENAME_EXCHANGE) instead
  of rename(2), which fails with ENOTEMPTY on a non-empty destination.
- R3.4: normalize the ProviderIndex schema — map keyed by SessionId, with
  VolumeEntry holding only { image_path, vm_id } and not repeating the id.
@gominimal-aw-bot

This comment has been minimized.

- `/dev/vdb` — per-VM writable RAW ext4, carries `minimal_state_dir =
/var/lib/minimal` (cache, `tasks/`, `sandboxes/`, `sessions/`).

Provisioning is guest-driven on first boot: the host creates a sparse raw file;

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.

I'm not sure if the sparse raw file thing works, but might be worth testing.

There are two other options:

  1. qcow2 file, which automagically handles sparsity itself. We keep the split between the rootfs and the mutable file as per this spec.
  2. We use two files on disk, and rely on the qcow2 "backing file" / "overlay" feature. In this world, the ext4 rootfs is the base qcow2, and the mutable qcow2 file holds the delta from that. In this world, we only need to krun_add_disk* the mutable one (which has a pointer to the backing file), and the diff goes in there, and the VM is never the wiser that theres actually two files involved.

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.

You're right it's untested: I've added Proof Artifact 3 as a decision gate.


**Purpose:** Resolve the `EXDEV` failure by placing the cache and rootfs-staging
trees on a shared writable ext4 volume. Introduces the host provisioner, the
`krun_add_disk3` FFI binding, and the guest mount step.

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.

Is there a specific reason why we want to use krun_add_disk3 rather than one of the older variants?

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.

Nvm, answered down the bottom. I'll add that we probably want to experiment with sync mode and direct IO, they will have performance implications.

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.

R1.9 now makes both MINVMD_DISK_SYNC/MINVMD_DISK_DIRECT_IO tunable instead of hardcoded, and Proof Artifact 4 times a build across the matrix so the default comes from measurement.

Hold the RAW two-disk topology (Phase 1 scope per #583) and answer the
review concerns with measurement rather than assertion.

- R1.9: MINVMD_DISK_SYNC / MINVMD_DISK_DIRECT_IO tunables; stop
  hardcoding sync_mode/direct_io in R1.4 so the durability/throughput
  posture is measurable and per-platform tunable.
- Proof Artifact 3 (decision gate): verify allocate-on-write sparsity
  and characterize reclaim-on-discard on APFS; a failing reclaim result
  is the documented trigger to reopen qcow2 (#647).
- Proof Artifact 4: time a build workload across the sync/direct_io
  matrix so the default is chosen from data.
- Design Considerations: why RAW is held against the qcow2 data-disk and
  qcow2 backing-file-overlay alternatives (VMM gate, crash surface,
  Phase-3 topology).
- Open Question 4: durability granularity vs. the single shared volume.

Refs: #583
@gominimal-aw-bot

This comment has been minimized.

@norrietaylor

Copy link
Copy Markdown
Member

Measured results — RAW sparse /dev/vdb on libkrun 1.19.0 / Apple Silicon HVF

Executed the Unit 1 decision slice (host FFI + provisioner + /dev/vdb attach + the R1.9 sync tunables) and ran the two proof gates end-to-end against a booted VM. This answers both review threads directly.

Proof Artifact 3 — sparsity gate: PASS on both axes

Measurement Result
Allocate-on-write — guest writes 2 GiB to raw /dev/vdb host image st_blocks*512: 4 KiB → exactly 2048 MiB (allocates what's written, not the 8 GiB logical size)
Reclaim-on-discard — BLKDISCARD [0, 2 GiB) host image 2048 MiB → 0 MiB (libkrun forwards virtio UNMAP to a host hole-punch on APFS)
virtio-blk discard capability /sys/block/vdb/queue/discard_max_bytes ≈ 2 TiB, discard_granularity 4096

Provisioning uses ftruncate (File::set_len), which is sparse on APFS — not fallocate without KEEP_SIZE (which would allocate eagerly and defeat the gate).

@twitchyliquid64 — re "not sure if the sparse raw file thing works": it works, both directions. Lazy allocate-on-write and reclaim-on-TRIM. The RAW space story is sound, so there's no reclamation problem to escape from — no reason to take on qcow2's VMM backing-file gate or added crash surface. Holding the RAW two-disk narrowing.

Proof Artifact 4 — sync/direct_io throughput

Sync-heavy pattern (O_DSYNC per 1 MiB block — exercises VIRTIO_BLK_F_FLUSH on every write, where the sync-mode cost shows most):

MINVMD_DISK_SYNC direct_io throughput
none false 4.7 GB/s
relaxed false 3.1 GB/s ← default
full false 254 MB/s (~12–18× slower)
relaxed true 1.2 GB/s

Bulk 2 GiB + single fsync is less differentiated (3.0–4.5 GB/s) since the flush amortizes.

@twitchyliquid64 — re "experiment with sync mode and direct IO, they have performance implications": confirmed. full (strict drive flush) collapses to 254 MB/s under frequent flushes on APFS; direct_io=true halves throughput. Default = relaxed + direct_io=false (R1.9): honors flush so ext4 journal ordering holds → replay works on unclean shutdown, without full's drive-flush penalty. Both are env-tunable per platform.

One caveat for the shipped product

Reclaim requires the guest to issue discard — mount -o discard (needs ext4, i.e. R1.7's e2fsprogs) or periodic fstrim (util-linux). The current microvm-rootfs (base + socat) has neither, so reclaim above was driven via an injected static BLKDISCARD helper. This is exactly the gap R1.7 closes.

Spec correction found while executing

R1.1 declares krun_add_disk3(… sync_mode: bool). The libkrun header (libkrun.h:278-284) types it uint32_t — a tri-state (KRUN_SYNC_{NONE,RELAXED,FULL}), not a bool. Implemented correctly as a SyncMode enum; R1.1/R1.9 text should be corrected to match.


Net: the sparsity gate that would have reopened qcow2 passes, so RAW two-disk stands on measurement, not assertion. Continuing into the rest of Unit 1 (guest mount, state-dir relocation, e2fsprogs in the rootfs closure — which also makes reclaim work without the injected helper).

…/KVM leg

Fold the executed proof artifacts into the spec and extend them to cover
Linux hosts, since two axes are host-dependent.

- Proof Artifacts 3 & 4: record the macOS/HVF/APFS results (allocate-on-
  write 2 GiB exact; BLKDISCARD reclaim to 0; sync sweep none/relaxed/full
  = 4.7/3.1/0.25 GB/s) and add a required Linux/KVM leg (runs in CI; no
  /dev/kvm on a macOS host).
- New Design Consideration "Platform differences": sync_mode RELAXED is
  identical to FULL on Linux (macOS drive-flush relaxation does not apply),
  so the three-point curve collapses to two; reclaim depends on the Linux
  host FS + KVM virtio-blk backend and must be verified, not inferred.
- Non-Goals: Phase-2 reflink needs xfs/btrfs on Linux (ext4 has none),
  unlike APFS clonefile — a Linux host-FS prerequisite (#647).
- Correct R1.1/R1.2/R1.9: krun_add_disk3 sync_mode is a u32 tri-state
  (KRUN_SYNC_{NONE,RELAXED,FULL}), not a bool; default RELAXED.
- Verification matrix + Open Question 4 updated to the per-host split.

Refs: #583
@gominimal-aw-bot

This comment has been minimized.

Reducing the RAM stop-gap when the cache leaves the tmpfs is only safe once a
failed volume mount is fatal (no silent tmpfs fallback) and the floor is
measured against real in-VM build memory pressure. Neither is settled by this
spec, so the reduction moves to a separate memory-pressure spec. Recorded as a
Non-Goal; R1.9 is left un-renumbered.

Refs: #583
@gominimal-aw-bot

This comment has been minimized.

@gominimal-aw-bot

This comment has been minimized.

@twitchyliquid64

twitchyliquid64 commented Jul 8, 2026

Copy link
Copy Markdown
Member

Very cool. I think its okay that the guest doesnt issue trim, if that becomes a problem we can add a cron to do so. I expect ext4 to at least attempt to reuse its free blocks.

SGTM for raw + sparse, sync=relaxed + direct=false. Given it seems relaxed sync means "honor flush", those perf measurements make sense lol.

…rait

Drop the single-use VolumeProvisioner trait / BlankRawProvisioner from the spec
to match the implementation (code review: no abstraction for single-use code).
R1.3 now specifies `ensure_sparse_raw(path, size)` provisioning at the literal
path plus `resolve_data_volume_path()`; R1.4 reflects attach-on-every-boot (not
env-gated) and the relaxed sync default. Rewords the Intro overview, the Phase-2
"seam" non-goal, affected areas, and the volume-size open question accordingly.

Refs: #583
@norrietaylor
norrietaylor force-pushed the spec/vm-ext4-volume-205fa69651149a87 branch from 89c9b7b to af16a85 Compare July 8, 2026 15:48
@gominimal-aw-bot

This comment has been minimized.

Record Tom's review suggestion (#658): the guest does not proactively issue
TRIM, so the RAW host image only shrinks on an explicit discard; if host-side
growth becomes a problem, a periodic guest fstrim (e.g. a cron) reclaims freed
blocks. ext4 reuses its own free blocks in place regardless. Captured as a
Non-Goal / follow-up.

Refs: #583
@gominimal-aw-bot

Copy link
Copy Markdown
Contributor Author

sdd-validate — Spec boundary findings

Boundary resolved: Spec (docs/specs/08-spec-vm-ext4-volume/08-spec-vm-ext4-volume.md adds a *-spec-*.md file under docs/specs/)

Tracking issue: #583 (labels: sdd:spec, kind:epic — full-path feature, no fast-path markers)


Gate 1 — Acceptance criteria testable: ✅ PASS

All requirement IDs (R1.1–R1.9, R2.1–R2.5, R3.1–R3.5) map to observable pass/fail behaviors. The Verification table at the end of the spec provides explicit test commands and CLI assertions for each requirement cluster. No vague aspirations detected at ≥80% confidence.


Gate 2 — No implementation leakage: ⚠️ WARNING

Multiple functional requirements prescribe specific file paths, function names, function signatures, data structure names, and implementation constants — implementation directives rather than behavioral statements. Examples:

  • R1.1 (08-spec-vm-ext4-volume.md): prescribes krun_add_disk3 exact FFI signature including parameter types, and names a SyncMode #[repr(u32)] enum.
  • R1.3 (08-spec-vm-ext4-volume.md): prescribes pub fn ensure_sparse_raw(path: &Path, size_bytes: u64) -> Result<(), VolumeError> with exact Rust type names, environment variable names (MINVMD_DATA_VOLUME_PATH, MINVMD_VOLUME_BYTES), and constant defaults.
  • R1.5 (08-spec-vm-ext4-volume.md): prescribes reading superblock magic 0x53EF at byte offset 1080 — a code-level implementation constant that belongs in a design note or Technical Considerations.
  • R3.4 (08-spec-vm-ext4-volume.md): prescribes ProviderIndex struct, VolumeEntry { image_path: PathBuf, vm_id: String } field names and types, and method names (insert, get_by_session, remove, flush).

The tracking issue #583 carries no plan:provided label, so the plan-translation relaxation does not apply. Behavioral reformulations would satisfy the gate (e.g., "the writable volume shall support per-call durability configuration via environment variable overrides" in place of prescribing the env var name and the Rust enum type).

This is pervasive throughout all three units. Advisory; does not block merge.


Gate 3 — Assumptions explicit: ✅ PASS

Design Considerations and Technical Considerations sections make key assumptions explicit:

  • Two-disk vs. single read-write root rationale (rootfs upgrade, per-VM divergence).
  • Sparse-raw provisioning: allocate-on-write and reclaim-on-discard treated as independent properties with distinct verification requirements.
  • Guest-side mkfs rationale (host macOS has no mke2fs).
  • Platform divergence: KRUN_SYNC_RELAXED ≡ full sync on Linux, three-point curve on macOS.
  • Quiesce ordering: Shutdown RPC → syncfs/unmount → SIGTERM.
  • sessions/ persistence contract distinguished from providers/ boot reset.

Gate 4 — Proof artifacts present and behavioral: ⚠️ WARNING

Unit-by-unit count:

Unit Artifacts In range? Notes
Unit 1 4 ⚠️ Exceeds 1–3 Artifact 4 explicitly marked "Does not gate merge" — informational throughput measurement for default selection
Unit 2 2
Unit 3 2

All artifacts pass the empty-PR rule: each would fail against the current codebase before implementation. Specifically:

  • Unit 1 Artifact 2 (boot test): current code emits READY via /run tmpfs with no /dev/vdb requirement; the artifact checks /dev/vdb is mounted before READY — fails today.
  • Unit 2 Artifact 2 (loud-failure test): current code emits READY even without a second disk; the artifact asserts no READY on missing disk — fails today.
  • Unit 3 Artifact 1 (self-heal test): Store::new currently hard-fails on corrupt index.json after READY; the artifact asserts READY is reached — fails today.

No Blocker: no unit lacks a proof artifact. The over-count on Unit 1 is advisory. Artifact 4 being explicitly non-gating is acceptable; it quantifies the cost tradeoff that informs R1.9's default values.


Gate 5 — Demoable unit sized to a reviewable PR: ✅ PASS (advisory)

All three units are substantive scope with distinct dependency edges (Unit 2 depends on Unit 1; Unit 3 depends on Unit 1 and Unit 2). No under-sized units or candidates for folding identified.


Summary

Gate Result Severity
1 — Acceptance criteria testable Pass
2 — No implementation leakage Finding ⚠️ Warning
3 — Assumptions explicit Pass
4 — Proof artifacts present and behavioral Finding (Unit 1 count) ⚠️ Warning
5 — Demoable unit sized to reviewable PR Pass

No Blockers. No needs-human applied. The spec is ready to merge.

The implementation leakage (Gate 2) is pervasive and likely inherited from the detailed tracking-issue plan; the behavioral intent is clear even where API names are specified. The Unit 1 artifact count (Gate 4) exceeds the guideline but all four are behavioral and Artifact 4 is explicitly non-gating.

sdd-validate · spec boundary · PR #658 · workflow run

Generated by sdd-validate for issue #658 ·

@norrietaylor
norrietaylor merged commit 248f869 into main Jul 8, 2026
12 checks passed
@norrietaylor
norrietaylor deleted the spec/vm-ext4-volume-205fa69651149a87 branch July 8, 2026 17:47
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.

spec: pkg cache on the in-VM ext4 volume (volume-aware)

2 participants