# Defense-in-Depth Score: Buttercup

**Repo:** https://github.com/trailofbits/buttercup · **Commit:** `298c01fbff3bf0bf166c87856acd7e42f94f7157` · **Reviewed:** 2026-10-03
**What it is:** Cyber reasoning system (DARPA AIxCC) that finds and patches vulnerabilities via AI-assisted fuzzing and patch agents
**Category:** Cybersecurity
**Scored configuration:** Local minikube Helm deployment as the README leads with (make setup-local, make deploy): all charts in deployment/k8s/values.yaml enabled, one budgeted LiteLLM virtual key, in-cluster Docker-in-Docker daemon, patcher find_tests agent enabled.
**Agent surface (default):** code execution yes · filesystem write yes · network egress yes · external credentials yes · persistent memory yes · untrusted input yes · third party extensions no · sub agents yes · external communication yes

## Score: 2.2 / 10.0 (Minimal)

| # | Criterion | S | C | D | B | Raw | Cap | Score | Confidence |
|---|---|---|---|---|---|---|---|---|---|
| C1 | Identity & least privilege | L1 | L1 | L1 | L1 | 0.25 | — | **0.25** | High |
| C2 | Approval gates | L0 | L0 | L0 | L1 | 0.05 | C2-POWERBYPASS | **0.05** | High |
| C3 | Tool & action scoping | L0 | L1 | L1 | L1 | 0.17 | — | **0.17** | High |
| C4 | Code-execution isolation | L1 | L1 | L2 | L0 | 0.25 | C4-HOSTROOT | **0.25** | High |
| C5 | Untrusted input blast radius | L0 | L0 | L0 | L1 | 0.05 | — | **0.05** | High |
| C6 | Memory, context & configuration integrity | L1 | L1 | L2 | L1 | 0.30 | — | **0.30** | High |
| C7 | Third-party extensions | L1 | L1 | L1 | L0 | 0.20 | — | **0.20** | High |
| C8 | Secrets & sensitive-data protection | L1 | L1 | L0 | L1 | 0.20 | — | **0.20** | High |
| C9 | Audit & traceability | L1 | L1 | L2 | L1 | 0.30 | — | **0.30** | High |
| C10 | Limits & kill switch | L2 | L1 | L2 | L1 | 0.38 | — | **0.38** | High |


Buttercup is a fully autonomous pipeline with no human approval step: LLM agents explore target code, write test scripts and patches, and the results are submitted to the competition API on their own. The dominant risk is execution of model-written shell commands and test scripts in --privileged containers on a Docker-in-Docker daemon that is itself a privileged pod, with unrestricted network and no per-command timeout. There are real controls elsewhere: a WASI sandbox for model-written seed generators, an Argon2-authenticated task API, a per-key LLM budget, and bounded patch-retry and recursion limits. Defaults still ship unauthenticated Redis, DEBUG logging, and a UI without authentication.

## Critical gaps
- Patcher shell commands and LLM-written test scripts run in docker containers started with --privileged on a Docker-in-Docker daemon in a privileged pod, with no network or resource restrictions (C4-HOSTROOT, C4 B at L0). (ASI05, T11, LLM05; C4) — [common/src/buttercup/common/challenge_task.py:538](https://github.com/trailofbits/buttercup/blob/298c01fbff3bf0bf166c87856acd7e42f94f7157/common/src/buttercup/common/challenge_task.py#L538); [deployment/k8s/charts/dind-daemon/templates/daemonset.yaml:27](https://github.com/trailofbits/buttercup/blob/298c01fbff3bf0bf166c87856acd7e42f94f7157/deployment/k8s/charts/dind-daemon/templates/daemonset.yaml#L27)
- The most powerful action path (arbitrary shell via the sh tool and automatic test-script runs) has no approval gate, and generated patches are submitted without review (C2-POWERBYPASS). (ASI09, ASI02, T10; C2) — [patcher/src/buttercup/patcher/agents/context_retriever.py:528](https://github.com/trailofbits/buttercup/blob/298c01fbff3bf0bf166c87856acd7e42f94f7157/patcher/src/buttercup/patcher/agents/context_retriever.py#L528); [patcher/src/buttercup/patcher/patcher.py:161](https://github.com/trailofbits/buttercup/blob/298c01fbff3bf0bf166c87856acd7e42f94f7157/patcher/src/buttercup/patcher/patcher.py#L161)
- Task-supplied OSS-Fuzz helper code runs in the patcher pod through a subprocess that inherits the pod environment, including the LLM key (C7 B at L0). (ASI04, T17, LLM03; C7) — [common/src/buttercup/common/challenge_task.py:388-393](https://github.com/trailofbits/buttercup/blob/298c01fbff3bf0bf166c87856acd7e42f94f7157/common/src/buttercup/common/challenge_task.py#L388-L393); [common/src/buttercup/common/challenge_task.py:347](https://github.com/trailofbits/buttercup/blob/298c01fbff3bf0bf166c87856acd7e42f94f7157/common/src/buttercup/common/challenge_task.py#L347)

## Criterion details

### C1 Identity & least privilege — 0.25 (high)

LLM calls use a dedicated, budget-limited LiteLLM virtual key rather than the master key, and the task API authenticates with Argon2-hashed credentials. Everything else is broad: the patcher, builders and seed generator all mount the Docker socket of a privileged Docker-in-Docker daemon, run under the default service account with no security contexts, share an unauthenticated Redis, and the UI pod holds the CRS token and a GitHub PAT in plain environment variables. Default credential handling in the shipped charts is not locked down. A hijacked agent would inherit root on the DinD daemon plus write access to the competition API.

- **S L1:** Agents get a dedicated budget-capped virtual LLM key, but also the Docker socket of a privileged daemon (admin-equivalent), and one shared 'llm-user' key serves every service. — [deployment/k8s/templates/litellm-user-keys-setup-script.yaml:47-51](https://github.com/trailofbits/buttercup/blob/298c01fbff3bf0bf166c87856acd7e42f94f7157/deployment/k8s/templates/litellm-user-keys-setup-script.yaml#L47-L51); [deployment/k8s/charts/patcher/templates/deployment.yaml:35-41](https://github.com/trailofbits/buttercup/blob/298c01fbff3bf0bf166c87856acd7e42f94f7157/deployment/k8s/charts/patcher/templates/deployment.yaml#L35-L41); [deployment/k8s/charts/dind-daemon/templates/daemonset.yaml:26-27](https://github.com/trailofbits/buttercup/blob/298c01fbff3bf0bf166c87856acd7e42f94f7157/deployment/k8s/charts/dind-daemon/templates/daemonset.yaml#L26-L27) (verified)
  - *To reach the next level:* Authority is not narrowed per capability: Docker access, Redis access and the LLM key are all-or-nothing for each pod.
- **C L1:** Helper subprocesses started by ChallengeTask inherit the whole pod environment (including the LLM key in the patcher pod) when no env override is given, and every pod reaches the same Redis with no auth. — [common/src/buttercup/common/challenge_task.py:384-393](https://github.com/trailofbits/buttercup/blob/298c01fbff3bf0bf166c87856acd7e42f94f7157/common/src/buttercup/common/challenge_task.py#L384-L393); [deployment/k8s/values.yaml:133-134](https://github.com/trailofbits/buttercup/blob/298c01fbff3bf0bf166c87856acd7e42f94f7157/deployment/k8s/values.yaml#L133-L134) (verified)
  - *To reach the next level:* No shared authorization layer; subprocesses and sibling services use ambient pod credentials.
- **D L1:** The default budgeted key exists, but default credential handling is not locked down, the Docker socket is mounted by default, and the UI pod receives the CRS token and a GitHub PAT. — [deployment/k8s/templates/common-env.yaml:380-396](https://github.com/trailofbits/buttercup/blob/298c01fbff3bf0bf166c87856acd7e42f94f7157/deployment/k8s/templates/common-env.yaml#L380-L396); [common/src/buttercup/common/llm.py:151](https://github.com/trailofbits/buttercup/blob/298c01fbff3bf0bf166c87856acd7e42f94f7157/common/src/buttercup/common/llm.py#L151) (verified)
  - *To reach the next level:* Default is not near-minimal: least privilege needs manual hardening of the charts.
- **B L1:** If authorization fails, the attacker gets root on a privileged DinD daemon with shared node-local storage, spend up to the LLM key budget, and the ability to submit to the competition API; the budget cap and per-model rate limits survive. — searched `rg -n -S 'kind: (Role|ClusterRole|ServiceAccount|NetworkPolicy)|runAsNonRoot|readOnlyRootFilesystem|seccompProfile'` in `deployment/k8s/templates deployment/k8s/charts` → 5 hits (all 5 hits are the one-off litellm-user-keys setup job's Role; no runtime workload has RBAC, NetworkPolicy or a security context); [deployment/k8s/templates/_helpers.tpl:193-198](https://github.com/trailofbits/buttercup/blob/298c01fbff3bf0bf166c87856acd7e42f94f7157/deployment/k8s/templates/_helpers.tpl#L193-L198) (verified)
  - *To reach the next level:* No credential is read-only or scoped to one task, and DinD access reaches shared storage for all tasks.
- **Cap:** none
- **Notes:** The create_llm fallback to BUTTERCUP_LITELLM_KEY is the budgeted key in the k8s charts (secret litellm-api-user); the master key lives in the litellm pod.

### C2 Approval gates — 0.05 (high)

There is no human approval anywhere in the system. The find-tests agent runs arbitrary shell commands, LLM-written test scripts run automatically, and generated patches are pushed to the queue and submitted to the competition API with no review. Work happens on disposable copies of the task directory and the output is a patch text, which limits damage, but the most powerful action path has no gate at all.

- **S L0:** No approval mechanism exists; an LLM judge only decides whether generated test instructions look valid. — [patcher/src/buttercup/patcher/agents/context_retriever.py:587-604](https://github.com/trailofbits/buttercup/blob/298c01fbff3bf0bf166c87856acd7e42f94f7157/patcher/src/buttercup/patcher/agents/context_retriever.py#L587-L604); searched `rg -n -i 'approval|approve|human_in_the_loop|interrupt\(|interrupt_before|ask_user'` in `patcher/src seed-gen/src/buttercup common/src/buttercup/common orchestrator/src/buttercup/orchestrator/scheduler` → 0 hits (verified)
  - *To reach the next level:* A deterministic human approval for shell commands, persisted test scripts and patch submission.
- **C L0:** The shell tool and the test-script runner, the most powerful paths, are ungated, and patch hand-off to the submission queue is automatic. — [patcher/src/buttercup/patcher/agents/context_retriever.py:528-557](https://github.com/trailofbits/buttercup/blob/298c01fbff3bf0bf166c87856acd7e42f94f7157/patcher/src/buttercup/patcher/agents/context_retriever.py#L528-L557); [patcher/src/buttercup/patcher/patcher.py:152-162](https://github.com/trailofbits/buttercup/blob/298c01fbff3bf0bf166c87856acd7e42f94f7157/patcher/src/buttercup/patcher/patcher.py#L152-L162) (verified)
  - *To reach the next level:* The shell tool is exempt; nothing is gated.
- **D L0:** Approval does not exist, so it cannot be on by default; find_tests (which enables the sh tool) defaults to True and the serve path never passes it. — [patcher/src/buttercup/patcher/patcher.py:34](https://github.com/trailofbits/buttercup/blob/298c01fbff3bf0bf166c87856acd7e42f94f7157/patcher/src/buttercup/patcher/patcher.py#L34); [patcher/src/buttercup/patcher/__cli__.py:31-37](https://github.com/trailofbits/buttercup/blob/298c01fbff3bf0bf166c87856acd7e42f94f7157/patcher/src/buttercup/patcher/__cli__.py#L31-L37) (verified)
  - *To reach the next level:* An approval gate that is on by default.
- **B L1:** Commands act on disposable task copies and the patch output is text, but submissions to the competition API cannot be recalled and there is no spend or recipient bound per approval. — [common/src/buttercup/common/challenge_task.py:982-998](https://github.com/trailofbits/buttercup/blob/298c01fbff3bf0bf166c87856acd7e42f94f7157/common/src/buttercup/common/challenge_task.py#L982-L998); [orchestrator/src/buttercup/orchestrator/scheduler/submissions.py:370](https://github.com/trailofbits/buttercup/blob/298c01fbff3bf0bf166c87856acd7e42f94f7157/orchestrator/src/buttercup/orchestrator/scheduler/submissions.py#L370) (verified)
  - *To reach the next level:* No rollback or preview for external submissions and no rate limit on consequential actions.
- **Cap:** C2-POWERBYPASS — The most powerful action path (arbitrary shell and test scripts in a privileged container) is not gated at all in the default configuration.

### C3 Tool & action scoping — 0.17 (high)

The patch-agent read tools (ls, cat, grep, get_lines, code-query tools) pass model arguments into a shlex-quoted command list, and patch targets are mapped to existing files under the task source directory. The find-tests agent, however, gets a raw bash tool and a script runner with no argument validation, enabled by default with no deploy-time switch. Everything runs in the target project's container with network access.

- **S L0:** The sh tool takes an arbitrary bash string and test_instructions runs an arbitrary script; the read tools quote arguments but do not validate paths or flags. — [patcher/src/buttercup/patcher/agents/context_retriever.py:528-545](https://github.com/trailofbits/buttercup/blob/298c01fbff3bf0bf166c87856acd7e42f94f7157/patcher/src/buttercup/patcher/agents/context_retriever.py#L528-L545); [patcher/src/buttercup/patcher/agents/tools.py:74](https://github.com/trailofbits/buttercup/blob/298c01fbff3bf0bf166c87856acd7e42f94f7157/patcher/src/buttercup/patcher/agents/tools.py#L74) (verified)
  - *To reach the next level:* Allowlist validation of paths, commands and arguments, or narrow replacement tools.
- **C L1:** Only the patch-writing path validates: target files must map to an existing file under the task source directory, and edits must match code already in that file. — [patcher/src/buttercup/patcher/utils.py:93-104](https://github.com/trailofbits/buttercup/blob/298c01fbff3bf0bf166c87856acd7e42f94f7157/patcher/src/buttercup/patcher/utils.py#L93-L104); [patcher/src/buttercup/patcher/agents/swe.py:499](https://github.com/trailofbits/buttercup/blob/298c01fbff3bf0bf166c87856acd7e42f94f7157/patcher/src/buttercup/patcher/agents/swe.py#L499) (verified)
  - *To reach the next level:* Read tools and the sh/test tools have no central validation layer.
- **D L1:** The shell and script tools are on by default for every task and can be disabled only through a CLI flag that the serve path does not pass. — [patcher/src/buttercup/patcher/agents/context_retriever.py:779-786](https://github.com/trailofbits/buttercup/blob/298c01fbff3bf0bf166c87856acd7e42f94f7157/patcher/src/buttercup/patcher/agents/context_retriever.py#L779-L786); [patcher/src/buttercup/patcher/patcher.py:34](https://github.com/trailofbits/buttercup/blob/298c01fbff3bf0bf166c87856acd7e42f94f7157/patcher/src/buttercup/patcher/patcher.py#L34) (verified)
  - *To reach the next level:* A read-only tool set by default with write/exec enabled explicitly.
- **B L1:** A misused tool runs arbitrary commands as root in a privileged container with the task source mounted read-write and open network. — [common/src/buttercup/common/challenge_task.py:536-545](https://github.com/trailofbits/buttercup/blob/298c01fbff3bf0bf166c87856acd7e42f94f7157/common/src/buttercup/common/challenge_task.py#L536-L545) (verified)
  - *To reach the next level:* Scope is not limited to read-only or bounded operations.
- **Cap:** none
- **Notes:** The WASI seed-generator sandbox (see C4) is the one place where model output is constrained structurally.

### C4 Code-execution isolation — 0.25 (high)

Model-written Python seed generators run inside a wasmtime WASI sandbox that preopens only a temp directory, caps memory at 50 MB, and refuses to run outside WASI; that path is well isolated. The far more powerful paths are not: every patcher command and LLM-written test script runs in a docker container started with --privileged on a Docker-in-Docker daemon that is itself a privileged pod, with open network. Fuzz targets run directly inside the fuzzer pod and task-supplied helper scripts run in the service pods with inherited environment. A container escape therefore lands on a privileged daemon with shared node storage.

- **S L1:** The main exec primitive is an ephemeral container with a separate filesystem, but --privileged removes capability, device and seccomp restrictions, so it is a working-directory-style separation rather than a hardened boundary. — [common/src/buttercup/common/challenge_task.py:536-545](https://github.com/trailofbits/buttercup/blob/298c01fbff3bf0bf166c87856acd7e42f94f7157/common/src/buttercup/common/challenge_task.py#L536-L545); [seed-gen/src/buttercup/seed_gen/sandbox/execute_llm_code.py:47-57](https://github.com/trailofbits/buttercup/blob/298c01fbff3bf0bf166c87856acd7e42f94f7157/seed-gen/src/buttercup/seed_gen/sandbox/execute_llm_code.py#L47-L57) (verified)
  - *To reach the next level:* A hardened container (non-root, dropped capabilities, seccomp, no privileged flag) or a microVM/gVisor runtime for exec paths.
- **C L1:** Seed-generator code is sandboxed in WASI, but the shell tool and test runner use the privileged container, fuzzers run unsandboxed in the fuzzer pod, and task-supplied helper.py runs in service pods. — [seed-gen/src/buttercup/seed_gen/sandbox/sandbox.py:22](https://github.com/trailofbits/buttercup/blob/298c01fbff3bf0bf166c87856acd7e42f94f7157/seed-gen/src/buttercup/seed_gen/sandbox/sandbox.py#L22); [patcher/src/buttercup/patcher/agents/context_retriever.py:552-557](https://github.com/trailofbits/buttercup/blob/298c01fbff3bf0bf166c87856acd7e42f94f7157/patcher/src/buttercup/patcher/agents/context_retriever.py#L552-L557); [fuzzer/src/buttercup/fuzzing_infra/runner_proxy.py:85](https://github.com/trailofbits/buttercup/blob/298c01fbff3bf0bf166c87856acd7e42f94f7157/fuzzer/src/buttercup/fuzzing_infra/runner_proxy.py#L85); [common/src/buttercup/common/challenge_task.py:440](https://github.com/trailofbits/buttercup/blob/298c01fbff3bf0bf166c87856acd7e42f94f7157/common/src/buttercup/common/challenge_task.py#L440) (verified)
  - *To reach the next level:* Not every model-reachable path is sandboxed; the high-power paths use the weakest isolation.
- **D L2:** Container execution is the only path for patcher commands (no host fallback in code) and the model cannot turn it off, but the privileged flag is hard-coded and the WASI path fails closed if its runtime is missing. — [common/src/buttercup/common/challenge_task.py:538](https://github.com/trailofbits/buttercup/blob/298c01fbff3bf0bf166c87856acd7e42f94f7157/common/src/buttercup/common/challenge_task.py#L538); [seed-gen/src/buttercup/seed_gen/sandbox/runner.py:45](https://github.com/trailofbits/buttercup/blob/298c01fbff3bf0bf166c87856acd7e42f94f7157/seed-gen/src/buttercup/seed_gen/sandbox/runner.py#L45) (verified)
  - *To reach the next level:* Disabling or hardening requires a code change; no operator flag and no per-call escalation model exists.
- **B L0:** Default execution uses --privileged containers on a daemon whose pod is privileged, and the Docker socket directory is a hostPath shared into the patcher, builder and seed-gen pods. — [deployment/k8s/charts/dind-daemon/templates/daemonset.yaml:26-27](https://github.com/trailofbits/buttercup/blob/298c01fbff3bf0bf166c87856acd7e42f94f7157/deployment/k8s/charts/dind-daemon/templates/daemonset.yaml#L26-L27); [deployment/k8s/templates/_helpers.tpl:193-198](https://github.com/trailofbits/buttercup/blob/298c01fbff3bf0bf166c87856acd7e42f94f7157/deployment/k8s/templates/_helpers.tpl#L193-L198); searched `rg -n -- '--network|--cap-drop|--memory|--pids-limit|NetworkPolicy|--security-opt'` in `common/src patcher/src deployment/k8s/charts deployment/k8s/templates` → 0 hits (no network, capability, memory or PID restriction for docker run or any pod) (verified)
  - *To reach the next level:* Needs no privileged flag, no shared Docker socket, workspace-only mounts, egress limits and resource limits per container.
- **Cap:** C4-HOSTROOT — The default exec containers run with --privileged on a Docker-in-Docker daemon in a privileged pod whose socket directory is mounted into agent pods.
- **Notes:** The WASI sandbox for seed generators is a strong, narrow control (memory cap, preopened temp dir only, symlink outputs skipped) but it has no CPU/fuel or wall-clock limit (consume_fuel is False).

### C5 Untrusted input blast radius — 0.05 (high)

Untrusted content (target source code, sanitizer stack traces, task tarballs and SARIF broadcasts) flows into agent prompts with nothing structural in the way: tool outputs are wrapped in tags and no code distinguishes them from instructions. A hijacked find-tests agent can run arbitrary commands with open network in a privileged container and its patch output is submitted automatically, with no human step. The LLM key and competition credentials are not placed in those containers by the docker run call, which keeps the unattended worst case below full secret loss.

- **S L0:** No injection defense exists beyond XML-style tags around tool output; no code reduces capabilities after untrusted content is read. — [patcher/src/buttercup/patcher/agents/tools.py:32-41](https://github.com/trailofbits/buttercup/blob/298c01fbff3bf0bf166c87856acd7e42f94f7157/patcher/src/buttercup/patcher/agents/tools.py#L32-L41); searched `rg -n 'untrusted|injection|spotlight|sanitize_prompt'` in `patcher/src/buttercup/patcher` → 1 hits (the single hit is an example question about SQL injection in a prompt, not a defense) (verified)
  - *To reach the next level:* Rule-of-Two enforcement: disable or gate egress and state-changing tools once untrusted content enters the session.
- **C L0:** Sanitizer output, tool results and file contents enter context with the same standing as instructions. — [patcher/src/buttercup/patcher/patcher.py:117](https://github.com/trailofbits/buttercup/blob/298c01fbff3bf0bf166c87856acd7e42f94f7157/patcher/src/buttercup/patcher/patcher.py#L117) (verified)
  - *To reach the next level:* Untrusted sources are not distinguished from instructions.
- **D L0:** There is no control to be on by default. — [patcher/src/buttercup/patcher/agents/context_retriever.py:779-786](https://github.com/trailofbits/buttercup/blob/298c01fbff3bf0bf166c87856acd7e42f94f7157/patcher/src/buttercup/patcher/agents/context_retriever.py#L779-L786) (verified)
  - *To reach the next level:* A default-on structural limit on what a hijacked session can do.
- **B L1:** A hijacked agent can run unattended commands with unrestricted egress (data exfiltration of task contents) and have a poisoned patch auto-submitted; the docker run call passes no secrets into the container, which keeps key theft out of the direct path. — [common/src/buttercup/common/challenge_task.py:536-545](https://github.com/trailofbits/buttercup/blob/298c01fbff3bf0bf166c87856acd7e42f94f7157/common/src/buttercup/common/challenge_task.py#L536-L545); [orchestrator/src/buttercup/orchestrator/scheduler/submissions.py:370](https://github.com/trailofbits/buttercup/blob/298c01fbff3bf0bf166c87856acd7e42f94f7157/orchestrator/src/buttercup/orchestrator/scheduler/submissions.py#L370) (verified)
  - *To reach the next level:* Egress and submission are not gated or restricted when untrusted content has been read.
- **Cap:** none
- **Notes:** The task API is authenticated (HTTP Basic with Argon2id, task_server/server.py:88-144), so inbound task content comes from an authenticated principal, but the content of the target repo itself is third-party.

### C6 Memory, context & configuration integrity — 0.30 (high)

The one persistent store an agent can write is a Redis hash of LLM-authored test scripts keyed by task id; scripts are only accepted after they run once and an LLM judges the output, then are reloaded and executed later without review. A test.sh shipped in the task's project directory is read and run automatically. Redis has authentication disabled by default. Patcher and seed-gen auto-load a .env file, but from the image working directory (/app/patcher, /app/seed-gen), not from the target repo, so repo-controlled configuration does not apply.

- **S L1:** Model-written test scripts are persisted after an LLM validity check with no human review or provenance tag, and task-supplied test.sh is read and run automatically. — [patcher/src/buttercup/patcher/agents/context_retriever.py:1337](https://github.com/trailofbits/buttercup/blob/298c01fbff3bf0bf166c87856acd7e42f94f7157/patcher/src/buttercup/patcher/agents/context_retriever.py#L1337); [patcher/src/buttercup/patcher/agents/context_retriever.py:1247](https://github.com/trailofbits/buttercup/blob/298c01fbff3bf0bf166c87856acd7e42f94f7157/patcher/src/buttercup/patcher/agents/context_retriever.py#L1247) (verified)
  - *To reach the next level:* Persisted entries need approval or deterministic validation, expiry and provenance.
- **C L1:** Only the custom test map exists as a model-writable store, and it has no controls beyond the LLM judge; LangGraph checkpointers are in-memory. — [patcher/src/buttercup/patcher/agents/context_retriever.py:1223](https://github.com/trailofbits/buttercup/blob/298c01fbff3bf0bf166c87856acd7e42f94f7157/patcher/src/buttercup/patcher/agents/context_retriever.py#L1223); [patcher/src/buttercup/patcher/agents/context_retriever.py:1228](https://github.com/trailofbits/buttercup/blob/298c01fbff3bf0bf166c87856acd7e42f94f7157/patcher/src/buttercup/patcher/agents/context_retriever.py#L1228) (verified)
  - *To reach the next level:* Retrieval of the persisted scripts is not provenance-tagged or integrity-protected.
- **D L2:** Entries are keyed by task id in the query, so one task does not read another's script, but Redis is single-tenant and unauthenticated for every pod. — [patcher/src/buttercup/patcher/agents/context_retriever.py:1223](https://github.com/trailofbits/buttercup/blob/298c01fbff3bf0bf166c87856acd7e42f94f7157/patcher/src/buttercup/patcher/agents/context_retriever.py#L1223) (verified)
  - *To reach the next level:* The model can still write to any key reachable through Redis; no per-tenant storage separation or retention.
- **B L1:** A poisoned script persists in Redis and later runs as a tool execution (in the privileged container) for the same task, and the Redis chart default persists the data. — [patcher/src/buttercup/patcher/agents/qe.py:530-540](https://github.com/trailofbits/buttercup/blob/298c01fbff3bf0bf166c87856acd7e42f94f7157/patcher/src/buttercup/patcher/agents/qe.py#L530-L540) (verified)
  - *To reach the next level:* Stored scripts are neither human-reviewed nor easily inspected.
- **Cap:** none
- **Notes:** C6-REPOCONFIG not applied: load_dotenv() and env_file='.env' read the image working directory (WORKDIR /app/patcher), not the task repo.

### C7 Third-party extensions — 0.20 (high)

Buttercup has no plugin, MCP or skill system, so the usual extension surface is absent. The closest equivalent is code that arrives with each task: the OSS-Fuzz infra/helper.py and project Dockerfiles in the task's fuzz-tooling tarball are executed automatically, verified only against a sha256 supplied in the same request. Default images use the moving main tag with pull policy Always, and the WASM Python runtime is downloaded without a checksum. The helper runs in the service pod with its full environment, which for the patcher includes the LLM key.

- **S L1:** Task sources are checked against a hash the task itself provides, service images track the moving main tag, and the WASM runtime is fetched by version without an integrity check. — [orchestrator/src/buttercup/orchestrator/downloader/downloader.py:74](https://github.com/trailofbits/buttercup/blob/298c01fbff3bf0bf166c87856acd7e42f94f7157/orchestrator/src/buttercup/orchestrator/downloader/downloader.py#L74); [deployment/k8s/values.yaml:6-9](https://github.com/trailofbits/buttercup/blob/298c01fbff3bf0bf166c87856acd7e42f94f7157/deployment/k8s/values.yaml#L6-L9); [seed-gen/Dockerfile:17](https://github.com/trailofbits/buttercup/blob/298c01fbff3bf0bf166c87856acd7e42f94f7157/seed-gen/Dockerfile#L17) (verified)
  - *To reach the next level:* Pinned digests or signatures for images and downloaded runtimes, and authenticity of task artifacts beyond a request-supplied hash.
- **C L1:** Only the downloaded tarball hash is verified; the unpacked helper script, project Dockerfiles and base images are not verified further. — [common/src/buttercup/common/challenge_task.py:187-188](https://github.com/trailofbits/buttercup/blob/298c01fbff3bf0bf166c87856acd7e42f94f7157/common/src/buttercup/common/challenge_task.py#L187-L188) (verified)
  - *To reach the next level:* Verification of every type of code loaded at runtime.
- **D L1:** Task-supplied helper code and Dockerfiles run automatically once a task is submitted through the authenticated API; no per-extension consent step exists, and no third-party plugin mechanism exists. — [orchestrator/src/buttercup/orchestrator/downloader/downloader.py:71](https://github.com/trailofbits/buttercup/blob/298c01fbff3bf0bf166c87856acd7e42f94f7157/orchestrator/src/buttercup/orchestrator/downloader/downloader.py#L71); searched `rg -n -w -i 'mcp|plugin|plugins|entry_points'` in `patcher/src seed-gen/src/buttercup common/src/buttercup/common orchestrator/src/buttercup/orchestrator/scheduler orchestrator/src/buttercup/orchestrator/downloader` → 0 hits (verified)
  - *To reach the next level:* Showing what will run and requiring operator consent per extension.
- **B L0:** The task-supplied helper runs as the pod user via a subprocess that inherits the pod environment (no env override for most commands), so in the patcher pod it can read the LLM key. — [common/src/buttercup/common/challenge_task.py:346-347](https://github.com/trailofbits/buttercup/blob/298c01fbff3bf0bf166c87856acd7e42f94f7157/common/src/buttercup/common/challenge_task.py#L346-L347); [common/src/buttercup/common/challenge_task.py:388-393](https://github.com/trailofbits/buttercup/blob/298c01fbff3bf0bf166c87856acd7e42f94f7157/common/src/buttercup/common/challenge_task.py#L388-L393); [deployment/k8s/charts/patcher/templates/deployment.yaml:41](https://github.com/trailofbits/buttercup/blob/298c01fbff3bf0bf166c87856acd7e42f94f7157/deployment/k8s/charts/patcher/templates/deployment.yaml#L41) (verified)
  - *To reach the next level:* Run such code in a sandbox with a scrubbed environment and scoped credentials.
- **Cap:** none
- **Notes:** Operator-submitted tasks are the intended input, so this is scored as extension-like code. C7-RCELOAD was not applied because remote code is not fetched at the model's request and the task is submitted through an authenticated API.

### C8 Secrets & sensitive-data protection — 0.20 (high)

Secrets are held as environment variables and Kubernetes secrets, the LLM key is wrapped in a SecretStr, and the setup script generates a fresh LiteLLM master key and CRS token. But default credential handling is not locked down, the UI pod and every subprocess carry tokens in plain environment, and the patcher and builder charts default to DEBUG logging with no redaction (the shared command runner can log env overrides). Provider keys sit only in the LiteLLM pod, which keeps the broadest keys away from agents.

- **S L1:** Secrets come from environment variables and the LLM key uses SecretStr; default credential handling in the shipped charts is not locked down. — [common/src/buttercup/common/llm.py:154](https://github.com/trailofbits/buttercup/blob/298c01fbff3bf0bf166c87856acd7e42f94f7157/common/src/buttercup/common/llm.py#L154); [scripts/common.sh:631](https://github.com/trailofbits/buttercup/blob/298c01fbff3bf0bf166c87856acd7e42f94f7157/scripts/common.sh#L631) (verified)
  - *To reach the next level:* Redaction before logs and model messages, and a secret manager or encrypted storage.
- **C L1:** Only the LLM key repr is masked; logs, subprocess environments and OTLP export are not filtered, and the task-server logs request bodies. — [common/src/buttercup/common/challenge_task.py:384-386](https://github.com/trailofbits/buttercup/blob/298c01fbff3bf0bf166c87856acd7e42f94f7157/common/src/buttercup/common/challenge_task.py#L384-L386); [orchestrator/src/buttercup/orchestrator/task_server/server.py:199](https://github.com/trailofbits/buttercup/blob/298c01fbff3bf0bf166c87856acd7e42f94f7157/orchestrator/src/buttercup/orchestrator/task_server/server.py#L199) (verified)
  - *To reach the next level:* Logs, telemetry and subprocess environments are unprotected.
- **D L0:** Patcher and build-bot charts default to DEBUG logging, which writes command text and settings to stderr, /tmp and the shared scratch volume with no redaction; OTLP export follows when an endpoint is configured. — [deployment/k8s/charts/patcher/values.yaml:3](https://github.com/trailofbits/buttercup/blob/298c01fbff3bf0bf166c87856acd7e42f94f7157/deployment/k8s/charts/patcher/values.yaml#L3); [patcher/src/buttercup/patcher/__cli__.py:26](https://github.com/trailofbits/buttercup/blob/298c01fbff3bf0bf166c87856acd7e42f94f7157/patcher/src/buttercup/patcher/__cli__.py#L26) (verified)
  - *To reach the next level:* INFO-level default with redaction always on.
- **B L1:** A leaked LLM key is long-lived but capped by a budget; other service credentials are long-lived and their default handling is not locked down. — [deployment/env.template:66-67](https://github.com/trailofbits/buttercup/blob/298c01fbff3bf0bf166c87856acd7e42f94f7157/deployment/env.template#L66-L67) (verified)
  - *To reach the next level:* Keys are not short-lived or per-task.
- **Cap:** none

### C9 Audit & traceability — 0.30 (high)

Services log to stderr, a /tmp file, an optional persistent directory on the shared scratch volume, and OTLP when configured; scheduler submissions emit tracing spans. Tool calls are logged as unstructured info lines that include the command or path, but command output is deliberately not logged, there is no approver (there is no approval), and no tamper-evident storage. Logs are written by the same process that runs the agents.

- **S L1:** Patcher tools log a message per call with the target path or command, but results and arguments are not recorded in a structured record. — [patcher/src/buttercup/patcher/agents/tools.py:88](https://github.com/trailofbits/buttercup/blob/298c01fbff3bf0bf166c87856acd7e42f94f7157/patcher/src/buttercup/patcher/agents/tools.py#L88); [common/src/buttercup/common/challenge_task.py:545](https://github.com/trailofbits/buttercup/blob/298c01fbff3bf0bf166c87856acd7e42f94f7157/common/src/buttercup/common/challenge_task.py#L545) (verified)
  - *To reach the next level:* A structured per-tool-call record with arguments, status and timestamps.
- **C L1:** Main patcher tool calls log an info line; docker execution output, sub-agent calls and persisted test scripts are not recorded, and external submissions carry OTLP spans only when telemetry is configured. — [orchestrator/src/buttercup/orchestrator/scheduler/submissions.py:360-364](https://github.com/trailofbits/buttercup/blob/298c01fbff3bf0bf166c87856acd7e42f94f7157/orchestrator/src/buttercup/orchestrator/scheduler/submissions.py#L360-L364) (verified)
  - *To reach the next level:* All tool calls, including sub-agents and test-script execution, are not recorded.
- **D L2:** Logging is on by default and writes to /tmp and the scratch volume, outside the task workspace, but the agent processes write it and nothing makes it append-only. — [common/src/buttercup/common/logger.py:83](https://github.com/trailofbits/buttercup/blob/298c01fbff3bf0bf166c87856acd7e42f94f7157/common/src/buttercup/common/logger.py#L83); [common/src/buttercup/common/logger.py:89](https://github.com/trailofbits/buttercup/blob/298c01fbff3bf0bf166c87856acd7e42f94f7157/common/src/buttercup/common/logger.py#L89) (verified)
  - *To reach the next level:* Logs are written by the same process that runs the agent and can be altered by it.
- **B L1:** Logging is best-effort: standard Python handlers swallow failures and actions proceed without a record, and OTLP export is batched. — [common/src/buttercup/common/logger.py:76](https://github.com/trailofbits/buttercup/blob/298c01fbff3bf0bf166c87856acd7e42f94f7157/common/src/buttercup/common/logger.py#L76) (verified)
  - *To reach the next level:* No guarantee that a high-risk action is recorded before it runs.
- **Cap:** none

### C10 Limits & kill switch — 0.38 (high)

Patch generation is bounded by a patch-retry cap (15 by default in the chart), per-graph recursion limits, a 30-minute PoV phase, and a LiteLLM per-key spend cap that the key-setup job refuses to skip. Fuzzer runs have timeouts. But docker command execution has no timeout, the WASM seed sandbox has no CPU or fuel limit, and cancellation only marks the task in a registry that the patcher checks when it picks up an item, so a running patch loop and in-flight containers are not interrupted.

- **S L2:** Iteration caps and recursion limits plus a code-enforced token/cost budget at the LLM proxy and per-call LLM timeouts; halt is a cooperative registry flag. — [patcher/src/buttercup/patcher/agents/config.py:15-21](https://github.com/trailofbits/buttercup/blob/298c01fbff3bf0bf166c87856acd7e42f94f7157/patcher/src/buttercup/patcher/agents/config.py#L15-L21); [deployment/k8s/templates/litellm-user-keys-setup-script.yaml:40](https://github.com/trailofbits/buttercup/blob/298c01fbff3bf0bf166c87856acd7e42f94f7157/deployment/k8s/templates/litellm-user-keys-setup-script.yaml#L40); [patcher/src/buttercup/patcher/agents/leader.py:27](https://github.com/trailofbits/buttercup/blob/298c01fbff3bf0bf166c87856acd7e42f94f7157/patcher/src/buttercup/patcher/agents/leader.py#L27) (verified)
  - *To reach the next level:* No overall wall-clock cap per task, no tool-side rate limits, and no halt that interrupts in-flight execution.
- **C L1:** Limits apply to the top-level patch graph and the fuzzer subprocess; docker exec (shell tool, test scripts) has no timeout and the WASM sandbox has no CPU limit. — searched `rg -n 'timeout'` in `common/src/buttercup/common/challenge_task.py` → 3 hits (hits are the reproduce-pov kwarg and the git-apply call; the docker exec runner at line 545 has none); [seed-gen/src/buttercup/seed_gen/sandbox/execute_llm_code.py:39](https://github.com/trailofbits/buttercup/blob/298c01fbff3bf0bf166c87856acd7e42f94f7157/seed-gen/src/buttercup/seed_gen/sandbox/execute_llm_code.py#L39) (verified)
  - *To reach the next level:* Tool and spawned-process timeouts are missing on the main execution paths.
- **D L2:** Sensible defaults (retries 15, recursion 80, 30-minute PoV phase, budget 100) are operator-configurable through environment variables and the model cannot raise them. — [deployment/k8s/templates/common-env.yaml:305-310](https://github.com/trailofbits/buttercup/blob/298c01fbff3bf0bf166c87856acd7e42f94f7157/deployment/k8s/templates/common-env.yaml#L305-L310); [patcher/src/buttercup/patcher/agents/config.py:17](https://github.com/trailofbits/buttercup/blob/298c01fbff3bf0bf166c87856acd7e42f94f7157/patcher/src/buttercup/patcher/agents/config.py#L17) (verified)
  - *To reach the next level:* No hard ceilings that configuration cannot exceed, and the budget has no time window.
- **B L1:** Spend is capped, but compute is not bounded per command, and cancellation does not stop a running patch loop or kill containers; operators must undeploy. — [orchestrator/src/buttercup/orchestrator/scheduler/cancellation.py:74](https://github.com/trailofbits/buttercup/blob/298c01fbff3bf0bf166c87856acd7e42f94f7157/orchestrator/src/buttercup/orchestrator/scheduler/cancellation.py#L74); [patcher/src/buttercup/patcher/patcher.py:147](https://github.com/trailofbits/buttercup/blob/298c01fbff3bf0bf166c87856acd7e42f94f7157/patcher/src/buttercup/patcher/patcher.py#L147) (verified)
  - *To reach the next level:* Stopping does not cancel pending calls or kill orphaned containers.
- **Cap:** none

## Rule-of-Two check
[A] untrusted input: Target source, sanitizer stack traces and task tarballs enter agent prompts (patcher/src/buttercup/patcher/patcher.py:117) · [B] sensitive data/systems: Budgeted LLM key and competition API credentials in pod env; shared task storage on node-local hostPath (deployment/k8s/charts/patcher/templates/deployment.yaml:41) · [C] state change / egress: sh tool runs arbitrary bash in a privileged container with open egress (patcher/src/buttercup/patcher/agents/context_retriever.py:528); patches auto-submitted (orchestrator/src/buttercup/orchestrator/scheduler/submissions.py:370) · Same default session? Yes

## Highest-impact improvements
1. Run exec containers without --privileged: non-root, dropped capabilities, seccomp, no host mounts beyond the task copy, and a default-deny egress policy. — C4 B L0→L3, +0.150 before caps (Playbook 3)
2. Use a hardened container or gVisor/Kata runtime for the patcher exec path, as already done for seed generators with WASI. — C4 S L1→L3, +0.150 before caps (Playbook 3)
3. Replace the raw sh tool with narrow test-runner tools and validate persisted test scripts deterministically before they are stored or run. — C3 S L0→L3, +0.225 before caps (Playbook 3)
4. Add an operator approval step for LLM-authored test scripts and optionally for patch submission. — C2 S L0→L3, +0.225 before caps (Playbook 5)
5. Add timeouts to every docker exec and kill the containers on cancellation or task expiry. — C10 C L1→L3, +0.150 before caps (Playbook 3 step 3)

## Re-audit log
- No changes.

## Limitations
- Static source review of the pinned commit only; nothing was executed, installed, built, or probed.
- Not examined in depth: seed-gen and program-model loop and iteration limits, the competition-api test service chart, Terraform/AKS and Tailscale production manifests, and the OSS-Fuzz infra scripts fetched per task (they are not in this repo).
- The oss-fuzz infra/helper.py and project Dockerfiles run by build and reproduce steps arrive in task tarballs and were not available to review; their docker flags are unknown and the scoring assumes the operator-submitted task is the explicit instruction.
- OpenTelemetry content capture by the openlit library (C8) is inferred from library behaviour, not verified in this repo; telemetry only starts when an OTLP endpoint is configured.
- Scored the local minikube configuration; the AKS/Tailscale production values use the same charts, but their exposure (Tailscale ingress for task-server and UI) was not scored separately.
- No text aimed at steering reviewers was found in the files read (README.md, CLAUDE.md, code comments); this was not an exhaustive scan of every file.
