Understanding AI-Native Security (Part 5)
Fuzzing, and Where RAPTOR Enters the Story
Understanding AI-Native Security (Part 5)
Fuzzing, and Where RAPTOR Enters the Story
Part 5 of 8. Why fuzzing exists, why static analysis can never replace it, and — for the first time in this series — a look at a specific open-source framework that wires together everything we’ve covered so far.
This week I want to tell you about the moment I realized that all the smart code analysis in the world has a hard ceiling — and that ceiling isn’t a failure of intelligence. It’s a fundamental limit of reasoning about things without actually doing them.
I was staring at a CodeQL output for a compiled binary. The dataflow path was beautiful: tainted bytes flowing from a socket read, through three helper functions, all the way to a memcpy with a length field I controlled. The SMT solver had checked the path constraints and come back SAT. Every light was green. And yet I could not tell you with confidence whether, given this particular allocator state, this particular glibc version, this particular register layout, a carefully crafted 47-byte packet would actually crash the program. There was only one way to find out: actually run the program.
That’s the moment fuzzing stops being a curiosity and starts being essential. And it’s the perfect place to bring in the first concrete framework we’re discussing in this series.
Series navigation
- Understanding AI-Native Security (Part 1): What this all actually means — and a vocabulary primer
- Understanding AI-Native Security (Part 2): Pattern Matching at Scale — Why a regex isn’t enough
- Understanding AI-Native Security (Part 3): Dataflow Analysis — When pattern matching isn’t enough
- Understanding AI-Native Security (Part 4): SMT Solvers and the Math of Killing False Positives
- 📌 Understanding AI-Native Security (Part 5): Fuzzing, and Where RAPTOR Enters the Story (this blog post!)
- Understanding AI-Native Security (Part 6): Binary Exploit Feasibility — From crash to constraints (coming soon!)
- Understanding AI-Native Security (Part 7): The LLM Validation Pipeline (coming soon!)
- Understanding AI-Native Security (Part 8): Putting It All Together — Honestly (coming soon!)
In this post
- What RAPTOR actually is — the full framework, not just a fuzzing wrapper
- The two-layer design that separates deterministic Python orchestration from Claude Code reasoning
- How the full pipeline flows, and where fuzzing’s output lands inside it
- The fundamental wall that every static technique hits — and why it’s a wall, not a speed bump
- How coverage-guided mutation makes fuzzing tractable at scale: AFL++ in three parts
- What RAPTOR adds on top of raw AFL++: target detection, harness generation, corpus management, parallel orchestration, sanitizer integration, coverage analysis
- How crashes get triaged: GDB automation, crash classification, root-cause analysis
- What AFL++ is genuinely good at, and what it genuinely isn’t
- Notes for AI/ML engineers: fuzzing vs. LLM analysis, RAPTOR’s design patterns, and the competitive landscape
The wall every static technique hits
Pattern matchers can flag suspicious shapes. Dataflow analyzers can trace tainted data across function boundaries. SMT solvers can prove path conditions satisfiable or impossible. All three reason about code without running it.
That gives them one fundamental limitation neither cleverness nor compute can solve: they can argue a bug might exist, but they cannot hand you the exact input that triggers it.
Sometimes “might” is good enough. A confirmed dataflow from request.args to cursor.execute with no sanitizer between is overwhelmingly likely to be exploitable; a human reviewer doesn't need a working payload to file the bug. But sometimes "might" is not nearly enough — especially in compiled binaries where the question isn't "does taint reach the sink" but "given this particular allocator state, this particular libc version, this particular set of registers, does a 47-byte input crash the program, and if so, where?"
The only way to answer that is to actually run the program with the actual bytes. Fuzzing exists because no amount of source-level reasoning can substitute for empirical observation of a running binary.
Think of it like suspecting a car has a hydraulic leak: you can reason through hose diameters, pressure, and material fatigue and conclude a leak probably exists. Useful. But it’s not the same as filling the reservoir and watching where the drip appears.
The breakthrough that made fuzzing tractable at scale was coverage-guided mutation: instead of generating inputs blindly, feed the binary instrumented bytes, watch which code paths each input exercises, and bias future mutation toward inputs that hit new paths. American Fuzzy Lop (AFL), originally written by Michał Zalewski, established the technique. AFL++, maintained by a research collective and described in Fioraldi et al., WOOT 2020, is its modern descendant and the de-facto standard.
Don’t worry if “coverage-guided mutation” sounds like a mouthful — we’re about to make it very concrete.
How coverage-guided fuzzing actually works
The feedback loop has three parts:
Part 1: Instrumentation. Before fuzzing, the target binary is recompiled with the fuzzer’s instrumentation pass (usually afl-clang-fast or afl-clang-lto). The compiler inserts code at every branch that, at runtime, updates a shared memory bitmap recording which edges of the control-flow graph were traversed. Every conditional jump becomes an edge tally — left branch taken or right branch taken.
Part 2: Mutation. The fuzzer maintains a corpus of inputs known to exercise interesting code. It picks one, applies one of dozens of mutation strategies (bit flips, byte arithmetic, splicing two corpus inputs together, dictionary-token insertion), and runs the program against the mutated input.
Part 3: Triage. After the run, the fuzzer compares the new edge bitmap against the cumulative map. If the input hit any edges not previously seen, it’s added to the corpus — coverage gain is the signal that this mutation lineage is worth pursuing. If it caused a crash, it’s saved to a crashes/ directory. Otherwise it's discarded.
Repeat for millions of iterations. The corpus grows; over time it accumulates inputs that probe deep into the program’s state space. Bugs that require specific 12-byte magic numbers, or particular sequences of malformed-then-valid records, get discovered through the mutation lineage even when no human would think to test for them.
Now, here’s where it gets interesting. The genius of the approach is that it requires no semantic understanding of the input format. AFL doesn’t know what a PNG file is; it just knows that certain mutations of seed PNGs reach more edges, so it keeps mutating those. Empirically this works extraordinarily well: AFL and AFL++ have found thousands of CVEs across virtually every parser, decoder, and binary format you can name.
Don’t worry if this feels abstract — we are about to make it very concrete with RAPTOR’s actual fuzzing orchestration.

Figure 1 — The three-part loop. The corpus grows monotonically because every input that hits a new edge is promoted, and its children inherit a starting point closer to that edge. Crashes are the by-product the operator wants; the loop’s actual job is to push coverage as deep into the program as possible.
Where RAPTOR enters
Up to this point, the series has been tool-agnostic. The techniques we’ve covered — pattern matching, dataflow analysis, SMT solving, coverage-guided fuzzing — are each implemented by multiple open-source projects, each useful on its own, none of them integrated with each other.
The integration is where the work actually lives. Running each of these tools, normalising their outputs, deduplicating findings, and validating the survivors — that’s where security engineers spend their time, and where AI assistance has the most leverage.
What RAPTOR actually is
RAPTOR is an open-source autonomous security research framework — not a scanner wrapper, not a chatbot that summarises alerts. It’s a pipeline where the LLM is a reasoning layer — deciding which findings to investigate, what the attack path is, whether a crash is exploitable — built on top of deterministic infrastructure that handles what a computer does better.
The architecture is explicit about this separation. Python handles orchestration: running subprocesses, normalising SARIF output across scanners, deduplicating overlapping findings, metering costs, managing the run lifecycle, writing to disk. Claude — the Claude Code decision layer — handles the rest: which findings matter, what the attack path looks like, whether a SIGSEGV is a null deref in dead code or a heap overflow with a viable exploit chain. The design rule is hard: never put decisions in Python, never put execution in Claude.
RAPTOR integrates all four pillars we’ve built up across this series — Semgrep for pattern matching, CodeQL for dataflow, Z3 for SMT pre-screening, AFL++ for coverage-guided fuzzing — and adds an eight-stage LLM validation pipeline on top. Posts 6 and 7 go deep on the exploit feasibility layer and LLM pipeline respectively.
The pipeline, compressed
Here’s what the workflow looks like from the outside. You run /agentic — RAPTOR's one-command entry point — and the following sequence happens:
Semgrep and CodeQL scan in parallel. Their SARIF outputs merge and deduplicate into a unified finding set.
The LLM validation pipeline runs:
Stage A gives each finding a quick one-shot assessment.
Stage B does systematic attack path analysis, generating hypotheses and scoring proximity to real impact.
Stage C — and this is the architectural insight Post 7 will spend a lot of time on — spawns a fresh verification session with no access to Stage B’s reasoning, given only the claims and the source code, and asked to check them independently.
Stage D applies disqualifiers, assigns CVSS 3.1 vectors, and issues verdicts.
Stage E handles binary findings: exploit feasibility analysis that mechanically checks mitigations, available ROP gadgets, and constraint satisfaction via Z3.
Stage F does a final cross-stage consistency check.
Stage 1 writes the report.
Fuzzing output enters at Stage E. The crash AFL++ finds — a saved 47-byte input that triggers a SIGSEGV — is not the final answer.
It’s the input to a feasibility analysis that asks: given this crash, the binary’s protections, the available ROP gadgets, and the memory layout, is there a workable path to code execution? That question is Post 6’s subject.
Why the series waits until Post 5 to name the framework
The reason Posts 1 through 4 don’t mention RAPTOR by name is that the techniques are general. RAPTOR is one expression of how to wire them together — one with specific opinions about what the LLM should handle versus what the Python layer should handle, about fresh-context verification, about cost tracking, about what a verdict should look like. If you’d rather use Semgrep, CodeQL, Z3, and AFL++ yourself, in your own orchestration, with your own validation logic — everything we’ve covered so far still applies, and you might make different trade-offs that suit your context better.
RAPTOR isn’t the destination. The techniques are. The framework exists because doing all of this by hand, for every target, at any scale, is the part that doesn’t actually get done.
With that map in mind, let’s look at what RAPTOR does specifically on the fuzzing pillar.
What RAPTOR’s fuzzing orchestration adds
Running raw AFL++ requires a lot of setup work: building an instrumented target, preparing a seed corpus, deciding on harness shape, configuring parallel fuzzing topology, and triaging crashes. RAPTOR automates each of these. Let’s go through them.
Target detection
Before fuzzing, the framework examines the target binary to determine:
- Binary type — ELF format, architecture, whether it’s stripped
- Input mode — stdin,
argv[1]file path, or environment variable - Sanitizer presence — ASAN, MSAN, UBSAN (catches bugs that wouldn’t otherwise crash)
- Capability constraints — network port, setuid, subprocess requirements
These drive every subsequent harness decision.
Harness generation
Some binaries have built-in fuzz harnesses (e.g., libraries that ship with a LLVMFuzzerTestOneInput entry point). Many don't. For binaries that don't, RAPTOR generates a wrapper that adapts the binary's normal input interface to AFL's expectations — reading the mutated input from a file or stdin and invoking the target binary's main code path.
Corpus management
A fuzzer is only as good as its starting corpus — random bytes generate garbage the parser immediately rejects; valid seeds let it probe interesting code from day one.
RAPTOR seeds with format-aware inputs:
- For image parsers: small valid PNG/JPEG/GIF samples
- For archives: minimal valid tar/zip/gz files
- For network protocols: handshake samples
- For text parsers: minimal valid examples in the target format
Then it runs afl-cmin to minimize the corpus — removing inputs that don't add coverage relative to others already present. A minimized corpus runs faster (less duplicate work) and converges faster (the fuzzer spends time on genuinely different starting points).
Parallel orchestration
AFL++ runs single-threaded by design — one input per fuzzer process. To use multiple CPU cores, you run multiple fuzzer instances in master/secondary topology: one designated master uses deterministic mutations (systematic bit-flips) while secondaries use randomized mutations. They share their corpus through a common output directory, so any input one fuzzer finds interesting becomes available to all the others.
RAPTOR sets up this topology automatically based on the host’s core count.
Sanitizer integration
If the target was compiled with AddressSanitizer, MemorySanitizer, or UndefinedBehaviorSanitizer, runtime memory errors that wouldn’t otherwise crash get caught. A buffer overread returning garbage instead of crashing? ASAN catches it.
The tradeoff: sanitizer-instrumented binaries run slower (typically 2–3× for ASAN) and use more memory. That’s a worthwhile tradeoff for finding bugs that would otherwise hide for years.
Coverage analysis
Optionally, the framework generates coverage visualizations using afl-showmap. This tells you which edges the corpus exercises and which it doesn't — useful for deciding when to stop fuzzing (diminishing returns) or where to add new seeds to push into uncovered territory.
Crash triage: what AFL++ finds vs. what’s exploitable
When AFL++ finds a crash, you get a saved input that triggers it. That’s the start of the work, not the end. RAPTOR’s binary analysis layer kicks in next:
1. GDB automation loads the crashing input into a debugger and extracts:
- Stack trace at crash time
- Register state (especially the instruction pointer and faulting address)
- Memory state around the faulting address
- Signal classification (SIGSEGV vs SIGABRT vs SIGBUS, with sub-causes)
2. Crash classification assigns a type based on the captured state. Here are the types you’ll encounter:
- SEGFAULT — null deref — faulting address is
0x0(or near it) - SEGFAULT — wild pointer — faulting address is a random-looking value (likely an uninitialized read or freed pointer)
- SEGFAULT — out of bounds — faulting address is a small offset from a known buffer
- Stack smash — return address corruption, detected by stack canary
- Heap corruption —
glibcaborts (malloc_consolidate(): invalid chunk size,double free or corruption) - ASAN reports — categorized further by the sanitizer’s classification
3. Root-cause analysis produces a human-readable explanation: what the crash means, how reachable it is from input, and a preliminary exploitability assessment.
This is where the framework hands off to the Stage E binary exploit feasibility pipeline — the topic of the next post. Whether the crash is actually exploitable (not just crashing) is a different set of questions entirely.
What AFL++ is best and worst at
Rhetorical question time: why doesn’t every team just fuzz everything and stop worrying about static analysis? Because fuzzing is genuinely terrible at some classes of target, and knowing the limits is as important as knowing the strengths.
Best:
- File-format parsers (image, audio, video, document, archive)
- Network protocol decoders that take a buffer and return structured data
- Compression libraries
- Anything with a clean “input bytes → process → output” shape
Worst:
- Programs that require stateful network interaction (multi-round handshakes, session protocols)
- Programs whose behavior depends heavily on environment / filesystem state
- Code paths that require complex valid input that’s hard to mutate into (deeply structured grammars where random mutations almost always produce invalid input)
For the worst-case scenarios, structured fuzzing (libProtobuf-Mutator, custom grammars) does better than vanilla AFL++. We’ll come back to this in Post 8’s limitations section — it’s one of several places where the current framework has honest gaps.
For the AI/ML engineers reading this
Fuzzing is in some ways the opposite discipline from LLM-based analysis, and the contrast is instructive. Understanding this contrast actually tells you something important about where to use each tool.
- Fuzzing is empirical truth. When a fuzzer reports a crash, it’s reproducible byte-for-byte. There’s no probability, no judgment call, no possibility that the model hallucinated. The output is “run binary B with file F and observe signal S.” This is the ground truth that LLM analyses can be checked against.
- Fuzzing has no priors. The fuzzer doesn’t know what an “interesting” input looks like; it only knows what produces new coverage. LLMs are the opposite — they have strong priors about what an interesting input should look like, often more useful than coverage feedback in narrow domains but actively misleading in others.
- The pipeline benefits from both. A fuzzer finds a crash; the LLM explains why it crashes and how exploitable it is. The two combine into actionable security research in a way neither does alone. Neither is doing the other’s job.
- There’s an emerging literature on LLM-guided fuzzing. OSS-Fuzz-Gen from Google — LLM-guided harness generation at scale — is the most prominent open-source example. RAPTOR doesn’t integrate this yet, but the design space is worth watching.
- The two-layer architecture generalises beyond security. The principle — deterministic layer handles orchestration and retries; non-deterministic layer handles only what determinism genuinely cannot — applies to any LLM system that needs to be reliable at scale, not just security tooling.
- The fresh-context verifier pattern is the architectural fix for LLM sycophancy. Asking the same model to verify its own reasoning is not verification — it’s confirmation bias. Stage C above is the mechanism; Post 7 is the full treatment.
- Cost tracking is a first-class requirement, not an afterthought. Every LLM call is metered against a configurable budget ceiling; the framework stops gracefully rather than silently truncating mid-analysis. Designing this in from the beginning — not bolting it on after the first surprise invoice — is the right order of operations.
The competitive landscape
Before closing out this post, a word on what else is in this space.
On the fuzzing side:
**Mayhem** by ForAllSecure is probably the closest commercial equivalent for binary fuzzing. It autonomously finds crashes, reproduces them, and classifies severity — similar goals to RAPTOR’s fuzzing and Stage E layers. It’s cloud-hosted, closed-source, and commercially priced. If you need a managed solution with SLA support and don’t need a full source-analysis pipeline, Mayhem is worth a serious look.
**OSS-Fuzz (Google) provides continuous, large-scale fuzzing infrastructure for qualifying open-source projects — free, but not a local tool, and requires your project to be accepted into their programme. [OSS-Fuzz-Gen](https://github.com/google/oss-fuzz-gen)** extends this with LLM-guided harness generation, which is the closest open-source equivalent to RAPTOR’s harness generation feature. Worth watching closely.
**Honggfuzz** (Google) is a coverage-guided fuzzer that competes with AFL++ for targets where perf-event hardware counters give better branch coverage than compiler instrumentation.
**ClusterFuzz** (Google) is the distributed fuzzing infrastructure underlying OSS-Fuzz — open-source, but designed for large-scale cloud deployment rather than local research workflows.
On the broader AI-native security side:
**Semgrep Code** has added an AI layer on top of the same pattern-matching engine RAPTOR uses. Their AI prioritises findings and generates autofixes — tighter IDE integration than RAPTOR’s CLI workflow, but cloud-hosted and not open to the same degree of pipeline customisation.
**Snyk Code** uses a proprietary semantic analysis engine augmented by LLM-generated explanations and fix suggestions. Strong on developer experience; closed-source AI layer; SaaS pricing. Targets the “find bugs in PRs” workflow more than the “research a target deeply” workflow RAPTOR is built for.
**GitHub Advanced Security** bundles CodeQL — the same dataflow engine RAPTOR uses — with secret scanning, dependency review, and Copilot-generated fix suggestions. If you’re in the GitHub ecosystem and want CodeQL findings surfaced directly in pull requests with AI-generated patches, GHAS is the low-friction path. It doesn’t include fuzzing, SMT pre-screening, or RAPTOR’s multi-stage LLM validation pipeline.
**PentestGPT** is an open-source LLM-guided penetration testing assistant that uses the LLM as an orchestrator for interactive test flows. Similar philosophy in placing the LLM in the reasoning role, but without deterministic pipeline stages, mechanical pre-screening, or fresh-context verification.
The honest summary: RAPTOR’s differentiation is local-first, open-source, and integrated across all four pillars with a multi-stage LLM validation pipeline. If you need a cloud-managed binary fuzzing solution, Mayhem is better fit. If you’re scanning source code in CI without needing deep binary analysis, GitHub Advanced Security covers most of what you’d want. If you need to run the full pipeline yourself, customise the model, tune the validation stages, and keep your code off a third-party server — that’s where RAPTOR fits. Post 8 will be completely honest about where the current implementation falls short. It is not a solved problem.
Next in series
Post 6 — Binary Exploit Feasibility. What separates “the program crashed” from “the program is exploitable” — and why those are wildly different things.
Sources and further reading
- Fioraldi et al., “AFL++: Combining Incremental Steps of Fuzzing Research” — WOOT 2020. The paper that consolidated AFL’s research community around the AFL++ fork.
- Zalewski, “Technical Whitepaper for afl-fuzz” — the original AFL design document. Still the clearest explanation of why coverage-guided mutation works.
- *AFL++ documentation — practical fuzzing setup, harnessing, and tuning.*
- Klees et al., “Evaluating Fuzz Testing” — CCS 2018. The paper that pointed out how many fuzzing benchmarks are statistically meaningless and started a methodology-cleanup wave in the literature.
- *OSS-Fuzz-Gen — Google’s LLM-guided fuzzing harness generator. The most active open-source project at the LLM-fuzzing intersection.*
- *Mayhem by ForAllSecure — commercial autonomous fuzzing platform; useful benchmark for what a managed binary analysis service looks like.*
- *PentestGPT — open-source LLM-guided penetration testing assistant. Different architectural trade-offs from RAPTOR; worth reading as a contrast.*
메타데이터
- post_id
- 7e5350ebc7b8
- slug
- fuzzing-and-where-raptor-enters-the-story-7e5350ebc7b8
- url
- https://medium.com/@meeraman/fuzzing-and-where-raptor-enters-the-story-7e5350ebc7b8
- canonical_url
- https://medium.com/@meeraman/fuzzing-and-where-raptor-enters-the-story-7e5350ebc7b8
- author_url
- https://medium.com/@meeraman
- status
- ok
- fetched_at
- 2026-06-23 03:48:11