fix(l4d2-web): write sandbox script tmpfile under LEFT4ME_ROOT, not /tmp

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) <noreply@anthropic.com>
This commit is contained in:
mwiegand 2026-05-08 17:14:21 +02:00
parent 023cc5c9b0
commit 406f2196f8
No known key found for this signature in database

View file

@ -31,6 +31,18 @@ SCRIPT_SANDBOX_HELPER = "/usr/local/libexec/left4me/left4me-script-sandbox"
DISK_BUDGET_BYTES = 20 * 1024**3 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): class BuildError(RuntimeError):
"""Raised by builders when a build fails for a builder-specific reason """Raised by builders when a build fails for a builder-specific reason
(e.g. disk-budget exceeded). Distinct from subprocess-level (e.g. disk-budget exceeded). Distinct from subprocess-level
@ -189,7 +201,11 @@ def run_sandboxed_script(
) -> None: ) -> None:
"""Write `script_text` to a tmpfile and exec it inside the privileged """Write `script_text` to a tmpfile and exec it inside the privileged
sandbox helper. Used by ScriptBuilder.build and by the wipe route.""" 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 "") f.write(script_text or "")
script_path = f.name script_path = f.name
# NamedTemporaryFile creates 0600 owned by the web user; the sandbox runs # NamedTemporaryFile creates 0600 owned by the web user; the sandbox runs