Real-World Bugs Found by Fuzzing: Case Studies from Heartbleed to Chrome

Real-World Bugs Found by Fuzzing: Case Studies from Heartbleed to Chrome

Fuzzing's reputation is built on results. The technique has found some of the most significant software vulnerabilities of the past 30 years — not because it's clever, but because it's systematic and relentless.

These case studies examine real-world bugs found by fuzzing, what each reveals about target selection and corpus design, and what lessons apply to your own fuzzing programs.

Heartbleed (CVE-2014-0160)

OpenSSL's Heartbleed vulnerability is the canonical example of the kind of bug fuzzing is built to find. It wasn't found by fuzzing when it was introduced in 2012, but it would have been trivially found by modern fuzzing infrastructure.

The bug

The TLS heartbeat extension allowed a client to send a heartbeat request with a payload. The server was supposed to respond with the same payload. The implementation read the payload length from the client's message and allocated that much memory for the response — but then copied data based on the claimed length, not the actual payload length.

// Simplified vulnerable code
uint16_t payload_len = /* read from packet, attacker-controlled */;
uint8_t *response = malloc(payload_len + overhead);
memcpy(response + overhead, payload, payload_len);  // reads payload_len bytes from payload
// but payload might be much shorter than payload_len claims

Result: up to 64KB of server memory leaked to any connected client. Private keys, session tokens, user credentials.

Why fuzzing would find it

A fuzzer generating TLS heartbeat messages would quickly try:

  • payload_len = 0 (boundary)
  • payload_len = 65535 (maximum value)
  • Mismatches between claimed length and actual payload length

With AddressSanitizer enabled, the out-of-bounds read crashes immediately. The fuzzer finds it in minutes.

Lesson

Input validation on attacker-controlled length fields is a fuzz target priority. Any protocol or format that has length-prefixed fields where the length is attacker-controlled should be fuzzed extensively with ASan.

The OpenSSL TLS handshake fuzzer in OSS-Fuzz has been running since 2016. Dozens of vulnerabilities have been found — none as severe as Heartbleed, but the approach would have prevented Heartbleed if applied earlier.

ImageMagick — The Long Tail of Format Vulnerabilities

ImageMagick supports dozens of image formats. In 2016, security researchers discovered "ImageTragick" (CVE-2016-3714) — a series of vulnerabilities allowing remote code execution via crafted image files.

But before and after that disclosure, fuzzing found hundreds of additional ImageMagick bugs. The OSS-Fuzz integration alone has found 200+ unique vulnerabilities in ImageMagick.

The bug pattern

ImageMagick parses dozens of formats with format-specific parsers, many written before modern secure coding practices. Common patterns:

  • Unchecked return values from allocation functions (malloc failure → null dereference)
  • Integer overflow in dimension calculations before allocation
  • Out-of-bounds reads in format parsers trusting attacker-provided sizes
  • Use-after-free in error handling paths

None of these are architecturally interesting. They're implementation bugs in format-specific code that sees less review than core code.

Why fuzzing finds them systematically

ImageMagick's format support is too broad for manual security review. Fuzzing with one harness per format — fuzz_png, fuzz_tiff, fuzz_svg, etc. — can run continuously without human attention.

OSS-Fuzz runs ~20 ImageMagick fuzzing targets continuously. Each target uses a seed corpus of valid examples of that format. Coverage grows over months and years, reaching parsing code that manual review would miss.

Lesson

Broad attack surface with multiple format parsers needs continuous fuzzing, not point-in-time reviews. Software that parses formats written by others (standards from the 1990s, or attacker-supplied data) should have fuzzing in CI and continuous fuzzing at scale.

libpng — Integer Overflow Before Memory Allocation

libpng (CVE-2002-1363 and others) contained a series of integer overflow vulnerabilities. The pattern: a width and height from the PNG header were multiplied to compute allocation size. If the product overflowed a 32-bit integer, a small allocation was made, then data was written beyond it.

// Simplified vulnerable pattern
uint32_t width = /* from PNG header, attacker-controlled */;
uint32_t height = /* from PNG header, attacker-controlled */;
uint32_t rowbytes = width * bytes_per_pixel;  // integer overflow if large
uint8_t *buffer = malloc(rowbytes * height);  // small allocation
// then writes width * height * bytes_per_pixel bytes...

Width = 0x80000000, bytes_per_pixel = 2 → rowbytes overflows to 0. malloc(0) returns a valid pointer. Buffer overflow on write.

Why fuzzing finds it

The fuzzer generates edge-case values for width and height fields. FuzzedDataProvider::ConsumeIntegralInRange or raw byte manipulation eventually hits boundary values. With ASan, the overflow is detected immediately.

Modern fuzz targets for PNG parsers use integer overflow sanitizers (UBSan with -fsanitize=integer-overflow) specifically to catch this class.

Lesson

Integer overflow in size calculations before allocation is endemic in legacy code. Add -fsanitize=unsigned-integer-overflow and -fsanitize=signed-integer-overflow to fuzz builds targeting format parsers.

Google Chrome — ClusterFuzz and Continuous Fuzzing at Scale

Chrome runs ClusterFuzz — Google's distributed fuzzing infrastructure — continuously against the Chrome codebase. The numbers:

  • ~5 billion test cases executed per day
  • 25,000+ bugs found since 2011
  • Average 1 high-severity bug found per day

V8 (JavaScript engine) alone has a dedicated fuzzer that generates valid JavaScript programs and executes them, finding type confusion bugs, JIT compiler errors, and memory safety issues.

What continuous fuzzing found in Chrome

ArrayBuffer detachment during iteration (CVE-2017-5121): A race condition between GC and ArrayBuffer access caused a use-after-free. The fuzzer generated JavaScript that triggered concurrent operations at the right timing.

Heap overflow in SVG parsing (CVE-2018-6120): SVG path parsing with specific control points overflowed a heap buffer. The fuzzer found the specific coordinate combination that triggered it.

Integer overflow in IPC message parsing (CVE-2020-6557): Chrome's inter-process communication deserialization contained an integer overflow. The fuzzer generated IPC messages with boundary-value fields.

The ClusterFuzz model

ClusterFuzz runs:

  1. Build integration: fuzz every commit to Chrome
  2. Continuous campaigns: dedicated machines running 24/7
  3. Crash deduplication: groups similar crashes by stack trace
  4. Automatic bisection: identifies which commit introduced the bug
  5. Notification: assigned to the committer who introduced it

The automation is critical. At 5 billion tests/day, manual crash review would be impossible. Deduplication reduces the signal to unique bugs; bisection makes them actionable.

Lesson

Continuous fuzzing at scale requires automation. Crash deduplication, bisection, and assignment automation are not optional — they're what makes a large-scale fuzzing program operationally feasible.

For teams without ClusterFuzz infrastructure, OSS-Fuzz provides equivalent infrastructure for open source projects.

OpenSSH — Timing Side-Channels and Protocol Bugs

OpenSSH's user enumeration bug (CVE-2018-15473) allowed an attacker to determine valid usernames by measuring server response times. Timing differences between valid and invalid usernames leaked information.

AFL and specialized timing-sensitive fuzzers found related protocol handling bugs in OpenSSH's authentication flow by generating crafted authentication sequences.

More recently, Terrapin attack (CVE-2023-48795) — a prefix truncation attack against the SSH Chacha20-Poly1305 cipher — was discovered through protocol analysis aided by fuzzing the SSH handshake sequences.

Lesson

Protocol fuzzers need to understand the state machine. Stateless fuzzing of protocol implementations misses bugs that require specific message sequences. AFL++'s IJON and snapshot fuzzing modes, and libFuzzer's coverage-guided state machine exploration, enable state-aware protocol fuzzing.

For cryptographic protocols, consider differential fuzzing — comparing two implementations of the same protocol to find divergent behavior.

SQLite — The Thoroughly Fuzzed Database

SQLite is perhaps the most extensively fuzzed software in existence. It ships its own fuzz testing infrastructure and has found and fixed thousands of bugs through internal fuzzing.

SQLite's fuzzing approach:

  • SQL query fuzzing (random valid SQL)
  • File format fuzzing (corrupted SQLite database files)
  • AFL++ with a comprehensive seed corpus of edge-case queries
  • libFuzzer targets for the core expression evaluator

The result: SQLite is known as one of the most reliable pieces of software in existence. It's embedded in every iOS device, Android device, and most desktop browsers.

What the bugs looked like

Most SQLite bugs found through fuzzing are edge-case handling in the query optimizer, type coercion between SQL types, and handling of corrupt database files. None catastrophic — because the fuzzing found them before they reached production.

Lesson

Regular, sustained fuzzing converts a historically buggy class of software (database engines, SQL parsers) into reliable infrastructure. The investment in SQLite's fuzzing program is why it runs correctly in safety-critical environments.

systemd — Process Manager Vulnerabilities

systemd 245 (CVE-2020-13529) contained a use-after-free in the DNS stub resolver. Fuzzing the DNS processing code with a crafted response triggered the bug — and it was severe enough to affect millions of Linux systems.

OSS-Fuzz runs systemd fuzz targets continuously. The DNS, DHCP, and network configuration code has each been fuzzed extensively.

Lesson

System-level daemons parsing network input are high-value fuzzing targets. They run as root, they handle untrusted network data, and they're often less aggressively security-reviewed than application code.

Building Your Bug-Finding Fuzzing Program

The case studies share common patterns:

Target selection matters more than fuzzer sophistication: The most valuable targets are those that process attacker-controlled data, use C/C++, handle complex formats, and run with elevated privilege.

Continuous fuzzing finds more than point-in-time campaigns: Most of the bugs above were found by fuzzers running for days, weeks, or months — not one-off campaigns.

Sanitizers convert latent bugs into crashes: Without ASan, many of these bugs would have been missed. Integer overflows that don't crash without UBSan. Out-of-bounds reads that need ASan to terminate the process.

Good seeds matter: Chrome's JavaScript fuzzer generates syntactically valid JavaScript. libpng fuzzers start with valid PNG files. The fuzzer spends time on interesting variations, not reconstructing format basics.

Corpus sharing amplifies results: OSS-Fuzz corpora represent years of accumulated coverage. Starting from an existing corpus dramatically reduces time to finding real bugs.

Summary

The history of fuzzing is the history of finding bugs that manual review and testing missed — bugs hidden in format parsing edge cases, in boundary conditions, in state transitions that normal usage never reaches.

The lesson from each case study: instrument the right targets, enable sanitizers, start with good seeds, and run continuously. The bugs will come.

Read more

Start now free