From 7a25c2453c799ea1e5808026e0b6667193562df7 Mon Sep 17 00:00:00 2001 From: mwiegand Date: Fri, 15 May 2026 01:33:13 +0200 Subject: [PATCH] fix(left4me-script-sandbox): self-wrap into PID 1's mount namespace MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .../usr/local/libexec/left4me/left4me-script-sandbox | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/deploy/files/usr/local/libexec/left4me/left4me-script-sandbox b/deploy/files/usr/local/libexec/left4me/left4me-script-sandbox index aa533f0..d3ce299 100755 --- a/deploy/files/usr/local/libexec/left4me/left4me-script-sandbox +++ b/deploy/files/usr/local/libexec/left4me/left4me-script-sandbox @@ -19,6 +19,18 @@ # not signal-able due to UID mismatch. 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