SNAPSHOT_SCRUB_PATHS is expanded by /bin/sh inside the container as root.
For an entry ending `/*` the parent is a path *component*, resolved by both
the glob expansion and the `rm -rf`. The agent has passwordless sudo, so
`ln -s /workspace/myproject /var/log/apt` turned the next commit into a
recursive delete of the user's real files on the host — reproduced end to
end against a live container.
snapshot_scrub_script now routes every deletion through one `scrub_in`
function that validates the parent before touching anything inside it:
`cd -P` for a TOCTOU-free handle, `pwd -P` equality to reject a symlinked
component, a hardcoded containment allowlist that is deliberately not
derived from the path list, and an st_dev comparison against `/` so a bind
mount or a volume is refused even though it is not a symlink. It fails
closed when `stat` is missing.
Also:
- /tmp/triple-c-drops/* and /tmp/clipboard_*.png are age-limited to 14 days
instead of scrubbed unconditionally. They hold the user's own files, and
scrubbing them meant "drop a file, change a setting, lose it silently";
removing them from the list would restore unbounded growth instead.
- M11: scrub_writable_layer returns a ScrubOutcome and skips cleanly when
the container is not running, so a migration no longer logs "could not
run … committing anyway" on every run.
- M12: the scrub exec is bounded by a 120s timeout; on expiry it logs and
lets the commit proceed.
- The script is now fold-safe (self-terminating lines, no `#` comments).
disk.rs folds it onto one `RUN` line and the previous form was a
`"do" unexpected` syntax error there, so compaction had been scrubbing
nothing at all.
Tests: the substring check on the script text is replaced by a behavioural
test that runs the real generated script with a real symlink planted in a
throwaway tree, plus structural tests over each containment construct and
a `sh -n` check of the folded form.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBq2rGum6GX7xXgsas1fDc