emrg: confine linux commands with bubblewrap (bash tool v2, P3) - #1541
Merged
Merged
Conversation
This was referenced Sep 22, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What this is
Phase 3 of the bash tool v2 design (
~/.emrg/designs/bash-tool-v2-design.md, §1.6 / §D7): the Linuxboundary. 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 runthat said so (
mode=danger-full-access, no lie — design §D4-a). This PR replaces that deviation withan enforced rung.
The change
emrg/sandbox/providers/linux.py(new) — the bwrap backend, mirroring the blueprint'sbwrapProfileArgs(dshsandbox-localprofiles.ts, mergeddefc45fbc/ PR #4469):--ro-bind / /,--dev /dev,--unshare-pid,--proc /proc,--die-with-parent, then forworkspace-writea fresh--tmpfs /tmpfollowed 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_BINis a module constant so tests can inject a fake runner.emrg/sandbox/providers/__init__.py— the linux chain now carries[bwrap]as its singlecandidate, 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) areretired 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: workspacewritable, an interpreter write outside the workspace refused by the kernel, read-only refuses even a
write inside the workspace, and
/tmpis a sandbox-private tmpfs (nothing left on the host).The real-boundary cases skip where
bwrapis absent, with the recipe to get one in-comment.DEVELOPMENT.md— the boundary section now covers macOS (Seatbelt) + Linux (bubblewrap), andsays what Linux needs:
bwrapplus 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):
Real boundary, measured in a privileged container (this host cannot create a mount namespace at all,
so the host measurement is unmeasurable, not green):
Observed inside:
sh: 1: cannot create /work/outside_dir/f: Read-only file systemfor an out-of-workspacewrite, workspace write allowed under
workspace-write, and/tmpwrites confined to a private tmpfs.Mutation arms (each reverted the profile, in the container, restored afterwards — baseline green again):
--tmpfs /tmptest_bwrap_gives_the_sandbox_a_private_tmp--bind <workspace>test_bwrap_lets_a_command_write_inside_the_workspaceReviewing this
bwrap cases skip, and that is the expected reading, not a gap.
surface stays byte-for-byte with
dsh.