Skip to content

feat(sandbox): seccomp syscall allowlist for the bubblewrap backend - #365

Merged
quickbeard merged 1 commit into
mainfrom
bwrap-seccomp
Sep 14, 2026
Merged

quickbeard merged 1 commit into
mainfrom
bwrap-seccomp

Conversation

@quickbeard

Copy link
Copy Markdown
Owner

Summary

The Linux desktop sandbox (bwrap) isolated the guest with namespaces only. Namespaces control what the guest can see; they do nothing to shrink what it can say to the shared host kernel, so a single kernel LPE reachable from inside the guest was a full host escape with no second wall. docs/seccomp.md planned the fix; this PR implements it.

bwrap now installs a seccomp BPF program, generated on the host at every sandbox start, before it execs the guest:

  • Strict allowlist derived from Docker's default seccomp profile, minus every entry that profile only grants to a capability the guest happens to hold inside its user namespace (mount, setns, unshare, keyctl, bpf, …).
  • Kill set (SIGSYS): bpf, perf_event_open, userfaultfd, init_module/finit_module/delete_module, kexec_*, keyctl/add_key/request_key.
  • Argument rules: clone/unshare with CLONE_NEWUSER is fatal, any other CLONE_NEW* is refused with EPERM; socket/socketpair limited to AF_UNIX, AF_INET, AF_INET6 and AF_NETLINK+NETLINK_ROUTE (getifaddrs), everything else EAFNOSUPPORT.
  • Architecture gate: non-native arch and x32-ABI numbers are killed before any number is compared.
  • Default deny: EPERM below the unified numbering (424), ENOSYS at/above it so glibc fallbacks (clone3 → clone, faccessat2 → faccessat) keep working. io_uring_* and clone3 are deliberately unlisted.
  • Modes: enforce (default), audit (every non-allow verdict becomes SECCOMP_RET_LOG: kernel logs it, nothing is blocked), off. Machine-wide default via CODEV_SANDBOX_SECCOMP; a persisted per-sandbox seccomp field wins over it. No UI yet.

Departures from docs/seccomp.md

Each is recorded in a new "What shipped" section of that doc.

  • The program is generated in TypeScript (src/seccomp/bpf.ts), not precompiled with libseccomp in CI and shipped via the image manifest. That makes the PR self-contained (no Rust tool, no storage publish). The arm64-correctness concern is met by generating the syscall tables straight from the kernel uapi headers (script/gen-syscall-tables.ts → syscalls-{x64,arm64}.ts) and by tests that run the real program bytes through a cBPF interpreter for every syscall number on both arches. Trade-off: the policy versions with the app, not with the rootfs image.
  • --seccomp, not --add-seccomp-fd: one program, and the older flag is accepted by every bwrap release, which removes the degradation question.
  • ptrace → EPERM, not kill: strace/gdb should fail with a message, not vanish.
  • AF_INET/AF_INET6 allowed: the guest's own server, the broker stub, and any dev server the agent starts are loopback TCP; --unshare-net already leaves nothing else reachable.
  • Default enforce, not audit-first: with no fleet to collect audit logs from, an audit default would have shipped no protection. audit/off remain the escape hatches.
  • WSL is untouched (boots the same rootfs but not through bwrap).

Verification

  • bun test in packages/codev-sandbox: 194 pass. Includes a cBPF interpreter test that checks the real program bytes (allowlist passes, kill/deny sets don't, arch/x32 gate, clone/unshare flags, socket domains, audit mode, every nr 0–1023 terminates within the kernel instruction limit), and real-bwrap integration tests on this host (bwrap 0.6.1): ordinary work passes, mount is refused, unshare -U exits 159 (SIGSYS).
  • Manual smoke run inside bwrap under enforce with the host's Node 24, git and python3: loopback HTTP server + fetch, child_process, worker_threads, fs.watch, os.networkInterfaces(), dns.lookup, git init/commit, python AF_INET bind all work; python AF_PACKET gets EAFNOSUPPORT.
  • bun typecheck clean in packages/codev-sandbox and packages/desktop.
  • Not verified: a boot of the real published guest image under the filter (no image on this machine). The smoke run covers the same runtime the launcher uses (node importing the bundle and calling Server.listen), but a first real Work session on Linux is the remaining check.

Docs / bookkeeping

  • packages/codev-sandbox/README.md: new seccomp/ bullet, provider/args notes, two Known-gaps entries (no audit phase yet; WSL has no filter).
  • SECURITY.md: "No Sandbox" scoped to CLI/local sessions; new "Desktop sandbox (Work sessions)" section puts escapes from the desktop sandbox in scope. Row added to DIVERGENCES.md.
  • CHANGELOG.md: Sandbox entry under Unreleased.

🤖 Generated with Claude Code

https://claude.ai/code/session_0124etPWktHa6gqvcXrjcCm4

The Linux sandbox isolated the guest with namespaces only; a kernel bug
reachable from inside was a full host escape. bwrap now installs a
classic-BPF seccomp program, generated on the host at every start from
a strict allowlist (Docker's default profile minus capability-gated
entries), before it execs the guest.

- src/seccomp/policy.ts: allowlist, kill set (bpf, perf_event_open,
  userfaultfd, module loading, kexec, key syscalls), documented deny
  list, socket-domain and clone/unshare flag rules.
- src/seccomp/bpf.ts: cBPF assembler with labels; arch + x32 gate,
  argument checks, chunked allowlist, EPERM/ENOSYS default deny,
  audit mode via SECCOMP_RET_LOG.
- src/seccomp/syscalls-{x64,arm64}.ts generated from the kernel uapi
  headers by script/gen-syscall-tables.ts.
- bwrap/args.ts gains seccompFd -> --seccomp; bwrap/provider.ts writes
  runtime/seccomp.bpf and inherits it as stdio slot 3; per-sandbox
  `seccomp` config field; desktop reads CODEV_SANDBOX_SECCOMP for the
  machine-wide default (enforce).
- Tests: a cBPF interpreter runs the real program bytes for every
  syscall number on both arches; real-bwrap integration tests show
  ordinary work passing, mount refused, unshare -U killed.
- Docs: README, docs/seccomp.md "What shipped", SECURITY.md scoping
  (+ DIVERGENCES row), CHANGELOG.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0124etPWktHa6gqvcXrjcCm4
@quickbeard
quickbeard merged commit bb07131 into main Sep 14, 2026
0 of 5 checks passed
@quickbeard
quickbeard deleted the bwrap-seccomp branch September 14, 2026 03:46
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