CyberGym: a local harness for scoring AI agents on real vulnerability reproduction
CyberGym is a large-scale, high-quality cybersecurity evaluation framework designed to rigorously assess the capabilities of AI agents on real-world vulnerability analysis tasks.
At a glance
- What is it?
- CyberGym is a Python and Docker framework that turns ARVO and OSS-Fuzz bug reports into PoC-generation tasks and grades submissions by running them against vulnerable and fixed builds. It is heavy infrastructure, not a library you pip install and forget.
- Who is it for?
- Adopt CyberGym if you have a dedicated machine with hundreds of gigabytes (or terabytes) of disk, a Docker host you control, and a need to measure whether an agent can produce a crashing input for a known vulnerability rather than answer questions about one. Do not adopt it if you want a hosted API, a small CI job, or a benchmark that runs without compiling target binaries.
- Can I use it commercially?
- Yes. Apache-2.0 is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 19 days ago.
- What is it written in?
- Mainly Python, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on September 15, 2026, and from our analysis. They are not legal advice.
DEEP OPEN-SOURCE ANALYSIS
The gap CyberGym targets: grading exploit reproduction, not exploit explanation
Most security evaluations for language models ask the model to read code and describe a flaw, or to answer a multiple-choice question about a CVE. CyberGym is built for a harder signal. According to the README, it assesses agents on real-world vulnerability analysis tasks, and the task format makes the unit of success concrete: the agent must produce a proof-of-concept input that triggers the bug in a vulnerable build and does not trigger it in the fixed build. The repository ships tasks derived from ARVO and OSS-Fuzz identifiers, which is visible in the task naming used throughout the README (arvo:47101, oss-fuzz:42535201, and so on). That naming matters because it tells you the benchmark is anchored to bugs that were actually found and fixed in real projects, not to synthetic exercises written for the benchmark. The audience is narrow: agent researchers, evaluation engineers, and security teams who need a reproducible pass or fail signal on a capability that is otherwise easy to fake in prose. If your question is whether a model can write a convincing vulnerability report, CyberGym answers a different question.
How a task is assembled and scored
The pipeline has three pieces. The task generator, invoked as python3 -m cybergym.task.gen_task, takes a task id, a data directory, a server URL, a mask map and a difficulty level, and writes an output directory. The README shows the resulting layout: description.txt, README.md, repo-vul.tar.gz and submit.sh. So the agent receives a description, a repository snapshot at the vulnerable revision, and a submission script that posts the candidate input to the server. The server, started as python3 -m cybergym.server, accepts the PoC, saves it under a log directory, records it in a SQLite database at the path given by --db_path, and returns JSON containing an exit code and the captured output of the target. The third piece is verification: scripts/verify_agent_result.py reads the PoC database and reports, per agent id, the task id, PoC hash, PoC length, and two exit codes, vul_exit_code and fix_exit_code. Those two fields are the whole scoring model. A submission is interesting when the vulnerable build fails and the fixed build does not. The mask_map.json file passed to both the generator and the server is not explained in the README beyond its role as an argument, and that is a real gap for anyone trying to modify task difficulty rather than run the defaults.
Installation and the data volume problem
Installation is two commands for the code and one for the data. The package installs with pip3 install -e '.[dev,server]', which tells you the extras are split and that the server dependencies are optional relative to the core. Python and Docker are both required. Then comes the part that decides whether CyberGym is viable for you. The benchmark data is downloaded with git lfs install followed by a clone of the Hugging Face dataset sunblaze-ucb/cybergym, and the README states the result is roughly 240GB. Server-side data is separate and worse. A binary-only mode, which the README describes as sufficient for static analysis without the dynamic compilation environment, takes about 130GB and is fetched by running scripts/server_data/download_binary_only_runners.py and then extracting a 7z archive from the cybergym-server-binary dataset. The full server data, which includes the Docker images with the compilation environment, is described as roughly 10TB and is pulled by scripts/server_data/download.py with a tasks file. There is a middle path: scripts/server_data/download_subset.py fetches ten named tasks, five the README says an agent can solve and five it says are not easy. That subset is the only sane first step, and the README is explicit that the full path is large.
Network binding is the part people get wrong
The README carries a warning in capital letters: deploy everything locally and do not expose any part of CyberGym to the public internet. The reason is spelled out in the evaluation section. Binding to 0.0.0.0 exposes a partly unauthenticated service on any machine with a public IP. The recommended bind address is the gateway of the Docker network the agent containers run on, because that address is reachable from those containers and from the host but is not routable from outside. The README gives the exact command to discover it: docker network inspect cybergym-internal -f '{{(index .IPAM.Config 0).Gateway}}', or the same against bridge if you are not using the firewall. It then warns that the two gateways are not interchangeable, since cybergym-internal is created with internal=True and containers on it have no route to the default bridge. A server bound to 172.17.0.1 is simply unreachable from firewalled agents. The subnet is assigned at network creation time, so the gateway is host-specific and must be queried rather than hardcoded. The firewall module, python3 -m cybergym.firewall start, prints it as host_gateway, and FirewallProxyManager.start() adds that gateway to NO_PROXY and to the proxy allowlist so agent traffic to the server bypasses Squid. There is also a CYBERGYM_API_KEY environment variable whose default value is a public placeholder, and the README says plainly that setting your own is not a substitute for keeping the server private. Treat the key as a speed bump, not a boundary.
Limits, failure modes, and where the framework is the wrong choice
The first limitation is disk and time. A 10TB pull is not a benchmark you try on a laptop, and even the 240GB dataset clone is a commitment. The binary-only mode exists precisely because the full compilation environment is expensive, but it restricts you to static analysis tasks, which is a different capability than the one most people want to measure. The second limitation is operational. The server saves PoCs to a directory and a SQLite file, and the README's own example output shows poc_length of 662 bytes and a truncated fuzzer log, which means the harness is passing raw bytes to a compiled target and capturing whatever the sanitizer prints. That is fine for scoring, but it gives you no isolation story beyond Docker and no rate limiting. The third is documentation depth: mask_map.json is passed to both gen_task and the server and never described, so anyone wanting to change what is masked from the agent is reading source, not docs. There is also no release history in the supplied material, so there is no changelog to consult before upgrading. Finally, CyberGym is the wrong tool if you want to evaluate an agent that cannot execute code, if you need a hosted endpoint, or if your targets are not already built as ARVO or OSS-Fuzz cases. The benchmark is bounded by its corpus.
The alternative: fuzzing harnesses and static analyzers solve adjacent problems
The obvious comparison is a fuzzer such as libFuzzer or AFL, which is what is actually running underneath these tasks. The difference in approach is who supplies the input. A fuzzer generates inputs itself through mutation and coverage feedback, and its output is a corpus plus crashes; it does not need an agent and it does not care about reasoning. CyberGym inverts that: it fixes the input generation step outside the system, hands the agent a repository snapshot and a description, and asks the agent to produce one input. The fuzzer is the oracle, not the subject. A second comparison is a static analyzer or a linter-driven security scanner, which reads code and reports candidate defects without executing anything. That approach scales to repositories you cannot build and produces findings in seconds, but it cannot tell you whether a reported defect is reachable, which is exactly what the vul_exit_code and fix_exit_code pair measures. If your goal is finding new bugs in your own codebase, CyberGym is not the instrument; it grades an agent against bugs that are already known and already fixed.
Maintenance, licensing, and what to check before you commit
CyberGym is Apache-2.0, which is permissive and includes an explicit patent grant, but the licence covers the framework code, not the benchmark data. The dataset lives on Hugging Face under sunblaze-ucb/cybergym and the server data under separate repositories, and each may carry its own terms; the README does not state them, so check the dataset cards before redistributing anything. The repository is not archived and was last pushed in August 2026 according to the metadata, with no releases retrieved, which means you are tracking main rather than pinned versions. Practically, that means an upgrade is a git pull plus a reinstall of the package extras, and if the task schema or the mask map format changes, previously generated task directories may not be reusable. Budget for re-downloading data if the server-side image layout changes, because the 7z archive and the Docker image pull are both versioned by URL rather than by a package manager. The firewall module and the server are separate entry points, so a partial upgrade that updates one and not the other is a plausible failure mode.
A concrete first run
The smallest end-to-end check the README supports uses the subset data and one task. Start the server with python3 -m cybergym.server --host $HOST --port 8666 --mask_map_path mask_map.json --log_dir ./server_poc --db_path ./server_poc/poc.db, then generate a task with python3 -m cybergym.task.gen_task --task-id arvo:10400 --out-dir ./cybergym_tmp --data-dir ./cybergym_data/data --server http://$HOST:8666 --mask-map mask_map.json --difficulty level1. Submit a dummy input with bash ./cybergym_tmp/submit.sh ./cybergym_tmp/poc and confirm you get JSON back with an exit_code. Then run scripts/verify_agent_result.py against the PoC database with an agent id taken from logs/args.json and check that vul_exit_code and fix_exit_code are both populated. If the dummy input produces the same exit code on both builds, the harness is working correctly and your agent simply has not solved the task. That distinction, between a broken harness and an unsolved task, is the one thing worth confirming before you spend a week generating tasks.
Editorial conclusion
Adopt CyberGym if you have a dedicated machine with hundreds of gigabytes (or terabytes) of disk, a Docker host you control, and a need to measure whether an agent can produce a crashing input for a known vulnerability rather than answer questions about one. Do not adopt it if you want a hosted API, a small CI job, or a benchmark that runs without compiling target binaries. Before committing, verify three things on your own hardware: that the git-lfs clone of the dataset completes at the size you budgeted, that the gateway address returned by docker network inspect for cybergym-internal is reachable from your agent containers, and that a single arvo task round-trips through gen_task, submit.sh and verify_agent_result.py. If that round trip works, the rest of the harness is the same loop repeated.
Community notes