Kernel fuzzers find bugs by taking a test program and mutating it — changing an argument here, adding a system call there — hoping the change reaches new code, but with dozens of arguments per test and a hand-tuned coin-flip picking which one to change, most mutations are wasted; this paper (Purdue and Google DeepMind) trains a graph neural network that reads the test together with the kernel code it touched and predicts which arguments to mutate to reach a chosen uncovered branch, discovering new coverage about five times faster and turning up 86 previously-unknown Linux kernel crashes.
The argument in one glance
-
Problem
To test the kernel, a fuzzer mutates programs — but it barely knows which change will help
A kernel fuzzer hunts for bugs by running small user-space programs that make system calls, watching which kernel code they exercise, and looking for crashes. To keep exploring, it mutates the programs it already has: inserting a new system call, or changing the value of an argument, hoping the changed program reaches kernel code the original missed. The trouble is the search space. A single test carries, on average, more than sixty mutable arguments — not just top-level system-call arguments but the fields of nested structs, flags, buffer offsets, and more — and only a few of them, if changed, will actually steer the program into new kernel code. Finding that needle is extremely hard, because the effect of an argument on which kernel branch gets taken is buried deep in the kernel and poorly documented. So the fuzzer is mostly guessing which of dozens of arguments to change, and most guesses waste a test.
-
What people did
Guide the mutation with hand-crafted heuristics and randomness
The state-of-the-art fuzzer, Syzkaller, encodes expert heuristics about system calls in a specialized language, assigns weights to different mutation choices, and then picks semi-randomly — favoring the prioritized options but occasionally trying others in case the heuristics are wrong. This lets it generate new tests very fast and has made it the standard tool for kernel fuzzing.
-
Why it fell short
Fast but blind: most mutations reach nothing new
Heuristics plus randomness are cheap per test but inefficient in aggregate, because they do not really understand which argument matters for reaching a specific piece of uncovered code. The paper measures the cost: after a thousand argument mutations of a base test, Syzkaller on average finds only about forty-four new tests that trigger kernel branches the base missed. The heuristics are hand-built, hard to extend, and cannot learn from the mountains of fuzzing that the kernel community already runs continuously. Enormous compute is spent shaking the test in the hope that something falls out.
-
This paper's idea
Learn a white-box mutator that predicts which arguments to change
A precise white-box mutator would analyze the executed code path and solve for the mutation that reaches an uncovered branch, but doing that with symbolic execution is so slow it can only be a last-resort fallback. Snowplow instead learns to do it. Its core is a graph neural network, the Program Mutation Model, that reads the test program and the kernel coverage it produced as a single connected graph — user-space calls and arguments joined to kernel basic blocks across the system-call boundary — and, given a target uncovered block, predicts which arguments to mutate to reach it. Trained on real Syzkaller mutations, it plugs into the fuzzer to pick arguments, discovering new coverage roughly five times faster and finding 86 previously-unknown crashes.
Why this problem is worth solving
The whole enterprise of kernel fuzzing rests on making good mutations, and the argument-choice step is where most of the difficulty lives. A kernel fuzzer generates tests by starting from programs it already has and applying small, controlled changes — a new system call, a different argument value — then executing the result and measuring which kernel code blocks it covered, keeping the tests that reach new code and looking for crashes along the way. The bet is that a base test plus a slightly different initial condition will slip into a kernel branch the base missed. But the space of possible changes is enormous. For argument mutation alone, a typical test carries dozens of arguments, often more than a hundred, because each system call can encode nested structures, sub-fields, flags, typed buffers, and pointer manipulations, and the paper measures an average of more than sixty mutable arguments per test. Only a handful of those, if changed, actually alter which kernel path runs, and which handful depends on control dependencies inside the kernel that are implicit and under-documented. So the mutator faces a needle-in-a-haystack choice at every step, and getting it wrong means an executed test that discovers nothing. Making that choice well is the difference between a fuzzer that explores the kernel efficiently and one that spins.
The existing answers sit at two unsatisfying extremes, and the gap between them is exactly where a learned method could live. Modern kernel fuzzers are “gray-box”: they watch coverage but choose mutations with hand-crafted heuristics and randomness, which are cheap to run but do not reason precisely about how to reach a specific uncovered branch, so they waste most of their attempts. At the other pole, “white-box” mutation performs precise analysis of the executed path and solves the constraints of the uncovered branches to guide the change directly — the right idea in principle, but building it typically means symbolic execution, which is so costly and scales so poorly on kernel-sized code that fuzzers only invoke it as a fallback after a branch has already been repeatedly missed, by which point substantial testing resources have been wasted. Meanwhile, the Linux kernel is fuzzed continuously, by the developer community, for years, producing an abundance of examples of tests, their mutations, and the coverage before and after. That data sits unused by the heuristic approaches. The opening Snowplow pursues is to get white-box precision at gray-box cost by learning the mutation choice from that data, rather than deriving it slowly with a solver or guessing it with heuristics.
What people did before, and where it broke
- Heuristics plus randomness (Syzkaller). Encode expert knowledge about system calls, weight the mutation choices, and pick semi-randomly. Where it broke. Cheap per test but blind to which argument reaches a specific uncovered branch; a thousand argument mutations of a base test yield on average only about forty-four tests that hit new branches.
- Symbolic execution white-box mutation (for example HFL). Analyze the executed path and solve constraints to compute a mutation that reaches an uncovered branch. Where it broke. So slow and poorly-scaling on kernel-sized code that it can only run as a last-resort fallback after branches are repeatedly missed, after resources are already spent.
- Directed fuzzers with resource heuristics (for example SyzDirect). For reaching chosen code regions, encode which resources each system call needs and craft resource-specific mutation rules. Where it broke. Requires heavy static analysis and complex hand-built templates per system call, and still reasons through fixed rules rather than learning what actually reaches a target.
- Ignore the abundance of existing fuzzing data. All of the above choose mutations without learning from the continuous, years-long fuzzing the kernel community already runs. Where it broke. A rich record of which mutations reached which coverage goes unused, so the same hard choices are re-made from scratch every time.
- The gap. No mutator delivered white-box precision — reasoning about which specific argument to change to reach a specific uncovered branch — at the low, per-test cost that fuzzing demands, by learning from the data fuzzing already produces. Building that learned white-box mutator is what Snowplow contributes.
How Snowplow works
From a mountain of random mutations to a model that picks the right argument
-
-
new
-
new
-
new
-
new
-
new
The idea
Turn mutation choice into a learning problem, and learn it from the fuzzer’s own output. The reframing at the heart of Snowplow is that choosing which argument to mutate is not a task for a hand-tuned heuristic or a slow solver but a prediction that can be learned — if you have examples of mutations that worked. Those examples are free: run Syzkaller’s ordinary random mutations, execute them, and every time a changed argument reaches kernel code the base test missed, record it as a successful mutation. Then invert the direction — instead of “this argument produced that new coverage,” ask “given this test, its coverage, and a block I want to reach, which argument should I change” — and train a model on the answer. Using machine learning to replace a hand-crafted decision inside a systems loop, learning the policy from data the system already generates rather than encoding it by hand, is the same data-driven-over-heuristic instinct the profile-guided runtime prefetch-generation work covered elsewhere on this site is built on, deriving what to do from observed behavior instead of a fixed rule.
Represent the program and the kernel it touches as one graph, and let a graph network reason across the boundary. The reason this is hard to learn is that the two halves of the problem live in different worlds: the test is a user-space program, naturally a tree of calls and arguments, while coverage is a graph of kernel basic blocks, and the thing that connects an argument to a branch is a system call crossing into the kernel. Snowplow’s move is to build a single graph that contains both — argument and call nodes on one side, kernel basic-block nodes on the other, and explicit kernel-user context-switch edges tying each call to the entry and exit of its handler — so information can flow from a target branch back to the arguments that could reach it. A graph neural network then labels each argument as worth mutating or not, embedding kernel blocks from their assembly with a Transformer pretrained on the compiled kernel. Casting structured, relational data into a graph and learning over it with a graph neural network is the same representational bet the temporal graph-network training work covered elsewhere on this site makes, trusting the graph to carry the dependencies the model must reason about.
Slot the learned model into a real fuzzer without slowing it down, and catch real bugs. A model is only useful here if it makes the whole fuzzer faster, not just more accurate in isolation, so Snowplow is engineered to be a drop-in argument localizer for Syzkaller: because a prediction takes longer than a raw mutator iteration, it is requested asynchronously while the fuzzer keeps busy with other mutation types, a low-probability random fallback still runs in case the model misses, and the number of argument mutations scales with how many arguments the model finds promising. The result keeps fuzzing throughput essentially unchanged while spending each test more wisely, and over a week it surfaces 86 previously-unknown crashes in stable kernels. Building an automated tester that reasons its way to inputs which expose real defects in a mature, heavily-tested system is the same goal the automated database-system-testing work covered elsewhere on this site pursues, scaling a generator of tests until it shakes out bugs the existing tools missed.
Following a mutation query through Snowplow
Choosing the argument that reaches an uncovered branch
A trace, from the paper, of how Snowplow decides which argument of a test to mutate to cover a target kernel block.
Step 1 of 5
The fuzzer picks a base test and a target it has not covered
Syzkaller selects a base test from its corpus. The test has already been executed, so Snowplow knows which kernel basic blocks it covered, and it identifies uncovered blocks one branch away — one of which becomes the target to reach.
A typical test has more than sixty mutable arguments, so choosing which to change is the hard part.
Why it works
- The training data is free and abundant. There is no oracle for the perfect mutation, but running Syzkaller’s random mutations produces examples of changes that reached new coverage → but those recorded base-test, argument, and new-coverage tuples are exactly the supervised examples a predictor needs → so Snowplow learns a white-box policy from data the fuzzer already generates rather than from a hand-built model.
- One graph lets the model reason from a target back to an argument. An argument and the kernel branch it affects live on opposite sides of the system-call boundary → but joining the program graph and the coverage graph with context-switch edges lets information flow across that boundary → so the network can propagate from a desired uncovered block to the arguments most likely to reach it.
- Learning beats guessing in a huge argument space. With dozens of arguments per test only a few matter, and random choice mostly misses → but the model predicts which arguments to mutate with far higher accuracy, scoring an F1 about 2.7 times a random baseline → so each executed test is far likelier to discover new code.
- Asynchronous inference hides the model’s cost. A prediction is slower than a raw mutator step, which could stall the loop → but requesting it asynchronously lets the fuzzer keep mutating in other ways while inference runs → so Snowplow’s fuzzing throughput stays essentially equal to Syzkaller’s despite the added model.
- What it learns transfers. The model is trained on one kernel’s mutations, and kernels change release to release → but it learns argument-to-coverage structure rather than memorizing one build, so it generalizes → so it speeds up fuzzing on newer kernel releases without any retraining or fine-tuning.
What it costs (trade-offs and scope)
- It learns one mutation sub-decision, not the whole fuzzer. Snowplow is demonstrated on argument-mutation localization — choosing which argument to change — and leaves instantiation and other mutation types (like inserting system calls) to Syzkaller’s existing heuristics; the approach could extend to other mutation types, but that is future work, not what is shown.
- The upfront cost is large and only amortizes on heavily-fuzzed targets. Generating the dataset and training the model is expensive — the paper uses eight high-end datacenter GPUs for around three days and explores over a hundred hyperparameter settings — and the authors are candid that this pays off only because the kernel is fuzzed continuously for years; for a rarely-fuzzed target the training cost might not be worth it.
- Serving the model needs GPUs and adds latency. Inference takes about 0.69 seconds per query and requires GPU serving; asynchronous requests hide this during undirected fuzzing, but in directed fuzzing the inference overhead can make Snowplow only match or slightly trail a specialized directed fuzzer in some cases.
- Coverage non-determinism threatens data quality. Kernel coverage is non-deterministic under normal fuzzing (concurrent tests, background kernel activity), which injects systematic noise into training; Snowplow mitigates it with fixed VM snapshots, sequential single-threaded execution, and replacing network RPC with virtio, but this adds engineering and does not eliminate the hazard.
- It complements, rather than replaces, hand-crafted fuzzing. Snowplow is a hybrid: it still relies on Syzkaller for instantiation, keeps a random-localization fallback, and depends on Syzkaller-quality seed tests and mutators; it is an accelerator layered on the existing tool, not a standalone fuzzer.
The evaluation
Choosing which argument to mutate: heuristics, symbolic execution, or a learned model
| Property | Syzkaller heuristics | Symbolic execution | Snowplow (learned model) |
|---|---|---|---|
| How the argument is chosen | |||
| Cost per mutation | |||
| Coverage discovery speed | Baseline | ||
| Total edge coverage in 24 hours | Baseline | n/a | |
| New crashes in a 7-day campaign | n/a |
Check yourself
Check your understanding
1. Why is choosing which argument to mutate so hard for a kernel fuzzer?
2. What is the core of Snowplow, and how does it get its training data?
3. How does Snowplow avoid slowing the fuzzer down, given that model inference is expensive?
4. What did Snowplow achieve, and what are its main limitations?
Sources
The paper is “Snowplow: Effective Kernel Fuzzing with a Learned White-box Test Mutator” by Sishuai Gong, Rui Wang, Deniz Altinbueken, Pedro Fonseca, and Petros Maniatis (Purdue University and Google DeepMind), published at ASPLOS 2025 (Volume 2), DOI 10.1145/3676641.3716019. It is released under a Creative Commons Attribution license, and the implementation is publicly available.
This explainer is written from the full text of the authors’ open-access version. Everything above comes from the paper’s own sections: the introduction and motivation on mutation-based kernel fuzzing, the huge argument search space (an average of more than sixty mutable arguments per test), and the measurement that a thousand argument mutations of a base test yield on average only about forty-four new-branch tests under Syzkaller, alongside the two prior extremes of hand-crafted gray-box heuristics and slow, fallback-only white-box symbolic execution; the observation that the Linux kernel is fuzzed continuously, producing abundant unused data; the design of the Program Mutation Model, including generating a dataset of successful argument mutations from Syzkaller’s own random mutations, the unified graph representation of the test program and its kernel coverage joined by kernel-user context-switch edges, the choice of a noisy target set for training robustness, the graph neural network with an assembly Transformer pretrained on the compiled kernel and learned embeddings for program nodes, and the asynchronous integration into Syzkaller as the argument localizer with a random fallback; and the evaluation reporting an F1 about 2.7 times a random baseline, a 4.8 to 5.2 times speedup in discovering new edge coverage with 7.0 to 8.6 percent higher total coverage, generalization to newer kernels without retraining, 86 previously-unknown crashes over a seven-day campaign, an 8.5 times average speedup on 19 directed-fuzzing targets plus 2 additional locations reached, and performance characteristics of about 57 queries per second and 0.69 seconds average inference latency with fuzzing throughput near Syzkaller’s.