The Numbers
Microsoft published internal analysis in 2019 showing that roughly 70% of their CVEs over the preceding decade were caused by memory safety issues. Google reported the same figure for Chrome in 2020. The Android security team found the same proportion in Android's C/C++ code. These are not outliers β they are the baseline across any large C/C++ codebase.
The persistence of these numbers is what makes them striking. Despite decades of tools β ASAN, Valgrind, fuzzers, static analysers, code review β the rate has not meaningfully declined. The tools catch individual bugs, but the underlying language model that allows these bugs to be written in the first place is unchanged.
"We can keep playing whack-a-mole with individual bugs. Or we can use languages where this class of bug is structurally impossible." β common framing in the memory-safety debate
Buffer Overflow
A buffer overflow occurs when data is written past the end of an allocated buffer. In C, the language has no automatic bounds checking β the programmer is responsible for ensuring writes stay within allocation boundaries. When that check is missing or incorrect, adjacent memory is overwritten.
The classic stack buffer overflow overwrites a function's return address with an attacker-controlled value. When the function returns, execution jumps to the attacker's location β traditionally shellcode on the stack, in modern exploits a ROP (Return-Oriented Programming) chain that bypasses NX/DEP by stringing together existing code gadgets.
Heap buffer overflows are often more useful to attackers because heap metadata controls memory allocator behaviour. Overflowing a heap chunk overwrites the size or forward pointer of an adjacent free chunk. When that chunk is subsequently freed or allocated, the corrupted metadata causes the allocator to write attacker-controlled data to attacker-controlled addresses β a write-what-where primitive.
Use-After-Free
Use-after-free (UAF) is the most commonly exploited memory safety bug class in browsers and operating system kernels today. It occurs when a pointer is used after the memory it references has been freed. The freed memory can be reallocated to a different object, so reads and writes through the stale pointer access the new object's data β type confusion that can corrupt critical fields or leak pointers.
The exploitation pattern is: free the vulnerable object, allocate a carefully-sized object in the same memory location (heap spray or targeted allocation), use the stale pointer to interact with the new object as if it were the old type. In browser exploitation this commonly means manufacturing a fake ArrayBuffer that points to arbitrary memory, giving JavaScript full read/write access to the process.
Heap Corruption
Double-free bugs β freeing the same pointer twice β corrupt the allocator's free list. In glibc's ptmalloc, a double-free creates a cycle in the tcache (thread-local cache) free list. The next two allocations of the same size return the same address β two distinct heap allocations pointing to the same memory. One allocation's write is another's read, enabling overlap-based type confusion.
Integer overflows that feed into size calculations for allocations are another major source. A multiplication like count * sizeof(element) overflows a 32-bit integer when count is large, producing a small allocation that is subsequently written past its end. This is the root cause of several class-break vulnerabilities in image parsing libraries and network protocol parsers.
Why They Persist in C/C++
The tools for catching memory safety bugs in C/C++ are impressive. AddressSanitizer catches the majority of spatial and temporal memory errors at runtime with 2x overhead. Valgrind's Memcheck is more thorough but 20x slower. Fuzzers like libFuzzer and AFL+ have found enormous numbers of bugs. And yet the bugs keep appearing in new code.
The reason is that these tools are detective, not preventive. They catch bugs that are triggered during testing β which means the bug must exist in the code, reach a state where it is exercised by a test or fuzz input, and produce a symptom detectable by the tool. UAFs in rarely-executed error paths, overflows that require specific input combinations, and race conditions that need concurrent execution often survive all tooling and ship to production.
The fundamental issue: In C and C++, the language lets you write code that is undefined behaviour. The compiler is allowed to assume UB never happens and optimise accordingly β sometimes in ways that eliminate the very check you added to prevent the overflow.
How Rust Prevents Them
Rust's ownership and borrowing system enforces memory safety at compile time. The compiler tracks ownership and lifetime of every value. The rules are:
- Every value has exactly one owner. When the owner goes out of scope, the value is dropped (freed).
- You can have multiple immutable references (
&T) or one mutable reference (&mut T), but never both simultaneously. - References cannot outlive the value they point to. The borrow checker enforces this at compile time, not runtime.
These rules make UAF impossible by construction β if you free the memory (drop the owner), all references to it have already expired. They make data races impossible β you cannot have a mutable reference simultaneously with any other reference. Buffer overflows in safe Rust are caught by bounds checks that are emitted automatically (and often optimised away by LLVM when bounds can be proven statically).
The unsafe keyword exists for FFI, hardware access, and performance-critical cases where the borrow checker is too conservative. Unsafe code requires explicit opt-in and can be audited specifically. In a well-structured Rust codebase, unsafe blocks are small, isolated, and heavily reviewed β unlike C/C++ where every line is implicitly unsafe.
Migration Strategy
A full C-to-Rust rewrite is rarely practical. The realistic approach for most organisations is incremental replacement at module boundaries:
- Identify the highest-risk components: Parsers, serialisers, network protocol handlers, and anything touching untrusted input first. These are where memory safety bugs matter most.
- Use Rust via FFI: Rust can expose a C-compatible API. Rewrite one module, link it into the existing C/C++ binary, and test. This lets you ship incrementally without a flag day.
- Bindgen and cbindgen: These tools automate generating Rust bindings to C APIs and C headers from Rust interfaces respectively. They eliminate the manual binding maintenance burden.
- Use the Android and Chromium models: Both projects now write new code in Rust, keeping existing C/C++ for the long tail but ensuring the expanding attack surface uses the safer language.
- Maintain tooling discipline during the transition: Keep ASAN and fuzzing on C/C++ components throughout. Don't drop C/C++ hardening just because you're moving toward Rust β the migration takes years.
The return on investment: Google's Project Zero data shows that Rust components in Chrome and Android have a significantly lower CVE rate than equivalent C/C++ components of similar age and complexity. The upfront migration cost is real; so is the long-term reduction in security debt.