A buffer overflow attack exploits a fundamental programming mistake: when software writes more data into a block of memory than that block was designed to hold, the excess spills into adjacent memory. An attacker who carefully controls what that excess data looks like can overwrite critical information the program relies on, potentially hijacking execution and running their own code on the target machine. Despite being one of the oldest classes of software vulnerability, buffer overflows remain a persistent and serious threat, responsible for some of the most damaging security breaches in computing history.
How a Buffer Overflow Actually Works
Every running program sets aside chunks of memory called buffers to store temporary data, whether that is a username someone typed, a network packet that just arrived, or the contents of a file being read. These buffers have a fixed size. If the program allocates room for 64 characters of input but does not check whether the user actually sent 64 characters or 640, those extra characters write past the end of the buffer into whatever memory lies next door.
That adjacent memory is not empty space. It often holds things the program needs to function correctly: local variables, function return addresses, pointers to other data structures. When the overflow overwrites a return address, the program no longer knows where to jump back to when it finishes its current task. An attacker who can control the overwritten value can redirect execution to a memory location of their choosing, which might contain malicious instructions they smuggled in as part of the overflow itself.
The vulnerability is conceptually simple, which is part of why it has persisted for decades. The C programming language, which underpins enormous amounts of the world’s software infrastructure, was designed over 40 years ago and does not enforce automatic bounds checking on memory operations.1PubMed Central. Defeating Buffer Overflow: A Trivial but Dangerous Bug When a programmer uses a function that copies data into a buffer without verifying the length first, the language does nothing to stop the overflow. The program simply writes wherever it is told.
What an Attacker Can Actually Do
The consequences of a successful buffer overflow range from crashing a program to full system compromise. At the mild end, overwriting random memory simply causes the application to behave unpredictably or terminate. But a skilled attacker does not aim for a crash. By crafting their input so the overflow places precisely chosen values at precisely the right offsets, they can achieve something far more dangerous: arbitrary code execution. This means the attacker’s code runs on the victim’s machine as if it were a legitimate part of the program.
The privilege level the attacker inherits depends on the privilege level of the vulnerable program. A buffer overflow in a web server running as root gives the attacker root access. A buffer overflow in a setuid binary on a Linux system can escalate a low-privilege user to administrator. Research on stack-based overflows has demonstrated that these attacks are most commonly used for privilege escalation or gaining remote code execution on the target system.2Procedia Computer Science. Exploiting stack-based buffer overflow using modern day techniques In the worst case, an attacker can install backdoors, exfiltrate data, or pivot deeper into a network, all stemming from a single unchecked memory write.
This is why buffer overflows consistently rank among the most critical vulnerability classes in security advisories. A bug that lets you crash a program is an annoyance. A bug that lets you run arbitrary code with elevated privileges is a catastrophe.
Stack-Based and Heap-Based Overflows
Not all buffer overflows work the same way, and the distinction matters for understanding both attacks and defenses. The two main categories are stack-based and heap-based overflows, named for the regions of memory they target.
The stack is a structured, last-in-first-out region of memory that tracks function calls. Every time a program calls a function, it pushes a new “frame” onto the stack containing local variables, the return address, and bookkeeping data. Stack-based overflows are the classic, most studied form: the attacker overflows a local buffer on the stack, overwrites the return address, and redirects execution. They are relatively straightforward to exploit because the stack’s layout is predictable. The return address sits at a known offset from any given local buffer, so the attacker knows exactly how many bytes of padding to send before placing their malicious address.
Heap-based overflows are a different beast. The heap is a less structured memory region used for dynamically allocated data. Overflows here do not directly overwrite return addresses, but they can corrupt metadata that the memory allocator uses to track allocated and free blocks. By manipulating this metadata, an attacker can trick the allocator into writing a value of their choice to an arbitrary memory location, which can eventually lead to code execution. Heap exploits are generally harder to develop because heap layout is less predictable and varies between allocator implementations, but they are far from theoretical. Major real-world exploits have targeted heap overflows in browsers, media players, and operating system components.
Off-by-One and Other Subtle Variants
Buffer overflows are not always dramatic. Some of the trickiest vulnerabilities involve writing just a single byte past the end of a buffer, a class of bug known as an off-by-one error. These arise from common programming mistakes like miscounting the space needed for a string’s null terminator, or using the wrong comparison operator in a loop condition.
A well-documented example involves the strncat() function in C. A program might allocate a buffer, append one string safely, and then append a second string while calculating the remaining space incorrectly. The function automatically adds a null terminator byte at the end, and if the programmer did not account for that extra byte, it lands one position beyond the buffer’s boundary.3Common Weakness Enumeration. CWE-193: Off-by-one Error One byte does not sound like much, but on the stack, that single byte can partially overwrite critical values. If it modifies the least significant byte of a saved frame pointer, for instance, it can shift the stack frame for the calling function, ultimately giving the attacker indirect control of execution.
Integer overflows represent another subtle path to buffer overflows. A program might use an integer to calculate how much memory to allocate. If an attacker can cause that integer to wrap around (say, by providing a length value so large that it overflows back to a small number), the program allocates a tiny buffer but then proceeds to fill it with a much larger amount of data. The result is a classic buffer overflow triggered by what initially looks like an arithmetic bug rather than a memory-safety issue.
Why C and C++ Are Especially Prone
Buffer overflows are overwhelmingly associated with C and C++. These languages give programmers direct access to memory and treat arrays as raw pointers without built-in bounds checking. Standard library functions like strcpy(), gets(), and sprintf() will happily write past the end of a buffer because they were designed for performance and flexibility rather than safety. The function gets(), which reads a line of input into a buffer with no size limit at all, became so notorious that it was eventually removed from the C standard entirely.
Research into buffer overflow detection has focused heavily on identifying these unsafe function calls, since they represent well-known entry points for vulnerabilities. Techniques that analyze how programs invoke these functions and trace the flow of untrusted data through them can detect many overflow-prone patterns before they are exploited.4Journal of Physics: Conference Series. Buffer Overflow Vulnerability Detection Based on Unsafe Function Invocation
Languages like Rust, Java, Python, and Go handle memory very differently. They either enforce bounds checking automatically (throwing an error if you try to access past the end of an array), use garbage collection to manage memory lifetimes, or both. Rust in particular has gained significant traction in security-conscious projects because its compiler enforces memory safety at compile time without requiring a garbage collector, making it suitable for systems programming where C and C++ have traditionally dominated.
Despite this, C and C++ are not going away anytime soon. Operating system kernels, embedded firmware, networking stacks, and performance-critical libraries are overwhelmingly written in these languages, and rewriting them is a generational effort. The practical reality is that buffer overflows will remain a threat for as long as this code runs, which means defenses need to exist at multiple levels beyond just the programming language.5PubMed Central. Defeating Buffer Overflow: A Trivial but Dangerous Bug
Software Defenses in Modern Systems
Modern operating systems and compilers deploy several layers of defense that make buffer overflow exploitation significantly harder than it was in the 1990s and early 2000s, though not impossible.
- Stack canaries: The compiler places a small random value (the “canary”) between local variables and the return address on the stack. Before a function returns, it checks whether the canary has been modified. If an overflow has overwritten the canary on its way to the return address, the program detects the corruption and terminates rather than jumping to the attacker’s code.
- Address Space Layout Randomization (ASLR): The operating system randomizes the base addresses of the stack, heap, and shared libraries each time a program runs. An attacker who needs to redirect execution to a specific address now faces a moving target. Without knowing where their injected code or useful library functions live in memory, crafting a reliable exploit becomes much harder.
- Non-executable memory (DEP/NX): The processor marks memory pages as either executable or writable, but not both simultaneously. Even if an attacker manages to inject code into a buffer, the processor refuses to execute it because the data region is flagged as non-executable. This defense forced attackers to develop more sophisticated techniques that reuse existing code rather than injecting new code.
Each of these defenses raises the bar, but none is a complete solution on its own. Stack canaries can sometimes be bypassed by leaking their value through a separate vulnerability. ASLR can be defeated if the attacker can discover a single address through an information disclosure bug, since relative offsets between code sections are often fixed. Non-executable memory led directly to the development of return-oriented programming, discussed below.
Return-Oriented Programming and Defense Bypasses
When non-executable memory became widespread, attackers adapted. Instead of injecting their own code, they began chaining together small snippets of legitimate code already present in the program or its libraries. Each snippet, called a “gadget,” typically ends with a return instruction. By carefully arranging addresses on the stack, the attacker causes the program to hop from one gadget to the next, stitching them together into an entirely new behavior that the original program was never designed to perform.
This technique, known as return-oriented programming (ROP), is powerful because it uses the program’s own code against it. Since the gadgets live in legitimate executable memory, non-executable memory protections do not help. ASLR makes ROP harder but not impossible: if the attacker can leak even one pointer value from the running process, they can calculate where the rest of the code lives and construct their chain.
ROP attacks have been demonstrated across multiple processor architectures, and automated tools exist that can scan a binary and identify usable gadgets. The defense community has responded with techniques like Control Flow Integrity (CFI), which restricts indirect branches to a set of valid targets, making it harder for an attacker to redirect execution to arbitrary gadgets. Some CFI implementations are coarse-grained and can still be bypassed with enough effort, but fine-grained CFI narrows the attacker’s options considerably.
Hardware-Level Protections
Recognizing that software-only defenses have limits, processor designers have started building protections directly into hardware. ARM’s Pointer Authentication Code (PAC) feature, available on recent ARM chips including those in Apple’s M-series and A-series processors, uses cryptographic signatures to protect pointers. Before a function returns, the processor verifies that the return address has not been tampered with by checking its embedded authentication code. If the signature does not match, the processor faults, stopping the attack before execution is redirected.
Research into leveraging PAC for stronger call-stack protection has produced approaches like PACStack, which chains authenticated return addresses together so that forging any single return address requires knowledge of the entire chain’s state.6arXiv. PACStack: an Authenticated Call Stack This makes it significantly harder for an attacker to corrupt the stack in a way that goes undetected, even if they have some ability to read or write memory.
Intel has introduced its own hardware mitigation called Control-flow Enforcement Technology (CET), which includes a “shadow stack,” a second copy of return addresses maintained by hardware that the program cannot write to directly. When a function returns, the processor compares the return address on the regular stack against the shadow stack. A mismatch triggers a fault. CET also includes indirect branch tracking to constrain where indirect jumps and calls can land, limiting the effectiveness of ROP-style attacks.
These hardware features represent a meaningful shift in the defense landscape. Software defenses like ASLR and stack canaries operate within the same trust boundary as the attacker’s exploit. Hardware protections operate below that boundary, making them fundamentally harder to subvert through software-only means.
Real-World Attacks That Shaped the Field
Buffer overflows are not an abstract academic concern. Some of the most consequential cyberattacks in history exploited them. The Morris Worm in 1988, often cited as the first major internet worm, spread partly through a buffer overflow in the Unix finger daemon. It brought roughly 10% of the internet’s connected machines to a halt and prompted the creation of the first Computer Emergency Response Team (CERT).
Code Red in 2001 exploited a buffer overflow in Microsoft’s Internet Information Services (IIS) web server, infecting hundreds of thousands of machines in hours and launching a denial-of-service attack against the White House’s website. The SQL Slammer worm in 2003 exploited a buffer overflow in Microsoft SQL Server and spread so aggressively that it doubled in size every 8.5 seconds, causing widespread internet disruption within minutes of its release.
More recently, buffer overflows have appeared in critical infrastructure software, IoT device firmware, and even security products themselves. The Heartbleed vulnerability in 2014, while technically a buffer over-read rather than a classic overflow, exploited the same fundamental problem of insufficient bounds checking in C code. It affected a staggering proportion of the internet’s encrypted communications and demonstrated how a single bounds-checking failure in a widely used library can have global consequences.
Common Misconceptions
One persistent misconception is that buffer overflows are “solved” because modern systems have ASLR, stack canaries, and non-executable memory. These defenses significantly raise the difficulty of exploitation, but they do not eliminate the vulnerability class. Attackers adapt, developing techniques like ROP and information leaks to circumvent each new layer. The security community treats buffer overflow defense as a continuous arms race, not a problem with a final solution.
Another misconception is that buffer overflows require the attacker to inject and run their own machine code. That was true in the early days, but modern exploitation techniques like ROP achieve arbitrary computation using only the code already present in the target program. The attacker never writes a single instruction; they just rearrange the order in which existing instructions execute.
People sometimes assume buffer overflows only matter for network-facing services. In reality, any program that processes untrusted input is potentially vulnerable. A media player parsing a malformed video file, a PDF reader opening a crafted document, or an image library decoding a specially constructed PNG can all be exploited through buffer overflows. The attack surface extends far beyond programs that listen on a network port.
Finally, there is a tendency to blame individual programmers for writing “bad code.” While coding discipline matters, the deeper issue is systemic. Languages that do not enforce memory safety place the entire burden of correctness on the programmer, and humans make mistakes. A single missed bounds check in millions of lines of code is all it takes. Framing buffer overflows as a failure of individual competence rather than a failure of tooling leads to solutions that do not scale.
Fuzzing and How Overflows Get Found
Most buffer overflows in modern software are discovered before attackers exploit them, thanks in large part to a testing technique called fuzzing. A fuzzer generates huge volumes of semi-random or mutated inputs and feeds them to a target program, monitoring for crashes, hangs, or unexpected behavior. When a particular input causes a crash, it is likely triggering a memory-safety bug.
Coverage-guided fuzzers like AFL and libFuzzer have become standard tools in the software industry. Google’s OSS-Fuzz project continuously fuzzes hundreds of critical open-source projects and has found thousands of bugs, many of them buffer overflows, before they could be exploited in the wild. Microsoft runs similar internal fuzzing infrastructure for Windows components.
Static analysis tools take a different approach, examining source code without running it to identify patterns likely to result in overflows, such as calls to unsafe string functions or arithmetic that might produce integer overflows feeding into memory allocations. These tools produce both true positives and false positives, so they work best as a complement to fuzzing rather than a replacement. The most effective strategy uses both: static analysis to catch obvious patterns during development, and fuzzing to catch the subtle, context-dependent bugs that static analysis misses.
Bug bounty programs have also played a significant role. By paying researchers to find and responsibly disclose vulnerabilities, organizations create an economic incentive for skilled people to hunt for overflows and report them rather than sell them on the black market. Many of the highest-value bounty payouts in history have been for buffer overflow vulnerabilities in browsers, operating systems, and cloud infrastructure.
The Shift Toward Memory-Safe Languages
The most effective long-term defense against buffer overflows is to write new code in languages that prevent them entirely. The U.S. Cybersecurity and Infrastructure Security Agency (CISA), the National Security Agency (NSA), and similar bodies in other countries have issued guidance urging organizations to adopt memory-safe languages for new development. The White House Office of the National Cyber Director published a report in 2024 calling memory safety a matter of national security.
Rust has become the flagship language for this transition. The Linux kernel began accepting Rust code for certain drivers and modules starting in 2022. Android’s security team reported that the proportion of memory-safety vulnerabilities in Android dropped as new code was increasingly written in Rust and other memory-safe languages rather than C. Microsoft has experimented with Rust for components of Windows, and the Chromium project has begun introducing Rust alongside its existing C++ codebase.
The transition is slow because of the sheer volume of existing C and C++ code. Rewriting a mature, battle-tested codebase introduces its own risks: new bugs, behavioral differences, and the loss of decades of optimization work. The pragmatic approach most large projects are taking is to write new components in memory-safe languages while incrementally replacing the most security-critical legacy components. Meanwhile, existing C and C++ code continues to receive the layered defenses, canaries, ASLR, CFI, hardware protections, and relentless fuzzing, that make exploitation progressively harder without eliminating the root cause.

