What Is Fuzzing and Why Does It Actually Work?

Complete Fuzzing Series - Blog 1 of N

# Blog Link
1 What Is Fuzzing and Why Does It Actually Work? What Is Fuzzing and Why Does It Actually Work?
2 The Taxonomy: Every Type of Fuzzing Explained How a Fuzzer Works: Mutations, Seeds, and Corpus Explained
3 What Happens Inside a Program When It Crashes Types of Fuzzing Explained: The Complete Fuzz Testing Guide
4 Sanitizers: Your Fuzzing Superpowers coming soon
5 Coverage: The Feedback That Makes Fuzzing Smart coming soon

Fuzzing is one of the most powerful vulnerability discovery techniques in modern security research. In this guide, you’ll learn what fuzzing is, how coverage-guided fuzzers like AFL++ explore program code, and why crashes often signal serious security vulnerabilities.

I want to be honest with you before we start.

When I first heard the word “fuzzing,” I thought it was just throwing random garbage at a program until something breaks. Like a monkey smashing a keyboard and calling it security research.

That’s not what it is. And the part I was wrong about is actually the most important part.

Let me explain.


What Developers Actually Test (And What They Miss)

When a developer builds a program that reads a username, they test it like this:

"john"     → works fine ✅
"john123"    → works fine ✅
""           → empty string, handled ✅

They’re happy. They ship it.

But they never tested:

A username that is 10,000 characters long
A username that is just spaces "     "
A username with NULL bytes "ram\x00esh"
A username with emoji "ram🔥"

Not because they’re bad developers. Just because when you’re building something, your brain stays in the “normal use” zone. You think about inputs that make sense. You don’t think about inputs that are technically possible but nobody would ever actually type.

Those untested inputs that’s what I call gaps.

The code exists to handle them. The program will do something if you send those inputs. But nobody ever checked what. Maybe it handles them fine. Maybe it crashes. Maybe it silently corrupts memory and keeps running like nothing happened.

Fuzzing finds those gaps. Automatically. Without you having to think of them.


What Fuzzing Actually Is

Fuzzing is an automated technique that generates unexpected inputs, feeds them to a program, and watches what happens.

That “random garbage” idea I had? Partially right. But the important word is automated.

A human tester might think of 20 edge cases in a day. A fuzzer generates and tests millions of inputs per hour. It finds gaps that no human would ever think to check not because it’s smarter, but because it never gets bored and never assumes an input is too weird to try.

Fuzzing is not testing what you expect. It’s finding what you never expected.

That one line is the whole idea.


Why a Crash Is Not Just an Error

Here’s something that took me a while to understand.

When a normal user sees a crash, they think: “oh, the program broke, how annoying.”

When a security researcher sees a crash, they think: “something happened that the developer didn’t account for. What exactly happened? And can it be used?”

A crash is a signal. It tells you the program hit a state it couldn’t handle. And depending on what kind of crash it is, it might be exploitable.

For example:

  • Buffer overflow → the program wrote past the end of memory it owned. An attacker might be able to control what gets overwritten. That can mean running arbitrary code.
  • Use after free → the program used memory it already freed. An attacker can sometimes control that freed memory. Very exploitable.
  • Null pointer dereference → the program tried to read address zero. Usually not directly exploitable, but tells you the logic is broken somewhere.

Not every crash is a vulnerability. But every crash is worth looking at. It’s the program telling you: “I found a situation I wasn’t designed for.”

The fuzzer’s job is to find those situations. Your job as a researcher is to understand what they mean.


But Wait Can’t a Fuzzer Just Try Everything?

Okay so when I first learned about fuzzing, my immediate thought was:

“Why don’t we just try every possible input? Like, brute force the whole thing?”

Seemed logical to me. Just throw everything at the program until something breaks. Simple.

Then I did the math. And I felt a little stupid.

Here’s what I mean.

Let’s say a program takes a 4-byte input. That’s tiny. Smaller than a single word. How many possible combinations can 4 bytes have?

Before I answer that, let me back up for a second because I didn’t understand bytes and bits when I started, and nobody explained it cleanly.

A bit is a single 0 or 1. That’s it. The smallest unit of data a computer works with.

A byte is 8 bits. So 4 bytes = 32 bits.

Now here’s where it gets wild. Each bit can be either 0 or 1. So for every bit you add, the number of possible combinations doubles.

  • 1 bit → 2 possibilities (0 or 1)
  • 2 bits → 4 possibilities (00, 01, 10, 11)
  • 8 bits (1 byte) → 256 possibilities
  • 32 bits (4 bytes) → 4,294,967,296 possibilities

Over 4 billion combinations. For just 4 bytes.

And a fast fuzzer can run maybe 10,000 inputs per second. So to try every single 4-byte combination:

4,294,967,296 ÷ 10,000 = 429,496 seconds ≈ 5 days

5 days. Non-stop. Just for 4 bytes.

Real program inputs are hundreds or thousands of bytes long. The math doesn’t just get hard it becomes completely impossible. Not “takes a while” impossible. “The universe ends first” impossible.

So the “just try everything” idea I had? Dead on arrival.

This is why purely random fuzzing is useless at scale. You need something smarter something that doesn’t waste time trying the same dead ends over and over.

That’s exactly what coverage-guided fuzzing solves. But we’ll get to that in a moment.


How Smart Fuzzers Actually Work- The Building Analogy

Imagine the program is a building with many rooms. Some rooms are easy to find they’re right at the entrance. Some are deep inside, behind locked doors that only open if you have the right input.

A dumb fuzzer wanders randomly. It might visit the same 3 rooms a million times and never find the hidden ones.

A smart fuzzer keeps a map.

Every time it sends an input, it watches which rooms that input actually entered which parts of the code actually executed. If a new input enters a room nobody has been in before, the fuzzer marks it: interesting. Keep this input. Use it as a base for more mutations.

If an input visits only rooms, we’ve already seen throw it away.

Over thousands of iterations, the fuzzer builds a map of the entire building and systematically explores every corner of it.

This is called coverage-guided fuzzing. The fuzzer tracks code coverage how much of the program has been reached and always tries to push that number higher.

Input A → touches functions 1, 2, 3       → not new, discard
Input B → touches functions 1, 2, 3, 4    → new room! keep it
Input C (mutated from B) → touches 1,2,3,4,5,6 → even more new rooms! keep it

The fuzzer is not random. It’s building a map and exploring every corner it hasn’t seen yet.


What About Error Handling Doesn’t the Program Reject Bad Inputs?

This is a great question and I had it too.

Yes, developers write validation checks:

if (input_length > MAX_LENGTH) {
    return ERROR;
}

But here’s the thing. Those checks aren’t always there. They aren’t always complete. And sometimes the check itself has a bug.

The fuzzer sends malformed data deep into the program. Most of the time the program says “invalid input, goodbye.” But sometimes the malformed data slips past the check and reaches a function that was never designed to handle it.

That’s where crashes live. And that’s where vulnerabilities live.

Think of it like a building with security guards at some doors but not all. Most weird inputs get stopped at the entrance. But if one slips through to a room that never expected visitors that’s your finding.


Testing vs Fuzzing What’s the Real Difference?

People confuse these two all the time. Here’s the simplest way I can put it:

Testing: You think of inputs yourself and check if the program behaves correctly. You’re working from what you know.

Fuzzing: Automated, coverage-guided, keeps mutating inputs to reach new code paths. It finds what you never knew to look for.

Testing checks your assumptions. Fuzzing attacks them.

A development team might have 1,000 hand-written test cases. A fuzzer running for 24 hours might generate and test 100 million inputs. The fuzzer doesn’t know anything about the program’s logic it just keeps pushing until something breaks.


One More Thing Not All Bugs Crash

I want to leave you with something that will make more sense as we go deeper in this series.

Some of the most dangerous bugs never crash. Memory gets corrupted silently. Data gets leaked quietly. The program keeps running like nothing happened.

Crashes are the loud bugs. The ones that yell at you. Those are actually the easy ones to find with a fuzzer.

The silent bugs are harder. A program can read memory it shouldn’t, or use uninitialized data, and just keep going. No crash. No signal. Nothing.

That’s why in Blog 5 we’re going to talk about sanitizers tools that make the silent bugs loud. They instrument the program to scream when something bad happens even if it wouldn’t have crashed on its own.

For now, just keep this in mind: crashes are the beginning, not the end.


What I Found Confusing (And Now Don’t)

“Isn’t fuzzing just random inputs?” - No. Random fuzzing is useless at scale. Smart fuzzers track code coverage and only keep inputs that reach new code. They’re guided, not random.

“Why would a fuzzer keep sending ‘wrong’ data if the program rejects it?” - Because validation isn’t always complete. Missing or incomplete error handling means malformed data sometimes gets through. When it does crash, security issue.

“Is every crash a vulnerability?” - No. But every crash is a signal worth investigating. The crash type tells you what happened. Your job is to figure out if it can be exploited.


What We Learned

Fuzzing - An automated technique that generates unexpected inputs, feeds them to a program, and watches what happens. The goal is to find crashes and bugs that developers never thought to test for.

Gap - Any input the developer never thought about. The code exists to handle it, but nobody tested it. Feed it to the program and it might crash, misbehave, or do something unintended.

Coverage-guided fuzzing - A type of fuzzing where the fuzzer tracks which parts of the code an input actually executes. Inputs that reach new code are kept and mutated further. Inputs that reach only already-seen code are discarded.

Code coverage - A measure of how much of a program’s code has been executed. Fuzzers try to maximize this. Higher coverage = more of the program has been explored = more chances to find bugs.

Mutation - Taking an existing input and slightly modifying it flipping bits, changing values, inserting bytes to produce new inputs that might trigger different behavior.

Crash - When a program hits a state it can’t handle and terminates. In security research, a crash is a signal it means the program found a situation it wasn’t designed for, which might be exploitable.

Buffer overflow - A crash type where a program writes past the end of memory it owns. Potentially exploitable for code execution.

Use after free (UAF) - A crash type where a program uses memory it already freed. Frequently exploitable.

Null pointer dereference - A crash type where a program tries to read or write address zero. Usually not directly exploitable but indicates broken logic.

Silent bug - A bug that doesn’t cause a crash. Memory corruption or data leakage that the program doesn’t notice. Harder to find, often more dangerous.


→ Next: Blog 2 - What Happens Inside a Fuzzer: Mutations, Seeds, and Corpus

← Series Start: You are here