What Happens Inside a Program When It Crashes? Understanding Segmentation Faults

Complete Fuzzing Series — Blog 4 of N

Part Title Status
1 What Is Fuzzing and Why Does It Actually Work? :white_check_mark:
2 How a Fuzzer Works - Mutations, Seeds, and Corpus Explained :white_check_mark:
3 The Taxonomy: Every Type of Fuzzing Explained :white_check_mark:
4 - You are here What Happens Inside a Program When It Crashes :white_check_mark:
5 Sanitizers Your Fuzzing Superpowers :soon_arrow:
6+ More parts coming… :soon_arrow:

→ Next: Blog 5 - Sanitizers: Your Fuzzing Superpowers
← Previous: Blog 3 - The Taxonomy: Every Type of Fuzzing Explained


Before We Start - AFL Found Crashes. Now What?

In Blog 2, Jack ran AFL on his JPEG parser. Twenty minutes later he had this:

output/crashes/
    id:000000,sig:11
    id:000001,sig:11
    id:000002,sig:06

Three crash files sitting in a folder.

AFL’s job was done. It found something. It saved the inputs that caused the crashes.

But here is the question I sat with for a while after that.

What actually happened inside the program when it crashed?

AFL gives you a filename with sig:11 in it. That tells you nothing about what went wrong inside memory. Was it a buffer that overflowed? A pointer that pointed to nothing? Memory that got used after it was freed?

AFL does not know. AFL does not care. It just saw the program die and saved the evidence.

Figuring out what actually broke - that is your job as the researcher. And you cannot do that job if you do not understand what is happening inside a program’s memory when these crashes happen.

That is what this blog is about.

I want to be honest - when I first started digging into this, I did not know what a stack was. I did not know what a heap was. Words like “use-after-free” and “double free” sounded like advanced hacker stuff that was way above my level.

It is not. Once I understood how memory is laid out, every single crash type became obvious. Not memorization - just logic.

Let’s go through it the same way I learned it.


Part 1 - Memory: The Two Places Things Live

Before we talk about any crash, we need to talk about where things live inside a running program. Because every single crash we are going to study is just memory going wrong in a specific way.

Get memory clear and every crash type becomes obvious.

When your C program runs, it has memory divided into regions. Two of them matter most for understanding crashes.

PROGRAM MEMORY (simplified)

┌─────────────────────────────┐  ← High address
│                             │
│           STACK             │
│      (grows downward ↓)     │
│                             │
├─────────────────────────────┤
│                             │
│       (unused space)        │
│                             │
├─────────────────────────────┤
│                             │
│           HEAP              │
│      (grows upward ↑)       │
│                             │
├─────────────────────────────┤
│      Global variables       │
├─────────────────────────────┤
│      Program code           │
└─────────────────────────────┘  ← Low address

The Stack is where local variables live. When you call a function, its variables get created on the stack automatically. When the function finishes, that memory gets cleaned up automatically. You do not manage it. The program does it for you.

The Heap is where dynamic memory lives. When your program needs memory that it decides on at runtime not at compile time it asks for it using malloc(). The heap gives it a chunk. But here is the important part: you are responsible for giving it back. You do that with free(). The heap does not clean itself. If you forget to free that memory stays allocated forever until the program exits. That is called a memory leak.

Let me show you the difference in actual code so it is crystal clear:

// STACK — automatic, you do nothing
void foo() {
    char buf[100];   // created automatically when foo() starts
    buf[0] = 'A';
}                    // buf disappears automatically when foo() ends
                     // you did not call malloc. you did not call free.
                     // the stack handled it.


// HEAP — manual, you manage everything
void bar() {
    char *buf = malloc(100);  // YOU ask for 100 bytes
    buf[0] = 'A';
    free(buf);                // YOU give it back
}                             // if you forget free() — memory leak

One automatic. One manual. That is the whole difference.

Now the question a lot of beginners ask here why does the heap even exist? Why not put everything on the stack?

Because the stack has a fixed, limited size. It is fast but small. And you have to know the size at compile time. If you are writing a program that reads a file and you do not know how big that file will be until the user actually runs it you cannot use the stack. You use malloc() to ask for exactly the right amount of memory at runtime.


Part 2 - The Stack Frame: What Lives Next to Your Variable

Now I need to go one level deeper on the stack. Because this is the thing that makes stack overflows dangerous not just “the program crashes” but why it crashes in a specific way.

Every time you call a function, the program does not just store your local variables on the stack. It stores a whole stack frame a structured chunk of memory that contains everything needed to run that function and return from it properly.

A stack frame contains three important things:

┌─────────────────────────────┐
│      local variables        │  ← your char buf[100], int x, etc
├─────────────────────────────┤
│      saved registers        │  ← CPU state saved before the call
├─────────────────────────────┤
│      return address         │  ← WHERE to go back after function ends
└─────────────────────────────┘

That return address is the critical one.

Think about it. When main() calls vulnerable(), the program needs to remember: “after vulnerable() finishes, come back to this exact line in main().” That “come back here” address is what gets stored as the return address.

When vulnerable() finishes, the CPU reads that return address and jumps back to it. Normal execution continues.

Now here is the thing that matters for crashes.

The return address sits on the stack. Right next to your local variables. There is nothing separating them. They are just bytes sitting next to each other in memory.

STACK (growing downward)

┌─────────────────────────────┐  ← High address
│      return address         │  ← "come back to main() line 12"
├─────────────────────────────┤
│      saved registers        │
├─────────────────────────────┤
│   buf[7]                    │
│   buf[6]                    │
│   buf[5]                    │
│   buf[4]                    │  ← buf lives here, 8 bytes
│   buf[3]                    │
│   buf[2]                    │
│   buf[1]                    │
│   buf[0]                    │  ← buf starts here
└─────────────────────────────┘  ← Low address

If you write more data into buf than it can hold that data does not stop at buf[7]. It keeps going upward. Into saved registers. Into the return address.

That is a stack buffer overflow. And that is exactly what we are about to look at.


Part 3 — Stack Buffer Overflow: The Classic Crash

Here is the vulnerable code. Read every line carefully I am going to explain each one.

void vulnerable(char *input) {
    char buf[8];        // line 1: allocate 8 bytes on the stack
    strcpy(buf, input); // line 2: copy input into buf — no size check
}

int main() {
    vulnerable("AAAAAAAAAAAAAAAA"); // 16 A's — way more than 8 bytes
    return 0;
}

Line 1: char buf[8] this creates 8 bytes of space on the stack. Valid positions are buf[0] through buf[7]. That is it. Eight slots.

Line 2: strcpy(buf, input) strcpy copies bytes from input into buf. Here is the critical thing about strcpy: it does not check the size. It just keeps copying until it hits a null terminator \x00. If input is 16 bytes, it copies 16 bytes. It does not care that buf only has 8 bytes of space.

So what happens to those extra 8 bytes?

They go into whatever memory comes after buf on the stack. Which is the saved registers. And then the return address.

Watch it happen:

BEFORE strcpy stack looks like this:

┌─────────────────────────────┐
│   return address: 0x08048612│  ← legitimate address back to main()
├─────────────────────────────┤
│   saved registers           │
├─────────────────────────────┤
│   buf[7] = 00               │
│   buf[6] = 00               │
│   buf[5] = 00               │
│   buf[4] = 00               │
│   buf[3] = 00               │  ← empty, waiting for data
│   buf[2] = 00               │
│   buf[1] = 00               │
│   buf[0] = 00               │
└─────────────────────────────┘


AFTER strcpy with 16 A's (0x41 in hex):

┌─────────────────────────────┐
│   return address: 0x41414141│  ← OVERWRITTEN with A's ← CORRUPTED
├─────────────────────────────┤
│   41 41 41 41               │  ← saved registers — also trashed
├─────────────────────────────┤
│   buf[7] = 41               │
│   buf[6] = 41               │
│   buf[5] = 41               │
│   buf[4] = 41               │  ← buf filled up at byte 8
│   buf[3] = 41               │
│   buf[2] = 41               │
│   buf[1] = 41               │
│   buf[0] = 41               │  ← strcpy started here
└─────────────────────────────┘

Now vulnerable() finishes. The CPU reads the return address to go back to main(). It reads 0x41414141. It tries to jump there. That address does not exist anywhere in the program.

The OS kills the process immediately.

CRASH. Signal 11. SIGSEGV.

AFL saves the file. Jack sees sig:11 in his crashes folder. This is that moment.

Now here is the important thing to understand about why this is dangerous beyond just crashing:

AFL sent AAAAAAAAAA... randomly it had no idea buf was 8 bytes. But an attacker is not AFL. An attacker can send a carefully crafted input where the bytes that land on the return address are not random A’s they are a specific address the attacker chose. An address that points to code the attacker wants to run.

The program then jumps to that address and executes the attacker’s code. That is code execution from a buffer overflow. That is why this crash type is one of the most serious in security research.

But that is a deeper topic. For now understand the mechanics. Overflow the buffer, corrupt the return address, crash.


Part 4 - Heap Buffer Overflow: Same Idea, Different Place, Different Danger

Stack overflow corrupts the return address. Heap overflow corrupts something different the heap chunk header.

Let me show you what I mean.

When you call malloc(100), the heap does not just give you exactly 100 bytes. It gives you 100 bytes plus a small header attached to the front of that chunk. That header is metadata internal bookkeeping information the heap manager uses to track allocations.

It contains things like: how big is this chunk, is it free or in use, where is the next chunk.

HEAP MEMORY LAYOUT

┌─────────────────────────────┐
│   chunk header              │  ← heap manager's metadata
│   (size, flags, pointers)   │     size of chunk, in-use flag, etc
├─────────────────────────────┤
│   buf[0]                    │
│   buf[1]                    │
│   ...                       │  ← your 100 bytes what malloc() returned
│   buf[99]                   │  ← last valid byte
├─────────────────────────────┤
│   NEXT chunk header         │  ← next allocation starts here
│   (size, flags, pointers)   │
├─────────────────────────────┤
│   next chunk data           │
└─────────────────────────────┘

Now look at this vulnerable code:

void vulnerable(int size) {
    char *buf = malloc(size);  // allocate 'size' bytes on heap
    buf[size] = 'A';           // write ONE byte past the end
    free(buf);
}

buf has size bytes. Valid positions: buf[0] through buf[size-1]. But the code writes to buf[size] one byte past the last valid position.

That one byte lands on the next chunk’s header. The heap manager’s metadata is now corrupted.

HEAP AFTER OVERFLOW BY ONE BYTE

┌─────────────────────────────┐
│   chunk header              │  ← still fine
├─────────────────────────────┤
│   buf[0]                    │
│   ...                       │
│   buf[size-1]               │  ← last valid byte
├─────────────────────────────┤
│   buf[size] = 'A'           │  ← WRITTEN HERE — one byte past end
│                             │     this byte belongs to next chunk's header
│   NEXT chunk header         │  ← CORRUPTED
│   (garbage now)             │
└─────────────────────────────┘

Now when free() runs and tries to read that next chunk’s header to manage memory it reads garbage. The heap manager’s internal state is broken.

This is where heap overflow is different from stack overflow.

Stack overflow crashes the moment the function returns the corrupted return address gets read immediately. The bug and the crash are at the same location.

Heap overflow does not crash immediately. The program keeps running. The header is corrupted but nothing reads it yet. Then, maybe 500 lines later, another malloc() runs. It walks through the heap chunks reading headers to find a free one. It hits the corrupted header. Crash at a completely different place in the code than where the actual bug was.

STACK OVERFLOW:
  overflow happens → function returns → CRASH immediately
  bug location = crash location ✅ easy to find

HEAP OVERFLOW:
  overflow happens → program keeps running → later malloc/free reads corrupt header → CRASH
  bug location ≠ crash location ❌ harder to find

This is why heap bugs are harder to debug. When you see a heap crash in AFL’s output, the crash file shows you the input that triggered the issue but the actual overflow might have happened long before the crash.


Part 5 - Use-After-Free: The Ghost Memory Bug

This one confused me the most when I first read about it. Once it clicked, it became obvious. Let me walk through it the way it finally made sense to me.

char *buf = malloc(100);  // step 1: allocate 100 bytes, buf points to 0x1000
strcpy(buf, "hello");     // step 2: use it
free(buf);                // step 3: free it — tell heap "I'm done with this"
                          //
buf[0] = 'A';             // step 4: USE IT AGAIN — BUG

Step 3 is where ownership ends. After free(buf), that memory at 0x1000 no longer belongs to your program. You handed it back to the heap manager. The heap manager marks it as available ready to be given out to the next malloc() call.

But here is what C does NOT do: it does not change the value of buf. buf still holds the address 0x1000. The pointer is still there. Pointing at memory you no longer own.

This pointer is now called a dangling pointer.

AFTER free(buf):

Your code:        buf = 0x1000   ← still holds the old address
Heap manager:     0x1000 = available ← marked as free, can give to anyone

Now two things can happen:

Scenario A - you write through the dangling pointer:

buf[0] = 'A';  // write to 0x1000

You are writing to memory the heap manager considers free. If the heap manager has already given that chunk to another malloc() somewhere else in the program you are overwriting someone else’s data. Two parts of the program now share the same memory without knowing it.

0x1000 ←── buf        (dangling pointer, your old allocation)
0x1000 ←── ptr2       (new allocation the heap gave to someone else)

buf writes 'A' to 0x1000
ptr2 reads from 0x1000 — gets garbage
ptr2 writes to 0x1000 — corrupts what buf wrote

Both are writing to the same address. Neither knows.
CRASH — or silent corruption — or attacker control.

Scenario B — nobody else got that chunk yet:

You write to 0x1000 and it still looks fine for now. But later, malloc() gives that chunk to someone else. They start using it. They find your leftover data there. Unpredictable behavior.

This is why use-after-free is one of the most exploited vulnerability classes in modern security research especially in browsers. An attacker can time their allocations so they get that freed chunk, plant controlled data there, and then your dangling pointer reads the attacker’s data as if it were trusted program data.


Part 6 - Double Free: Freeing the Same Memory Twice

This one is short. Once you understand malloc and free, double free is obvious.

char *buf = malloc(100);  // malloc gives you chunk at 0x1000
free(buf);                // first free — correct. heap takes it back.
free(buf);                // second free — BUG. freeing it again.

After the first free(), that chunk at 0x1000 goes back into the heap manager’s internal tracking a list of available chunks it can hand out.

After the second free(), the heap manager tries to add that same 0x1000 chunk to its list again. But it is already there. The heap manager’s internal records now contain the same address twice.

Think of the heap manager keeping a notebook of available chunks:

HEAP MANAGER'S NOTEBOOK

After first free():
  Available: [ 0x1000, 0x2000, 0x3000 ]   ← correct

After second free() of same address:
  Available: [ 0x1000, 0x1000, 0x2000, 0x3000 ]  ← 0x1000 listed twice

Now when two different malloc() calls run, they can both get 0x1000. Two completely separate parts of your program, each thinking they own that memory exclusively. Both writing to the same address. Heap corruption. Crash.

In AFL’s output, double free often shows up as sig:06 SIGABRT. Modern heap implementations detect double free and intentionally abort the program rather than letting the corruption silently continue.


Part 7 — Null Pointer Dereference: The Zero Address Crash

This is the simplest crash type. But a lot of beginners are confused about why it crashes. Let me make it clear.

char *buf = NULL;   // buf = 0x0  — NULL means address zero
buf[0] = 'A';      // try to write to address 0x0

NULL is just the value 0. When you write char *buf = NULL, you are saying: buf holds the address 0x0. That address means “nothing” it is a way to say “this pointer is not pointing at any real memory yet.”

Now buf[0] in C is just pointer arithmetic. It means: go to address buf, move 0 bytes forward, read/write there. So:

buf[0] = 'A'
↓
write 'A' to address (buf + 0)
↓
write 'A' to address (0x0 + 0)
↓
write 'A' to address 0x00000000

Now here is what happens at the hardware level. The CPU tries to access address 0x00000000. Before it does, the Memory Management Unit (MMU) a hardware component that controls memory access checks if this process is allowed to access that page of memory.

Modern operating systems deliberately leave page 0 unmapped. No process is allowed to touch address zero. The OS does this on purpose so that null pointer bugs crash loudly instead of silently corrupting memory.

CPU tries to access 0x00000000
↓
MMU checks: is this process allowed here?
↓
Answer: NO page 0 is unmapped
↓
Hardware page fault
↓
OS receives fault signal
↓
OS sends SIGSEGV to the process
↓
Process crashes sig:11

Most null pointer dereferences happen because a function returns NULL on failure and the programmer forgets to check before using the result:

char *buf = malloc(100);
// malloc failed and returned NULL
// programmer did not check if buf is NULL
buf[0] = 'A';   // crash — SIGSEGV

Null pointer dereferences are usually not directly exploitable on modern systems the OS protection makes sure of that. But they always tell you something useful: the programmer forgot to handle a failure case. And forgotten failure cases often have worse bugs hiding nearby.

One interesting note on old embedded devices and IoT hardware without an MMU, address 0x0 might be real valid memory. On those devices, a null pointer dereference does NOT crash. It silently reads or writes to address zero. This makes bugs much harder to detect on embedded targets — and it is one reason IoT fuzzing has unique challenges we will cover in Phase 8 of this series.


Part 8 - Integer Overflow Leading to Crash: The Math Bug

This one is sneaky because the bug is not in memory directly. It is in the math that happens before memory gets touched.

Look at this code carefully:

void vulnerable(int len, char *input) {
    char *buf = malloc(len + 1);  // allocate len+1 bytes
    memcpy(buf, input, len);      // copy len bytes into buf
}

At first glance this looks safe. You are allocating len + 1 bytes and only copying len bytes. Always one byte of extra space. Clean.

But what if len is 2147483647?

That number is INT_MAX the maximum value a signed 32-bit integer can hold. Every bit except the sign bit is set to 1.

INT_MAX in binary:
0 1111111 11111111 11111111 11111111
↑
sign bit = 0 (positive)

Now add 1 to it:

INT_MAX + 1:
1 0000000 00000000 00000000 00000000
↑
sign bit flipped to 1 = NEGATIVE

The result wraps around to -2147483648. The most negative number a 32-bit signed integer can hold.

len             =  2147483647   (INT_MAX)
len + 1         = -2147483648   (wrapped — now negative)
malloc(-2147483648)             ← what does malloc do with a negative size?

malloc receives a negative number. Internally it treats size as an unsigned value so -2147483648 becomes an enormous positive number. malloc either fails and returns NULL, or allocates a tiny amount. Either way, the buffer is nowhere near big enough to hold the data.

Then memcpy tries to copy 2147483647 bytes into that tiny or null buffer.

CRASH.

THE MATH BROKE BEFORE MEMORY WAS TOUCHED:

  programmer thinks:  len + 1 = safe, always bigger
  reality:            len + 1 = negative when len = INT_MAX
  malloc gets:        garbage size
  memcpy does:        overflow into whatever comes after the tiny buffer
  result:             crash

This class of bug is called an integer overflow the arithmetic result exceeds what the integer type can hold, so it wraps around to a wrong value.

They are common in size calculations, length fields, loop counters anywhere math is done on values that come from user input. A fuzzer naturally finds these because it tries extreme values 0, -1, 65535, 2147483647 — exactly the values where integer math breaks.


Part 9 — How AFL Actually Sees a Crash

Okay. We have covered all six crash types. Now let’s come back to AFL’s perspective.

AFL does not look inside your program’s memory. It does not know if a stack overflowed or a heap header got corrupted. It watches one thing only: how did the child process exit?

When a program crashes, the OS sends it a signal a notification that something went wrong. AFL catches that signal from the child process.

WHAT AFL WATCHES:

Target program runs
↓
Something goes wrong in memory
↓
OS sends a signal to the process
↓
AFL catches that signal from the child
↓
AFL saves the input file that caused it
↓
AFL names the file with the signal number
↓
AFL forks a new child and continues fuzzing

The three signals you will see most in AFL crash output:

SIGSEGV  =  sig:11  →  segmentation fault
                        invalid memory access
                        stack overflow, null pointer, UAF, heap overflow

SIGABRT  =  sig:06  →  program called abort()
                        heap corruption detected by allocator
                        double free, heap metadata corruption

SIGFPE   =  sig:08  →  floating point exception
                        division by zero, some integer math errors

Now look at what AFL actually names the crash file:

id:000000,sig:11,src:000003,op:havoc,rep:4
   ↑        ↑       ↑          ↑        ↑
   crash    signal  which       what     how many
   number   type    corpus      mutation  times to
                    file this   strategy  reproduce
                    came from   was used

Every piece of that filename is information. src:000003 tells you which corpus file was being mutated when this crash was discovered. op:havoc tells you AFL was in its aggressive random mutation phase. rep:4 tells you AFL confirmed the crash reproduces 4 times.

AFL saves the exact bytes that caused the crash. You can take that file right now and feed it manually to the program:

./jpeg_parser output/crashes/id:000000,sig:11,src:000003,op:havoc,rep:4

If it crashes again that is a confirmed, reproducible bug. Your job as the researcher now begins. Figure out which of our six crash types it is. Is it exploitable?

That process is called crash triage and we will do a full blog on it in Blog 18.


Part 10 - The Bug AFL Never Found

Jack ran AFL for 24 hours. He found 3 crashes. He looked at his terminal:

total execs    :  86,400,000
corpus size    :  312 inputs
crashes found  :  3
branches hit   :  8,420 / 12,000

He felt good. He fixed those 3 crashes. He shipped the update.

But there was a fourth bug.

It was in the image processing function a section that read pixel data. When a specific malformed header was present, the parser read 10 bytes past the end of the pixel buffer. It got garbage data. It processed that garbage data silently. No crash. The program kept running like nothing happened.

BUG HAPPENS:
  out-of-bounds read parser reads 10 bytes past end of buffer
  ↓
  program processes garbage data
  ↓
  program keeps running
  ↓
  NO CRASH
  ↓
  AFL sees: no signal from child process
  ↓
  AFL thinks: nothing interesting happened
  ↓
  AFL discards that input
  ↓
  Bug never recorded. Never found.

AFL only detects crashes moments where the program terminates with a signal. If the program keeps running, AFL has no way to know something bad happened.

But something bad DID happen. Memory was read that should not have been read. On a different system, that leaked memory might contain a password, an encryption key, or sensitive user data. On an attacker’s crafted input, that out-of-bounds read might be the first step of a more serious exploit.

AFL missed it completely. Not because AFL is bad — because AFL was only watching for crashes and this bug did not crash.

This is the problem that Blog 5 solves.

Sanitizers are tools that instrument your program to detect bad memory events the moment they happen — even if they would not normally crash. An out-of-bounds read becomes a crash. A use of uninitialized memory becomes a crash. Memory leaks get reported. The silent bugs get loud.

Without sanitizers, you are only finding bugs that happen to crash on their own. With sanitizers, you find everything.


What I Found Confusing (And Now Don’t)

“What’s the difference between the stack and the heap?”
Stack is automatic local variables, managed by the program, cleaned up when the function returns. Heap is manual you call malloc() to get it, you call free() to give it back. Forget to free and you have a memory leak.

“Why is the return address on the stack next to my variables? That seems like a design flaw.”
It is the way stack frames are designed — all the context needed for a function call lives together in one contiguous chunk. The danger only exists because C allows writing past the end of a buffer with no automatic checks. Languages like Python and Java do not have this problem because they check bounds automatically.

“Stack overflow crashes immediately. Heap overflow crashes later. How do I know where the real bug is?”
You use sanitizers (Blog 5) and a debugger. The sanitizer detects the overflow the moment it happens and reports the exact line. Without sanitizers, the crash location can be hundreds of lines away from the actual bug.

“Double free shows up as sig:06, not sig:11. Why?”
Modern heap allocators (like glibc’s malloc) detect double free and intentionally call abort() to crash the program before the corruption spreads further. abort() sends SIGABRT = signal 6. The allocator is trying to help you it detected something wrong and stopped immediately rather than letting silent corruption continue.

“AFL saved a crash file but I cannot reproduce it manually. Why?”
A few reasons. The crash might depend on timing (race condition). It might depend on memory layout that changes between runs (ASLR). AFL uses the rep field in the filename to tell you how many times it confirmed the crash. Low rep numbers are less reliable. High rep numbers are solid.

“If AFL misses silent bugs, what is the point of fuzzing at all?”
Fuzzing finds the bugs that DO crash. Sanitizers find the ones that do not. You use both together. AFL + AddressSanitizer (ASAN) is the standard combination in serious security research. Blog 5 covers exactly this.


Glossary

Stack - A region of memory for local variables and function call information. Automatically managed. Created when a function is called, cleaned up when it returns. Fast but limited in size.

Heap - A region of memory for dynamic allocations. Manually managed with malloc() and free(). Large and flexible. You are responsible for freeing what you allocate.

Stack frame - The chunk of stack memory allocated for one function call. Contains local variables, saved CPU registers, and the return address.

Return address - The memory address stored on the stack that tells the CPU where to go back to after a function finishes. Corrupting this is the core danger of stack buffer overflows.

malloc() - A C function that requests memory from the heap. Returns a pointer to the allocated chunk, or NULL on failure. You must call free() when done.

free() - A C function that returns heap memory back to the heap manager. After calling free(), that memory no longer belongs to your program.

Buffer overflow - Writing more data into a buffer than it has space for. The extra bytes overwrite adjacent memory. On the stack this corrupts the return address. On the heap this corrupts chunk headers.

Stack buffer overflow - A buffer overflow that happens on the stack. Overwrites the return address. Program crashes when the function tries to return. Classic exploit primitive for code execution.

Heap buffer overflow - A buffer overflow that happens on the heap. Overwrites the next chunk’s metadata header. Crash happens later when malloc/free reads that corrupted header.

Heap chunk header - Metadata stored by the heap manager at the start of each allocated chunk. Contains size, flags, and pointer information used to track and manage allocations.

Use-after-free (UAF) - Using a pointer after the memory it points to has been freed. The freed memory may be reallocated to another part of the program, causing two pointers to share the same address with neither knowing.

Dangling pointer - A pointer that still holds an old address after the memory at that address has been freed. The pointer is not null it still points somewhere but that somewhere no longer belongs to your program.

Double free - Calling free() twice on the same pointer. Corrupts the heap manager’s internal free list. Can allow the same chunk to be handed out twice to two different callers.

Null pointer dereference - Trying to read or write through a pointer that holds the value NULL (address 0x0). The OS keeps page 0 unmapped so this always crashes with SIGSEGV. Usually caused by forgetting to check if malloc() or another function returned NULL.

Integer overflow - When arithmetic on an integer produces a result that exceeds the maximum value the type can hold. The value wraps around to a wrong number. Common in size calculations INT_MAX + 1 becomes a large negative number, causing under-allocation and subsequent overflow.

Signal - A notification the OS sends to a process when something goes wrong. SIGSEGV (11) for invalid memory access. SIGABRT (6) for intentional abort. SIGFPE (8) for math errors. AFL catches these signals to detect crashes.

SIGSEGV - Signal 11. Segmentation fault. The process tried to access memory it was not allowed to access. Most buffer overflows, null pointer dereferences, and UAF crashes produce this signal.

SIGABRT - Signal 6. Abort. The process called abort() intentionally usually because the heap allocator detected corruption (double free, heap overflow) and chose to stop the program rather than let it continue in a corrupted state.

Crash triage - The process of taking a crash file saved by AFL and manually investigating it to confirm reproducibility, identify the crash type, and determine if it is exploitable.

Silent bug - A memory bug that does not cause a crash. The program continues running while bad memory access happens in the background. Out-of-bounds reads, use of uninitialized memory, and memory leaks are examples. AFL cannot detect these without sanitizers.

Sanitizer - A compile-time instrumentation tool that detects memory errors the moment they happen, even if they would not normally crash. AddressSanitizer (ASAN) is the most common. Makes silent bugs loud. Covered in Blog 5.

ASLR - Address Space Layout Randomization. An OS security feature that randomizes where the stack, heap, and libraries are placed in memory on each run. Makes crashes sometimes non-reproducible between runs.


→ Next: Blog 5 - Sanitizers: Your Fuzzing Superpowers
← Previous: Blog 3 - The Taxonomy: Every Type of Fuzzing Explained