microvm-rootfs: ship e2fsprogs + util-linux and /var/lib/minimal mountpoint - #365
Conversation
…tpoint Unblocks Unit 1 of the per-VM writable ext4 volume spec (gominimal/minimal #583, PR #658 R1.7). The guest minimald mounts a per-VM writable volume (/dev/vdb) at /var/lib/minimal, formats it with mkfs.ext4 on first boot, and reclaims space via discard/fstrim. The rootfs was missing all three pieces: - Promote e2fsprogs from a build-only dep (its files were stripped from the image) to the runtime closure, so mkfs.ext4/mke2fs ship in the guest. mke2fs stays on the build PATH via the injected runtime closure, so it still packs the image — the separate build_deps entry is now redundant and removed. - Add util-linux to the runtime closure for fstrim (space reclaim). - Stage /var/lib/minimal as an empty directory. The root is mounted read-only, so minimald cannot mkdir the mountpoint at runtime; it must ship in the image or the mount fails with ENOENT/EROFS. Image stays raw ext4 (rootfs.img); size grows ~26 MiB (from ~444 MB to ~472 MB). Verified in the built image: /var/lib/minimal is empty, mkfs.ext4 -V and fstrim --help resolve, filesystem TYPE=ext4. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (2)
🚧 Files skipped from review as they are similar to previous changes (2)
📝 WalkthroughWalkthroughAdds Changesmicrovm-rootfs util-linux and writable-volume support
Estimated code review effort: 2 (Simple) | ~10 minutes Possibly related PRs
Suggested reviewers: 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
…ed libs e2fsprogs and util-linux both ship a libblkid.so.1 and libuuid.so.1 with the same soname; the staging composition resolved them to e2fsprogs's older, unversioned forks. util-linux's libmount then loaded those and warned "libblkid.so.1: no version information available" on every fstrim/mount/blkid. Repoint both sonames at util-linux's versioned libs (an ABI superset — e2fsprogs's own mke2fs/e2fsck link them fine, verified by running a real mkfs.ext4 format) and drop the e2fsprogs forks. A guard fails the build if a package bump renames the util-linux targets so this can't silently regress. Verified in the rebuilt image: fstrim --help and mount -V emit zero stderr lines, mkfs.ext4 -V still resolves. Net image size drops ~175 KB. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
🧹 Nitpick comments (1)
packages/microvm-rootfs/build.sh (1)
50-69: 📐 Maintainability & Code Quality | 🔵 Trivial | 🏗️ Heavy liftConsider fixing the duplicate soname issue upstream in
e2fsprogs's build, instead of patching it here.The comment correctly diagnoses the root cause: both
e2fsprogsandutil-linuxshiplibblkid/libuuidwith the same soname. The conventional fix for this well-known conflict is to disable e2fsprogs's own copies at build time (--disable-libblkid --disable-libuuid) so only util-linux's versions ever ship, per Linux From Scratch: e2fsprogs's configure flags "prevent building and installing the libuuid and libblkid libraries... as Util-Linux installs more recent versions." Fixing this inpackages/e2fsprogs/build.ncl/build.shwould eliminate the duplication at the source instead of every downstream consumer needing to repoint sonames and prune forks like this.That said, this workaround is well-guarded (fails loudly if the util-linux filenames change) and the hardcoded versions (
libblkid.so.1.1.0,libuuid.so.1.3.0) do match current util-linux upstream releases, so this is not a blocker.One minor asymmetry: the
rm -fat Line 66 removes the e2fsprogs fork files by hardcoded name with no existence guard (unlike the util-linux check above it). If e2fsprogs renames these in a future bump,rm -fsilently no-ops rather than failing — the finalln -sfstill repoints the soname correctly either way, but the stale e2fsprogs file would linger unpruned.🤖 Prompt for 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. In `@packages/microvm-rootfs/build.sh` around lines 50 - 69, The cleanup step for the old e2fsprogs libblkid/libuuid forks is too silent because the rm -f calls can miss renamed files without surfacing a problem. Update the build.sh logic around the ul_blkid and ul_uuid handling to verify the e2fsprogs fork filenames before removing them, or otherwise fail loudly if they are absent, so the repointing of libblkid.so.1 and libuuid.so.1 stays fully guarded against future package renames.
🤖 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.
Nitpick comments:
In `@packages/microvm-rootfs/build.sh`:
- Around line 50-69: The cleanup step for the old e2fsprogs libblkid/libuuid
forks is too silent because the rm -f calls can miss renamed files without
surfacing a problem. Update the build.sh logic around the ul_blkid and ul_uuid
handling to verify the e2fsprogs fork filenames before removing them, or
otherwise fail loudly if they are absent, so the repointing of libblkid.so.1 and
libuuid.so.1 stays fully guarded against future package renames.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 2284545f-3816-461b-ad3c-3fca9bf9f054
📒 Files selected for processing (1)
packages/microvm-rootfs/build.sh
…le-volume-support # Conflicts: # packages/microvm-rootfs/build.ncl # packages/microvm-rootfs/build.sh
|
Re CodeRabbit's nitpick (fix the duplicate-soname issue upstream in e2fsprogs, and the unguarded |
* feat(minvmd): per-VM writable /dev/vdb — attach, provision, sync tunables Host side of the per-VM writable ext4 volume (spec #583 Unit 1). Adds the krun_add_disk3 FFI + SyncMode tri-state (KRUN_SYNC_{NONE,RELAXED,FULL}, a u32 not a bool) and Context::add_disk_with_sync. volume.rs provisions a sparse raw image (ftruncate, allocate-on-write) at the resolved path — MINVMD_DATA_VOLUME_PATH override, else <state>/data-vol.raw honouring XDG_STATE_HOME — and is idempotent. vmm_child attaches /dev/vdb on every boot with the MINVMD_DISK_SYNC / MINVMD_DISK_DIRECT_IO tunables (default relaxed / false). Refs: #583 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(minimald): mount + relocate cache/state onto /dev/vdb The guest formats /dev/vdb on first boot (ext4 superblock-gated mkfs, sized 1 MiB below the device to survive libkrun's backing-file trailer shave) and mounts it read-write at /var/lib/minimal; cache + state then relocate onto it, resolving the EXDEV hardlink constraint and persisting session state across clean restarts. Mount presence uses try_exists so a stat error is not silently treated as "no volume". Refs: #583 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * build: pin pkgs rootfs shipping /var/lib/minimal + e2fsprogs microvm-rootfs now ships the /var/lib/minimal mountpoint and e2fsprogs + util-linux (gominimal/pkgs#365/#366) so the guest can mkfs.ext4 and mount /dev/vdb (and fstrim-reclaim it). Re-pin locked_commit to 12f324d8. Refs: #583 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * ci(minvmd,minimald): assert libkrun >= 1.19 in the VM boot jobs krun_add_disk3 (this PR's /dev/vdb attach) needs libkrun >= 1.19.0. The macOS "Verify libkrun" steps (boot-e2e / autospawn-e2e / build-macos) and the Linux/KVM job now assert the libkrun in use exports krun_add_disk3, failing with an actionable "upgrade the runner" message instead of a cryptic dyld symbol-not-found at VM boot. Refs: #583 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(minimald): format the data volume without a background lazy-init storm On first boot the guest mkfs.ext4's the (default 32 GiB) /dev/vdb. With ext4's default lazy inode-table init, the ext4lazyinit kernel thread zeroes ~512 MiB of inode tables in the background right after mount — sustained I/O that lands exactly when the guest is bringing up its vsock bridge + SSH server on 2 vCPUs. That starved the first host->guest connect: a gvproxy-forwarder control request timed out (5s) and `minimal ls` failed with `ssh connect: Disconnected` (autospawn-e2e). boot-e2e was unaffected because its check used a 2 GiB volume. Format with `-E lazy_itable_init=0,lazy_journal_init=0` (do the zeroing now, at mkfs, not in the background) and `-i 65536` (~524K inodes / ~128 MiB table instead of ~2M / ~512 MiB) so the synchronous init is small and one-time; later boots detect the superblock and skip mkfs entirely. Refs: #583 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(minimald): reject undersized data volumes before mkfs run_mkfs_ext4 derived the ext4 block count as `device_size_bytes.saturating_sub(MKFS_MARGIN_BYTES) / EXT4_BLOCK_BYTES`, which is 0 for any device <= the 1 MiB margin — handing mkfs.ext4 a nonsensical size argument. Reject a device below MKFS_MIN_DEVICE_BYTES (16 MiB, comfortably above ext4's journal minimum) with InvalidInput before invoking mkfs. Only guards a misconfigured MINVMD_VOLUME_BYTES or malformed device; the real volume is GiB-scale. Refs: #583 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(minimald): don't block SSH accept on the gvproxy egress-proxy publish Server::run awaited start_host_proxies before entering the accept loop, and the DM1 host-expose there (expose_proxy_on_host -> gvproxy /services/forwarder/expose) blocks up to a 5s control-request timeout when the host gvproxy is slow. In that window the SSH listener is bound but not accepting, so the first host->guest connect lands in the gap and fails with `ssh connect: Disconnected` — seen in autospawn-e2e, where `minimal ls` connects right after READY. Attaching the per-VM data volume made the host gvproxy slow enough to hit this on macOS/HVF, exposing the latent bug (main passes only because gvproxy responds fast). The expose is already best-effort (warns + continues on failure), so detach it: bind the proxy listeners synchronously as before, but spawn the host-expose so it never gates the SSH accept loop. Refs: #583 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * revert(minimald): restore awaited gvproxy egress-proxy publish This reverts commit 671bec4. Detaching the host-side expose removed an accidental serialization that main relies on. With the expose spawned, the first host->guest SSH connect overlaps the guest->host expose control request on libkrun's single vsock device and wedges it (#588), so cold `minimal ls` fails with `ssh connect: Disconnected` in autospawn-e2e. Awaiting the expose (as on main) serializes vsock usage so the accept loop's first connection stays clean. The 5s cold-boot delay when the host gvproxy is absent is pre-existing on main and out of scope here. Refs: #588 * fix(minvmd): warn on unrecognized disk-tunable env values resolve_disk_flags silently swallowed unrecognized MINVMD_DISK_SYNC and MINVMD_DISK_DIRECT_IO values, falling back to the defaults with no signal. Emit a tracing::warn! naming the env var and value when a set value is unrecognized, keeping the existing relaxed/false defaults. * fix(minvmd): treat concurrent data-volume creation as idempotent ensure_sparse_raw probes metadata then create_new; a provisioner racing between the two surfaced AlreadyExists as VolumeError::Create, breaking the documented idempotent contract. Treat an AlreadyExists creation race as success (the volume now exists) and keep the image as-is. * ci: cover crates/minimald in the macOS lane ci-macos boots minimald as pid-1 in the boot/e2e VM, yet its paths filter omitted crates/minimald — so guest-daemon-only changes skipped every macOS VM check, including autospawn-e2e. Add crates/minimald/** to the push + pull_request filters so guest-daemon changes get the coverage that exercises them. * ci: provision a host gvproxy for the macOS e2e job The Auto-spawn E2E boots the guest, which publishes its host-side proxy via the host gvproxy over the vsock shuttle. With no gvproxy the publish blocks until its 5s control-request timeout, holding Server::run's SSH accept loop closed; the cold `minimal ls` connect-retry deadline then expires and the list fails (`ssh connect: Disconnected`). This is a pre-existing ~40ms race that /dev/vdb's ~60ms first-boot mkfs tips red. Fetch gvproxy and export MINVMD_GVPROXY_BIN so the publish completes immediately, matching a real host. * refactor(minimald): gate the data-volume mount on a ListenArgs field Replace the second `is_minimal_microvm()` branch (per Tom's review) with a hidden `mk_mount_state_volume: Option<String>` field on ListenArgs, set once in the microVM config block. The mount is then driven by `if let Some(dev) = listen_args.mk_mount_state_volume`. State/cache relocation stays gated on the mount succeeding — pointing state at an unmounted /var/lib/minimal would land it on the read-only rootfs (that becomes eager once Unit 2 makes mount failure loud). * docs(minimald): frame the mkfs options as consequences, not tuning Per Tom's review: make explicit that the non-default mkfs.ext4 options are not independent tuning but fall out of two deliberate choices — eager inode/journal init (whose entailment is the reduced inode count) and surviving libkrun's backing-file trailer shave (whose entailment is the explicit block count + pinned block size). ext4's defaults are otherwise left alone. * refactor(minvmd): resolve data-volume path via paths::minimal_state_dir Per Tom's review, source the state-dir base from the shared `paths::minimal_state_dir()` resolver instead of the minvmd-local `StateDir::default_path`. Keep the explicit `XDG_STATE_HOME` override first: `dirs` (and thus `minimal_state_dir`) ignores it on macOS, and the e2e tests isolate state via `XDG_STATE_HOME` — without it the volume would escape the tests' temp dir onto the real state dir. Promote `paths` from a dev- to a regular dependency. * feat(minvmd): default the data volume to 256 GiB Resolves Tom's review point: the image is sparse and offline resize is hard, so size it big up front. Measured first-boot cost is flat in the volume size — mkfs.ext4 writes only ~5-8 MiB of metadata regardless (sparse backing + reduced inode ratio), so format+mount stays ~40 ms at 32/64/128/256 GiB on an M-series host. No boot-time penalty, so the default goes to 256 GiB; MINVMD_VOLUME_BYTES still overrides. Refs: #583 * refactor(minimald): drop the redundant mkfs -i override Measurement (32/64/128/256 GiB) shows mke2fs already defaults to a ~65536 bytes/inode ratio at these volume sizes — `-i 65536` produces an identical inode count (32 GiB -> 524288, 256 GiB -> 4194304, both 65536 B/inode). And the eager inode-table init is cheap because the volume is sparse (zero-writes land in holes; ~sub-ms at every size), not because of the inode count. So the override earns nothing: drop it plus MKFS_BYTES_PER_INODE and correct the doc rationale. Per Tom's review. Refs: #583 * style(minvmd): rustfmt the path resolver + correct the flat-cost note The wrapped closure exceeded rustfmt width (fmt CI). Also drop the stale "reduced inode ratio" credit from the DEFAULT_VOLUME_BYTES note now that the -i override is gone — the flat first-boot cost is purely the sparse backing. * fix(minimald): cap the boot-path gvproxy expose so it can't stall SSH accept start_host_proxies awaits expose_proxy_on_host on Server::run's boot path, before the SSH accept loop serves. The `/services/forwarder/expose` control request runs to the full 5s GVPROXY_CONTROL_TIMEOUT even with a host gvproxy present (the forwarder control path isn't reachable over the shuttle in every deployment), holding the accept loop closed. The cold `minimal ls` connect-retry deadline (~5s) then expires as the loop would resume, so the first list fails with `ssh connect: Disconnected` — the volume's ~60ms first-boot mkfs tips a pre-existing race red on the e2e autospawn gate. Cap the publish at HOST_EXPOSE_PUBLISH_TIMEOUT (1s): a best-effort publish must never stall the SSH bridge. Best-effort semantics unchanged. Refs: #588 * ci: drop the ineffective gvproxy provisioning from the e2e job Provisioning a host gvproxy did not fix the autospawn stall — the boot log shows the `/services/forwarder/expose` control request still times out at 5s even with gvproxy present. The bounded expose (HOST_EXPOSE_PUBLISH_TIMEOUT) fixes it in the daemon instead, so the fetch step + MINVMD_GVPROXY_BIN export are dead weight; remove them. * ci: reap the Session-E2E VM before the auto-spawn step With boot-e2e and autospawn-e2e merged into one `e2e` job (upstream), the session harness's leftover `__krun-vmm` (its Drop kills minvmd but not the detached VMM grandchild) lingers into Auto-spawn. As a second concurrent VM it wedges the host->guest bridge over libkrun's vsock (#588): with the expose now bounded, the accept loop is up early yet the boot log shows no connection ever accepted. Reap the VMM so Auto-spawn runs single-VM, as it did when it was a standalone job. The proper fix is the session harness reaping its own grandchild (process group); tracked separately. --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
What
Adds the three pieces the guest rootfs needs to support a per-VM writable ext4 volume, unblocking Unit 1 of the per-VM writable volume spec (gominimal/minimal#583, PR #658 R1.7).
The in-guest
minimaldmounts a per-VM writable volume (/dev/vdb) at/var/lib/minimal, formats it withmkfs.ext4on first boot, and reclaims space via discard /fstrim. The current rootfs (base+git) ships none of these. This change:e2fsprogsfrom build-only to the runtime closure. It was previously abuild_depsentry whose files (usr/sbin,mke2fs, e2fsprogs libs) were deliberately stripped from the image bybuild.sh. Now its files ship, providingmkfs.ext4/mke2fsin the guest.mke2fsremains on the build PATH via the injected runtime closure, so it still packs the image — the redundantbuild_depsentry and the strip block are removed.util-linuxto the runtime closure forfstrim(space reclaim).mount -o discard(online discard) also works with plain ext4, so this is secondary toe2fsprogs./var/lib/minimalas an empty directory. The root image is mounted read-only in the guest, sominimaldcannotmkdirthe mountpoint at runtime — it must ship in the image or the mount fails withENOENT/EROFS.libblkid/libuuidto util-linux's versioned libs. e2fsprogs and util-linux both ship alibblkid.so.1/libuuid.so.1with the same soname; staging resolved them to e2fsprogs's older, unversioned forks, so util-linux'slibmountwarnedno version information availableon everyfstrim/mount/blkid.build.shnow repoints both sonames at util-linux's versioned libs (an ABI superset — e2fsprogs's ownmke2fs/e2fscklink them fine) and drops the e2fsprogs forks. A guard fails the build if a package bump renames the util-linux targets, so it can't silently regress.No packaging/format change: the output stays a raw ext4
rootfs.img.Verification
Built with
minimal patched-build microvm-rootfs, then mounted the resulting ext4 image (loopback, in a Linux container) and chrooted:/var/lib/minimal— present, empty (0 entries).mkfs.ext4 -V/mke2fs -V→mke2fs 1.47.4.mkfs.ext4is themke2fssymlink in/usr/sbin; a realmkfs.ext4 -Fformat of a scratch image succeeds against util-linux'slibblkid.fstrim --help/fstrim --version→fstrim from util-linux 2.42.1, zero stderr (no libblkid warning).mount -V→mount from util-linux 2.42.1, zero stderr.libblkid.so.1 -> libblkid.so.1.1.0andlibuuid.so.1 -> libuuid.so.1.3.0(util-linux); e2fsprogs forks removed.blkidon the image →TYPE="ext4"(raw ext4, boot layout unchanged).min check --packages microvm-rootfs— all checks Pass (including post-buildenumerate bins,output types valid,missing runtime_deps).Image size:
444,070,912→471,600,128bytes (+~27.5 MiB), bounded and expected for both packages' binaries + libs. (The bulk of the image — a ~192 MiB GCC in the closure — is pre-existing in both baseline and this build; not introduced here.)Follow-up (not in this PR)
The scoped
libblkid/libuuidrepoint above fixes the warning for this image. The upstream-correct fix is to builde2fsprogswith--disable-libblkid --disable-libuuid --disable-uuiddso util-linux is the sole provider ecosystem-wide (and the guest gets util-linux's modernblkidbinary too, rather than e2fsprogs's 2003-era one). That touches a foundational package and forces a rebuild + re-pin of everye2fsprogsconsumer, so it's deliberately out of scope here. Worth filing separately.Once this merges and the
microvm-rootfsartifact is published, gominimal/minimal must re-pin it in.minimal/minimal.tomlunder[outputs.minvmd-rootfs](locked_commit) to the new build, so the updated rootfs (with/var/lib/minimal,mkfs.ext4,fstrim) is materialized. The guest mount code (minimald mount_state_volume) and host attach already exist in gominimal/minimal and are waiting on this rootfs.Refs: gominimal/minimal#583, gominimal/minimal#658
🤖 Generated with Claude Code
Summary by CodeRabbit
/var/lib/minimaldirectory exists during staging under a read-only root.libblkid/libuuidlibraries to the providedutil-linuxversions, with build-time validation.util-linux) and support for reclaiming space viafstrim.