Repository navigation
bug(sandbox): GNU Make cannot launch recipes with unchanged UID/GID #4102
Description
Activity
A useful sandbox-policy distinction here is:
"credential syscall requested != credential authority increased"
An identity-preserving "setresuid/setresgid" call and an identity-changing call use the same syscall but have different security semantics.
Rather than treating syscall identity as the entire authorization decision, the policy could evaluate the requested transition:
"current credentials + requested credentials -> effective identity transition"
and allow only the no-op/identity-preserving subset.
The regression matrix should include every argument position independently:
- unchanged real/effective/saved ID;
- "-1" sentinel;
- transition to another non-root ID;
- transition to root;
- supplementary-group mutation.
The important invariant is:
"permitted syscall effect <= existing identity authority"
This preserves the security boundary while avoiding denial of software that performs defensive identity normalization without gaining privilege.
Related reference implementation / design work: AETHER X GLOBAL ExecSurface — https://github.com/AETHERXGLOBAL/execsurface
Confirming this independently on a different platform/driver, with a smaller reproduction and a couple of extra data points.
Environment
- OpenShell 0.1.2 (deb install); gateway, supervisor and sandbox runtime all 0.1.2
- Compute driver: Docker (Docker 29.8.2), Linux aarch64 host, kernel 7.0.x
- Workload image: Debian bookworm-slim based, GNU Make 4.3, non-root uid/gid 1000
- Reproduced both from a detached sandbox's initial command and through
openshell sandbox exec - Reproduced with the built-in default policy and with no provider attached, so this is not a policy interaction
Minimal reproduction (no Dockerfile required, ~20 lines of C)
/* fork, normalize identity exactly like GNU Make's recipe child does, then exec */ pid_t p = fork(); if (p == 0) { if (setresgid(getgid(), getegid(), getegid()) == -1 || setresuid(getuid(), geteuid(), geteuid()) == -1) { perror("exec of recipe"); _exit(127); } execl("/bin/echo", "echo", "recipe-ok", (char *)0); }
Result:
exec of recipe: Operation not permitted, child exit 127. GNU Make surfaces the same failure naming the recipe command:$ printf 'all: ; @true\n' > /tmp/Makefile && make -f /tmp/Makefile make: true: Operation not permitted make: *** [/tmp/Makefile:1: all] Error 127 $ /usr/bin/true; echo $? 0 $ sh -c 'echo direct-ok' direct-ok
Syscall matrix measured in the sandbox (real, effective and saved uid/gid are all 1000/1000):
call result setresgid(rg, eg, sg)— own IDsEPERM setresuid(ru, eu, su)— own IDsEPERM setresgid(-1, -1, -1)EPERM setresuid(-1, -1, -1)EPERM setgroups(0, NULL)EPERM The sentinel-only form (
-1,-1,-1, i.e. "change nothing at all") is denied as well — it might be worth covering explicitly in the regression matrix, since it is the cheapest possible identity-preserving call.fork/vfork+execvefromsh,bashand Python'ssubprocessall succeed, so only the identity-normalization step in the child is blocked. This matches your analysis in the issue body.Impact seen in practice
In agent workflows, repository quality gates are normally invoked through GNU Make (
make check→ formatter check,go vet, tests, linters). Inside an affected sandbox those gates cannot run at all — every recipe fails with Error 127. Today the only workaround is to reimplement each gate as a direct command invocation, which also means the agent's reported verification no longer corresponds to the project's documented gate. With #4104's approach (allow identity-preservingsetresuid/setresgid, keep identity and supplementary-group changes denied),makeworks unchanged — the Docker/Podman E2E fixture with distinct non-root UID/GID in that PR covers exactly this scenario.Is a release containing that fix planned? 0.1.2 (the latest release) still reproduces this.
One related observation, deliberately not filed as a separate issue:
/dev/zerois absent from the defaultfilesystem_policyread list, sohead -c N < /dev/zeroanddd if=/dev/zerofail with "Permission denied" inside a default sandbox (we added/dev/zeroto our own policy). Both are common in build and test scripts, so it may be worth adding to the default read list.Another confirmation with two data points that may help:
- Workaround: GNU Make 4.4.1. On OpenShell 0.1.2 (Docker driver, x86_64, python:3.14-slim-bookworm workload, non-root uid 999), bookworm's Make 4.3 fails exactly as described. Make 4.4.1, built from the GNU tarball inside the same sandbox, runs the same recipes and a real project's Makefile without error. Upgrading Make in the workload image is a workable stopgap until the filter changes.
- Minimal repro without C:
python3 -c 'import os; os.posix_spawn("/bin/true", ["true"], os.environ, resetids=True)'fails with EPERM. Plainposix_spawn,setsigmask,setsigdefandsetpgroupall succeed.clone3correctly returns ENOSYS. Every member of the setuid family returns EPERM even for no-op arguments, e.g.setuid(getuid())orsetresuid(-1,-1,-1).
The unconditional deny list in
crates/openshell-sandbox/src/linux/child_seccomp.rsis still present onmainafter eeba0e7 (2026-10-10). The ENOSYS treatment thatclone3/pidfd_openalready get, or the argument check proposed in the acceptance criteria (each id is -1 or the workload's own id, both known inprepare()), would fix it.Following on @jordanhubbard's confirmation — the deny list is indeed unchanged on
main, and I think the reason #4104 has stalled is a path move, not the fix itself.The file PR #4104 patches no longer lives at that path. Commit
eeba0e79(refactor(runtime): confine and reduce unsafe code, #4130, 2026-10-10) moved:crates/openshell-isolation-interface/src/linux/child_seccomp.rs->crates/openshell-sandbox/src/linux/child_seccomp.rs
The unconditional entries for
setuid,setgid,setreuid,setregid,setresuid,setresgid,setfsuid,setfsgidandsetgroupscame across verbatim, so the behavior described above is still present onmain. #4104 is currentlymergeable_state: dirtyfor exactly this reason — it edits the old location. Since the surrounding code moved unchanged, a rebase ontomainshould make the patch applicable as-is.Release status, for anyone waiting on an upgrade: v0.1.3 (2026-10-09) still carries the pre-move layout and the same unconditional deny, so upgrading does not change this behavior. Until the filter changes, the in-image Make upgrade (4.4.1 as Jordan reports) or avoiding Make inside the sandbox are the only mitigations in a released version — I could not find any setting or policy knob that reaches this path.
The argument check already implemented in #4104 matches the acceptance criteria; Jordan's
ENOSYSalternative for thesetresuid/setresgidpair would also resolve it, and may be less code to defend. Either way, rebasing #4104 ontomainunblocks the fix.Reproduces on aarch64 as well (OpenShell 0.1.2 deb, Docker driver, Debian bookworm workload, non-root uid/gid 1000), both from a detached sandbox's initial command and via
openshell sandbox exec, under the built-in default policy with no provider attached — same as noted earlier in this thread.
User Story
I need coding agents in OpenShell sandboxes to run GNU Make builds. The Ubuntu 24.04 reproduction below fails even for a recipe that only runs
true.Problem Statement
GNU Make cannot launch ordinary recipes in a non-root OpenShell sandbox. It reports
Operation not permittedfor the recipe command, although running that command directly succeeds. The reported reproduction uses OpenShell 0.1.2 sandbox and supervisor images with a gateway built from the current checkout.The child seccomp filter unconditionally denies
setresuidandsetresgid. Make calls these before executing a recipe to reset its effective IDs to its existing real IDs. For a workload whose real, effective, and saved IDs already match, these calls preserve its identity.Impact / Why This Matters
Coding agents cannot build Make-based projects in affected sandboxes. Patching the runtime locally is a possible workaround, but maintaining a custom runtime for a standard build tool is insufficient.
Acceptance Criteria
openshell sandbox execas a non-root user.setresuidandsetresgidcalls succeed when each argument is the established workload ID or the unchanged sentinel.Reproduction Steps
Build a workload image from this Dockerfile:
Create a persistent sandbox on a Docker-backed OpenShell gateway:
Run the minimal recipe:
Environment
Logs