run: fail fast on unusable sandbox bind paths (+ empty-src bind_try skip) #1

Merged
chad merged 2 commits from run/bind-dest-validation into master 2026-09-24 15:27:51 -04:00
Collaborator

Engine-side hardening that came out of the batch-3/4 packaging work: fail fast on sandbox bind paths that cannot work at exec time.

The problem: the staged rootfs (composefs render over FUSE, or the compose overlay) is read-only, and bwrap creates bind destinations by mkdir inside it — dying LATE with the cryptic Can't mkdir <dst>: Read-only file system (tmux hit this in the pkgs batch-3 gate). --dir is no escape: same mkdir, same EROFS (verified against bwrap 0.11.2). The repo's own conventions (tmux/nvim RUN mkdir -p, distroless /dirs/ COPY) exist precisely because of this, but nothing told you that was the fix.

The change: validate_bind_paths runs before bwrap on both the direct and compose-inner paths and turns each case into an early error that names the bind and the fix. Destination existence is judged against the SANDBOX VIEW, not the bare rootfs — host-overlaid dirs (/run /var /etc /home /mnt /media /opt /srv) stat host-side; /tmp /dev /proc are engine-mounted writable pseudo-fs; everything else stats the rootfs.

Bonus fix (real latent bug): bwrap does NOT treat an empty bind_try source as "missing" — it falls into the dst mkdir and EROFS. An unset $VAR in an entrypoint expands to exactly that empty string, so pkgs#14's openssh-clients agent-socket design (bind_try $XDG_RUNTIME_DIR) was broken on any host with no runtime dir, despite passing CI (which sets the var). push_bind now skips empty-source try binds with a warning, making that design actually degrade as documented; required binds with empty/missing sources error loudly via the validator.

Deliberately rejected: auto-creating mountpoints engine-side. The FUSE server is the pinned upstream composefs-fuse serving an immutable EROFS image — creation would mean forking it or prerendering per-run images. Hard error + actionable message instead (maintainer call, 2026-09-24; happy to revisit if we ever want a writable-root layer).

9 new unit tests (116 total green), clippy --all-targets clean, release build green. Third commit is drive-by newer-clippy fallout in sources/home (empty format literal, useless vec! in tests) so the bar stays green on rust:bookworm's toolchain.

Engine-side hardening that came out of the batch-3/4 packaging work: **fail fast on sandbox bind paths that cannot work at exec time.** **The problem**: the staged rootfs (composefs render over FUSE, or the compose overlay) is read-only, and bwrap creates bind destinations by mkdir inside it — dying LATE with the cryptic `Can't mkdir <dst>: Read-only file system` (tmux hit this in the pkgs batch-3 gate). `--dir` is no escape: same mkdir, same EROFS (verified against bwrap 0.11.2). The repo's own conventions (tmux/nvim `RUN mkdir -p`, distroless `/dirs/` COPY) exist precisely because of this, but nothing told you that was the fix. **The change**: `validate_bind_paths` runs before bwrap on both the direct and compose-inner paths and turns each case into an early error that names the bind and the fix. Destination existence is judged against the SANDBOX VIEW, not the bare rootfs — host-overlaid dirs (/run /var /etc /home /mnt /media /opt /srv) stat host-side; /tmp /dev /proc are engine-mounted writable pseudo-fs; everything else stats the rootfs. **Bonus fix (real latent bug)**: bwrap does NOT treat an empty bind_try source as "missing" — it falls into the dst mkdir and EROFS. An unset `$VAR` in an entrypoint expands to exactly that empty string, so pkgs#14's openssh-clients agent-socket design (`bind_try $XDG_RUNTIME_DIR`) was broken on any host with no runtime dir, despite passing CI (which sets the var). `push_bind` now skips empty-source try binds with a warning, making that design actually degrade as documented; required binds with empty/missing sources error loudly via the validator. **Deliberately rejected**: auto-creating mountpoints engine-side. The FUSE server is the pinned upstream composefs-fuse serving an immutable EROFS image — creation would mean forking it or prerendering per-run images. Hard error + actionable message instead (maintainer call, 2026-09-24; happy to revisit if we ever want a writable-root layer). 9 new unit tests (116 total green), clippy --all-targets clean, release build green. Third commit is drive-by newer-clippy fallout in sources/home (empty format literal, useless vec! in tests) so the bar stays green on rust:bookworm's toolchain.
The staged rootfs (composefs render over FUSE, or the compose overlay) is
read-only: bwrap creates bind destinations by mkdir in that root and dies
late with the cryptic 'Can't mkdir <dst>: Read-only file system' — --dir
fares no better (verified against bwrap 0.11.2). validate_bind_paths now
runs before bwrap on both the direct and compose-inner paths and turns
each case into an early error that names the bind and the fix (image-side
'RUN mkdir -p <dst>' — the tmux/nvim and distroless /dirs/ pattern).

Destination existence is judged against the sandbox view: host-overlaid
dirs (/run /var /etc /home /mnt /media /opt /srv) stat host-side, /tmp
/dev /proc are engine-mounted writable pseudo-fs, everything else stats
the rootfs. bind_try with an empty expanded source is skipped by
push_bind (warning) rather than reaching bwrap, which does NOT treat an
empty source as missing and would EROFS on the dst mkdir — this makes
the openssh-clients agent-socket design safe on hosts with no runtime
dir at all. Required binds with empty/missing sources error loudly.

Auto-creating mountpoints engine-side was evaluated and rejected: the
FUSE server is the pinned upstream composefs-fuse serving an immutable
EROFS image; creation would mean forking it or prerendering per-run
images. Hard error + actionable message instead (maintainer call,
2026-09-24). 9 new unit tests (116 total).
chad approved these changes 2026-09-24 15:27:49 -04:00
chad merged commit 3892331213 into master 2026-09-24 15:27:51 -04:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
chad/bubblebox!1
No description provided.