Home · ASPLOS 2024 · Compilers & Programming Languages

The Mutators Reloaded: Fuzzing Compilers with Large Language Model Generated Mutation Operators

Xianfei Ou, Cong Li, Yanyan Jiang and Chang Xu

Conference
ASPLOS 2024
Published
2024
Pages
298-312
Citations
22

Compilers must be correct, and fuzzers hunt their bugs by mutating seed programs with hand-written mutation operators — but each good mutator demands deep compiler-internals expertise, so there are only a few and they're shallow; recent work throws an LLM at the problem by generating test programs directly, which is wasteful (each program is used once, many are invalid, and matching a real fuzzer's scale would cost about a million dollars in API calls); MetaMut (Nanjing University) instead has the LLM synthesize the MUTATORS themselves — reusable code artifacts each applied hundreds of thousands of times — by wrapping compiler internals in LLM-friendly APIs, constraining the design space, and auto-validating each mutator against six graded goals with a self-repair loop, producing 118 semantic-aware mutators at about 50 cents each that found 131 confirmed bugs in GCC and Clang, most reaching deep into the optimizer where blind fuzzers never get.

The argument in one glance

  1. Problem

    Finding compiler bugs needs good mutators, and good mutators need experts

    Compilers are trusted infrastructure — a bug that silently miscompiles correct source code introduces failures and vulnerabilities nobody can see in the source. So compilers are fuzzed hard, and the most effective style is MUTATION-BASED fuzzing: start from seed programs and apply MUTATORS (mutation operators — reusable code transformations) to produce a flood of new test programs, exploring the space of each seed's possible descendants. The catch is the mutators themselves. A good one must understand both the syntax AND the semantics of the language, so its mutants still compile and stay well-defined while probing interesting behavior, and writing one demands deep expertise in compiler internals. As a result, only a handful of shallow mutators exist, and that scarcity directly caps how many bugs a fuzzer can find.

  2. What people did

    Point a large language model at it — have the LLM write test programs

    Large language models know a lot about code, so the recent wave of LLM fuzzers (Fuzz4All, FuzzGPT, WhiteFox) uses them the obvious way: ask the model to generate individual test programs directly, then feed those to the compiler. The LLM is the test-case generator, producing program after program to throw at GCC or LLVM. It seems like the natural way to get diverse, sophisticated test inputs without hand-writing anything.

  3. Why it fell short

    Generating test cases one at a time is wasteful, invalid-prone, and ruinously expensive

    Using the LLM as a per-test generator squanders it. Each generated program is used ONCE and thrown away, so the model's expensive reasoning buys a single test case. Many of those programs are invalid (they do not compile or are ill-defined), wasting the call entirely. Diversity is limited, because the model tends to produce similar things. And the cost is prohibitive at fuzzing scale: the paper estimates that generating enough programs end-to-end to match a real fuzzer's throughput would cost about $1,000,000 in GPT-4 API calls. A fuzzer needs to try millions of programs per day; paying the LLM per program is a non-starter. The LLM is being used at the wrong granularity — as a consumable output generator, when its real value is in producing something reusable.

  4. This paper's idea

    Have the LLM synthesize the MUTATORS, not the test cases

    MetaMut (Nanjing University — Ou, Li, Jiang, Xu) changes what the LLM produces: instead of one-off test programs, it synthesizes the MUTATORS themselves — reusable code artifacts, each applied hundreds of thousands of times across many seeds. One valid mutator generates far more test cases than a single LLM-generated program, so the model's cost is amortized to almost nothing. To make an LLM actually capable of writing correct, semantic-aware mutators, MetaMut injects compiler-domain knowledge: it wraps Clang's internals in readable APIs, constrains the design space to an [Action] on a [Program Structure], guides implementation with a code template and a worked example, and then AUTO-VALIDATES each mutator against six graded goals with a self-repair feedback loop. The result: 118 semantic-aware mutators at about $0.50 each, which found 131 confirmed bugs in GCC and Clang — most reaching deep into the optimizer.

Why this problem is worth solving

Compiler correctness underwrites all the software built on top of it, and fuzzing is the workhorse for finding the bugs, so the bottleneck in fuzzing is a bottleneck in trust. Mutation-based fuzzing’s power comes entirely from its

the reusable code transformations that turn one program into many. A mutator takes a seed program and rewrites some part of it — swap two operands, inline a call, change a return type, restructure a loop — producing a new test program (a mutant). The set of mutators defines the entire search space the fuzzer can explore: a seed’s reachable mutants are exactly those its mutators can produce, so richer, deeper mutators mean a larger and more interesting space of tests. Crucially, a good mutator must be SEMANTIC-AWARE — it has to respect the language’s rules so the mutant still compiles and is well-defined, because a mutant that is rejected at the front-end never exercises the optimizer and back-end where the subtle bugs live. Writing such a mutator requires understanding compiler internals deeply, which is why only a few exist
. That scarcity is the ceiling: with few and shallow mutators, the fuzzer keeps exploring the same shallow region and the deep bugs stay hidden. Breaking the ceiling means producing many high-quality mutators without needing an expert to hand-craft each one.

The obvious way to enlist an LLM misuses it, and seeing why points at the fix. The reason per-test generation is so expensive is a matter of

the level at which you apply an expensive resource. An LLM call is costly, so what you get for that cost matters enormously. If the LLM produces a single test program, that one call yields exactly one test — used once, then discarded — and a fuzzer needs millions of tests, so the cost scales linearly with testing and becomes astronomical (about a million dollars to match a real fuzzer). If instead the LLM produces a MUTATOR, that one call yields a reusable tool that generates hundreds of thousands of tests over its lifetime, so the expensive reasoning is paid once and amortized across every application. The difference is not incremental: it is the gap between a consumable output and a reusable generator
. This reframing is the whole idea. It also changes what the LLM must be good at — not producing a clever program, but writing a correct, general transformation — which is harder, and is exactly why MetaMut has to surround the model with domain scaffolding and a validation loop to make it succeed. The payoff is that once the LLM writes a good mutator, that mutator works forever at native fuzzing speed, so the model’s cost vanishes into the noise.

What people did before, and where it broke

  • Hand-written mutators (traditional mutation fuzzers). Experts craft a small set of semantic-aware transformations. High quality per mutator, but writing each needs deep compiler-internals expertise, so there are only a few shallow ones — the scarcity that caps bug discovery.
  • Grammar-based program generators (Csmith, YARPGen). Generate whole random programs from a language grammar, guaranteeing validity (99.8% compilable). Effective historically, but they produce programs in a narrow, always-valid style and found far fewer new crashes in the comparison (Csmith 0, YARPGen 2 unique crashes) — always-valid can mean never-surprising.
  • Coverage-guided byte-level fuzzers (AFL++). Mutate the program as raw bytes guided by coverage. General, but blind to language structure, so only 3.53% of its mutants even compile — most die at the front-end and never reach the optimizer where deep bugs are.
  • LLM-as-test-generator (Fuzz4All, FuzzGPT, WhiteFox). Ask the LLM to write individual test programs. Each is used once, many are invalid, diversity is limited, and matching a fuzzer’s scale would cost about $1,000,000 — the LLM applied at the wrong granularity.
  • The gap. No one used the LLM to synthesize the REUSABLE mutators — the tools, not the outputs — with enough domain scaffolding and validation to make them correct and semantic-aware. MetaMut does, turning a per-test LLM cost into a one-time per-tool cost and reaching the deep bugs blind fuzzers cannot.

How MetaMut works

Synthesize reusable mutators, scaffold the LLM, and validate with self-repair

  1. Reframe: build the tool, not the output new

    MetaMut uses the LLM to synthesize MUTATORS — reusable code transformations — rather than one-off test programs. Because a single mutator is applied hundreds of thousands of times across seeds, the LLM's expensive call is amortized to near zero, and the semantic knowledge the model encodes is reused across every mutant the mutator produces.

  2. Invent a mutator from a curated design space new

    The LLM is prompted for a semantic-aware operator that performs an [Action] on a [Program Structure] — actions like Add, Modify, Swap, Inline, Combine, Inverse; structures like BinaryOperator, IfStmt, ArrayDimension. Constraining the space keeps proposals coherent, while the LLM still invents creative operators beyond the predefined grid (e.g. "change a function's return type to void").

  3. Synthesize the implementation with domain scaffolding new

    The LLM fills a code template via chain-of-thought, using uAST APIs — readable wrappers around Clang's AST internals (removeParamFromFuncDecl, checkBinop, randElement) — plus one complete worked example. This scaffolding is what makes correct mutator synthesis tractable for the model, encoding the compiler expertise the LLM lacks.

  4. Validate against six graded goals new

    MetaMut generates test programs containing the target structure, compiles and runs the mutator on them, and checks a difficulty ladder of six goals: the mutator (1) compiles, (2) does not hang, (3) does not crash, (4) produces output, (5) actually modifies the code, and (6) produces COMPILABLE mutants. Each goal is a concrete, checkable correctness condition.

  5. Self-repair failures automatically new

    When a goal fails, MetaMut feeds the error back to the LLM, which iteratively fixes its own implementation — across 50 fully-automated mutators it repaired 107 bugs (about 4 per mutator), reaching a 65.8% validity rate with no human help. Only mutators passing the goals enter the library.

  6. Fuzz with the mutator library new

    A coverage-guided micro-fuzzer (uCFuzz) picks random mutators, applies them to seed programs, and keeps mutants that expose new branch coverage; a production macro-fuzzer scales this out with parallelism, resource limits, and compiler-flag randomization. Throughput is about 11 mutants per second — native fuzzing speed, with no LLM in the loop at fuzz time.

Applied to C compilers via Clang's AST, evaluated against Csmith, YARPGen, GrayC, and AFL++ over 24-hour coverage/crash runs and an 8-month field campaign on GCC-12/14 and Clang-17/18. 118 mutators total (68 with prompt supervision, 50 fully automated) at about $0.50 each; ~2 weeks of prompt refinement plus ~2 days to build the uAST APIs.

The idea

Make the model build a reusable tool, not a disposable output. The single reframing that carries the paper is a shift in what the LLM produces. Used as a test-case generator, each expensive call yields one program, used once — so at fuzzing scale the cost is about a million dollars and most of it is wasted on invalid or redundant programs. Used as a MUTATOR synthesizer, each call yields a reusable transformation applied hundreds of thousands of times, so the model’s cost is amortized to roughly $0.50 per mutator and its semantic knowledge is baked into a tool that runs forever at native speed. This is the amortize-a-fixed-cost-over-many-uses discipline the site keeps rewarding — the graph-mining page packing many cheap compute units behind one expensive control block, the compiler pages solving an expensive plan once and reusing it — but applied to the LLM itself: pay the model to build the machine, not to stamp out each part. Choosing the right granularity at which to spend an expensive resource is the whole game.

Scaffold the model with the domain knowledge it lacks. An LLM cannot, on its own, reliably write a correct semantic-aware mutator against Clang’s sprawling internals — so MetaMut does not ask it to. It wraps Clang’s AST in readable uAST APIs, constrains the design space to an [Action] on a [Structure], supplies a code template and a worked example, and lets the model reason within that structure. The scaffolding encodes exactly the compiler expertise the model is missing, turning an intractable synthesis task into a tractable one. This is the give-the-model-the-right-interface discipline the site sees whenever a hard task is made learnable by presenting it in the right representation — the same instinct as reifying an operation into a clean abstraction so a search or a model can work with it. The LLM supplies broad reasoning; the human supplies the domain-shaped interface; neither alone would produce good mutators.

Do not trust the output — validate it and let the model repair itself. Because the LLM is neither sound nor complete, MetaMut treats every synthesized mutator as a hypothesis to be checked, not a result to be trusted. It generates targeted tests, runs the mutator, and grades it against six escalating goals (compiles, does not hang, does not crash, produces output, modifies code, produces compilable mutants), feeding any failure back for the LLM to fix — 107 automatic repairs across 50 mutators. This is the generate-and-validate-with-self-repair loop the site just met in the hardware-repair page (mutate, test against an oracle, evolve) and the optimal-inlining page (generate candidates, judge against the real compiler): when a generator is unreliable but its output is cheaply checkable, wrap it in a verifier and a feedback loop, and the combination is far stronger than the generator alone.

Why building the tool beats generating the output

From a million-dollar generator to a 59-dollar mutator library

Using an LLM for compiler fuzzing. Numbers from MetaMut's evaluation.

Step 1 of 5

The mutator bottleneck

Mutation fuzzing is only as good as its mutators, and a good one must be semantic-aware — respecting the language so its mutants still compile and reach the optimizer — which takes deep compiler-internals expertise to write. So only a few shallow mutators exist, and the fuzzer keeps exploring the same shallow region while deep bugs stay hidden.

the mutators define the entire search space of a seed's descendants — few mutators, small space
a mutant rejected at the front-end never exercises the middle-end or back-end where subtle bugs live

Why it works

  • A mutator is applied hundreds of thousands of times while a generated test program is used once → synthesizing mutators amortizes the LLM’s cost across all their applications → 118 mutators cost about 59totalversusabout59 total versus about 1,000,000 for equivalent-scale test generation.
  • At fuzz time the mutators run as ordinary code with no LLM involved → throughput is native (~11 mutants per second) → the fuzzer explores millions of programs at the speed of a hand-written fuzzer.
  • An LLM alone cannot navigate Clang’s internals reliably → wrapping them in readable uAST APIs and constraining the design space encodes the missing expertise → the model can synthesize correct, semantic-aware mutators it otherwise could not.
  • The LLM is unsound, so its mutators may be wrong → validating each against six graded goals with an automatic self-repair loop → only correct mutators enter the library (107 bugs auto-fixed across 50 mutators).
  • Semantic-aware mutators keep mutants valid enough to compile → 63% of the resulting bugs pass the front-end into the optimizer and back-end → the fuzzer reaches the deep bugs blind byte-level fuzzers (3.53% compilable) never touch.

What it costs

  • Neither sound nor complete. MetaMut is not guaranteed to synthesize implementations that match their descriptions, nor to cover all possible mutators, so its mutators’ correctness rests on manual validation after generation (two authors cross-checked every one) — the six goals are necessary but not sufficient conditions.
  • The design template limits creativity. Constraining mutators to a single [Action] on a single [Structure] rules out the multi-structure transformations a human might design (a control-flow injection spanning several constructs), so some powerful hand-written mutators lie outside MetaMut’s reach.
  • The LLM struggles with the hard repairs. Automated self-repair handles many failures but not the subtle ones — 6 of 50 unsupervised mutators needed human diagnosis (e.g. for mutator hangs) — and weak validation tests can miss edge cases, with 10 of 20 likely-correct mutators failing harder author-crafted tests.
  • Validation is only as good as its tests. Because correctness is judged by running the mutator on generated tests, gaps in those tests let subtly-wrong mutators through, which is why human cross-validation was still needed.
  • Per-language setup cost. Supporting C took about two weeks of prompt engineering plus two days to build the uAST APIs; porting to another language means reimplementing that AST-wrapper layer on its parser infrastructure.

The evaluation

C-compiler fuzzing via Clang’s AST, with 118 LLM-synthesized mutators, compared against Csmith, YARPGen, GrayC, and AFL++ over 24-hour runs (60 parallel instances each) measuring coverage and unique crashes, plus an 8-month field campaign against GCC-12/14 and Clang-17/18.

Ways to (or not to) use an LLM for compiler fuzzing

PropertyHand-written mutatorsLLM generates test cases (Fuzz4All)MetaMut (LLM synthesizes mutators)
What is produced
Reuse per LLM calln/a (written by hand)
Cost at fuzzing scale
Semantic validity of mutants
Bugs foundLimited by mutator scarcity
MetaMut's compilable-mutant ratio (~74%) is lower than grammar generators like Csmith (99.86%) — but that is a feature, not a defect: always-valid generators stay in a safe, well-trodden region, while MetaMut's semantic-aware mutators push into harder, buggier territory, which is why they find dramatically more crashes (90 vs Csmith's 0) despite compiling less often.Underlined values explain themselves when clicked.

Check yourself

Check your understanding

  1. 1. Prior LLM fuzzers use the model to generate test programs directly; MetaMut instead has it synthesize reusable mutators. Predict why this change of what-the-LLM-produces is the crux of the paper.

  2. 2. MetaMut wraps Clang's internals in readable uAST APIs, constrains mutators to an [Action] on a [Structure], and supplies a template and worked example. Predict why this scaffolding is essential rather than optional polish.

  3. 3. MetaMut validates each synthesized mutator against six escalating goals and feeds failures back for the LLM to repair. Predict why this generate-and-validate loop is necessary given how the mutators are produced.

  4. 4. MetaMut's mutants compile only ~74% of the time, versus 99.86% for the Csmith generator — yet MetaMut finds vastly more crashes (90 vs 0). Predict what this apparent paradox reveals.

Sources

Related papers on Compilers & Programming Languages

See all 162 →