Skip to content

Start every bwrap jail under a seccomp deny list - #161

Open
M3gA-Mind wants to merge 6 commits into
tinyhumansai:mainfrom
M3gA-Mind:feat/p7b-bwrap-seccomp
Open

M3gA-Mind wants to merge 6 commits into
tinyhumansai:mainfrom
M3gA-Mind:feat/p7b-bwrap-seccomp

Conversation

@M3gA-Mind

Copy link
Copy Markdown
Collaborator

Summary

The bubblewrap jail backend installed no seccomp filter, so keyctl (the user's kernel keyrings), bpf, perf_event_open, io_uring, mount, setns, open_by_handle_at and similar calls stayed reachable from inside a jail. This was the first item of the backend's known gaps.

Every jail now starts under a deny list compiled with seccompiler for x86_64 or aarch64. bwrap installs it on the child after its own setup, through two --add-seccomp-fd pipes mapped into the child only, the same way #160 maps the grant descriptors.

  • A denied call fails with EPERM.
  • clone3 fails with ENOSYS, so the C library falls back to clone, whose flags the filter can read.
  • Every filter starts with an architecture check that kills a mismatched program, such as i386 code on x86_64. The x32-ABI twins of the denied calls are denied too.
Denied Why
keyctl, add_key, request_key kernel keyrings hold credentials
process_vm_readv/writev other processes' memory
bpf, perf_event_open, userfaultfd, io_uring_* privilege-escalation surface
every mount call, pivot_root, setns the jail's view is fixed by bwrap
clone/unshare with any CLONE_NEW* flag no new namespaces, on top of --disable-userns
open_by_handle_at, name_to_handle_at handles open files outside the grants
modules, kexec, reboot, swap, acct, host clock, syslog, quotactl, vhangup host-wide or privileged
ioctl(TIOCSTI/TIOCLINUX) typing into the controlling terminal
iopl, ioperm, modify_ldt, uselib, _sysctl (x86_64) legacy and privileged

Left allowed on purpose:

  • ptrace on Linux 4.8 and later, where seccomp is re-checked after a tracer edits a call and the pid namespace confines it to the jail's own processes. On older kernels it is denied.
  • Plain unshare/clone without namespace flags.
  • personality.
  • Socket families, which the network namespace already confines.

A bwrap without --add-seccomp-fd, or another architecture, leaves the backend unavailable, with the reason in the debug log. A jail is refused rather than started unfiltered.

Stacked on #160; review only the last commit.

Related issue

Part of #104 (item 2, seccomp), with the follow-up to #27.

API or behavior changes

  • New BubblewrapBackend::installs_seccomp().
  • New Linux dependencies in tinybox-jail: seccompiler 0.5 (default features off; Apache-2.0 OR BSD-3-Clause; depends only on libc) and a direct libc (already in the lockfile).
  • Jailed programs get EPERM for the calls above. Debuggers still work on Linux 4.8+.
  • Hosts whose bwrap lacks --add-seccomp-fd (bubblewrap older than 0.5) no longer get the bwrap backend.
  • The 2.1 native resource launcher (tinybox-linux) is not filtered yet; it follows once its descriptor passing lands.

Validation

On our Linux CI runner (kernel 7.0.0, bubblewrap 0.9.0, python3 3.12). The gates and the full runs are at a2b222b524; the head differs only by applying cargo fmt's one-hunk patch. The module run and the first three red controls are from an earlier build of this commit (c4f3bcc5bb) that had the same filter and wiring, before the fake-bwrap fixtures learned --help:

  • cargo build --locked --all-targets --all-features: ok. The new lockfile entries are exactly what cargo resolves.
  • cargo deny --locked check: seccompiler and libc add no findings.
  • cargo clippy --all-targets --all-features -- -D warnings: 0 errors, same as main. RUSTDOCFLAGS="-D warnings" cargo doc: ok.
  • TINYBOX_REQUIRE_BWRAP=1 cargo test --locked --all-features --no-fail-fast -p tinybox-jail: 278 passed, 0 failed. That includes all 24 live bwrap_isolation tests under the filter.
  • cargo test -p tinybox-module native shell and stdio: 48 passed. The jailed tests ran under the filter.
  • cargo test --workspace --no-fail-fast: 1592 passed, 0 failed, with no failures unique to this branch.
  • Red controls, each restored afterwards:
    • dropping keyctl: the live test sees keyctl -1 EKEYREVOKED, so not denied, and fails;
    • denying ptrace everywhere: the trace test sees ptrace -1 EPERM and fails;
    • not passing the filter to bwrap: the EPERM test fails;
    • letting the no-seccomp fixture list the flag: the refusal test fails.

Tests

  • Unit tests:
    • kernel release parsing;
    • ptrace denied only before 4.8;
    • the escape calls are in the list;
    • an architecture without a filter is refused;
    • both filters compile to whole BPF instructions;
    • one rule per namespace flag.
  • Availability (fake bwrap fixtures, which now answer --help):
    • a bwrap whose --help lacks --add-seccomp-fd is not used, even though it passes the isolation probe;
    • one that lists it installs the filters.
  • Live tests (bwrap_isolation, raw syscalls through python3 inside the jail):
    • keyctl returns -1 EPERM while getpid works;
    • a jailed process can ptrace its own child.
    • Every existing live bwrap test passes under the filter.
  • Red controls:
    • removing keyctl from the list fails the EPERM test;
    • denying ptrace everywhere fails the trace test;
    • not passing the filter to bwrap fails the EPERM test, which proves bwrap really loads it.
    • letting the no-seccomp fixture list --add-seccomp-fd fails the refusal test, so the refusal really keys on that flag.

Documentation

tinybox-jail/src/bwrap/mod.rs: the isolation text, a new Seccomp section, and the known gap removed. Module docs in bwrap/seccomp.rs.

Checklist

  • The change is focused on one logical change
  • No new #[allow(...)], #[ignore], or relaxed lints
  • No secrets, tokens, or .env contents in the diff or the description

A nested grant's symlink check ran before bwrap re-resolved the grant's path,
so a program with write access to the parent could swap in a link between
them. Where bwrap lists --bind-fd and the kernel has openat2, each grant is
now opened once as an O_PATH descriptor, a nested one beneath its parent with
RESOLVE_NO_SYMLINKS | RESOLVE_BENEATH, and bound with --bind-fd. Descriptors
are mapped only in the child after fork (command-fds), never inheritable in
the host. Elsewhere grants stay bound by path, as before, and
BubblewrapBackend::binds_by_descriptor reports which is in effect.
The bubblewrap backend installed no seccomp filter, so keyctl, bpf, perf,
io_uring, mount, setns, file handles and similar calls stayed reachable from a
jail. A deny list compiled with seccompiler now goes to bwrap as two
--add-seccomp-fd filters on pipes mapped into the child: EPERM for the deny
list, ENOSYS for clone3 so the C library falls back to a clone whose flags the
filter can check. ptrace stays allowed on Linux 4.8 and later; unshare and
clone are denied only with a CLONE_NEW* flag. A bwrap without
--add-seccomp-fd, or an architecture other than x86_64 and aarch64, leaves the
backend unavailable rather than starting jails unfiltered.
@M3gA-Mind

Copy link
Copy Markdown
Collaborator Author

Stacked on #160 (its first commit is #160's). Please merge #160 first; this PR's diff then reduces to the seccomp commit.

@coderabbitai

coderabbitai Bot commented Nov 11, 2026 •

Copy link
Copy Markdown

Important

  • Trigger review

This repository does not receive automatic reviews because it has fewer than 10 stars.

Run configuration

  • Source mode: Auto
  • Repository: Unavailable
  • Branch: Unavailable
  • File: No configuration file from this repository applied
  • Commit: Unavailable
  • Layers: Organization UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 8907fd70-c53a-4dec-ae26-36fef4adfab0
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

@tinysweeper

tinysweeper Bot commented Nov 11, 2026 •

Copy link
Copy Markdown

Tiny Sweeper review

Reviewed head: 38deeb420619547a1173d255d7c713a4e9ceeed0

Verdict: Ready for maintainer review · Priority: none
Coverage: Reviewed 14 of 15 files; not reviewed: Cargo.lock.
Pull request purpose (title): Start every bwrap ja…

Diagnostics and supporting details

Full summary

Pull request purpose (title): Start every bwrap jail under a seccomp deny list. Tiny Sweeper completed its review; deterministic results follow.

Unreviewed paths

  • Cargo.lock

Review snapshot

Change surface Files Review signal Count
Production 4 Active findings 0
Tests 9 Noted findings 0
Documentation 0 Resolved findings 0
Configuration 1 Pending checks/questions 0

Completeness: Complete
Test assessment: Test coverage is assessed from changed tests and lane evidence; execution is not claimed without trusted check data.

What changed

The jail now compiles two seccomp filters (an EPERM deny list with x32 twins and a clone3-to-ENOSYS filter) for x86_64 and aarch64 from kernel release and process architecture; a bwrap without --add-seccomp-fd or a filter for the architecture makes the backend unavailable. Spawn prep appends --add-seccomp-fd arguments for filters numbered after grants and maps all descriptors via command_fds in the child only. The launcher script takes a keep-count and spares fds 10 and up while closing every other inherited descriptor above 2. Grants may be bound from O_PATH descriptors (nested with RESOLVE_NO_SYMLINKS|RESOLVE_BENEATH) instead of by path, with binds_by_descriptor reporting which is in effect; module docs gain a Seccomp section and the gap is narrowed accordingly. New Linux dependencies are added: command-fds, nix (fs), seccompiler (default features off) and libc.

Features

  • Added — Seccomp deny list installed on every jail: Two serialized filters (EPERM deny list including x32 twins on x86_64, plus clone3-to-ENOSYS) are compiled for x86_64 and aarch64 from kernel release and process architecture; an unsupported architecture or non-compiling filter refuses the backend instead of starting unfiltered, and installs_seccomp reports the state. (crates/tinybox-jail/src/bwrap/mod.rs fn run_probe(program: &Path, disable_userns: bool, root: &Path, host_only: &Path, crates/tinybox-jail/src/bwrap/mod.rs impl BubblewrapBackend {, crates/tinybox-jail/src/bwrap/mod.rs static DEFAULT_PROBE: OnceLock<Option<Verified>> = OnceLock::new();)
  • Modified — Filter and grant descriptors mapped into the child and kept by the launcher: prepare_command numbers filters after grants, appends --add-seccomp-fd, and maps all descriptors into the child only after fork; the launcher keeps fds 10 and up within the keep count open while still closing stray inherited descriptors, so bwrap receives exactly the descriptors installed. (crates/tinybox-jail/src/bwrap/mod.rs fn prepare_command(verified: &Verified, jail: &Jail, cmd: &Command) -> io::Resul, crates/tinybox-jail/src/bwrap/launcher.rs for fd in /proc/self/fd/*; do, crates/tinybox-jail/src/bwrap/launcher.rs pub(super) fn command()
  • Added — Grants bound from descriptors when supported: Grants are opened once as O_PATH descriptors with openat2 and bound with --bind-fd/--ro-bind-fd, closing the check-then-use gap on nested grants where bwrap lists --bind-fd and the kernel has openat2; otherwise paths are used and the gap remains, as reported by binds_by_descriptor. (crates/tinybox-jail/src/bwrap/args.rs pub(super) fn jail_args(, crates/tinybox-jail/src/bwrap/args.rs enum Kind {, crates/tinybox-jail/src/bwrap/mod.rs fn is_trusted_binary(path: &Path) -> bool {)
  • Added — New Linux dependencies for descriptor passing and filter compilation: Adds command-fds, nix (fs only), seccompiler with default features off, and a direct libc dependency to the jail crate. (crates/tinybox-jail/Cargo.toml landlock = { version = "0.4", optional = true })

Tests

  • unit — Grant descriptors are opened from 10 upward, grants bind via --bind-fd/--ro-bind-fd in shallowest-first order, baseline paths stay path-bound, a symlink swapped in after the check inside a grant raises ELOOP, a top-level host symlink still opens, a missing read-only grant is skipped, and path binding is used when fd support is off.: Exercises the descriptor-binding path directly, including the symlink-swap race and ordering needed for nested grants. (crates/tinybox-jail/src/bwrap/args_tests.rs fn a_denial_through_a_baseline_symlink_also_covers_the_link_target() -> io::Resu, crates/tinybox-jail/src/bwrap/args_tests.rs use super::*;)
  • unit — A launcher passed a keep count of 2 delivers descriptors 10 and 11 to the bwrap stand-in while an inheritable stray descriptor is still closed; a launcher that cannot open the args file exits 126 without running bwrap; the launcher runs with an empty environment.: Pins the fd-keep logic from both sides: needed descriptors survive, stray ones do not. (crates/tinybox-jail/src/bwrap/launcher_tests.rs fn the_args_file_is_gone_once_the_launcher_has_started_bwrap() -> io::Result<()>, crates/tinybox-jail/src/bwrap/launcher_tests.rs fn the_launcher_runs_with_an_empty_environment() -> io::Result<()> {, crates/tinybox-jail/src/bwrap/launcher_tests.rs fn run(shell: &str, body: &str, items: &[&str]) -> io::Result<String> {)
  • integration — A fixture whose --help lacks --add-seccomp-fd is not used even though it passes the isolation probe, while one that lists it installs the filters.: Keys the refusal on the flag itself and checks the installs_seccomp accessor. (crates/tinybox-jail/src/bwrap/mod_tests.rs fn a_launcher_that_passes_the_probe_is_available_with_process_isolation_minimum(, crates/tinybox-jail/tests/fixtures/fake-bwrap-noseccomp.sh, crates/tinybox-jail/tests/fixtures/fake-bwrap-probe.sh)

Findings

No active actionable findings.

Before merge

None.

Agent review details

critique

  • Conclusion: Success
  • Scope reviewed: all assigned evidence
  • Positive: The grant-descriptor keep count is threaded consistently through all callers (probe passes 0, prepare_command passes fds.len()) and the spared range matches the descriptors actually mapped; no reachable input breaks it.
  • Lane summary: The change threads a grant-descriptor count through the launcher so the close loop spares fds 10..10+k while still closing every other inherited descriptor, and all callers (probe passes 0, prepare_command passes fds.len()) plus the tests are updated consistently. The count comes from the same fds vector that is mapped onto 10, 11, …, so the spared range matches exactly what bwrap needs; I found no reachable input that breaks it.

security

  • Conclusion: Success
  • Scope reviewed: all assigned evidence
  • Positive: bwrap's bind-FD descriptors survive the launcher's close loop while everything else, including stray inheritable fds outside the reserved range, is still closed.
  • Lane summary: The change threads a grant-descriptor count through the launcher so bwrap's bind-FD descriptors (10 and up) survive the launcher's descriptor-closing loop while everything else, including stray inheritable fds outside the reserved range, is still closed. Callers in mod.rs already pass `fds.len()` or 0 correctly, and the new test exercises both the kept and the closed cases; I found no defect in these two files.

tests

  • Conclusion: Success
  • Scope reviewed: all assigned evidence
  • Positive: A real test opens mapped descriptors inheritably and asserts fds 10 and 11 reach bwrap while a stray inheritable descriptor is still closed, so the keep count and range check are pinned.
  • Lane summary: The launcher change that keeps only the intended grant descriptors open is pinned by a real test: `the_grant_descriptors_reach_bwrap_and_nothing_else_does` opens the mapped descriptors inheritably in a bash launcher and asserts fds 10 and 11 reach bwrap while a stray inheritable descriptor is still closed, so it fails if the `$2` count or the range check regresses. The remaining changed call sites (`grant_fds` of 0 in the other launcher tests, the `--args` probe call) are updated coherently, and the seccomp/`--bind-fd` behaviour was already covered in the files classified as behaviour in this revision. I found no defect worth reporting.

commits

  • Conclusion: Success
  • Scope reviewed: all assigned evidence
  • Positive: No sensitive content in committed files.
  • Lane summary: No sensitive content in committed files.

description

  • Conclusion: Success
  • Scope reviewed: all assigned evidence
  • Positive: The description's claims match the diff: the keep count passed to the launcher matches the number of mappings installed on the child, and the seccomp-focused body reflects the last commit.
  • Lane summary: This commit threads a grant-descriptor count through the launcher so its script keeps fds 10..10+k open for bwrap instead of closing them. The change is self-consistent: `prepare_command` passes `fds.len()` and maps exactly those numbers from `FIRST_GRANT_FD`, the probe path passes 0, and the new test checks both keeping and closing. The description's claims still match the diff.
Evidence and run details
  • Models: ladder/vectors-oai3, deepseek-v4.1-flash
  • Spend: $0.003769
  • Tokens: 440958 input · 5699 output · 165376 cached · 892 embedding
  • Updated: 2026-11-12T07:31:57Z
  • Continuity: summary cache chain restarted at the storage ceiling.
Head State Pass summary
46c7657e2174 incomplete 0 active finding(s), 0 resolved finding(s) (at 2026-11-11T12:40:58Z)
b2c774bcad7e ready for maintainer review 0 active finding(s), 0 resolved finding(s) (at 2026-11-11T14:17:05Z)
38deeb420619 ready for maintainer review 0 active finding(s), 0 resolved finding(s) (at 2026-11-12T07:31:57Z)

tinysweeper 0.1.2

@tinysweeper tinysweeper Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

tinysweeper could not complete coverage (9 unanswered item(s)), so this is not an approval.

Diagnostics

Total spend: $0.0463

@tinysweeper tinysweeper Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

tinysweeper found nothing blocking. Approving.

Diagnostics

Total spend: $0.0132

M3gA-Mind added a commit to M3gA-Mind/tinybox that referenced this pull request Nov 12, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

priority: p3 Whenever. Cosmetic, a nicety, or a cleanup with no user visible effect.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant