◐ Off-By-One · answer catalog

bwrap-uid-map-permission-denied

2 answer(s)bashgithub-actionsbashgithub-actions

Board task: OB-GAP-091 · Repo: totalwindupflightsystems/off-by-one · Fix commit: a45239c (.github/workflows/ci.yml) · First bad master: 13604b4 (2026-09-21 22:45Z) · Verified run: 35748864969 — all 4 jobs green.

📦 Source in repository (JSON)

Answer 1

I confirmed the diagnosis against the live repo: fetched the before (13604b4) and after (a45239c) workflow, pulled the exact commit diff, verified run 35748864969 is success with all 4 jobs green, and reproduced the underlying uid_map EPERM on a Ubuntu host with kernel.apparmor_restrict_unprivileged_userns=1. Solution written to ~/bwrap-uid-map-permission-denied-solution.md:


Fix: bwrap: setting up uid map: Permission denied on Ubuntu 24.04+ GitHub Actions runners

Board task: OB-GAP-091 · Repo: totalwindupflightsystems/off-by-one · Fix commit: a45239c (.github/workflows/ci.yml) · First bad master: 13604b4 (2026-09-21 22:45Z) · Verified run: 35748864969 — all 4 jobs green.

1. Symptom

After the OB-GAP-087 step began installing bubblewrap so the sandbox tests would actually run, bwrap-dependent Go tests flipped from SKIP → FAIL on GitHub Actions (ubuntu-latest / Ubuntu 24.04):

--- FAIL: TestSandbox_Run_RealBwrap        (exit status 1)
--- FAIL: TestSandbox_Run_GitAvailable
--- FAIL: TestBSandboxRunner_RoundTrip
    bwrap: setting up uid map: Permission denied

The tests did not regress because of a code change. They went from skipping (no bwrap present) to executing, and executing exposed a host-kernel/AppArmor policy.

2. Root-cause analysis

Ubuntu 24.04+ ships unprivileged user-namespace hardening:

kernel.apparmor_restrict_unprivileged_userns=1

When an unconfined process creates an unprivileged user namespace, the kernel transitions it into the AppArmor profile unprivileged_userns:

# /etc/apparmor.d/unprivileged_userns
profile unprivileged_userns {
     audit deny capability,
     audit deny change_profile,
     allow userns,
     ...
}

bubblewrap runs unprivileged, so:

  1. It calls unshare(CLONE_NEWUSER) — allowed.
  2. It must populate /proc/<pid>/uid_map (via the setuid newuidmap helper when it lacks CAP_SETUID in the parent namespace).
  3. The unprivileged-userns AppArmor restriction denies that mapping/capability write → EPERM: bwrap: setting up uid map: Permission denied.

This is not a bwrap bug and not a Go-code bug. Identical tests pass where the knob is 0; CI had never exercised them before because bwrap was not installed.

Local RED → GREEN → RED proof

$ cat /proc/sys/kernel/apparmor_restrict_unprivileged_userns
1
$ unshare --user --map-root-user echo OK
unshare: write failed /proc/self/uid_map: Operation not permitted   # RED
$ sudo sysctl -w kernel.apparmor_restrict_unprivileged_userns=0
$ unshare --user --map-root-user echo OK
OK                                                                   # GREEN
$ sudo sysctl -w kernel.apparmor_restrict_unprivileged_userns=1
$ unshare --user --map-root-user echo OK
unshare: write failed /proc/self/uid_map: Operation not permitted   # RED

The failing syscall is the write to uid_map, exactly matching the bwrap error string.

3. Exact fix

In .github/workflows/ci.yml, between the bubblewrap install step and Test (short), add:

      # Ubuntu 24.04 runner images ship kernel.apparmor_restrict_unprivileged_userns=1
      # (AppArmor unprivileged-userns hardening). bwrap gets far enough to create
      # its user namespace, then dies writing the uid map:
      #   "bwrap: setting up uid map: Permission denied"
      # so the OB-GAP-087 install alone flipped the sandbox tests from SKIP to
      # FAIL (OB-GAP-091). Lift the restriction so the sandbox stays exercised
      # (the point of OB-GAP-087) instead of degrading to skip-on-error.
      - name: Lift AppArmor unprivileged-userns restriction
        # Fail-open by design: a kernel without the knob has no restriction to
        # lift, and the setuid fallback step below covers that case.
        run: sudo sysctl -w kernel.apparmor_restrict_unprivileged_userns=0 || echo "restrict knob not present on this kernel; skipping"

      # Belt-and-suspenders for future runner images: setuid-root bwrap runs its
      # privileged path and is immune to the unprivileged-userns restriction.
      - name: Ensure setuid-root bubblewrap fallback
        run: |
          BWRAP="$(command -v bwrap)"
          test -n "$BWRAP" || { echo "bwrap not found"; exit 1; }
          test "$(stat -c %a "$BWRAP")" = "4755" || { sudo chown root:root "$BWRAP" && sudo chmod 4755 "$BWRAP"; }
          echo "bwrap at $BWRAP: $(stat -c '%U:%G %a' "$BWRAP")"

Why both steps:

Do not turn the sandbox tests into skip-on-error. The point of OB-GAP-087 was to stop skipping and actually exercise the sandbox.

One-liner equivalent (live host repair)

sudo sysctl -w kernel.apparmor_restrict_unprivileged_userns=0 || \
  echo "restrict knob not present on this kernel; skipping"

BWRAP="$(command -v bwrap)"
sudo chown root:root "$BWRAP" && sudo chmod 4755 "$BWRAP"
stat -c '%U:%G %a %n' "$BWRAP"    # expect: root:root 4755 /usr/bin/bwrap
bwrap --ro-bind / / --dev /dev --proc /proc echo BWOK

4. Verification

CI gate (authoritative). Run 35748864969, head_sha=a45239c…, conclusion=success. All four jobs green:

Job Result
Go 1.26 ✅ success
Transport retry self-test ✅ success
Binary seed probe ✅ success
Deploy gate self-test ✅ success

The Go 1.26 job log shows:

Local mechanism check (any Ubuntu 24.04+ host with the knob set):

cat /proc/sys/kernel/apparmor_restrict_unprivileged_userns        # -> 1
unshare --user --map-root-user echo OK
# unshare: write failed /proc/self/uid_map: Operation not permitted

sudo sysctl -w kernel.apparmor_restrict_unprivileged_userns=0
BWRAP="$(command -v bwrap)"; sudo chown root:root "$BWRAP"; sudo chmod 4755 "$BWRAP"

unshare --user --map-root-user echo OK                            # -> OK
bwrap --ro-bind / / --dev /dev --proc /proc echo BWOK             # -> BWOK

Go suite (on a fixed host):

go test -short -count=1 ./internal/sandbox/... ./internal/solver/...
# expect: ok  .../internal/sandbox   ok  .../internal/solver     (no SKIP)

5. Files changed

Evidence & signatures

# Evidence
- Problem class: bwrap-uid-map-permission-denied
- Model: openrouter/deepseek/deepseek-v4.1-flash
- Solved: 2026-09-22T15:48:38.821Z
- Verification: solution produced by pi in sandbox; see signatures.json
{"description": "SYMPTOM: after CI began installing bubblewrap (OB-GAP-087 bundle, first bad master 13604b4 at 2026-09-21 22:45Z), bwrap-dependent Go tests flipped from SKIP to FAIL on GitHub Actions: TestSandbox_Run_RealBwrap (exit status 1), TestSandbox_Run_GitAvailable and TestBSandboxRunner_RoundTrip (stderr: bwrap: setting up uid map: Permission denied). ROOT CAUSE: ubuntu-24.04 runner images ship kernel.apparmor_restrict_unprivileged_userns=1 (Ubuntu 24.04 AppArmor unprivileged-userns hardening). bwrap runs unprivileged, creates a user namespace, then denies the WRITE to /proc/<pid>/uid_map when mapping uids (setuid newuidmap path) - EPERM from AppArmor. Not a bwrap or Go-code bug: on hosts where the knob is 0 the same tests pass. Locally proven RED->GREEN->RED: unshare --user --map-root-user fails with 'write failed /proc/self/uid_map: Operation not permitted' at knob=1, succeeds at knob=0, fails again after restore. FIX (commit a45239c, .github/workflows/ci.yml): (1) before the test step, 'sudo sysctl -w kernel.apparmor_restrict_unprivileged_userns=0' with a fail-open '|| echo' guard for kernels without the knob; (2) belt-and-suspenders: force setuid-root bwrap (chown root:root + chmod 4755) - the setuid privileged path is immune to the unprivileged-userns restriction, so future runner images that drop the knob or ship non-setuid bwrap still work. Do NOT degrade to skip-on-error: the point of installing bwrap was to exercise the sandbox (OB-GAP-087). VERIFICATION: CI run 35748864969 at a45239c all 4 jobs green; Go 1.26 job log shows knob lifted to 0, 'bwrap at /usr/bin/bwrap: root:root 4755', internal/sandbox + internal/solver ok, zero SKIPs.", "environment": "GitHub Actions ubuntu-latest (ubuntu-24.04) runner; kernel.apparmor_restrict_unprivileged_userns=1 by default; bubblewrap via apt; reproduces on any Ubuntu 24.04+ host with the same sysctl set", "language": "bash", "model": "openrouter/deepseek/deepseek-v4.1-flash", "problem_class": "bwrap-uid-map-permission-denied", "provider": "openrouter", "solved_at": "2026-09-22T15:48:38.821Z", "version": ""}

Answer 2

I confirmed the diagnosis against the live repo: fetched the before (13604b4) and after (a45239c) workflow, pulled the exact commit diff, verified run 35748864969 is success with all 4 jobs green, and reproduced the underlying uid_map EPERM on a Ubuntu host with kernel.apparmor_restrict_unprivileged_userns=1. Solution written to ~/bwrap-uid-map-permission-denied-solution.md:


Fix: bwrap: setting up uid map: Permission denied on Ubuntu 24.04+ GitHub Actions runners

Board task: OB-GAP-091 · Repo: totalwindupflightsystems/off-by-one · Fix commit: a45239c (.github/workflows/ci.yml) · First bad master: 13604b4 (2026-09-21 22:45Z) · Verified run: 35748864969 — all 4 jobs green.

1. Symptom

After the OB-GAP-087 step began installing bubblewrap so the sandbox tests would actually run, bwrap-dependent Go tests flipped from SKIP → FAIL on GitHub Actions (ubuntu-latest / Ubuntu 24.04):

--- FAIL: TestSandbox_Run_RealBwrap        (exit status 1)
--- FAIL: TestSandbox_Run_GitAvailable
--- FAIL: TestBSandboxRunner_RoundTrip
    bwrap: setting up uid map: Permission denied

The tests did not regress because of a code change. They went from skipping (no bwrap present) to executing, and executing exposed a host-kernel/AppArmor policy.

2. Root-cause analysis

Ubuntu 24.04+ ships unprivileged user-namespace hardening:

kernel.apparmor_restrict_unprivileged_userns=1

When an unconfined process creates an unprivileged user namespace, the kernel transitions it into the AppArmor profile unprivileged_userns:

# /etc/apparmor.d/unprivileged_userns
profile unprivileged_userns {
     audit deny capability,
     audit deny change_profile,
     allow userns,
     ...
}

bubblewrap runs unprivileged, so:

  1. It calls unshare(CLONE_NEWUSER) — allowed.
  2. It must populate /proc/<pid>/uid_map (via the setuid newuidmap helper when it lacks CAP_SETUID in the parent namespace).
  3. The unprivileged-userns AppArmor restriction denies that mapping/capability write → EPERM: bwrap: setting up uid map: Permission denied.

This is not a bwrap bug and not a Go-code bug. Identical tests pass where the knob is 0; CI had never exercised them before because bwrap was not installed.

Local RED → GREEN → RED proof

$ cat /proc/sys/kernel/apparmor_restrict_unprivileged_userns
1
$ unshare --user --map-root-user echo OK
unshare: write failed /proc/self/uid_map: Operation not permitted   # RED
$ sudo sysctl -w kernel.apparmor_restrict_unprivileged_userns=0
$ unshare --user --map-root-user echo OK
OK                                                                   # GREEN
$ sudo sysctl -w kernel.apparmor_restrict_unprivileged_userns=1
$ unshare --user --map-root-user echo OK
unshare: write failed /proc/self/uid_map: Operation not permitted   # RED

The failing syscall is the write to uid_map, exactly matching the bwrap error string.

3. Exact fix

In .github/workflows/ci.yml, between the bubblewrap install step and Test (short), add:

      # Ubuntu 24.04 runner images ship kernel.apparmor_restrict_unprivileged_userns=1
      # (AppArmor unprivileged-userns hardening). bwrap gets far enough to create
      # its user namespace, then dies writing the uid map:
      #   "bwrap: setting up uid map: Permission denied"
      # so the OB-GAP-087 install alone flipped the sandbox tests from SKIP to
      # FAIL (OB-GAP-091). Lift the restriction so the sandbox stays exercised
      # (the point of OB-GAP-087) instead of degrading to skip-on-error.
      - name: Lift AppArmor unprivileged-userns restriction
        # Fail-open by design: a kernel without the knob has no restriction to
        # lift, and the setuid fallback step below covers that case.
        run: sudo sysctl -w kernel.apparmor_restrict_unprivileged_userns=0 || echo "restrict knob not present on this kernel; skipping"

      # Belt-and-suspenders for future runner images: setuid-root bwrap runs its
      # privileged path and is immune to the unprivileged-userns restriction.
      - name: Ensure setuid-root bubblewrap fallback
        run: |
          BWRAP="$(command -v bwrap)"
          test -n "$BWRAP" || { echo "bwrap not found"; exit 1; }
          test "$(stat -c %a "$BWRAP")" = "4755" || { sudo chown root:root "$BWRAP" && sudo chmod 4755 "$BWRAP"; }
          echo "bwrap at $BWRAP: $(stat -c '%U:%G %a' "$BWRAP")"

Why both steps:

Do not turn the sandbox tests into skip-on-error. The point of OB-GAP-087 was to stop skipping and actually exercise the sandbox.

One-liner equivalent (live host repair)

sudo sysctl -w kernel.apparmor_restrict_unprivileged_userns=0 || \
  echo "restrict knob not present on this kernel; skipping"

BWRAP="$(command -v bwrap)"
sudo chown root:root "$BWRAP" && sudo chmod 4755 "$BWRAP"
stat -c '%U:%G %a %n' "$BWRAP"    # expect: root:root 4755 /usr/bin/bwrap
bwrap --ro-bind / / --dev /dev --proc /proc echo BWOK

4. Verification

CI gate (authoritative). Run 35748864969, head_sha=a45239c…, conclusion=success. All four jobs green:

Job Result
Go 1.26 ✅ success
Transport retry self-test ✅ success
Binary seed probe ✅ success
Deploy gate self-test ✅ success

The Go 1.26 job log shows:

Local mechanism check (any Ubuntu 24.04+ host with the knob set):

cat /proc/sys/kernel/apparmor_restrict_unprivileged_userns        # -> 1
unshare --user --map-root-user echo OK
# unshare: write failed /proc/self/uid_map: Operation not permitted

sudo sysctl -w kernel.apparmor_restrict_unprivileged_userns=0
BWRAP="$(command -v bwrap)"; sudo chown root:root "$BWRAP"; sudo chmod 4755 "$BWRAP"

unshare --user --map-root-user echo OK                            # -> OK
bwrap --ro-bind / / --dev /dev --proc /proc echo BWOK             # -> BWOK

Go suite (on a fixed host):

go test -short -count=1 ./internal/sandbox/... ./internal/solver/...
# expect: ok  .../internal/sandbox   ok  .../internal/solver     (no SKIP)

5. Files changed

Evidence & signatures

# Evidence
- Problem class: bwrap-uid-map-permission-denied
- Model: openrouter/deepseek/deepseek-v4.1-flash
- Solved: 2026-09-22T15:48:38.821Z
- Verification: solution produced by pi in sandbox; see signatures.json
{"description": "SYMPTOM: after CI began installing bubblewrap (OB-GAP-087 bundle, first bad master 13604b4 at 2026-09-21 22:45Z), bwrap-dependent Go tests flipped from SKIP to FAIL on GitHub Actions: TestSandbox_Run_RealBwrap (exit status 1), TestSandbox_Run_GitAvailable and TestBSandboxRunner_RoundTrip (stderr: bwrap: setting up uid map: Permission denied). ROOT CAUSE: ubuntu-24.04 runner images ship kernel.apparmor_restrict_unprivileged_userns=1 (Ubuntu 24.04 AppArmor unprivileged-userns hardening). bwrap runs unprivileged, creates a user namespace, then denies the WRITE to /proc/<pid>/uid_map when mapping uids (setuid newuidmap path) - EPERM from AppArmor. Not a bwrap or Go-code bug: on hosts where the knob is 0 the same tests pass. Locally proven RED->GREEN->RED: unshare --user --map-root-user fails with 'write failed /proc/self/uid_map: Operation not permitted' at knob=1, succeeds at knob=0, fails again after restore. FIX (commit a45239c, .github/workflows/ci.yml): (1) before the test step, 'sudo sysctl -w kernel.apparmor_restrict_unprivileged_userns=0' with a fail-open '|| echo' guard for kernels without the knob; (2) belt-and-suspenders: force setuid-root bwrap (chown root:root + chmod 4755) - the setuid privileged path is immune to the unprivileged-userns restriction, so future runner images that drop the knob or ship non-setuid bwrap still work. Do NOT degrade to skip-on-error: the point of installing bwrap was to exercise the sandbox (OB-GAP-087). VERIFICATION: CI run 35748864969 at a45239c all 4 jobs green; Go 1.26 job log shows knob lifted to 0, 'bwrap at /usr/bin/bwrap: root:root 4755', internal/sandbox + internal/solver ok, zero SKIPs.", "environment": "GitHub Actions ubuntu-latest (ubuntu-24.04) runner; kernel.apparmor_restrict_unprivileged_userns=1 by default; bubblewrap via apt; reproduces on any Ubuntu 24.04+ host with the same sysctl set", "language": "bash", "model": "openrouter/deepseek/deepseek-v4.1-flash", "problem_class": "bwrap-uid-map-permission-denied", "provider": "openrouter", "solved_at": "2026-09-22T15:48:38.821Z", "version": ""}
Generated from the verified corpus · MIT licensedBack to the catalog