Skip to main content

Buffer Overflow Attack

Buffer Overflow

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.
StepWhat happens
1. Input arrivesThe 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 endBecause no bound is enforced, bytes beyond the buffer's capacity are written to the adjacent memory.
3. Adjacent data is corruptedVariables, pointers or control data next to the buffer now hold attacker-chosen values.
4. The program acts on the corruptionAt 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-basedHeap-based
Where the buffer livesLocal variable in a function frameBlock returned by malloc, new or a custom allocator
What sits next to itOther locals, saved frame pointer, return addressOther objects, allocator metadata, function pointers inside objects
Typical causeUnbounded string copy into a fixed arrayWrong size arithmetic, off-by-one, trusted length field
First-line mitigationStack protector, non-executable stack, address randomisationHardened allocator, address randomisation, memory tagging
CWECWE-121CWE-122

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.

ErrorWhat goes wrongCWE
Off-by-oneA 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 allocationA 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 readThe program reads past the end instead of writing. No corruption, but memory contents leak: keys, passwords, addresses that defeat randomisation.CWE-125
Format stringUser input is passed as the format argument to printf, so the input controls reads and writes of the stack.CWE-134
Use after freeMemory 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 treat strcpy, strcat, sprintf, scanf with %s and 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::vector and std::span in 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=undefined for 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.

ControlWhat it doesWhat 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 integrityChecks that indirect calls and returns go to legitimate targets.Does not prevent corruption of ordinary data.
Hardened allocators and memory taggingDetect 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

  1. 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.
  2. Turn on the warnings. -Wall -Wextra -Wformat=2 -Wconversion on GCC and Clang, /W4 on MSVC, and fix what they report rather than silencing them.
  3. Add a sanitizer build to CI. Run the existing tests under it. Most teams find their first memory bug on the first run.
  4. 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.
  5. Grep for the known-dangerous functions and for every malloc(x * y), then review the length reasoning around each one.
  6. 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

A buffer is a fixed-size block of memory set aside for data, such as 16 bytes for a name. A buffer overflow happens when a program writes more data into it than it can hold, so the extra bytes spill into whatever memory sits next to it. That neighbouring memory may hold other variables or the data the program uses to decide where to run next, which is why the result ranges from a crash to full compromise.

No. A buffer overflow is a bug in a program, a weakness. Malware can exploit that weakness to get its code running, and famous worms have done exactly that, but the overflow itself is a programming error, not malicious software. Fixing the bug removes the opening regardless of which malware tries to use it.

A buffer overflow is writing past the end of a fixed-size buffer, on the stack or the heap, and it is a security vulnerability. A stack overflow is the whole call stack running out of space, usually from unbounded recursion, and it ends in a crash rather than attacker control. A stack-based buffer overflow is a buffer overflow in a buffer that happens to live on the stack. The website of the same name is unrelated to either.

Not in ordinary code. These languages check array and string bounds at run time and raise an exception instead of writing past the end, which is what memory safety means. They can still be affected indirectly: a native library they call, such as an image decoder written in C, can overflow, and code that deliberately disables the checks, such as unsafe blocks or native interfaces, is back in C territory.

Yes. Microsoft has reported that about 70 percent of the vulnerabilities it assigns a CVE each year are memory safety issues, and out-of-bounds writes and reads still appear in the CWE Top 25. Operating systems, browsers, network equipment, media libraries and embedded firmware remain largely C and C++, and that is where the bugs live. Modern mitigations make exploitation harder, not impossible.

In development, by compiling with a memory sanitizer and running the test suite and fuzzers against the instrumented build; the sanitizer reports the exact line of the out-of-bounds access. Static analysis flags unbounded copies and unchecked lengths before the code runs. In production, crash reports, compiler hardening such as stack protectors, and memory-tagging hardware turn silent corruption into a detectable fault.

Memory-safe languages prevent them by design: Rust, Go, Java, Kotlin, C#, Swift, Python, JavaScript and others. C and C++ are the main languages where they occur, with assembly and any language's unsafe or native escape hatches alongside. Government guidance since 2023 recommends memory-safe languages for new code for this reason.

Sources and further reading