fix(left4me-script-sandbox): self-wrap into PID 1's mount namespace

The web service runs with PrivateTmp=true, which puts it in its own
mount namespace. Worker invokes the sandbox helper via sudo from there;
the helper's pre-systemd-run `mount --bind --map-users=...` lands in
the web service's namespace. systemd-run then spawns transient units
in PID 1's namespace where the bind is invisible — the BindPaths lookup
finds an empty staging dir owned by root, and the sandbox uid hits
permission-denied on every write.

Mirror the pattern from left4me-overlay's ExecStartPre wrapper: enter
PID 1's mount namespace at the start of the helper via `nsenter
--mount=/proc/1/ns/mnt`. Sentinel env var avoids exec recursion. The
gameserver helper handles this at the unit level; the script helper
doesn't have a unit so we self-wrap.

Diagnosis: 5 failed builds all hit the same EACCES on the first
`mkdir`/`tar mkdir`. Direct SSH-sudo invocations of the same helper
succeeded because SSH-sudo doesn't inherit a private namespace; only
the worker-invoked path is affected.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
mwiegand 2026-05-15 01:33:13 +02:00
parent 48381089d3
commit 7a25c2453c
No known key found for this signature in database

View file

@ -19,6 +19,18 @@
# not signal-able due to UID mismatch. # not signal-able due to UID mismatch.
set -euo pipefail set -euo pipefail
# Self-wrap into PID 1's mount namespace before doing anything mount-related.
# The web app's left4me-web.service has PrivateTmp=true, which gives it a
# private mount namespace. When the worker invokes us via sudo, we inherit
# that namespace; our `mount --bind` would land there. systemd-run below
# spawns transient units in PID 1's namespace (where they don't see the
# private bind), so the sandbox would bind onto an empty staging dir and
# permission-deny on every write. The sentinel env var avoids an exec loop.
if [[ "${L4D2_SANDBOX_IN_PID1_MNT_NS:-}" != "1" ]]; then
exec env L4D2_SANDBOX_IN_PID1_MNT_NS=1 \
/usr/bin/nsenter --mount=/proc/1/ns/mnt -- "$0" "$@"
fi
[[ $# -eq 2 ]] || { echo "usage: $0 <overlay_id> <script>" >&2; exit 64; } [[ $# -eq 2 ]] || { echo "usage: $0 <overlay_id> <script>" >&2; exit 64; }
OVERLAY_ID=$1 OVERLAY_ID=$1