Most fuzzing tutorials show you the same win: throw malformed bytes at a parser, watch it segfault, ship the crash. Coverage-guided fuzzers like AFL and syzkaller are genuinely spectacular at that. But there's an entire class of bug they're structurally bad at finding — the kind that only appears when two things happen in the wrong order. That's the problem RaceFuzz, the OSIRIS Lab project I lead, exists to attack: race conditions in the Linux kernel's network stack.
A memory-safety bug is a property of a single input. Feed the same bytes twice, get the same crash twice. That reproducibility is exactly what makes coverage-guided fuzzing work — the fuzzer can attribute new coverage to a specific input and evolve it.
A race condition is a property of an interleaving. The bug isn't in any one syscall; it's in the fact that syscall X on CPU 0 touched a structure that syscall Y on CPU 1 was halfway through modifying. Run the same two syscalls again and the scheduler might interleave them differently, and the bug vanishes. To a normal fuzzer, a race looks like noise: coverage that appears and disappears for no attributable reason.
You can't mutate your way to a race the way you mutate your way to a crash. The input space isn't bytes — it's schedules.
The network stack makes this especially hard because it's stateful and concurrent by design. A socket has a lifecycle — created, bound, connected, shut down, closed — and the interesting races live in the transitions: closing a socket while another thread is mid-send, teardown racing against an incoming packet, two threads racing on the same sk_buff.
Purely random syscall soup rarely reaches those transitions. So RaceFuzz is semantic in two senses:
Finding a race once is almost worthless. If you can't reproduce it, you can't bisect it, you can't report it, and you can't prove your fix worked. Half the actual engineering is making a race deterministic after you've found it — capturing the interleaving, not just the inputs, and replaying it. A race you can trigger 1-in-10,000 times is a rumor; a race you can trigger on command is a bug report.
Coverage still matters, but we treat it as a map of where the state machine can go, not as the fitness function itself. The fitness function has to reward reaching concurrency-relevant states and widening race windows — otherwise the fuzzer optimizes for breadth of coverage and never lingers where races actually live.
If you're into kernel security, systems fuzzing, or just enjoy arguing about memory ordering, this is the kind of problem I find endlessly fun — it sits right at the seam between systems programming and security research. Happy to talk about it: get in touch.