Memory Safety
Why Rust prevents memory bugs at compile time
The Memory Problem
Memory safety bugs are the single largest class of security vulnerabilities in systems software. They have been responsible for decades of exploits, data breaches, and system crashes.
The numbers are staggering
- Microsoft reported that ~70% of all CVEs they assign are caused by memory safety issues (buffer overflows, use-after-free, etc.).
- Google's Chrome team found that ~70% of serious security bugs in Chrome are memory safety problems.
- Android saw memory-safety vulnerabilities drop from 76% to 24% of total vulnerabilities as the percentage of new memory-unsafe code decreased.
These are not obscure corner cases. They are the bread and butter of real-world exploits: buffer overflows, use-after-free, double-free, null pointer dereferences, and data races. C and C++ give programmers full control over memory, but that freedom comes with enormous responsibility -- and humans inevitably make mistakes.
Common Memory Bugs in C/C++ ──────────────────────────────── 1. Buffer overflow → write past array bounds 2. Use-after-free → access freed memory 3. Double free → free the same pointer twice 4. Null dereference → follow a NULL pointer 5. Data race → concurrent unsynchronized access 6. Uninitialized memory → read before writing ──────────────────────────────── Rust prevents ALL of these at compile time.
Buffer Overflow
A buffer overflow occurs when a program writes data beyond the bounds of an allocated memory buffer. In C, arrays have no built-in bounds checking, so writing past the end of an array silently corrupts adjacent memory.
#include <string.h>
void vulnerable(const char *input) {
char buffer[64];
// No bounds checking! If input > 64 bytes,
// it overwrites the stack frame, return address,
// and potentially allows arbitrary code execution.
strcpy(buffer, input);
}
int main() {
// An attacker sends 200 bytes...
char payload[200];
memset(payload, 'A', 199);
payload[199] = '\0';
vulnerable(payload); // BOOM: stack smash
return 0;
}Stack Layout During Overflow ───────────────────────────── High addresses ┌──────────────────────┐ │ return address │ ← overwritten by attacker! ├──────────────────────┤ │ saved base pointer │ ← corrupted ├──────────────────────┤ │ │ │ buffer[64] │ ← only 64 bytes allocated │ │ ├──────────────────────┤ Low addresses The attacker's 200 bytes overflow the 64-byte buffer and overwrite the return address, hijacking control flow.
This class of bug has been the basis of countless exploits since the 1988 Morris Worm. Stack canaries, ASLR, and DEP are mitigations, but they are band-aids -- not solutions. The root cause is that C trusts the programmer to stay in bounds.
Use-After-Free
A use-after-free bug occurs when a program continues to use a pointer after the memory it points to has been freed. The freed memory may be reallocated for a different purpose, leading to data corruption, crashes, or exploitable vulnerabilities.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
typedef struct {
char name[32];
int admin; // 0 = normal user, 1 = admin
} User;
int main() {
User *user = malloc(sizeof(User));
strcpy(user->name, "Alice");
user->admin = 0;
printf("User: %s, admin: %d\n", user->name, user->admin);
free(user); // Memory is freed
// ... later in the code, someone allocates again:
char *data = malloc(sizeof(User));
memset(data, 1, sizeof(User)); // Fill with 1s
// The old pointer still exists and is used:
printf("User: %s, admin: %d\n",
user->name, // reading freed memory!
user->admin); // now reads 1 → admin!
return 0;
} Timeline of a Use-After-Free
─────────────────────────────
1. malloc(User) → user points to 0x1000
2. user->admin = 0 → memory at 0x1000 is valid
3. free(user) → 0x1000 returned to allocator
(but 'user' still holds 0x1000!)
4. malloc(User) → allocator reuses 0x1000
memset(data,1) → fills 0x1000 with 0x01 bytes
5. user->admin → reads from 0x1000 → gets 1
user is now "admin"!This is not hypothetical. Use-after-free vulnerabilities have been exploited in browsers, operating systems, and network services. They are especially dangerous because the program appears to work normally until the freed memory is reused, making them difficult to detect through testing alone.
Rust's Solution: Ownership
Rust introduces a system of ownership with three simple rules, enforced entirely at compile time. These rules make buffer overflows and use-after-free impossible without any runtime cost.
The Three Rules of Ownership
- Each value in Rust has exactly one owner.
- There can only be one owner at a time.
- When the owner goes out of scope, the value is dropped (freed).
fn main() {
let user = String::from("Alice");
let new_owner = user; // ownership MOVES to new_owner
// println!("{}", user);
// ^^^^^^^^^^^^^^^^^^^^^^^^
// ERROR: value borrowed here after move
// Rust won't let you use 'user' after the move!
println!("{}", new_owner); // This is fine
} // new_owner is dropped here, memory freed exactly oncefn main() {
let buffer = [0u8; 64];
// Rust checks bounds at compile time when possible,
// and at runtime otherwise:
// let val = buffer[100];
// ^^^^^^^^^^^ index out of bounds!
// This panics instead of silently corrupting memory.
// Safe iteration -- no overflow possible:
for (i, byte) in buffer.iter().enumerate() {
// 'i' is always in bounds
println!("buffer[{}] = {}", i, byte);
}
}The ownership system also governs borrowing: you can have either one mutable reference OR any number of immutable references, but never both at the same time. This eliminates data races at compile time.
The Compiler as Guardian
In C/C++, the compiler trusts you. In Rust, the compiler verifies you. The borrow checker is a compile-time analysis pass that ensures every reference is valid for its entire lifetime. Let's see it catch a real dangling reference.
fn create_greeting() -> &str {
let s = String::from("Hello, world!");
&s // returning a reference to a local variable!
}
// s is dropped here, so the reference would dangle.
// COMPILER OUTPUT:
// error[E0106]: missing lifetime specifier
// error[E0515]: cannot return reference to local variable `s`
// --> dangling.rs:3:5
// |
// 3 | &s
// | ^^ returns a reference to data owned
// | by the current functionThe equivalent C code would compile without warning and produce undefined behavior. Rust catches this at compile time -- before any code runs, before any test is written, before any user is affected.
#include <stdlib.h>
#include <string.h>
// WARNING: This compiles but is undefined behavior!
char* create_greeting() {
char s[64];
strcpy(s, "Hello, world!");
return s; // returning pointer to stack memory!
}
// s no longer exists after this function returns.
// The caller gets a dangling pointer.Rust's Compile-Time Safety Net ───────────────────────────────── Bug type C/C++ Rust ────────────────────────────────────────────── Buffer overflow Compiles ✓ Compile error ✗ Use-after-free Compiles ✓ Compile error ✗ Double free Compiles ✓ Compile error ✗ Dangling pointer Compiles ✓ Compile error ✗ Data race Compiles ✓ Compile error ✗ Null dereference Compiles ✓ No null refs ✗ ────────────────────────────────────────────── ✓ = bug gets through ✗ = bug is caught
Summary
Rust achieves memory safety without a garbage collector. There is no pause-the-world collection, no reference counting overhead, no runtime safety checks beyond the occasional bounds check. All the heavy lifting is done at compile time.
Ownership eliminates memory bugs
The three rules of ownership -- one owner, one at a time, drop when out of scope -- make use-after-free, double-free, and memory leaks structurally impossible.
Borrowing eliminates data races
The rule that you can have either one mutable reference or many immutable references prevents concurrent mutation, the root cause of data races.
Zero runtime cost
All ownership and borrowing checks happen at compile time. The generated machine code is as fast as equivalent C/C++ -- with none of the memory bugs.
This is Rust's core promise: the performance of C, the safety of a managed language, and no garbage collector. The compiler is your partner, catching bugs that would otherwise lurk undetected until deployment.