Skip to content

bug(sandbox): GNU Make cannot launch recipes with unchanged UID/GID #4102

Description

@drew

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 permitted for 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 setresuid and setresgid. 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

  • The minimal Make recipe succeeds at workload startup and through openshell sandbox exec as a non-root user.
  • Identity-preserving setresuid and setresgid calls succeed when each argument is the established workload ID or the unchanged sentinel.
  • Attempts to select another real, effective, or saved UID/GID remain denied, including root.
  • Changing supplementary groups remains denied.

Reproduction Steps

  1. Build a workload image from this Dockerfile:

    FROM ubuntu:24.04
    RUN apt-get update && apt-get install -y make python3 \
        && useradd -u 10001 -m sandbox
    WORKDIR /sandbox
    USER sandbox
  2. Create a persistent sandbox on a Docker-backed OpenShell gateway:

    openshell sandbox create --from openshell/make-repro:ubuntu24-user10001 --name make-repro --detach
  3. Run the minimal recipe:

    openshell sandbox exec --name make-repro --no-tty -- sh -c 'id; /usr/bin/true; printf "all: ; @true\n" > /tmp/Makefile; make -f /tmp/Makefile'

Environment

  • Sandbox and supervisor runtime: OpenShell 0.1.2
  • Workload: Ubuntu 24.04, GNU Make, non-root UID 10001
  • Compute driver: Docker
  • Gateway in the reported reproduction: current checkout, 0.1.3-dev

Logs

make: true: Operation not permitted
make: *** [/tmp/Makefile:1: all] Error 127
make status: 2

Activity

  1. aetherxeg-source commented on Oct 2, 2026

    @aetherxeg-source

    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

  2. ikkun1222 commented on Oct 7, 2026

    @ikkun1222

    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 IDs EPERM
    setresuid(ru, eu, su) — own IDs EPERM
    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 + execve from sh, bash and Python's subprocess all 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-preserving setresuid/setresgid, keep identity and supplementary-group changes denied), make works 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/zero is absent from the default filesystem_policy read list, so head -c N < /dev/zero and dd if=/dev/zero fail with "Permission denied" inside a default sandbox (we added /dev/zero to our own policy). Both are common in build and test scripts, so it may be worth adding to the default read list.

  3. jordanhubbard commented on Oct 11, 2026

    @jordanhubbard

    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. Plain posix_spawn, setsigmask, setsigdef and setpgroup all succeed. clone3 correctly returns ENOSYS. Every member of the setuid family returns EPERM even for no-op arguments, e.g. setuid(getuid()) or setresuid(-1,-1,-1).

    The unconditional deny list in crates/openshell-sandbox/src/linux/child_seccomp.rs is still present on main after eeba0e7 (2026-10-10). The ENOSYS treatment that clone3/pidfd_open already get, or the argument check proposed in the acceptance criteria (each id is -1 or the workload's own id, both known in prepare()), would fix it.

  4. ikkun1222 commented on Oct 12, 2026

    @ikkun1222

    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, setfsgid and setgroups came across verbatim, so the behavior described above is still present on main. #4104 is currently mergeable_state: dirty for exactly this reason — it edits the old location. Since the surrounding code moved unchanged, a rebase onto main should 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 ENOSYS alternative for the setresuid/setresgid pair would also resolve it, and may be less code to defend. Either way, rebasing #4104 onto main unblocks 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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions