The no-seed branch used to mount the base rootfs read-only, mount
the freshly mkfs'd work disk read-write, sudo-cp /root from one to
the other, then flatten any accidental /root/root/ nesting. Five
sudo call sites packed into a fallback that the common image path
doesn't even exercise.
Replace with: `mkfs.ext4 -F -E root_owner=0:0` and nothing else.
mkfs already stamps inode 2 as root:root:0755 — sshd's StrictModes
walks that dir's ownership when the work disk mounts at /root in
the guest, so getting it right from mkfs means authsync can just
write authorized_keys without any repair pass.
Tradeoff: no-seed VMs lose the base rootfs's default /root dotfiles
(.bashrc, .profile). The no-seed path is explicitly the degraded
fallback — `banger doctor` already warns about it — and users who
want those back have two documented knobs: rebuild the image with
a work-seed, or land them via [[file_sync]].
Sudo call sites removed: 5 (MountTempDir × 2, sudo cp -a,
flattenNestedWorkHome's chmod/cp/rm). flattenNestedWorkHome itself
stays alive for now — authsync + image_seed still call it — and
gets deleted in commit 5 once its last caller goes away.
While here: fix the freshly-added EnsureExt4RootPerms helper.
`set_inode_field <2> mode N` overwrites the full i_mode word
instead of preserving the type nibble, so the initial
implementation that passed just the permission bits (0755) would
reset the fs root to regular-file shape and break the next kernel
mount with "Structure needs cleaning." The corrected call OR's in
S_IFDIR (0o040000) explicitly. Test updated to match.
Smoke: 21/21 scenarios green.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The daemon mounts every VM's work disk on the host via sudo, copies
files in as root, chmods+chowns them, and unmounts. That's ~18 of
banger's runtime RunSudo calls. The ext4 image is a regular file the
daemon user owns; e2cp / debugfs can write to it directly and bake
uid/gid/mode into the filesystem metadata without the caller being
root. `imagepull.ApplyOwnership` already proves this works in
production (OCI layer flattening writes 0/0/root-owned inodes from
an unprivileged daemon).
This commit adds the toolkit layer. Callers land in the next four
commits:
- MkdirExt4 — idempotent directory create + metadata reset, single
debugfs batch
- WriteExt4FileOwned — e2cp + debugfs-driven uid/gid/mode, auto-
cleans the host tempfile
- SetExt4Ownership — sif + set_inode_field batch for existing
inodes (no mkdir implied)
- EnsureExt4RootPerms — fixes inode <2> (the fs root, which is
`/root` once the work disk is mounted inside the guest), the
thing sshd's StrictModes walks
- Ext4PathExists — yes/no probe via `debugfs -R "stat ..."` with
"File not found" detection
- ReadExt4File — bytes-returning wrapper around the existing
ReadDebugFSText with the same path rejection
Design notes:
- extfsRun auto-switches Run ↔ RunSudo on imagePath's type: regular
files get the unprivileged path, block devices (dm-snapshot,
loops) get sudo. The same helper works for both patchRootOverlay
(dm device) and work-disk writes (user-owned file). No caller
flag needed — os.Stat tells us.
- debugfsScript batches set_inode_field + sif + mkdir lines into
one `debugfs -w -f -` stdin invocation on any Runner that
implements StdinRunner (production's system.Runner does). Matches
imagepull.ApplyOwnership's existing pattern; dramatically cheaper
than per-call subprocesses.
- Paths are escaped for debugfs on the way in: spaces get double-
quoted, double-quote/backslash/newline are rejected outright
(debugfs's hand-rolled parser doesn't reliably escape those and
we'd rather fail fast than silently scribble over the wrong
inode).
Tests: seven behaviour assertions via scripted + stdin-scripted
runners — existence probe (found + missing + rejection), read
passthrough, mkdir batch contents (new vs. pre-existing path), write
tempfile cleanup + mode line shape, root-inode addressing, and the
full rejectDebugfsUnsafePath matrix.
No production wiring change in this commit — the helpers land
unused. `make smoke` stays green (21/21) because nothing else
shifted.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>