Skip to content

emrg: confine linux commands with bubblewrap (bash tool v2, P3) - #1541

Merged
argszero merged 1 commit into
masterfrom
feature/bash-tool-v2-p3
Sep 22, 2026
Merged

argszero merged 1 commit into
masterfrom
feature/bash-tool-v2-p3

Conversation

@argszero

Copy link
Copy Markdown
Owner

What this is

Phase 3 of the bash tool v2 design (~/.emrg/designs/bash-tool-v2-design.md, §1.6 / §D7): the Linux
boundary
. P1 gave the tool a real confinement seam on macOS (Seatbelt) and landed as #1540; this PR
fills the Linux rung of the same seam with bubblewrap, so a confined command on Linux is confined
by the kernel rather than by a promise.

Until now the linux chain table was deliberately empty and confine() short-circuited to a bare run
that said so (mode=danger-full-access, no lie — design §D4-a). This PR replaces that deviation with
an enforced rung.

The change

  • emrg/sandbox/providers/linux.py (new) — the bwrap backend, mirroring the blueprint's
    bwrapProfileArgs (dsh sandbox-local profiles.ts, merge ddefc45fbc / PR #4469):
    --ro-bind / /, --dev /dev, --unshare-pid, --proc /proc, --die-with-parent, then for
    workspace-write a fresh --tmpfs /tmp followed by --bind <workspace> <workspace>.
    Order matters and is asserted: the workspace bind is applied after the tmpfs, so a workspace that
    lives under /tmp (pytest's default temp root is one) stays writable — last mount wins.
    BWRAP_BIN is a module constant so tests can inject a fake runner.
  • emrg/sandbox/providers/__init__.py — the linux chain now carries [bwrap] as its single
    candidate, so it is selected without a probe (the chain model is "one rung per platform" here;
    landlock as a second rung remains outstanding work B1 in the design, not in this PR).
  • tests/test_bash_v2_policy.py — the two Linux "deviation" pins (D4: linux runs unconfined) are
    retired and replaced by real assertions: linux selects its sole rung without probing, linux reaches
    the seam instead of being answered first, and a missing runner fails closed (SANDBOX_UNAVAILABLE)
    rather than falling back to a bare run.
  • tests/test_bash_v2_boundary.py — the D4 pin becomes a measured kernel boundary: workspace
    writable, an interpreter write outside the workspace refused by the kernel, read-only refuses even a
    write inside the workspace, and /tmp is a sandbox-private tmpfs (nothing left on the host).
    The real-boundary cases skip where bwrap is absent, with the recipe to get one in-comment.
  • DEVELOPMENT.md — the boundary section now covers macOS (Seatbelt) + Linux (bubblewrap), and
    says what Linux needs: bwrap plus a kernel that allows user namespaces.

Fail-closed is preserved: no rung that can confine ⇒ refusal, never a bare run.

Evidence

Local (macOS, this tree):

uv run --no-sync python3 -m pytest tests/ -q         # 5061 passed, 25 skipped
uv run --no-sync python3 -m pytest tests/test_bash_v2_boundary.py tests/test_bash_v2_policy.py -q
                                                     # 53 passed, 4 skipped

Real boundary, measured in a privileged container (this host cannot create a mount namespace at all,
so the host measurement is unmeasurable, not green):

docker run --rm -w /work -v <tree>:/work \
  --security-opt seccomp=unconfined --security-opt apparmor=unconfined --cap-add SYS_ADMIN \
  debian:bookworm-slim sh -c 'apt-get update && apt-get install -y python3-pip bubblewrap &&
    pip install --break-system-packages -e . pytest &&
    python3 -m pytest tests/test_bash_v2_boundary.py tests/test_bash_v2_policy.py -q'
# 46 passed, 11 skipped
# -k bwrap  -> 4 passed  (the four linux boundary cases really run there)

Observed inside: sh: 1: cannot create /work/outside_dir/f: Read-only file system for an out-of-workspace
write, workspace write allowed under workspace-write, and /tmp writes confined to a private tmpfs.

Mutation arms (each reverted the profile, in the container, restored afterwards — baseline green again):

arm mutation killed
A restore the D4 deviation (linux runs unconfined) 3 failed
B drop --tmpfs /tmp test_bwrap_gives_the_sandbox_a_private_tmp
C drop --bind <workspace> test_bwrap_lets_a_command_write_inside_the_workspace

Reviewing this

  • The darwin and linux real-boundary tests are mutually exclusive by platform; on macOS you will see the
    bwrap cases skip, and that is the expected reading, not a gap.
  • The design's §1.1 verbatim-copy list and the denial vocabulary are untouched by this PR; the result
    surface stays byte-for-byte with dsh.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant