Buffer Overflow Attack
Table of Contents
Quick Answer
A buffer overflow happens when a program writes more data into a fixed-size block of memory than it can hold, so the surplus spills into the memory next to it. C and C++ let this happen because they do not check bounds on every write and leave the programmer to track sizes. An attacker who controls the input can overwrite neighbouring variables or the data that decides where the program runs next, turning a crash into code execution. The fix is to stop the write: bounded copies, correct size arithmetic, sanitizers and fuzzing in testing, compiler hardening in production, and memory-safe languages for new code.
This defensive guide explains how buffer overflows arise, shows a stack and a heap example in C with the fixed versions, puts them in context with the memory errors they travel with, and sets out the controls that prevent, detect and contain them. The examples demonstrate the fault and how a sanitizer catches it. They are not exploit material, and the protections are described by what they do, not how they are defeated.
What is a buffer overflow?
A buffer is a contiguous region of memory reserved for data: sixteen bytes for a name, four kilobytes for a network packet, an array of a hundred records. In C the buffer has a fixed size and a starting address, and nothing else. Writing past the end of it does not stop the program or raise an error. The processor simply stores the bytes at the next addresses, whatever those addresses were being used for.
That neighbouring memory is never empty. On the stack it holds other local variables, the saved frame pointer and the return address that tells the processor where to continue when the function finishes. On the heap it holds other objects and the bookkeeping the allocator uses to manage them. Overwriting any of these changes the program's behaviour, and when the overflowing data came from a user, a file or the network, the person who supplied it chooses what the change is.
MITRE catalogues the general weakness as CWE-120, buffer copy without checking the size of input, with CWE-121 and CWE-122 for the stack-based and heap-based forms. The broader parents, CWE-787 out-of-bounds write and CWE-125 out-of-bounds read, appear in the CWE Top 25 Most Dangerous Software Weaknesses year after year, and OWASP maintains its own buffer overflow entry in the community vulnerability pages. The language is the common factor: these weaknesses belong almost entirely to C, C++ and the code generated from them.
How a buffer overflow attack works
Three conditions line up in every buffer overflow attack:
- A buffer with a fixed size. A local array, a heap allocation, a field inside a struct.
- A write whose length is decided by the input, not by the buffer. A copy that runs until it finds a terminator, a length field read from a packet and trusted, a loop whose bound comes from the data.
- Something worth overwriting within reach. A flag that says whether the user is an administrator, a function pointer, a saved return address, the size field of the next heap chunk.
| Step | What happens |
|---|---|
| 1. Input arrives | The program receives data longer than the buffer that will hold it: a form field, a filename, a packet, an image header. |
| 2. The copy runs past the end | Because no bound is enforced, bytes beyond the buffer's capacity are written to the adjacent memory. |
| 3. Adjacent data is corrupted | Variables, pointers or control data next to the buffer now hold attacker-chosen values. |
| 4. The program acts on the corruption | At best it crashes. At worst it returns into code the attacker chose, grants a permission it should not, or leaks memory contents. |
The historic form of the attack overwrote a function's saved return address on the stack so that, when the function returned, execution continued at an address the attacker supplied. Modern systems make that specific route much harder, as the defences section explains, so attackers now chain several weaknesses together: an out-of-bounds read to learn where things are in memory, then an out-of-bounds write to change them. The underlying bug is unchanged, which is why fixing the write remains the whole game for a developer.
Stack vs heap buffer overflow, with C examples
The two kinds differ in where the buffer lives and therefore in what sits next to it. Both are shown below as a bug, the sanitizer report, and the fix. Compile the vulnerable versions only on your own machine, inside a lab directory, to see the detector work.
Stack-based buffer overflow
Local variables live on the stack. strcpy copies until it meets a terminating NUL byte and has no idea the destination is sixteen bytes long.
#include <string.h>
#include <stdio.h>
void greet(const char *input) {
char name[16];
strcpy(name, input); /* copies until NUL, never checks 16 */
printf("Hello, %s\n", name);
}
int main(int argc, char **argv) {
if (argc > 1) greet(argv[1]);
return 0;
}
Built with the address sanitizer, the program reports the fault on the first long argument, naming the function, the line and the variable:
$ gcc -g -fsanitize=address -o greet greet.c
$ ./greet "a-name-longer-than-sixteen-bytes"
==4242==ERROR: AddressSanitizer: stack-buffer-overflow on address ...
WRITE of size 33 at ... thread T0
#0 strcpy
#1 greet greet.c:6
#2 main greet.c:11
Address ... is located in stack of thread T0 at offset 32 in frame greet
This frame has 1 object(s):
[32, 48) 'name' <== Memory access at offset 48 overflows this variable
The fix bounds the copy to the destination's size and guarantees termination. snprintf with sizeof name does both; strlcpy where available, or strncpy followed by an explicit terminator, are the alternatives. The point is not the function name but the rule: the destination decides the length, never the source.
void greet(const char *input) {
char name[16];
/* copy at most 15 bytes and always terminate */
snprintf(name, sizeof name, "%s", input);
printf("Hello, %s\n", name);
}
Heap buffer overflow
Heap buffers come from malloc and its relatives, and the classic heap overflow in C is an off-by-one in the size arithmetic. Here the allocation forgets the terminating NUL, so a copy that includes it writes one byte past the block.
#include <stdlib.h>
#include <string.h>
char *duplicate(const char *src) {
size_t len = strlen(src);
char *copy = malloc(len); /* forgot the terminating NUL */
memcpy(copy, src, len + 1); /* writes len + 1 bytes into len */
return copy;
}
A single byte sounds harmless. It is not: it lands in the allocator's bookkeeping or the first byte of the next object, and allocator metadata corruption has been the basis of many real exploits. The sanitizer reports it as a heap-buffer-overflow with the same precision as the stack case. The fix is the size arithmetic:
char *duplicate(const char *src) {
size_t len = strlen(src);
char *copy = malloc(len + 1);
if (copy == NULL) return NULL;
memcpy(copy, src, len + 1);
return copy;
}
Heap overflows are harder to spot in review than stack overflows because the buffer and the bug are often in different functions: one place allocates, another writes, and the size is passed between them in a variable nobody double-checks. Sanitizer builds and fuzzing catch what review misses.
| Stack-based | Heap-based | |
|---|---|---|
| Where the buffer lives | Local variable in a function frame | Block returned by malloc, new or a custom allocator |
| What sits next to it | Other locals, saved frame pointer, return address | Other objects, allocator metadata, function pointers inside objects |
| Typical cause | Unbounded string copy into a fixed array | Wrong size arithmetic, off-by-one, trusted length field |
| First-line mitigation | Stack protector, non-executable stack, address randomisation | Hardened allocator, address randomisation, memory tagging |
| CWE | CWE-121 | CWE-122 |
Related memory errors
Buffer overflows rarely travel alone. The same missing bounds discipline produces a family of bugs, and a review that looks only for strcpy will miss most of it.
| Error | What goes wrong | CWE |
|---|---|---|
| Off-by-one | A loop or size check uses <= where < was needed, or forgets the terminator. One byte too many, as in the heap example above. | CWE-193 |
| Integer overflow leading to a short allocation | A size is computed from untrusted numbers, wraps around to a small value, and the allocation is too small for the data that follows. | CWE-190 |
| Out-of-bounds read | The program reads past the end instead of writing. No corruption, but memory contents leak: keys, passwords, addresses that defeat randomisation. | CWE-125 |
| Format string | User input is passed as the format argument to printf, so the input controls reads and writes of the stack. | CWE-134 |
| Use after free | Memory is freed and then used again; another allocation may now own it. Not an overflow, but the same class of memory unsafety and often chained with one. | CWE-416 |
The integer case deserves its own example because it hides in code that looks careful:
/* count comes from the network */
uint32_t count = read_u32(packet);
size_t bytes = count * sizeof(struct record); /* can wrap to a small number */
struct record *items = malloc(bytes);
for (uint32_t i = 0; i < count; i++)
items[i] = parse_record(packet); /* writes far past 'bytes' */
Every line checks something except the one that matters: the multiplication can wrap, so bytes ends up small while count stays large, and the loop writes far past the allocation. The fix is to check the multiplication for overflow before allocating, or to use the checked arithmetic helpers that modern compilers and libraries provide.
Real incidents
The Morris worm, 1988
The first internet worm spread partly through a stack buffer overflow in the fingerd daemon, which read its input with gets, a function that cannot be told how big the destination is. The incident put the term into the security vocabulary, led to the formation of the first computer emergency response team, and gets was eventually removed from the C standard. The lesson has not changed in almost forty years.
Heartbleed, 2014
CVE-2014-0160 in OpenSSL was an out-of-bounds read, the overflow's quieter sibling. The TLS heartbeat extension let a client say how many bytes to echo back without the server checking that the client had sent that many. The server returned up to 64 kilobytes of whatever was next in memory, which included private keys and session data. Nothing was corrupted, nothing crashed, and the bug sat in widely deployed code for two years. It is the standard illustration that memory safety is about reads as well as writes.
libwebp, 2023
CVE-2023-4863 was a heap buffer overflow in the WebP image decoder, rated critical by Chromium and exploited in the wild before the fix. Because the same library is embedded in browsers, messaging apps, operating systems and electron-based desktop software, one image-parsing bug required coordinated patches across the industry. It is a modern reminder that a memory-unsafe dependency is a shared risk for every memory-safe application built on top of it.
Why these bugs still exist in 2026
The weakness has been understood since the 1970s, so the natural question is why it persists. The vendors who see the most data have published the answer. Microsoft's Security Response Center reported that roughly 70 percent of the vulnerabilities it assigns a CVE each year are memory safety issues, a share that stayed flat for over a decade despite enormous investment in tooling and training. Google reported that the proportion of memory safety vulnerabilities in Android fell from 76 percent to 24 percent over six years once new development moved to memory-safe languages, and that the bugs cluster overwhelmingly in new or recently changed code.
Those two findings point the same way. Memory-unsafe code keeps producing memory bugs at a steady rate as long as it is being written; the fix at scale is to stop writing it. In December 2023 CISA and partner agencies published The Case for Memory Safe Roadmaps, asking software manufacturers to publish plans for moving to memory-safe languages. For the C and C++ that already exists and will exist for decades, the controls below are what keep the bugs from becoming incidents.
Defences that stop buffer overflows
The controls work in layers. The first group removes the bug, the second detects it before release, the third makes a bug that ships harder to turn into an exploit. A serious codebase uses all three.
1. Write bounded code
- Never call
gets, and treatstrcpy,strcat,sprintf,scanfwith%sand friends as review findings. Use the length-bounded forms with the destination size, and always terminate. - Compute sizes once, in
size_t, and check multiplications and additions for overflow before allocating. - Validate every length that arrives in data: a header field, a count, an offset. The buffer's capacity is the limit, whatever the input claims.
- Prefer library types that carry their size with them, such as
std::string,std::vectorandstd::spanin C++, and use their checked accessors in code that handles untrusted input.
2. Detect before release
- Sanitizers. Build the test suite with
-fsanitize=address(and-fsanitize=undefinedfor integer problems). The address sanitizer reports the exact out-of-bounds access, as shown above, at a cost of roughly double the run time, which is fine for tests. - Fuzzing. Feed generated inputs to every parser and decoder under a sanitizer build. Most of the memory bugs found in open-source infrastructure over the last decade were found this way, and fuzzers run unattended in CI.
- Static analysis. Compiler warnings at the highest level, plus a static analyser, flag unbounded copies, tainted lengths and suspicious arithmetic before the code is run. Our SAST and DAST post covers where each kind of tool fits.
- Review. Every function that takes a pointer and a length is a review item: who guarantees the length is right?
3. Contain what ships
These are platform and compiler features. They do not fix bugs; they make a bug that reaches production harder to exploit and more likely to crash loudly instead of silently handing over control.
| Control | What it does | What it does not do |
|---|---|---|
| Stack protector (canaries) | Places a secret value between local buffers and the return address and aborts the program if it has been overwritten when the function returns. | Does not protect heap buffers or data overwritten within the same frame. |
| Non-executable memory (NX, DEP) | Marks stack and heap pages as data, so bytes written there cannot be run as code. | Does not stop an attacker reusing code that is already executable. |
| Address space layout randomisation (ASLR) | Loads code, stack and heap at unpredictable addresses so an attacker cannot know in advance where anything is. | Is weakened by any information leak, which is why out-of-bounds reads matter. |
| Control-flow integrity | Checks that indirect calls and returns go to legitimate targets. | Does not prevent corruption of ordinary data. |
| Hardened allocators and memory tagging | Detect or block writes outside a heap block, in software or with hardware support on recent processors. | Coverage depends on platform and configuration. |
Enable all of them. On most modern toolchains they are on by default, and a build that turns one off to make something work should be treated as a security change, not a build fix.
4. Choose memory-safe languages for new code
For new components, services and tools, a memory-safe language removes this entire class of bug at the design level, which is the recommendation of the memory-safety roadmaps and the reason the Android numbers moved. Where C or C++ is unavoidable, confine it to a small, heavily tested boundary and keep the untrusted-input parsing out of it where you can.
How to find buffer overflows in your own code
- Inventory the C and C++. Include native modules, vendored libraries and build-time dependencies. Memory-safe applications inherit the bugs of their decoders and drivers.
- Turn on the warnings.
-Wall -Wextra -Wformat=2 -Wconversionon GCC and Clang,/W4on MSVC, and fix what they report rather than silencing them. - Add a sanitizer build to CI. Run the existing tests under it. Most teams find their first memory bug on the first run.
- Fuzz every parser. Anything that reads a file format, a protocol or a serialisation format gets a fuzz target. Keep the corpus and run it continuously.
- Grep for the known-dangerous functions and for every
malloc(x * y), then review the length reasoning around each one. - Review with the right question. For each write: what bounds it, and does the bound come from the destination?
Testing for these issues belongs in authorised development and QA environments. Crashing a production system to demonstrate a bug is a denial of service, and in many jurisdictions an offence, however good the intention.
Developer review questions
- Does any copy, concatenation or format call take its length from the source rather than the destination?
- Is every length, count or offset that arrives in data validated against the buffer that will hold it?
- Can any size computation overflow, and is it checked before the allocation?
- Is every string buffer guaranteed a terminator after every write path?
- Do the tests run under the address and undefined-behaviour sanitizers in CI?
- Does every parser of untrusted input have a fuzz target?
- Are the stack protector, non-executable memory, address randomisation and control-flow integrity enabled in release builds, and is anything opting out?
- Which native dependencies handle untrusted data, and who tracks their advisories?
- Is new code being written in a memory-safe language where one is available?
- Are these checks performed only in environments you are authorised to test?
FAQs
unsafe blocks or native interfaces, is back in C territory.Sources and further reading
- MITRE CWE-120 - Buffer Copy without Checking Size of Input (Classic Buffer Overflow) — Weakness definition and relationships
- MITRE CWE-121 - Stack-based Buffer Overflow — Stack variant
- MITRE CWE-122 - Heap-based Buffer Overflow — Heap variant
- MITRE CWE-787 - Out-of-bounds Write — Parent weakness; recurring CWE Top 25 entry
- MITRE CWE-125 - Out-of-bounds Read — The read-side counterpart, as in Heartbleed
- MITRE - 2025 CWE Top 25 Most Dangerous Software Weaknesses — Current ranking of memory-boundary weaknesses
- OWASP - Buffer Overflow — Vulnerability description and background
- NVD - CVE-2014-0160 (Heartbleed) — OpenSSL heartbeat buffer over-read
- NVD - CVE-2023-4863 (libwebp) — Heap buffer overflow in the WebP decoder, Chromium severity critical
- Microsoft Security Response Center - A proactive approach to more secure code — About 70 percent of CVEs assigned each year are memory safety issues
- Google Security Blog - Eliminating Memory Safety Vulnerabilities at the Source — Android memory safety vulnerabilities fell from 76 to 24 percent over six years
- CISA - The Case for Memory Safe Roadmaps — December 2023 guidance on moving to memory-safe languages
- Clang documentation - AddressSanitizer — The -fsanitize=address detector used in the examples