From 406f2196f86520dddeb9edae7355ee8540eb7afa Mon Sep 17 00:00:00 2001 From: mwiegand Date: Fri, 8 May 2026 17:14:21 +0200 Subject: [PATCH] fix(l4d2-web): write sandbox script tmpfile under LEFT4ME_ROOT, not /tmp MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The web service unit has PrivateTmp=yes: its /tmp is a per-instance namespace at /tmp/systemd-private-X-left4me-web.service-Y/tmp/ from PID 1's perspective. When ScriptBuilder writes /tmp/tmpXXX.sh and passes that path to the sandbox helper, systemd-run asks PID 1 to set up BindReadOnlyPaths=${SCRIPT}:/script.sh — but PID 1 lives in the host namespace and can't resolve the web service's PrivateTmp path. The unit fails to start with status=226/NAMESPACE and "Failed to set up mount namespacing: /script.sh: No such file or directory". Move the tmpfile to ${LEFT4ME_ROOT}/sandbox-scripts/. /var/lib is not affected by PrivateTmp (only /tmp and /var/tmp are), so PID 1 can resolve the path. The web service has ReadWritePaths=/var/lib/left4me already, and the directory is created on demand by Python. Co-Authored-By: Claude Opus 4.7 (1M context) --- l4d2web/services/overlay_builders.py | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/l4d2web/services/overlay_builders.py b/l4d2web/services/overlay_builders.py index da1d95c..753986e 100644 --- a/l4d2web/services/overlay_builders.py +++ b/l4d2web/services/overlay_builders.py @@ -31,6 +31,18 @@ SCRIPT_SANDBOX_HELPER = "/usr/local/libexec/left4me/left4me-script-sandbox" DISK_BUDGET_BYTES = 20 * 1024**3 +def _sandbox_script_dir() -> Path: + """Where script tmpfiles live before being bind-mounted into the sandbox. + + Cannot live in /tmp because the web service unit has PrivateTmp=yes: + its /tmp is a per-instance namespace that PID 1 (which actually performs + the BindReadOnlyPaths during sandbox setup) cannot resolve. /var/lib is + not affected by PrivateTmp and is visible to PID 1, so the bind-mount + succeeds. + """ + return get_left4me_root() / "sandbox-scripts" + + class BuildError(RuntimeError): """Raised by builders when a build fails for a builder-specific reason (e.g. disk-budget exceeded). Distinct from subprocess-level @@ -189,7 +201,11 @@ def run_sandboxed_script( ) -> None: """Write `script_text` to a tmpfile and exec it inside the privileged sandbox helper. Used by ScriptBuilder.build and by the wipe route.""" - with tempfile.NamedTemporaryFile("w", suffix=".sh", delete=False) as f: + script_dir = _sandbox_script_dir() + script_dir.mkdir(parents=True, exist_ok=True) + with tempfile.NamedTemporaryFile( + "w", suffix=".sh", delete=False, dir=str(script_dir) + ) as f: f.write(script_text or "") script_path = f.name # NamedTemporaryFile creates 0600 owned by the web user; the sandbox runs