What Is a Buffer Overflow? The Out-of-Bounds Write Behind Real CVEs

- Buffer overflow: writing past the end of a fixed-size memory region, silently clobbering adjacent data without an immediate crash
- Stack buffer overflows are the most dangerous because they can overwrite the return address, redirecting execution to attacker-controlled code
- Heap buffer overflows corrupt malloc/free metadata, causing crashes in unrelated code long after the bad write happens
- Managed languages (Python, Java, Go) prevent this entirely with runtime bounds checking; C and C++ skip it for performance, trusting the programmer
- Stack canaries, ASLR, and AddressSanitizer are the three main mitigations: they don't eliminate the bug but make it harder to exploit and easier to detect
- Off-by-one errors are the most common source: allocate n bytes, write n+1, and the null terminator lands outside your buffer
You allocate 8 bytes. You write 24. Your program doesn't crash immediately. It just starts doing something wrong, somewhere else, for reasons that feel completely unrelated to the thing you just did.
That's a buffer overflow: writing past the end of a fixed-size region of memory and silently clobbering whatever lives next to it. One of the oldest, most studied bugs in software, and it still shows up in production code every year because C doesn't have opinions about what you do with memory.
What a Buffer Actually Is
A buffer is just a contiguous block of memory with a fixed size. When you declare char name[16] in C, you're asking the OS for 16 bytes in a row. That's it. There's no fence at the end, no bouncer checking your size at the door, no runtime throwing an exception when you walk right past it.
The problem is that memory doesn't stop at the edge of your buffer. Other variables, function call metadata, and bookkeeping data live right next to it. Write past your boundary and you start modifying things you didn't intend to touch. It's like being given one desk at an open office and quietly deciding the two desks next to you are also yours now.
The Simplest Buffer Overflow Example
Here's a function that copies a user-supplied name into a fixed-size buffer:
#include <stdio.h> #include <string.h> void greet(char *name) { char buffer[8]; strcpy(buffer, name); // copies until null terminator, no length check printf("Hello, %s!\n", buffer); } int main() { greet("Alice"); // 5 chars + '\0' = 6 bytes, fine greet("Alexander Hamilton"); // 18 chars + '\0' = 19 bytes into 8 bytes return 0; }
strcpy does not know or care how big buffer is. It copies until it sees a null terminator. The second call writes 19 bytes into a region that holds 8. The remaining 11 bytes land wherever the next thing in memory happens to be.
Sometimes the program crashes. Sometimes it corrupts a nearby variable and keeps running. That second outcome is what makes buffer overflows so hard to catch by observation alone. Your tests pass. Your demo works. Then a real user with a longer name shows up.
The Stack Gets It Worst
Most buffer overflows you'll encounter in interview prep or security courses are stack-based. When you call a function, the program pushes a stack frame: space for local variables, saved registers, and the return address (the instruction the CPU will jump to when the function finishes).
Your local buffer[8] lives on that same stack frame, right next to the return address.
Lower addresses
+--------------------+
| buffer[8] | <-- you control this
+--------------------+
| saved frame ptr |
+--------------------+
| return address | <-- overwrite this and you control where execution goes
+--------------------+
Higher addresses (grows downward on x86)
If you write enough bytes past buffer, you reach the return address and overwrite it. When greet returns, the CPU jumps to whatever address you wrote there instead of back to main. In an exploit, that destination is attacker-controlled code. In an interview bug, it's garbage, which produces a segfault with a stack trace pointing at absolutely nothing recognizable.
This is called stack smashing. The 1988 Morris Worm used exactly this technique against a buffer overflow in the fingerd daemon's use of gets(). The gets() function was so dangerous that the C11 standard removed it from the language entirely. It had one job, one input, no length parameter, and the committee eventually decided the kindest thing to do was delete it.
Heap Overflows Work Differently
A heap overflow happens when you write past the end of a dynamically allocated buffer:
#include <stdlib.h> #include <string.h> int main() { char *buf = malloc(8); memcpy(buf, "AAAAAAAAAAAAAAAAAAAAA", 21); // 21 bytes into 8 free(buf); return 0; }
You're not clobbering a return address here. You're clobbering heap metadata: the size headers and free-list pointers that malloc/free use to track allocations. Corrupted heap metadata can cause a crash in a completely different malloc call, hundreds of milliseconds later, in a part of the codebase you haven't touched in weeks. This is the kind of bug that makes senior engineers stare at their monitors and reconsider their career choices.
Why Managed Languages Don't Have This Problem
Python, Java, JavaScript, and Go all check array bounds before every access. They don't let you write past the end of a buffer because the runtime enforces the boundary.
buf = [0] * 8 buf[10] = 42 # IndexError: list assignment index out of range
int[] buffer = new int[8]; buffer[10] = 42; // ArrayIndexOutOfBoundsException: Index 10 out of bounds for length 8
The exception is not a bug, it's the protection mechanism. Instead of silently overwriting memory, the runtime throws a catchable error and stops the bad write. You pay a small performance cost for every array access, but you can't silently corrupt memory.
This is why C and C++ are still used in systems where raw performance matters: they skip bounds checking entirely, trusting the programmer to get it right. That trust has a price, as 35 years of CVEs will confirm. Even Rust, which guarantees memory safety at the language level, keeps an unsafe {} escape hatch for the rare cases where you need raw pointer access. Guess what C++ programmers reach for first.

Finding out you can still have buffer overflows in Rust. You just have to ask nicely.
See Array vs Linked List Performance for more on how memory layout differences affect performance.
What Gets Corrupted Depends on What's Adjacent
The exact consequence of a buffer overflow depends on the memory layout at the moment of the overflow:
- Local variables in the same stack frame get overwritten. A variable you haven't assigned yet suddenly has a non-zero value.
- The return address gets overwritten, causing the function to return to an unexpected location.
- Another heap allocation gets corrupted, breaking a future
mallocorfreecall. - Nothing visible happens, because the overwritten bytes were padding or alignment.
The last case is the dangerous one. A buffer overflow that causes no visible symptoms during testing can cause a crash in production with different input, or be silently exploited for years before anyone notices. The bug in sudo existed for ten years. Nobody's tests caught it. It was waiting.
The Exploits That Made This Famous
Buffer overflows have been behind some of the highest-profile vulnerabilities in software history.
The Morris Worm (1988) used a gets() overflow in fingerd to spread across thousands of Unix machines in a matter of hours. It was the first worm to gain significant internet-scale attention and led directly to the founding of CERT. The vulnerable code had been sitting there the whole time. It just needed one person with a longer-than-expected name to walk in.
CVE-2021-3156, discovered in January 2021, was a heap-based buffer overflow in sudo that had been present since 2011. A local user could exploit it to gain root privileges on almost every Linux distribution. The root cause: an off-by-one error in how sudoedit handled escape characters in command arguments. The function was probably reviewed before check-in. Probably.
The pattern is consistent. A programmer assumed the input would be shorter than it was, failed to check, and wrote past the end. That assumption held for years until someone tested the boundary deliberately.
For a deeper look at memory regions, Stack vs Heap Memory explains how the two areas are structured and why the stack is especially sensitive to corruption.
Off-by-One Errors: The Quiet Killer
Many buffer overflows are off-by-one errors. The programmer allocates n bytes but writes n+1. The most common source:
char buffer[16]; // strncpy copies at most 16 bytes, but doesn't guarantee null termination strncpy(buffer, input, 16); // if input is exactly 16 non-null chars, buffer is not null-terminated // the next call to strlen(buffer) walks off the end
The null terminator is the 17th byte. You allocated 16. One missing byte is enough to kick off a chain of confusion that ends somewhere completely unrelated to the function that caused it.
Off-by-one bugs in interview problems are the same category of mistake: the program treats memory it doesn't own as if it does. Your binary search writes to mid + 1 instead of mid. Your sliding window expands past the end of the array. The logic is different, but the root error is identical.
For more on this failure mode, What Is an Off-by-One Error? covers the most common sources in loops and pointer arithmetic.
How Modern Tools Catch This
Because manual auditing misses most buffer overflows, compilers and runtimes now include several mitigations:
Stack canaries: a random value placed between local variables and the return address. Before a function returns, the compiler checks that the canary hasn't changed. If it has, someone wrote past the buffer. The program aborts rather than returning to the corrupted address. It doesn't fix the bug. It makes the bug extremely loud.
Address Space Layout Randomization (ASLR): the OS places the stack, heap, and libraries at random addresses each run. An attacker who overwrites the return address doesn't know what address to put there, so their exploit fails with high probability. It's not bulletproof, but it turns a reliable exploit into a probabilistic one.
AddressSanitizer (ASAN): a compiler instrumentation tool that adds bounds checks to every memory access and reports buffer overflows with a precise diagnostic. Run with -fsanitize=address in Clang or GCC. It catches what static analysis misses, and the output tells you exactly which line caused the problem and how many bytes past the boundary you went.

Stack canaries, ASLR, and AddressSanitizer approaching their logical endpoint.
These mitigations don't eliminate the bug. They make it harder to exploit and easier to detect. The underlying write still happens. The defenses just prevent it from being useful to an attacker, or make it loud enough that you find it in testing instead of in a CVE database three years from now.
What This Means for Your Coding Interview
If you're interviewing at companies that ask systems or security questions, buffer overflows come up directly. Expect questions about why certain C functions are deprecated, what undefined behavior means, or how you'd harden a parser.
More broadly, the mental model matters: safe code validates its boundaries before accessing memory, not after something goes wrong.
When you write an array traversal in any language, you're implicitly relying on bounds checking. In Java and Python, the runtime enforces it. In C and C++, you do. The discipline is the same: know the size of your buffer, check the length of your input, and treat strlen + 1 as a trap waiting for you to forget the null terminator.
Practicing interview explanations out loud helps. SpaceComplexity lets you work through exactly these concepts in voice-based mock interviews with rubric-based feedback, which is how you verify you can actually explain the difference between stack and heap overflows when someone asks.