Safeguard
Application Security

An introduction to C and C++ memory-safety vulnerabilities

Microsoft has reported that roughly 70% of the CVEs it patches each year trace back to memory-safety bugs — here's what buffer overflows, use-after-free, and double-free actually look like.

Safeguard Research Team
Research
8 min read

If you learned to program in Python, Go, or Java, the idea that reading one byte past the end of an array can hand an attacker remote code execution probably sounds like folklore. It isn't. CVE-2014-0160 — Heartbleed — was a single missing bounds check in OpenSSL's heartbeat handler, an out-of-bounds read (CWE-125) that leaked private keys and session data from roughly half a million TLS-secured servers before it was patched in April 2014. CVE-2017-0144, EternalBlue, was a buffer overflow in Microsoft's SMBv1 implementation that the NSA reportedly held as an exploit for years before it leaked and powered WannaCry. These aren't relics: MITRE's CWE Top 25 Most Dangerous Software Weaknesses has ranked CWE-787 (Out-of-bounds Write) at or near #1 in recent editions, and CWE-416 (Use After Free) has sat consistently in the top ten. In memory-unsafe languages, the compiler will happily let you write past a buffer, free memory twice, or keep using a pointer after it's gone — and none of it will stop your program from compiling, or often even from running, until an attacker notices first. This post walks through the core vulnerability classes, why they still dominate critical CVE counts industry-wide, and what modern tooling does to catch them before shipping.

What is a buffer overflow, and why does C let it happen?

A buffer overflow (CWE-120, and its more specific modern cousin CWE-787, Out-of-bounds Write) happens when a program writes more data into a fixed-size block of memory than that block can hold, spilling into adjacent memory it was never meant to touch. In a memory-safe language, writing to array[10] on a 5-element array throws an exception immediately. In C and C++, arrays are just pointers with no attached length — array[10] is legal syntax that reads or writes whatever memory happens to sit at that offset, whether that's unrelated program data, a saved return address, or a function pointer. Classic functions like strcpy(), gets(), and sprintf() copy input until they hit a null terminator, not until they hit the end of the destination buffer, which is exactly the pattern CWE-120 describes. If the overflowed data lands on the stack, an attacker who controls the input can overwrite a return address and redirect execution to their own shellcode — the technique behind decades of remote-code-execution CVEs, including large parts of the Morris Worm's original 1988 payload and countless CVEs since.

What is use-after-free, and why is it so hard to catch by reading code?

Use-after-free (CWE-416) happens when a program keeps a pointer to a block of heap memory after that memory has been freed, and then dereferences it — reading or writing through a pointer that the allocator may have already handed to something else entirely. The memory isn't gone; free() just marks it available for reuse. If nothing has reallocated it yet, the dangling pointer might still "work" by accident, which is what makes these bugs so dangerous in testing: the bug is silent until, under different timing or memory pressure, the same address gets reused by an attacker-controlled allocation. At that point, dereferencing the stale pointer reads or writes attacker-controlled data through a type the program still trusts. Because the read/write and the free can be separated by thousands of lines of code, across different functions or even threads, use-after-free bugs are notoriously difficult to spot by manual review — which is exactly why Chromium's security team, in its own published engineering blog posts, has stated that roughly 70% of its high- and critical-severity security bugs are memory-safety issues, with use-after-free historically the single largest category among them.

What is a double-free, and how does it lead to exploitation?

A double-free (CWE-415) happens when free() is called twice on the same pointer without an intervening allocation, corrupting the heap allocator's internal bookkeeping rather than just leaking or exposing data. Most heap allocators maintain free lists — internal linked structures tracking which chunks of memory are available for reuse. Calling free() twice on the same pointer can insert that chunk into the free list twice, and a sufficiently informed attacker who controls allocation timing can exploit that corrupted state to trick the allocator into returning overlapping or attacker-chosen memory addresses on a subsequent malloc() call. That's a well-documented primitive for turning a "just" a crash into arbitrary memory write, which is why double-free bugs are graded as seriously as direct buffer overflows in most vulnerability-scoring guidance. In practice, double-frees usually emerge from the same root cause as use-after-free: unclear ownership. When it's ambiguous which part of a large C++ codebase is responsible for calling delete on an object, two different code paths eventually both think they own the job.

Why do these bugs still dominate critical CVEs after 30+ years?

These bugs persist because C and C++ were designed for manual memory management and raw pointer arithmetic with essentially no runtime safety net, and that design choice hasn't changed even as the software running on it has grown by orders of magnitude. Matt Miller of the Microsoft Security Response Center, in a widely cited 2019 BlueHat IL presentation, reported that roughly 70% of the CVEs Microsoft patched each year over the preceding decade were memory-safety issues — a single vendor's internal analysis, but one that's been echoed by other large C/C++ codebase owners since. In November 2022, the NSA published a Cybersecurity Information Sheet titled "Software Memory Safety," explicitly recommending that new development use memory-safe languages — Rust, Go, C#, Java, Swift — rather than C or C++, and noting that memory-safety issues are among the most common root causes of exploitable software vulnerabilities. The underlying problem isn't that C/C++ developers are careless; it's that correctness here depends on tracking object lifetimes and buffer bounds by hand, consistently, across every code path, in codebases with millions of lines and decades of accumulated changes.

How do ASan and fuzzing catch these bugs before shipping?

AddressSanitizer (ASan), which originated from a Google and academic research team's paper presented at USENIX ATC 2012 and now ships built into both LLVM/Clang and GCC, instruments memory allocations with "redzones" and shadow memory so that an out-of-bounds read or write, a use-after-free, or a double-free crashes immediately at the exact faulty instruction — instead of silently corrupting memory that only causes a crash somewhere unrelated, minutes later. Fuzzing pairs naturally with this: a fuzzer mutates input and feeds it to the program at high speed, and ASan turns memory bugs that would otherwise be invisible into loud, reproducible crashes with a stack trace. Google's OSS-Fuzz, launched in December 2016, runs continuous coverage-guided fuzzing against hundreds of open-source C/C++ projects and has been directly credited with finding a large share of the memory-safety bugs fixed in projects like FFmpeg, OpenSSL, and SQLite over the years. Neither tool prevents the bug class outright — they only catch what your test inputs happen to reach — which is why they're best treated as a detection layer, not a substitute for the underlying fix.

Can Rust interop actually eliminate these bugs instead of just catching them?

Rust interoperability addresses the root cause rather than detecting symptoms, because Rust's ownership model and borrow checker reject use-after-free, double-free, and most out-of-bounds writes at compile time rather than at runtime. Rather than rewriting an entire C++ codebase — usually impractical for large, mature projects — teams increasingly rewrite the highest-risk components (parsers, codecs, network-facing input handling) in Rust and bridge them into existing C++ with tooling like cxx or autocxx, or expose Rust through a C ABI consumed via bindgen. Chromium's security team has documented investing in exactly this incremental strategy, alongside hardened allocator work like MiraclePtr and PartitionAlloc, specifically because the ~70% memory-safety share of high-severity bugs wasn't moving through review and testing improvements alone. This doesn't make memory safety free — FFI boundaries introduce their own correctness burden, and unsafe blocks in Rust reintroduce the exact same risks locally — but for new, high-risk parsing surfaces, it removes an entire bug class rather than relying on catching every instance of it.

How Safeguard fits in

Safeguard doesn't run ASan or fuzz your C/C++ source directly, but memory-safety CVEs in third-party C/C++ dependencies are exactly the kind of finding that benefits most from reachability analysis. Safeguard's static and dynamic reachability support for C/C++ traces whether a vulnerable function in a dependency — say, a buffer-overflow CVE in an image-parsing library — is actually invoked from your call graph, rather than just present in your dependency tree. For a language where a single unreachable CVE can otherwise burn a day of manual code-path tracing, that distinction is what separates a real P0 from noise sitting in your backlog.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.