Memory-Safe by Design: What Rust’s Borrow Checker Actually Prevents (With Real CVEs)

Featured image for "Memory-Safe by Design: What Rust’s Borrow Checker Actually Prevents (With Real CVEs)"

The claim, stated precisely

Diagram contrasting C/C++ double-free with Rust ownership and move semantics

“Rust is memory safe” gets thrown around as marketing shorthand, and but the actual guarantee is narrow and well defined. The borrow checker enforces two rules at compile time: every value has exactly one owner (or an explicit, tracked transfer of ownership), and at any point in the program you can have either one mutable reference or any number of immutable references to a piece of data, never both. Everything the borrow checker prevents falls out of enforcing those two rules consistently, at zero runtime cost.

That’s the whole guarantee. It says nothing about your logic. It’s a type-system claim about pointer and lifetime validity. The gap between that claim and the sales pitch is where credibility gets lost with people who’ve been burned by hype before.

What this actually prevents

Use-after-free. In C/C++, nothing stops you from holding a pointer after the memory it points to has been free()’d or gone out of scope. The pointer is just an address. The compiler has no concept of whether that address is still “yours.” Rust ties a reference’s lifetime to the compiler’s static analysis of how long the underlying data stays valid. Try to keep a reference alive past the point where its owner is dropped, and you get a compile error: error[E0597]: borrowed value does not live long enough. The bug class never makes it to runtime.

fn get_ref() -> &i32 {
    let x = 5;
    &x // error: `x` does not live long enough
}

Double-free. This one falls out of the single-owner model almost for free (no pun intended). In C, two pointers can both think they own the same heap allocation, and both can call free() on it. In Rust, when a value’s owner goes out of scope, drop runs exactly once, because there’s exactly one owner by construction, move semantics transfer ownership instead of duplicating it. You can’t accidentally end up with two owners of a Box. The compiler tracks moves and refuses to compile code that uses a value after it’s been moved.

Data races. The “one mutable XOR many immutable” rule, combined with the Send/Sync marker traits, gets enforced across thread boundaries too. You can’t share a mutable reference across threads without going through a synchronization primitive (Mutex, RwLock, channels, atomics) the type system knows about. That’s a narrower claim than preventing all race conditions (more on that below), but the specific, extremely common bug of two threads mutating the same memory without synchronization is a compile-time error in safe Rust, not a Heisenbug you chase with a thread sanitizer at 2am.

Dangling pointers and iterator invalidation. Mutate a collection while holding a reference into it (push to a Vec while iterating it, say, which can trigger a reallocation and invalidate every pointer into the old buffer) and the same borrow rules catch it. Vec::push requires a mutable borrow, and the checker won’t let you take one while an immutable borrow, your iterator, is still live.

Three real bugs from this exact class

These aren’t hypotheticals. This bug class shows up over and over in security advisories for widely deployed C/C++ software.

CVE-2019-5786 was a use-after-free in Chrome’s FileReader implementation, actively exploited in the wild as part of a zero-day attack chain targeting Chrome users. A FileReader object got freed while a reference to it was still reachable from another code path, and the freed memory was later reused by an attacker-controlled object, giving arbitrary code execution in the renderer process. Textbook UAF: the C++ object model has no compile-time concept of “this pointer outlived its owner.”

CVE-2016-5195, better known as Dirty COW, was a race condition in the Linux kernel’s copy-on-write handling for memory mappings. A race between the MAP_PRIVATE write-fault handler and the madvise(MADV_DONTNEED) path let an unprivileged process trigger a write to a read-only, root-owned file mapping, leading to privilege escalation. It sat in the kernel for roughly nine years before anyone found it. This is the data-race/TOCTOU half of memory safety rather than strict use-after-free, but it’s exactly the class of concurrent-memory-access bug a type system that enforces exclusive mutable access is built to catch.

CVE-2020-0022, BlueFrag, was a use-after-free in Android’s Bluetooth stack (the net/bluetooth subsystem), reachable over the air by a nearby attacker with no user interaction required, and capable of remote code execution within the Bluetooth daemon’s privileges. Same shape as the Chrome bug: object freed on one path, referenced on another, memory reused in between.

These three alone span browser, kernel, and mobile-platform codebases, different teams, different review standards, different levels of sanitizer and fuzzing investment, and they all reduce to the same two or three root causes that the borrow checker structurally rules out. That’s the argument for Rust in security-sensitive code. C++ programmers aren’t careless; this bug class survives expert review, fuzzing, and static analysis at a nonzero, recurring rate, in exactly the projects with the most resources to prevent it.

What Rust does not solve

Logic bugs are still entirely on you. The borrow checker has no opinion about whether your authentication check is correct, whether your crypto runs in constant time, or whether your state machine has a reachable bad state. Memory safety and correctness are different axes. A Rust program can leak secrets, authorize the wrong user, or compute the wrong answer, all while being perfectly memory safe.

unsafe exists, and it’s load-bearing. Anywhere you need raw pointers, FFI into C libraries, certain performance-critical patterns, or to build safe abstractions like Vec itself, you drop into unsafe and the compiler’s guarantees stop applying to that block. Real-world Rust codebases aren’t unsafe-free. Crates like tokio and hashbrown, plus plenty of embedded and systems code, carry meaningful amounts of it, and bugs have in fact turned up in unsafe blocks in widely used crates. Auditing unsafe is a real, ongoing discipline, not a solved problem.

Panics are a denial-of-service vector. Rust converts a large class of memory-corruption bugs into panics: index out of bounds, unwrap on None, integer overflow in debug builds. That’s a strictly better failure mode than silent corruption, but it’s still a crash. In a network-facing service, an attacker who can trigger a reachable panic has a remote DoS primitive. Not segfaulting with corrupted memory is progress. It isn’t immunity.

Supply chain risk doesn’t care about your type system. cargo makes pulling in transitive dependencies trivially easy, and crates.io has had real incidents: typosquatted crate names, and at least one case of a maintainer shipping a version with intentionally malicious behavior. The borrow checker does nothing to vet the hundred transitive dependencies sitting in your Cargo.lock. It’s the same supply-chain problem npm and PyPI have.

Summary

Rust’s ownership model closes a specific, well-understood, historically expensive category of bugs, the kind that, per Microsoft’s and Google’s own internal audits of their C/C++ codebases, accounts for roughly 70% of the high-severity security vulnerabilities they fix. That’s a real, measurable, structural improvement, and the CVEs above aren’t edge cases, they’re representative. But it buys you nothing on logic correctness, its escape hatch needs the same scrutiny unsafe C++ always did, it trades segfaults for panics instead of eliminating crashes, and it inherits the supply-chain problem of every modern package ecosystem. Pick it for what it does, not for what the slide deck implies.

Comments

Leave a Reply