Anvil: An Autonomous Vulnerability-Research Platform
Anvil is not an AI that finds bugs. It is a vulnerability-research system where probabilistic agents do the repetitive work and deterministic infrastructure decides what is actually true.
Anvil is the system I built to run vulnerability research across complex codebases, starting with open-source media libraries used by browsers, phones, and messaging apps. It uses AI agents as the hands inside the research loop. An agent maps a codebase into modules, writes a fuzz harness for an API that has not been fuzzed, runs it, reads the crash, and writes down what it found and what it ruled out. The next agent picks up where that one stopped.
The agents are not the interesting part. The interesting part is everything around them that refuses to take their word for it. An agent can be confidently wrong - about whether a crash is real, whether anyone can reach the buggy code, whether it already reported this last week. So a claim only counts after it survives a fixed set of checks the agent does not control: the code builds, the reproducer triggers the crash under ASan, the affected path is mapped to a module, the reachability is written down, the finding file passes schema validation, and the whole thing lands in git. The agents do the repetitive research work. The checks decide what is true.
Anvil exists because mature projects are not researched evenly. A few hot paths get fuzzed for years; the rest carries the reputation without the testing, because reaching it takes awkward harnesses, stateful APIs, rare build flags, or just boring setup. That neglected-but-reachable surface is where a worker that does the dull parts without getting bored has an edge, and it is what Anvil is pointed at.
What AI actually changed
The least interesting version of this is "AI runs a fuzzer." Fuzzers have run on their own for years. What changed is that AI made the work around the fuzzer cheap enough to do every session, instead of skipping it.
The glue work was the bottleneck
Finding the bug is a small slice of a session. The rest is glue: reading unfamiliar code, finding the entry points an attacker can reach, writing a harness and fixing the build under a sanitizer, checking old notes so you do not re-run a dead end, deciding whether a crash is new, minimizing the input, tracing the call path up to a public function, and writing it down for whoever works on this next. People skip these because they are slow and dull, and they are exactly where the durable value sits: the coverage map, the negative results, the "already looked here." An agent does them without getting bored. The map gets built and kept current. Harnesses get written for the unglamorous APIs. A crash gets triaged and either confirmed or dropped the same session it appears.
And it is cheap. The entire run behind everything in this post came out of about $100 in Claude API credits. That is the number that changes the calculus: at that price, a research session stops being a rare, budgeted event and becomes something you can leave running.
The agent does not choose where to look
It is tempting to say the agent is "creative about where to look." It is not, and it should not be. Where to look comes from a ranking computed off past results; how to look comes from a catalog of techniques. The agent's judgement goes into the parts that need it - forming a hypothesis about a bug class, writing the harness that tests it, reading a crash and arguing for a cause. Even open-ended exploration is one entry in the catalog, handed out when the ranked options on a module are spent. The model is good with unfamiliar code. The system keeps it from wandering.
The research lives in Markdown
Anvil has no database. The whole research state is plain Markdown files with checked front-matter - one per target, finding, technique, and session - and that is on purpose. The research should outlive any tool that renders it, and it should be diffable in git so the history of a claim is legible. The files the agents edit are the record; the dashboard only renders them. There is no separate, prettier "source of truth" that a report gets summarized into, which means an agent cannot quietly believe something the files do not show. If a finding is not written down, with a reproducer and a reachability note attached, it does not exist.
data/targets/ one per research direction (the libraries under study)
data/findings/ one per finding (a candidate, with reproducer + reachability)
data/techniques/ the technique catalog (the methods, with hit-rate)
data/sessions/ one per working session (what was tried, what is left)
data/methodology/ the verification protocol and the impact rubric
A target carries the module map. Every source file belongs to a module, including ones nobody has opened yet, marked not-started. A script walks the real source tree and fails the build until the map accounts for every file, so it is an inventory rather than a guess - and the untouched modules are the point, because they show the next session where the unexamined surface is.
A finding keeps two things separate that people usually blur. Its status is where it sits in the lifecycle. Its evidence is how strongly it has actually been shown - a separate ladder, below. It also carries the reproducer and the reachability: the topmost public function that triggers it, and whether that function is on the public surface at all.
A session is the handoff. It records what was tried, what reproduced, what was dropped, and a note to the next agent. A session that finds nothing still gets written, because "ran these techniques on this module and found nothing" is a result that saves the next run from repeating it.
The agent proposes, the checks decide
This split is the thing the platform is built around, so here is which side each piece sits on.
The agent does the work that needs judgement and can be wrong sometimes:
read unfamiliar code and propose where a bug class might be hiding;
write a harness, fix the build, craft seeds that reach deep parser states;
read a crash and propose a cause and a path to a real caller.
The checks do the work that has to be exactly right, and the agent gets no vote:
the schema that validates every research file when the project builds, and rejects a bad field or a reference to a finding that does not exist;
the compiler and the sanitizer. If ASan reproduces a heap-buffer-overflow on the claimed input, the memory error is no longer just the agent's opinion - though whether it is reachable or security-relevant is a separate question;
the reproducer command, which reproduces or does not;
the source-tree audit that checks the claimed module map against the files on disk;
platform verification - running the input through a real operating-system image decoder and checking for the exact crash signal;
git, which records who claimed what and when.
When the agent is sure and the sanitizer is quiet, the sanitizer wins and the claim gets dropped. The model's confidence is not evidence. That is the only arrangement in which a worker that can hallucinate is safe to point at security claims.
Agents write research markdown through a schema check into the data store; a dashboard renders it read-only, and git records every scoped, per-target commit. The agents are the probabilistic worker; everything past the schema gate is a deterministic check.
What the agent sees at session start
None of this works if every session starts cold. When I name a target in plain language, the agent loads a fixed context before it touches any code:
the target's current state - status, priority, attack surface;
the previous sessions, including the handoff notes;
the unresolved findings, so it extends or dedupes instead of re-reporting;
the technique coverage - which techniques ran on which modules;
the module map of the codebase;
the methodology rules - the verification protocol and the impact rubric;
the current ranking of where to look next;
the files it is allowed to touch.
From that context the agent decides on its own whether to bootstrap a new target - map the tree, find the public APIs, stand up the first harnesses - or continue an existing one. I do not tell it which; it reads that off the files. The plain-language target name is the only thing I have to supply.
One agent per target
Anvil runs several agents at once in the same checkout, and the part I am happiest with is how they avoid stepping on each other. The rule is one agent per target. Each agent only writes files under its own target - that target's profile, its findings, its sessions - and commits go through a small lock, scoped to just those paths. Two agents working different targets in the same directory produce clean, non-overlapping commits and never collide in git. No branches to juggle, no separate worktrees, no merge conflicts: the file layout and the commit scope make normal target work non-overlapping by construction, and any collision structurally rare and mechanically visible. For an unattended overnight run the loop drops the commit step and keeps going, and I review and commit what it produced in the morning.
The loop itself is short: learn the target's state, attack the next module through the technique catalog, verify or drop each candidate the same session, and write the result back. A module is not "done" after one cheap pass - it is done when every technique has been run on it or marked not-applicable with a reason.
How Anvil decides where to look
The agent pulls its next job from a ranking, not a hunch, and the ranking is recomputed from the whole history of findings on every build. The unit is a cross-target pattern - integer math on image dimensions, container parsing, tiling, buffer lifecycle, and so on. Bugs cluster by pattern across libraries more than they cluster by library, so a pattern that paid off in one place is a lead in another. Each pattern earns a weight from its track record - how many findings, how severe, how many became CVEs - and each module is scored by that weight against how much of it is still untested and how big it is:
A big, never-examined module whose code matches a productive pattern rises to the top. A small module already audited to exhaustion sinks. A second ranking - which technique has actually found which pattern - picks the technique to try first, so a manual bounds-audit gets chosen over raw fuzzing exactly where audits have done better. None of this is the agent improvising; it is arithmetic over the record, and the dashboard shows the factors so a ranking reads back as a sentence rather than a black box.
What makes a result accepted
A crash is not a finding. Before anything is accepted into the record as real, it clears a fixed list:
it has a reproducer - the exact build and run commands, checked in;
it runs under the sanitizer or build it claims (ASan for a heap overflow, UBSan for an integer overflow, a release build to rule out sanitizer-only behavior);
the affected code path is mapped to a module;
the reachability is stated - the public API or real application that drives it, or an honest note that only a harness reaches it so far;
the finding file passes schema validation;
the session records what was tried and what is still open.
Miss any of these and it stays a candidate, not a result. The agent can propose anything; acceptance is mechanical.
The evidence ladder
The reachability requirement gets its own structure, because it is where most crashes quietly fail. A crash in a harness proves the code can misbehave when called directly. It says nothing about whether an attacker can get there through software people actually run. So every finding sits on a ladder, and the rungs are not interchangeable:
The line that matters sits between a harness crash and public reachability. Below it, a sanitizer crash in a harness is a test artifact. Above it, the bug has been reproduced through the library's public API, a real application, or - the strongest rung short of a CVE - a real operating-system decoder, with a pinned crash signal so the run is reproducible rather than anecdotal. A candidate that no real consumer can reach gets set aside rather than counted. That is the difference between a crash count and a finding count, and it is the rule that keeps the platform from overclaiming.
The pipeline files a candidate into the bottom of the ladder. Only the four rungs above the real-world bar count as verified; everything below, including a sanitizer crash in a harness, is a test artifact until it climbs.
A session, concretely
Here is how it comes together, with the specifics kept generic - the target is a mature image library everyone considers finished, and what follows is at the level of the architecture, not the bug.
Earlier sessions had fuzzed the library's most-exercised parsing paths to saturation: many iterations, a lot of executions, no crashes. That "no crashes" is recorded as a result, not thrown away - those modules are marked audited under coverage-guided fuzzing, which is a clean signal rather than an absence of effort. So the ranking does not send the next agent back there. It points at the gap: a less-tested API path the standard fuzz targets never exercise, reachable only through a harness that drives it directly. Surfacing exactly that kind of untested surface is what the map is for.
The attack-surface map keeps the whole tree visible: audited-clean modules in sage, never-examined surface in gray, findings ticked in red. The point is the gray - where the testing has not gone yet.
The agent writes the missing harness, runs it, and ASan reproduces a heap overflow. The finding earns harness-verified and nothing more. To climb higher it has to be reachable through software people run, so the agent traces it: this is not the dominant consumer path, and a survey of real tools shows the common ones do not drive it the way the harness does. Everyday use - the overwhelming majority of how the library is exercised - never touches this code. So the finding is bounded honestly: scoped to that API path, reproduced in a harness, with the realistic consumer surface written down rather than assumed. It stays below the real-world bar, and it waits there until disclosure is settled.
Every part of the system shows up in that one session. The map said the heavily-fuzzed paths were a dead end and a neglected one was untouched. The technique matrix said that path had no harness yet. The agent did the work that needs judgement - building the harness, reading the crash. And the ladder did the thing a human under deadline often skips: it kept a real, sanitizer-confirmed crash from being written up as more reachable than it is.
What is proven, and what is in the pipeline
The platform is built to generate candidates quickly, then spend most of its discipline proving, scoping, or dropping them. Discovery can move fast; responsible disclosure cannot. The public CVE count sits behind the research pipeline on purpose - every candidate still needs human confirmation, vendor coordination, safe sharing of reproducers, advisory writing, and sometimes CVE handling. In under two months, Anvil has produced more research traffic than I can comfortably process by hand, which is exactly the bottleneck I wanted it to expose: discovery is becoming continuous, while trust still has to be earned one claim at a time.
Snapshot
Active
under 2 months
Open-source targets researched
10+
Security findings / candidates tracked
80+
Disclosures sent to vendors
15+
CVE requests / CVE-track items pending
15+
Published CVE
1
To be clear about what is and is not automatic: discovery, codebase mapping, harness writing, fuzzing, crash triage, documentation, and disclosure prep are heavily automated and agent-driven. The last steps are not. Confirming a vulnerability for real, coordinating with a vendor, writing the advisory, deciding what to publish, and handling the CVE process stay with me. The platform queues the validated candidates and I work the queue on my own time - confirming impact, writing the advisory, running the vendor back-and-forth. That is the point of the design. It decouples the speed of discovery from the pace of disclosure: the machine can surface something on a Tuesday that I responsibly disclose weeks later. That human review is the current bottleneck, and it is the right place for the bottleneck to be: the slow part should be the one where a candidate becomes a public claim about someone else's software. The gap between candidates and CVEs is not the system failing - it is fast discovery meeting deliberate disclosure, which is how it should work.
The portfolio on the ladder: one disclosed CVE at the top, and a cluster of candidates still sitting below the real-world bar, waiting to be shown reachable before they count.
libheif - CVE-2026-48029
The one fully public result, and the clearest evidence the pipeline works end to end. An afternoon of fuzzing libheif surfaced a memory-safety bug in its grid-decode path: a tile index guarded only by a debug-build assert that gets compiled out in the release builds distributions actually ship, which turns a caught condition into a heap out-of-bounds read reachable from the public decode API. Reported 2 May 2026, fixed in 1.22.0 on 19 May 2026, tracked upstream as GHSA-6x5f-qchq-cxqv. The full writeup, with the disclosure timeline, is on this site at the CVE-2026-48029 page.
The rest of the portfolio
The other libraries - a JPEG 2000 codec, a PNG library, a rendering engine, and several more image and codec projects - have candidates at various rungs of the ladder, a few in active disclosure. I am keeping the specifics out until each one is settled, and deep technical case studies will follow as separate posts once they are safe to publish. The pattern is the same every time: a candidate that cannot be shown reachable through real software is set aside, not announced.
Where agents go wrong, and what stops them
Running an LLM as the worker brings specific, repeatable failure modes. Most of the checks exist to catch them, and naming them plainly is more useful than pretending they do not happen.
Overclaiming reachability. The most common one. An agent confirms a crash in a harness and then narrates a plausible path to a public API without actually walking it. The defense is structural: a finding has to name the topmost entry point and its call path, and that claim is checked, not taken. This is where most dropped candidates die - a real local defect that no shipping caller can actually reach.
Impossible harness states. An agent drives an API in an order no real caller would and reports the resulting crash as a library bug. It is a harness bug. Requiring a real consumer, not just the harness, filters these out.
Sanitizer artifacts. Behavior that only misbehaves under a sanitizer's instrumentation and is benign in a release build. Re-running across build flavors separates a real release-affecting bug from sanitizer-only undefined behavior before anything is called a finding.
Stale maps. An agent's mental model of the source tree drifts from what is on disk and coverage claims rot. The codemap audit walks the real tree and fails on any unmapped file, and a module marked done while techniques remain untried is flagged rather than trusted.
Fuzzy "public API" assumptions. "Public-ish" is not a category. Whether an entry point is on the public surface is recorded as a yes or no and checked against the headers, not asserted in prose.
Model confidence. The most persuasive failure mode. An agent's certainty about a root cause is treated as a hypothesis with no evidentiary weight. Only the result - did it build, did it crash, did a real consumer reproduce it - moves a finding up the ladder.
None of these are fixed by asking the model to be more careful. They are fixed by putting the claim through a check the model cannot talk its way past.
The models hallucinate. That is fine. The checks are a brick wall for false positives, and a hallucination does not survive contact with a compiler, a sanitizer, and a schema. By the time a finding clears all three, the invented parts are gone and what is left reproduces. The failure that does get through is the opposite one - the model missing a real bug. I can live with that. A false negative is cheap at a hundred dollars a run: it is not a dead end, just a reason to run the next pass.
The two jobs
Put it together and Anvil is not an AI that finds bugs. It is a research process that accumulates and that you can audit. AI made the glue work cheap - the mapping, the harnesses, the triage, the reachability tracing, the recording of dead ends - so it is finally affordable to be thorough where thoroughness used to get skipped. The checks - schema, build, sanitizer, reproducer, reachability, the ladder, git - are what make it safe to let a worker that can be wrong touch security claims at all.
The agent does the work. The evidence decides what counts. Keeping those two jobs apart is what lets discovery run fast while disclosure stays careful, and it is why the work compounds instead of just piling up.