A researcher recently published results from a 30-day kernel bug hunting campaign that turned up over 100 bugs. The number is striking. It is also, if you understand the tooling involved, less surprising than it looks — and that is precisely what makes it interesting. The real story is not the count; it is the workflow that produces it and what that workflow reveals about where the Linux kernel’s weakest surfaces are.
The Reconnaissance Phase Matters More Than the Fuzzing
Most write-ups on kernel fuzzing focus on the fuzzer itself. The part that actually determines whether a campaign is productive is the work that happens before the first crash: writing syscall descriptions.
syzkaller, the dominant kernel fuzzer, uses a typed grammar called syzlang to describe every system call. A description for a TCP socket operation looks something like this:
socket$inet_tcp(domain const[AF_INET], type const[SOCK_STREAM], proto const[IPPROTO_TCP]) fd[sock]
setsockopt$tcp_cork(fd fd[sock], level const[SOL_TCP], optname const[TCP_CORK],
val ptr[in, int32], len bytesize[val])
The type system tracks file descriptors, flags, lengths, pointer relationships, and struct layouts. When syzkaller generates a syscall program, it produces structurally valid argument sequences rather than random bytes. This matters enormously: a fuzzer that generates random values for socket options will almost never reach the deep validation paths in a protocol handler, because most inputs get rejected at the first bounds check.
When a kernel subsystem has no syzlang descriptions, syzkaller cannot fuzz it effectively. Writing those descriptions — reading through ioctl dispatch tables, struct definitions, and flag enumerations — is where a focused researcher invests the first week of a serious campaign. It is also where bugs start to appear even before the fuzzer runs; studying the interface closely enough to describe it formally surfaces inconsistencies.
Three Sanitizers, Three Bug Classes
Once fuzzing starts, what makes the bugs visible is the sanitizer stack. syzbot, Google’s continuous fuzzing infrastructure, runs multiple kernel configurations simultaneously, and each one catches fundamentally different classes of bugs.
KASAN (Kernel Address Sanitizer) instruments every memory access. Every 8 bytes of real kernel memory has a corresponding shadow byte that encodes whether the memory is valid, freed, or a redzone. A use-after-free write into a freed slab object triggers the shadow check and produces a report that shows exactly where the object was allocated, where it was freed, and where the stale access occurred. KASAN catches use-after-free and out-of-bounds heap writes, which are the two classes most commonly turned into exploits.
KCSAN (Kernel Concurrency Sanitizer) uses a probabilistic watchpoint mechanism to detect data races. When a thread accesses memory, there is a chance KCSAN installs a watchpoint on that address. Any concurrent access from another CPU without appropriate synchronization fires a report. Race conditions in the kernel networking stack and filesystem code turn up in volume once KCSAN is enabled — many of them involve missing READ_ONCE or WRITE_ONCE annotations that are technically benign on current hardware but indicate racy intent.
KMSAN (Kernel Memory Sanitizer, merged in Linux 6.1) tracks uninitialized memory by maintaining a shadow byte for every byte of kernel memory. When uninitialized kernel memory reaches a copy_to_user() call, KMSAN fires. These are information-leak bugs: struct padding bytes, unzeroed kmalloc buffers, local variables partially filled by ioctl handlers. They matter because they can leak kernel addresses that bypass KASLR.
Running all three sanitizers, along with UBSAN for undefined behavior, means the same fuzzing campaign surfaces four distinct bug classes. A campaign that runs only KASAN misses everything KCSAN and KMSAN find. The 100-bug count for a 30-day campaign largely follows from running this full configuration rather than any single fuzzer being exceptionally clever.
What the Breakdown Actually Looks Like
Not all 100 bugs carry equal weight. A realistic breakdown for a campaign of this size looks roughly like this: roughly half are genuine previously-unknown bugs that have not been reported by syzbot; a quarter are independent rediscoveries of bugs already open on the syzbot dashboard where the researcher hit the same code path syzkaller already found; some fraction are WARN_ON() assertions or debug-mode panics that indicate a programmer assumption was violated but do not represent exploitable memory corruption; and a small number — perhaps 5 to 10 percent — are the genuinely dangerous bugs: use-after-free or out-of-bounds writes with controllable data, reachable from unprivileged contexts or from within user namespaces.
That last category is what drives CVE assignments and exploitation research. The rest are real bugs in the sense that they represent incorrect kernel behavior, and they are worth fixing, but they do not represent immediate privilege escalation paths.
Writing reproducers is where the triage work concentrates. A syzkaller crash report includes a C reproducer — a standalone program that triggers the bug — but the reproducer often requires root, a specific network interface in a specific state, or a particular kernel config. Determining whether a bug is reachable from an unprivileged user namespace requires reading the code path carefully, not just running the reproducer.
The CVE Inflation Problem
Context is important when reading a “100 bugs” number in 2025 or 2026. In 2024, the Linux kernel project began self-assigning CVEs through its own CVE Numbering Authority. Previously, CVEs were conservatively assigned — only bugs with clear security impact got one. Under the new policy, nearly every fix merged into a stable branch receives a CVE.
The practical result is that the kernel generated somewhere between 2,000 and 4,000 CVEs in 2024 alone, compared to a few hundred in prior years. From a vulnerability scanner’s perspective, this is chaos. From a security researcher’s perspective, it means that a 30-day campaign can produce a large CVE count that includes everything from a trivial null pointer dereference in an obscure driver to a remotely reachable privilege escalation.
The number 100 is not meaningless, but it has to be read against this backdrop. The campaign found real bugs, and the methodology behind it is sound. The number itself is partly a function of policy changes that increased CVE density across the entire kernel project.
Where the Bugs Come From
The subsystems that show up repeatedly in these campaigns are not random. USB drivers are the single most productive fuzzing target: there are roughly 2,000 USB drivers in the kernel, most written by hardware vendors without adversarial threat modeling, and a dummy USB host controller driver (dummy_hcd) lets syzkaller emulate a malicious device without any hardware. A USB driver that trusts the endpoint count in a descriptor header without bounds-checking has a trivial out-of-bounds read. The driver author was thinking about valid hardware; syzkaller is not constrained by that assumption.
The netfilter/nftables subsystem has been another persistent source. It is accessible through user namespaces on most Linux distributions, which means CAP_NET_ADMIN inside a namespace is not a meaningful barrier. The subsystem had at least 8 to 10 high-severity CVEs in 2022 and 2023 alone. CVE-2024-1086, an nf_tables use-after-free found through code audit, was widely used in privilege escalation exploit chains throughout 2024.
io_uring, added in Linux 5.1, remains heavily scrutinized. It introduced a new execution model for asynchronous I/O that interacts with the kernel’s task and memory management in ways that took years to audit properly. Docker and Kubernetes both disabled io_uring by default in their seccomp profiles due to the frequency of bugs. It is the kind of subsystem that rewards a focused 30-day effort because the code is genuinely complex and the interactions with the rest of the kernel are non-obvious.
What Hardening Actually Stops
Kernel hardening in 2025 is substantially better than it was five years ago. CONFIG_RANDOM_KMALLOC_CACHES, introduced in Linux 6.3, randomizes heap slab placement, making use-after-free exploitation harder by preventing reliable reclaim of freed objects. init_on_alloc=1 zeroes all allocations before use, defeating most uninitialized-memory information leaks. CFI with Clang validates indirect call targets, preventing function pointer overwrites from redirecting execution arbitrarily.
None of these eliminate the bugs that a fuzzing campaign finds. They raise the cost of exploitation once a bug is identified. A use-after-free is still a use-after-free regardless of heap randomization; the sanitizers will still catch it, and it still needs a patch. Hardening and fuzzing serve different purposes and are not substitutes for each other.
What a campaign like this one demonstrates is that the kernel’s code surface is too large for any single approach to cover. syzbot has been running continuously since 2017 and still has over a thousand open bugs at any given time. A focused researcher with good syzlang descriptions and a full sanitizer stack can find a hundred more in a month. The kernel is not uniquely insecure — it is uniquely complex, and the gap between complexity and correctness is where all of these bugs live. The interesting question after reading a report like this is not whether the number is impressive; it is which subsystems still have no good syzlang descriptions and what it would take to write them.