SafetyCard Request a review

Buttercup

Cyber reasoning system (DARPA AIxCC) that finds and patches vulnerabilities via AI-assisted fuzzing and patch agents

github.com/trailofbits/buttercup · 2026-10-03 · 298c01f

Defense-in-depth score

2.2 / 10

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.

Safest setup

Buttercup gives its agents a budget-capped LLM key, sandboxes model-written seed generators in WASI, and bounds patch retries and recursion. It runs fully unattended, and its shell tool and test scripts run in privileged containers with open network and no switch to turn them off, so the safest setup keeps the budget and loop limits tight and builds the containment around the whole deployment yourself.

SetupHowWhat it gives you
Narrowly scoped credentialsGive each GitHub token and service credential the deployment uses only the permissions and lifetime its job needsWhatever a hijacked agent reaches, the provider limits what those credentials can do C1
A small LLM budgetKeep the budgeted virtual key and set LITELLM_MAX_BUDGET to the smallest amount the run needsLiteLLM itself stops model spend at that amount, for every service sharing the key C10
Tight patch-loop limitsKeep maxPatchRetries, maxContextRetrieverRecursionLimit and maxMinutesRunPOVs at their defaults or lowerEach patch attempt ends after a fixed number of retries, steps and minutes C10
A dedicated, disposable VMRun the cluster on its own VM that holds no other data or credentials, and rebuild it between campaignsAn escape from the privileged exec containers lands on a machine with nothing else to reach C4
Limited outbound networkAllow the VM outbound access only to the model providers, image registries and competition API the run usesCommands the agents run have a narrow path for task data to leave C5
Cluster-internal servicesKeep Redis and the UI on cluster-internal addresses and reach the UI only through a local port-forwardOnly the Buttercup pods and you can reach the shared queue and the dashboard C1
INFO-level loggingSet logLevel: "INFO" for the patcher and build-bot chartsFewer command details and settings land in the logs on the shared scratch volume C8
Telemetry off the clusterSet OTEL_ENDPOINT to a collector outside the clusterSubmission spans are kept where the agent pods can't alter them C9

Approving with confidence

  • Nothing pauses for approval and patches go out automatically, so the budget, limits and containment above are what bound an unattended run.

Handling data

  • Logs on the shared scratch volume and in /tmp carry command text without redaction; store them like the task sources they cover.
  • The telemetry library may capture prompt content once an endpoint is set; treat the collector like the task data.

Key gaps (3)

  1. 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). C4 · Code-execution isolation
  2. 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). C2 · Approval gates
  3. 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). C7 · Third-party extensions

Criteria

C1 Identity & least privilege

0.25 / 1.00

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.

C2 Approval gates

0.05 / 1.00

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.

C3 Tool & action scoping

0.17 / 1.00

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.

C4 Code-execution isolation

0.25 / 1.00

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.

C5 Untrusted input blast radius

0.05 / 1.00

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.

C6 Memory, context & configuration integrity

0.30 / 1.00

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.

C7 Third-party extensions

0.20 / 1.00

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.

C8 Secrets & sensitive-data protection

0.20 / 1.00

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.

C9 Audit & traceability

0.30 / 1.00

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.

C10 Limits & kill switch

0.38 / 1.00

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.