Blog / / 8 min read

The Bugs Fuzzers Can't See

Automated kernel bug-finding has converged on one technique: fuzzing. It has a ceiling built in. We do it differently. We read the source and reason about it, to find the bug classes a fuzzer is structurally blind to.

Most automated vulnerability research in the Linux kernel does one thing. It fuzzes. It generates enormous quantities of malformed input, throws them at the kernel under a sanitizer, and waits for a crash. This works. It has found thousands of real bugs, and the infrastructure behind it, Google’s syzkaller and the syzbot pipeline, is some of the best security engineering in existence.

But fuzzing has a ceiling, and the ceiling is built into how it works. It’s the whole reason our approach looks different.

The two things a fuzzer cannot do

A fuzzer can only test the surface it has been taught to describe. syzkaller reaches a subsystem through hand-written syzlang descriptions of its syscalls, ioctls, and netlink messages. Where a description exists, coverage-guided fuzzing is devastating. Where one does not, a newer driver, an obscure protocol, a device that needs a specific init handshake, the fuzzer never goes there. And everyone points their fuzzers at the same well-described surface. By now that surface is picked clean. The bugs that remain live where the fuzzer is not looking.

A fuzzer can only find bugs that crash. This is the deeper limit, and the one people underestimate. A fuzzer’s oracle is the sanitizer: KASAN, UBSAN, KCSAN. It notices a bug because memory was corrupted, an array was over-read, a lock was misused. Powerful, but blind by construction to an entire category of vulnerability. The bug that produces no crash at all.

A missing permission check does not corrupt memory. An operation on the wrong object, an integer that silently truncates, a capability check against the wrong namespace: none of these trip a sanitizer. Nothing is broken in the memory-safety sense. The code does exactly what it was written to do. It was just written to let the wrong person do it. No fuzzer will ever report that. There is nothing to crash.

More fuzzing does not fix this. Only reasoning does.

Reading the kernel instead of hammering it

Our approach starts from the opposite end. Instead of generating input and watching for crashes, we read the source and reason about it the way an auditor or an exploit developer would. What is this code assuming? Who is allowed to reach it? What invariant holds every object together, and where is it not enforced?

That reframing opens three veins of bugs that fuzzing misses.

1. The fix that was almost complete

When a security bug is fixed upstream, the fix usually covers the path that was reported. But a bug class rarely lives in one place. The same dangerous pattern, an unbounded length driving a copy, a lock that should have been held, a check that should have been made, gets copy-pasted across sibling drivers and adjacent paths the reporter never looked at.

A fuzzer has to rediscover each sibling by crashing it. Reasoning does better. It reads the fix, extracts the shape of the bug, and asks one sharp question across the whole tree: who else does this, and did the fix reach them? Again and again the answer is that someone was missed. A capability-check series that hardened eight tunnel drivers and skipped two with identical structure. A bounds check added to one parser but not the four siblings that share its code. Real, current, unfixed bugs, invisible to a fuzzer that has no reason to revisit patched territory.

2. The bug with no crash

This is the frontier, and almost nobody hunts it with automation, because you cannot fuzz it.

Take an authorization bug. An operation is gated behind a privilege check on its main entry point, but a second, quieter path reaches the same privileged effect without the check. Or the check runs against the wrong security context, so a caller privileged only inside a container reaches out and affects the host. Nothing crashes. The kernel carries out the operation. The bug is not in the memory. It is in the logic of who was allowed.

To find these you build a model of the system’s intended authorization, every privileged effect and every gate meant to protect it, then look for the effect that can be reached without passing the gate. That is reasoning from start to finish. It is the task the field has publicly called unsolved by automation, and it is exactly where an engine that reads and reasons beats a fuzzer.

3. Fresh code that skipped its family’s rules

The newest code in the kernel is where the most interesting corruption still lives. A brand-new feature is the easiest place to skip an invariant the rest of the subsystem enforces, even when the code is otherwise well written. A new deferred path that forgot the lock the rest of its family takes. A fresh parser that dropped the bounds check its siblings all have.

Fuzzers get there eventually. Eventually is the problem. Fresh regressions in popular subsystems are a race, and a crowded one. Being first matters. Reasoning about a change the day it lands, instead of stumbling into a crash weeks later, is the difference between finding a bug and re-finding someone else’s.

The part that actually matters: discipline

Here is the uncomfortable truth about the current wave of AI-assisted kernel patches. Most of it is noise. The cost of generating a plausible-looking patch has collapsed, and maintainers are drowning in low-quality, duplicate, and subtly-wrong submissions. An LLM that flags candidates is not valuable. An LLM that flags candidates with a high false-positive rate is negative value. It wastes the scarcest resource in open source: maintainer attention.

So the model that flags bugs is the easy, commoditized part. The moat is everything that happens after a candidate is generated.

  • Adversarial verification. Every candidate goes to an independent pass whose job is to refute it, to find the lock, the refcount, the check that makes the bug not a bug. Most candidates die here. That is the point. The ones that survive a real attempt to kill them are worth a human’s time.
  • Proof, not assertion. A finding is not real until it is demonstrated: a reproduced sanitizer crash, or a controlled before/after showing the exact behavior change. We reproduce the panic, capture the KASAN splat, and show the operation succeeding on the vulnerable build and rejected on the fixed one.
  • Deduplication against the world. Before anything is sent, it is checked against what has already been reported and fixed: git history, patch queues, mailing-list threads. Concurrent discovery is common on fresh, popular surfaces. Sending a duplicate wastes everyone’s time and burns credibility.
  • Honest negatives. Most leads, run down rigorously, turn out already defended. We treat a well-reasoned “this is safe, here is the invariant that protects it” as a successful outcome, not a failure. Being willing to conclude “nothing here” is what keeps the signal clean.

This is the difference between a tool maintainers accept and a tool maintainers learn to filter. It is unglamorous. It is the entire ballgame.

Where this sits, honestly

The honest version of where this stands is more convincing than the hyped one.

Fuzzing wins on volume, and it is not close. syzbot finds more bugs by sheer quantity than any reasoning approach will. We are not trying to out-fuzz the fuzzers. That is a losing game, and anyone claiming to win it is either lying or fuzzing the surface everyone else already exhausted. We do something different in kind. We find the incomplete-fix variants and the non-crashing logic bugs that fuzzing cannot see, and we write and prove the fix instead of filing a crash report for a human to solve.

The frontier LLM efforts from the largest labs are real and impressive. But by their own published accounts they still largely need seeding: a target function, a diff, a starting point. There is no public demonstration yet of an automated system finding a non-crashing logic or authorization bug from open-ended reasoning. That capability is the open problem, and it is the one we are pointed at.

Traditional penetration testing is a different universe. A two-week engagement maps known, published CVEs onto a client’s infrastructure. It does not find novel zero-days in the kernel or in the open-source components a client’s stack is quietly built on. That is not a criticism. It is simply not what the service is. It is, however, the gap that automated deep source analysis fills.

The proof is upstream

We do not ask anyone to take the approach on faith. The measure of whether reading and reasoning works is whether the people who own the code accept the fixes into it, not a number on a slide. They have. Our research has landed memory-safety fixes in the mainline Linux kernel, each reviewed by the subsystem’s own maintainers, several riding out to the stable trees that ship on billions of devices. And we are increasingly finding the class of authorization bug that no fuzzer would surface at all.

The counts change week to week. The method is the point. There is a large and growing space of real vulnerabilities living exactly where the dominant technique cannot look. Reaching them takes genuine reasoning about the code, done with enough discipline that the kernel community accepts the result instead of filtering it.

That space is where we work.