Fix six HIGH-severity sandbox findings + add scope-enforced search tool - #6
Merged
Conversation
All six were verified against the post-consolidation tree, not the paths the scanner reported. Three share one root cause — a tool child inherits the launcher's root/CAP_SYS_ADMIN under `--features enforce` — so the privilege drop is the load-bearing fix and the others are reasoned against it. - f028/f026/f010 (privilege retention): drop_privileges() in pre_exec now sets PR_SET_NO_NEW_PRIVS, SECBIT_NOROOT (+locks), and empties the ambient, bounding, and per-thread capability sets. The cgroup join is reordered to run BEFORE the drop, since writing cgroup.procs needs the authority the drop discards. NO_NEW_PRIVS closes the "only the first executable is checked" hole (f028); the capability drop stops a migrated process from writing another cgroup.procs (f026). - f030 (credential exposure): tool children now inherit an allowlist (PATH/HOME/… + LC_*) via env_clear instead of a four-name provider-key denylist, so AWS_SECRET_ACCESS_KEY / GITHUB_TOKEN / KUBECONFIG etc. no longer reach model-driven bash. - f020 (path traversal): scenario workdir paths are contained under an operator-declared --workdir-root (serde-skipped, never scenario-set), resolving symlinks and refusing absolute/`..` escapes rather than rebasing. - f018 (credential exfil): provider.api_key_env is constrained to the provider-key namespace (*_API_KEY + known names), enforced at model construction so the batch path cannot skip it; batch also announces which secret goes to which endpoint. - f022 (enforcement outliving teardown): scope teardown now kills the whole cgroup subtree (cgroup.kill, or repeated SIGKILL) before detaching the engine, and no longer swallows the failure; host-mode tool children get their own process group so backgrounded descendants are reaped. Tests: capability/NoNewPrivs readback, workdir containment (incl. symlink crossing), api_key_env namespace, and a descendant-cleanup test verified to fail without the fix. All feature builds + clippy + fmt clean. VM verification of the enforce-only paths (privilege drop, cgroup ancestor walk, subtree kill) is still pending — host tests cover the userspace logic. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
VM testing on the BPF-LSM kernel (ac-matrix-vm) surfaced a regression in the f028/f026 privilege drop: emptying the *entire* capability set while keeping uid 0 stripped CAP_DAC_OVERRIDE / CAP_DAC_READ_SEARCH, so a root tool child could no longer traverse a 0700 dir owned by another user. The kernel then denied at DAC *before* bee's security_file_open hook ran — which broke the core invariant that the eBPF policy is the file-access arbiter with a complete audit trail. Two matrix cases regressed: observe-mode (read wrongly denied instead of allowed+observed) and episode-file-deny (denial never reached the transcript because DAC pre-empted the LSM). Corrected drop_privileges(): - Keep CAP_DAC_OVERRIDE + CAP_DAC_READ_SEARCH; drop every other capability from the bounding set (CAP_SYS_ADMIN, CAP_BPF, CAP_SYS_PTRACE, …), which is what actually matters for f010 — a root child that can detach bee's own LSM. - Drop SECBIT_NOROOT and the capset/ambient dance: with NOROOT off, root-magic re-derives the post-exec set from the bounding set, so bounding-dropping the escape caps removes them from the running child while the DAC pair still comes through. No exploitable window — the full set exists only between the drop and the immediately following execve. VM matrix now 31/31 (28 prior + 3 new): priv-drop (NoNewPrivs=1, bounding set == the DAC pair, CAP_SYS_ADMIN gone), cgroup-migrate-enforced (child mkdir's a sub-cgroup, migrates in, denied read still denied — the ancestor walk), and teardown-kills-descendants (backgrounded descendant reaped before the engine detaches). observe-mode and episode-file-deny green again. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The .triage-state shards and TRIAGE/VULN artifacts from the re-run that produced the six HIGH findings fixed in this branch. Data only, no code. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…rker Adds a default `search` tool: regex search over file contents, returning `path:line:text` and honoring .gitignore. The design point is scope enforcement. ripgrep-api is a pure-Rust library (it wraps ripgrep's grep/ignore crates — no external `rg` binary), but running it in the harness would search *around* the sandbox: the harness is root, outside the scope cgroup, and not subject to the file policy. So instead the tool execs a hidden `bee search-worker` subcommand through the same run_child path as every other tool — the worker joins the scope cgroup, is hardened + credential- stripped, and every file the library opens there passes through the eBPF file_open policy. "In-process" is real; it just runs in that worker, not the harness (the only place cgroup-per-process enforcement can reach it). - src/search.rs: SearchArgs + the worker `run()` (SearchBuilder → for_each → path:line:text) + argv round-trip. Narrow arg surface (pattern/path/glob/ ignore_case) — none of ripgrep's command-executing options are exposed. - src/tools/search.rs: the model-facing tool; execs current_exe search-worker. - main.rs: hidden `SearchWorker` subcommand. - Added to both DEFAULT_TOOLS lists (tools + config layer) and the schema doc. Verified: host unit + integration tests (worker E2E via CARGO_BIN_EXE_bee, glob/case/argv), and VM matrix `search-in-scope` (32/32) — a policy-denied file in the search tree is invisible to the library search while a sibling is found, proving the search genuinely runs under the scope's LSM. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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.
Summary
Closes the six HIGH-severity findings from the latest triage run, and adds a
search(ripgrep) tool that runs genuinely inside the eBPF scope. All enforce-only behavior is VM-verified on the BPF-LSM kernel (ac-matrix-vm): 32/32 matrix cases pass.Security fixes (all verified against the post-consolidation tree)
PR_SET_NO_NEW_PRIVSinpre_exec—sh -ccan no longer reach a setuid/fcaps binaryresolve_scope) instead of exact-match, so a migrated process stays enforcedCAP_DAC_OVERRIDE/CAP_DAC_READ_SEARCH) — noCAP_SYS_ADMIN/CAP_BPFto detach bee's own LSMenv_clear+ PATH/HOME/LC_*), not a four-name denylist--workdir-root; symlink/../absolute escapes refusedprovider.api_key_envconstrained to the provider-key namespace, enforced at model construction so batch can't skip itThe DAC-cap nuance was caught by VM testing: dropping the whole capability set while keeping uid 0 made root unable to traverse a
0700dir, so the kernel denied at DAC before bee's LSM hook ran — breaking the "LSM is the file-access arbiter + complete audit" invariant. Keeping the DAC pair (and droppingSECBIT_NOROOT, letting the bounding set bound the post-exec child) fixes it while still stripping every escape-enabling capability.searchtool (ripgrep, scope-enforced)ripgrep-apiis a pure-Rust library, but running it in the harness would search around the sandbox (harness is root, outside the scope cgroup). Instead the tool execs a hiddenbee search-workersubcommand through the samerun_childpath as every other tool — the worker joins the scope cgroup and every file the library opens passes through thefile_openpolicy. Default tool; narrow arg surface (no ripgrep command-executing options exposed).Verification
priv-drop,cgroup-migrate-enforced,teardown-kills-descendants,search-in-scope.🤖 Generated with Claude Code