diff --git a/src/ClawdDotNet.Core/Sandbox/BwrapSandbox.cs b/src/ClawdDotNet.Core/Sandbox/BwrapSandbox.cs index b695344..58639ef 100644 --- a/src/ClawdDotNet.Core/Sandbox/BwrapSandbox.cs +++ b/src/ClawdDotNet.Core/Sandbox/BwrapSandbox.cs @@ -170,7 +170,9 @@ public sealed class BwrapSandbox : ISandboxRunner foreach (var path in request.ReadOnlyPaths) args.AddRange(["--ro-bind", Path.GetFullPath(path), Path.GetFullPath(path)]); - args.AddRange(["--chdir", SandboxPaths.Work]); + // The root bwrap builds is a tmpfs of its own — writable unless it is remounted. Only + // /work and /tmp, each a mount of its own, stay writable. + args.AddRange(["--remount-ro", "/", "--chdir", SandboxPaths.Work]); var environment = new Dictionary(StringComparer.Ordinal) { @@ -186,9 +188,10 @@ public sealed class BwrapSandbox : ISandboxRunner args.AddRange(["--seccomp", "3", "--"]); - // Inner: the process limit, set inside the sandbox's own user namespace. bash calls it - // -u, dash (Debian's and Ubuntu's sh) -p; if neither takes, nothing runs. - args.AddRange([Shell, "-c", """ulimit -u "$1" 2>/dev/null || ulimit -p "$1" || exit 126; shift; exec "$@" """, "sh", + // Inner: fd 3 — the filter bwrap read — is closed; bwrap hands it on otherwise. Then + // the process limit, set inside the sandbox's own user namespace. bash calls it -u, + // dash (Debian's and Ubuntu's sh) -p; if neither takes, nothing runs. + args.AddRange([Shell, "-c", """exec 3<&-; ulimit -u "$1" 2>/dev/null || ulimit -p "$1" || exit 126; shift; exec "$@" """, "sh", limits.MaxProcesses.ToString(CultureInfo.InvariantCulture), request.Executable]); args.AddRange(request.Arguments); return args;