Abstract

Despite recent advances, frontier large language model (LLM) agents remain limited in discovering and patching complex vulnerabilities in real-world software. Generally available agents can already aid attackers, who only need to find one exploitable weakness, while defenders must continuously identify and patch all vulnerabilities across fast-growing codebases. Stronger defensive agents would help close this gap, yet the scarcity of security training data with reproducible build and execution environments remains a bottleneck.

We present CyberForge, a framework that synthesizes executable, repository-level security training data by injecting vulnerabilities into real C/C++ projects. It validates each instance dynamically: the injected build must pass the project’s unit tests, and a generated proof-of-vulnerability (PoV) must trigger on the injected build and not on the clean one. CyberForge is not limited by the availability of disclosed vulnerabilities, therefore it can scale in comparison to data augmentation techniques which rely on historic CVE data. The resulting corpus holds 1,034 validated vulnerabilities across 80 projects and 63 weakness categories, with edit locality similar to real CVE patches under a real-versus-real noise floor.

Fine-tuning on trajectories collected over this corpus improves SEC-bench patch repair by +3.3 to +14.7 points, in all six configurations of three model scales and two teachers, with the 31B student reaching its GPT-5.4-mini teacher, 72.7% against 74.0%. These gains generalize out of distribution to PatchEval, a corpus containing other programming languages, where every configuration also improves and the 31B student passes its teacher.

The problem

Agentic systems have started finding real vulnerabilities, and that favors attackers: an attacker needs one exploitable weakness and can retry indefinitely, while a defender has to secure an entire codebase continuously. The balance shifts back only when automated detection and repair become dependable, in models that defenders can deploy and adapt themselves.

Training data is the binding constraint. Software-engineering agents improved by double digits once datasets of packaged repositories with reproducible build and test environments existed. Security has no comparable pipeline because its validation problem is harder: a functional bug fails a test, but a security weakness must stay latent under the existing test suite and surface only under a crafted input. Existing runnable vulnerability datasets are assembled from disclosed CVEs and bug-bounty reports, are mostly built for evaluation, and are capped by the rate of human discovery and disclosure. Capture-the-flag tasks avoid that dependency but transfer poorly to real software, and function-level injection does not produce a runnable project at all.

Approach

CyberForge targets C and C++ projects enrolled in OSS-Fuzz, which ship per-project Docker images with build scripts, sanitizer configuration, and libFuzzer harnesses. A project enters the pool only if its own test suite builds, runs unattended, and passes at a 100% rate across five runs on the unmodified code; flaky projects are rejected. This left 100 qualified projects, 80 of which contributed at least one validated instance. Two complementary pipelines then inject weaknesses and feed a shared oracle.

  • Pipeline 1, fuzzer-guided injection. OSS-Fuzz metadata (Fuzz Introspector reports, harness definitions, coverage) is parsed into a reachability map of functions reachable from at least one harness. Each reachable function is scored on structural role, call depth, fanout, and coverage, and on triggerability (parser proximity, guard-to-sink distance, nearby blockers), then diversified across harnesses, roles, and vulnerability categories. An LLM agent is given the site, the inferred weakness type, the harness input format, and category-specific edit constraints, and makes one minimal edit that weakens an existing check. It then writes a deterministic PoV; a post-hoc libFuzzer run with a format-aware seed corpus provides a second path to a confirmed trigger.
  • Pipeline 2, agentic in-context injection. This pipeline reaches sites beyond existing harnesses. Candidate sites come from hybrid retrieval against PrimeVul CVE functions (AST n-gram structural similarity fused with dense semantic similarity by reciprocal rank fusion, then reranked), which also supplies an aligned secure/vulnerable example as an in-context template, and from planner-directed specialist agents that explore the codebase by weakness family. Each target is specified with its CWE and its CodeQL-recovered call chain and dataflow. After injection, agent-based taint analysis finds attacker-controlled paths to the site, and a PoV stage uses sanitizer reports, side effects, and clean-versus-injected output differences to build a trigger, with retry loops back to PoV generation and then to injection.
  • Differential validation. Both pipelines converge on two conditions: the injected build must pass every existing unit test, so the weakness is latent, and the PoV must trigger on the injected build and not on the clean build under identical input. A verifier also checks that the sanitizer reports the expected error type at the expected location.
  • Task formation and training. Validated (vulnerability, patch) pairs become SEC-bench-style tasks. Teacher agents (GPT-5.4-mini and Gemma 4 31B) run on Mini-SWE-Agent inside the project’s OSS-Fuzz container with no network access and no access to the reference patch; success is decided by the differential oracle, not the agent’s own report. Only successful trajectories are used to fine-tune Gemma 4 E4B, 12B, and 31B students with LoRA (r=32, alpha=64, three epochs on one H200).
OSS-Fuzz C/C++ project Docker build, tests, harnesses P1: fuzzer-guided injection harness-reachable site, one minimal edit P2: agentic in-context injection CVE-retrieved template, taint analysis, retries Differential crash oracle unit tests still pass PoV triggers on injected build only sanitizer type checked rejected: 16,172 attempts → 1,034 validated Validated (vuln, patch) diff + PoV + sanitizer report Agent SFT teacher runs patch tasks; student learns from successes executable, repository-level tasks with reproducible builds
Two injection pipelines propose candidate weaknesses in real OSS-Fuzz projects; a differential crash oracle keeps a candidate only if the project's own tests still pass and the proof of vulnerability triggers on the injected build alone, and the surviving (vulnerability, patch) pairs become agent tasks for teacher-trajectory collection and student fine-tuning.

Example

One instance from the released corpus, guetzli/vulnerability_FZ_24 (Appendix E of the paper). Guetzli is Google’s JPEG compressor. In ProcessAPP, the original code validates a segment’s declared length in two steps: VERIFY_INPUT checks that it lies in the JPEG-legal range (2 to 65535) and VERIFY_LEN checks that the input buffer actually holds that many bytes. CyberForge deletes the second check and nothing else.

Input: injected edit (inject_vulnerability.diff)

diff --git a/guetzli/jpeg_data_reader.cc b/guetzli/jpeg_data_reader.cc
@@ -398,7 +398,7 @@ bool ProcessAPP(const uint8_t* data, size_t* pos, ...)
   VERIFY_LEN(2);
   size_t marker_len = ReadUint16(data, pos);
   VERIFY_INPUT(marker_len, 2, 65535, MARKER_LEN);
-  VERIFY_LEN(marker_len - 2);
+
   // Save the marker type together with the app data.
   std::string app_str(reinterpret_cast<const char*>(
       &data[*pos - 3]), marker_len + 1);
{
  "id": "vulnerability_FZ_24",
  "project": "guetzli",
  "producer": "fuzz_poc_guided",
  "cwe_id": "CWE-125",
  "cwe_group": "Post buffer operation",
  "secure_base_commit": "214f2bb42abf5a577c079d00add5d6cc470620d3"
}

Proof of vulnerability

A 504-byte file that opens with FF D8 FF E0 FF FF 4A 46 49 46: FF D8 is the JPEG start marker, FF E0 opens an APP0 segment, and FF FF declares a segment length of 65,535 bytes, the largest value VERIFY_INPUT accepts. Only 504 bytes are present, so the deleted check would have rejected the file; without it, the std::string constructor copies from the declared length and reads 65,035 bytes past the end of the input.

Output: validation outcome

Accepted. The injected build passes all 10 of Guetzli’s unit tests ("expected_passing_count": 10), and the PoV triggers on the injected build only, with the sanitizer reporting the expected error type at the expected location:

==432==ERROR: AddressSanitizer: heap-buffer-overflow
READ of size 65531 at 0x6fc93620b1f8 thread T0
    #0 ProcessAPP jpeg_data_reader.cc:403:15
SUMMARY: AddressSanitizer: heap-buffer-overflow
    jpeg_data_reader.cc:403:15 in guetzli::ProcessAPP

The paper notes that this follows the same pattern as Heartbleed (CVE-2014-0160): an attacker-supplied length larger than the accompanying data. Conforming JPEGs never exercise the missing check, so ordinary unit tests do not catch it, which is exactly the latent-under-tests property the oracle enforces.

Results

Model Teacher SEC-bench (%) PatchEval strict (%) PatchEval PoV (%)
GPT-5.4-mini (teacher)   74.0 13.0 15.2
Gemma 4 31B (teacher)   58.0 12.2 14.4
Gemma 4 E4B (base)   6.0 2.6 3.9
CyberForge-E4B Gemma 4 31B 10.7 (+4.7) 5.2 (+2.6) 6.5 (+2.6)
CyberForge-E4B GPT-5.4-mini 9.3 (+3.3) 9.1 (+6.5) 10.4 (+6.5)
Gemma 4 12B (base)   8.7 3.9 3.9
CyberForge-12B Gemma 4 31B 16.0 (+7.3) 6.1 (+2.2) 8.7 (+4.8)
CyberForge-12B GPT-5.4-mini 16.7 (+8.0) 12.8 (+8.9) 14.1 (+10.2)
Gemma 4 31B (base)   58.0 12.2 14.4
CyberForge-31B Gemma 4 31B 64.7 (+6.7) 12.4 (+0.2) 15.7 (+1.3)
CyberForge-31B GPT-5.4-mini 72.7 (+14.7) 14.8 (+2.6) 16.5 (+2.1)

Source: Table 2 of the paper. SEC-bench (150 C/C++ instances, in-domain) and PatchEval (230 CVEs in Python, JavaScript, and Go, out-of-distribution) report the percentage of instances patched; parentheses give the gain over the corresponding base model. Higher is better.

  • Every configuration improves. All six student-teacher pairs gain +3.3 to +14.7 points on SEC-bench. Gemma 4 31B under the GPT-5.4-mini teacher rises from 58.0% to 72.7%, within 1.3 points of its teacher; 12B roughly doubles from 8.7% to 16.7%.
  • Self-distillation works. Students taught by Gemma 4 31B improve at every scale (31B: 64.7%), so the corpus carries signal that does not depend only on a stronger teacher.
  • Out-of-distribution transfer. On PatchEval every configuration improves on both criteria; the 12B student gains +8.9 strict and +10.2 PoV, and the 31B student reaches 14.8% strict against its teacher’s 13.0%.
  • Scaling and complementarity. With the 12B student and Gemma teacher fixed, SEC-bench rises monotonically from 3.6% to 12.1% to 16.0% as the trajectory corpus doubles twice (at 220 trajectories the student scores below its own base). Running both students and keeping the successful run reaches 18.0%, 25.3%, and 82.0% at E4B, 12B, and 31B.

The corpus

Corpus Count
Validated instances 1,034
Pipeline 1 (fuzzer-guided) 643
Pipeline 2 (agentic) 391
OSS-Fuzz projects qualified / contributing at least one instance 100 / 80
Distinct weakness categories (CWE) / CWE groups 63 / 25
C++ projects / instances 73 / 697
C projects / instances 27 / 337

Source: Table 4 of the paper. 16,172 injection attempts produced the 1,034 validated instances; teachers yielded 1,194 accepted trajectories from GPT-5.4-mini and 880 from Gemma 4 31B.

Pipeline 2 workflow ablation Injected (%) Validated (%)
Naive single pass 68.2 0.0
Taint analysis only 75.5 2.8
Retry loops only 77.6 3.5
Full workflow 77.6 7.5

Source: Table 3 of the paper. "Injected" is the share of attempts that compile and pass unit tests; "Validated" is the share that also pass the differential PoV oracle. Higher is better.

  • Validation is the hard part. A naive agent produces plausible, test-passing injections on 68.2% of attempts but none pass validation; a PoV that never triggers is the largest single failure cause in both pipelines (44.6% and 33.1%).
  • Edits are small and local. 1,025 of 1,034 instances change a single file and 944 confine the change to one hunk (median 2 lines changed). The Kolmogorov-Smirnov distance between CyberForge edits and real SEC-bench CVE patches is 0.165, below the 0.190 floor between two real CVE corpora.
  • Behavior transfer. The 12B base completes an edit-verify cycle on 20.7% of instances; after fine-tuning, 82.7%. Format violations for 12B fall from 38 instances to 2 (Gemma teacher) and 0 (GPT teacher).

Resources

Quick start

From the repository README (needs Python 3.13+, uv, Docker, and API keys in src/.env). With the released dataset symlinked as vuljector-projects/ next to the repo, no OSS-Fuzz clone or init step is needed.

uv pip install -e .            # or: uv sync
cp src/.env.example src/.env   # then add your API keys

vuljector setup <project>      # pulls the prebuilt vuljector/<project>:setup image
vuljector run <project> static_analysis <model_id> --num-vulnerabilities 3