rust-memory-safety-examples src/use_after_free_prevention.rs: Ownership and Lifetimes Mastery¶
What This File Does¶
This file demonstrates how Rust's ownership system and lifetime rules prevent use-after-free vulnerabilities at compile time. Use-after-free is one of the most dangerous classes of memory safety bugs—it occurs when a program accesses memory after that memory has been freed, leading to undefined behavior, crashes, and exploitable security vulnerabilities. The C code at the top of this file illustrates exactly this pattern: allocating memory, freeing it, then returning a dangling pointer that points to memory no longer owned by the program.
What makes this file particularly valuable is that it doesn't just show safe Rust code—it shows the unsafe patterns that would exist if Rust didn't have its ownership system, marked as comments that deliberately won't compile. This approach lets you see both sides of the safety boundary: the dangerous pattern you might write in C, and how Rust's compiler stops you from making the same mistake.
Section 1: The Vulnerability We're Preventing¶
Before diving into Rust's solutions, we need to understand what use-after-free actually looks like in vulnerable code. The module-level documentation presents a C function called vulnerable_use_after_free that commits this cardinal sin: it allocates memory, stores a value, frees the memory, and then returns a pointer to that freed memory. The caller then tries to read through that dangling pointer.
This pattern is deceptively simple to create in C. There's no syntax error, no warning in many compilers, and the code might even appear to work during testing. The freed memory might still contain the value 42, purely by accident—the memory hasn't been overwritten yet. But this is exactly what makes use-after-free bugs so insidious. They can lurk in code for years, only manifesting when the memory allocator reuses that freed block for something else.
The real-world impact is severe. The CVE examples mentioned—SMBGhost and the Chrome FileReader vulnerability—weren't theoretical concerns. They were exploitable bugs that allowed attackers to execute arbitrary code. When you access freed memory, an attacker who can control what gets allocated in that space can effectively control what your code reads, potentially redirecting program flow to malicious code.
See: Companion Code Section 1
Section 2: Ownership as the First Line of Defense¶
The first safe pattern in the file demonstrates ownership preventing use-after-free in the most direct way possible. The function ownership_prevents_uaf creates a Box<i32>—a heap-allocated integer—then copies its value, explicitly drops the box, and finally uses the copied value.
The critical insight here is understanding what drop(data) actually does. In Rust, drop is how you explicitly free heap memory before a value would naturally go out of scope. After this call, the memory that data pointed to has been returned to the allocator. The box is gone. And here's where Rust's ownership system shines: the compiler knows this. The data variable is no longer valid after the drop call.
The commented-out line println!("{}", *data) isn't just a style suggestion—it's code that physically cannot compile. The compiler tracks ownership through every statement in your function. Once data is dropped, attempting to use it produces a compile-time error. There's no runtime check happening here, no garbage collector intervening. The invalid access simply cannot be expressed in valid Rust.
Notice that we can still use value after the drop. That's because we copied the integer value before dropping the box. Integers implement the Copy trait, meaning the *data expression creates a bitwise copy of the integer that exists independently of the box. This distinction between copying values and moving ownership is fundamental to understanding Rust's memory model.
See: Companion Code Section 2
Section 3: Lifetimes Preventing Dangling References¶
The lifetime_prevents_dangling_ref function demonstrates a subtly different scenario: what happens when you try to hold a reference to data that goes out of scope. This is the reference version of use-after-free—you're not freeing memory explicitly, but the automatic cleanup at scope boundaries has the same effect.
The pattern here involves declaring a reference variable in an outer scope, then trying to make it point to data created in an inner scope. The inner scope creates a String, and the commented-out code attempts to store a reference to that string in the outer variable. When the inner scope ends, the String is automatically dropped—its heap memory freed—but we'd still have a reference claiming to point to valid data.
Rust prevents this through lifetime analysis. Every reference in Rust has a lifetime—a region of code during which the reference is valid. The compiler analyzes these lifetimes and refuses to compile code where a reference would outlive its referent. In this case, the String has a lifetime limited to the inner block, while the reference variable has a lifetime extending beyond it. These lifetimes are incompatible, and the compiler rejects the code.
What makes this particularly elegant is that the analysis happens entirely at compile time. There's no runtime tracking of reference validity, no performance cost for this safety. The compiler simply won't produce an executable that contains this class of bug.
See: Companion Code Section 3
Section 4: Borrowing Rules and Aliasing Control¶
The borrowing_prevents_uaf function reveals another dimension of Rust's safety guarantees: the interaction between immutable references and mutation. This pattern catches a class of bugs that's slightly more subtle than the previous examples—bugs where memory might be freed indirectly through mutation while references still exist.
The setup creates a vector and takes an immutable reference to its first element. The commented-out data.clear() call would, if allowed, deallocate the vector's backing storage. The reference would suddenly point to freed memory. This is use-after-free through collection invalidation—a common source of bugs in languages that allow arbitrary mutation.
Rust's borrowing rules prevent this through a simple but powerful principle: when an immutable reference exists, the underlying data cannot be mutated. The compiler sees that reference holds an immutable borrow of data inside the vector, and it therefore blocks any operation that would mutate the vector until that borrow ends.
The borrow ends when reference is last used—in this case, after the println! statement. After that point, data.clear() compiles and runs successfully. This is Rust's Non-Lexical Lifetimes in action: borrows don't extend to the end of their scope, but only until their last use. This refinement, introduced in Rust 2018, makes the borrow checker more permissive while maintaining safety.
See: Companion Code Section 4
Section 5: Reference Counting for Shared Ownership¶
Not all ownership patterns fit neatly into single-owner semantics. Sometimes multiple parts of your program legitimately need to share ownership of data, and you don't know at compile time which one will use it last. The shared_ownership_safe function demonstrates Rc<T>, Rust's reference-counted smart pointer for exactly this scenario.
Rc stands for "Reference Counted." Each call to Rc::clone doesn't copy the underlying data—it increments a counter and returns a new handle to the same allocation. The memory is freed only when the last Rc is dropped and the count reaches zero. This is a form of garbage collection, but it's deterministic and explicit rather than automatic and opaque.
The key safety property here is that you cannot get a dangling reference to data inside an Rc. The reference count ensures the data lives as long as any handle to it exists. The function deliberately drops each clone in sequence, and the memory is freed only after the final drop(data) call—when we're absolutely certain no references remain.
There's an important caveat: Rc is not thread-safe. The reference count isn't updated atomically, so sharing an Rc across threads would introduce data races. For thread-safe shared ownership, Rust provides Arc<T> (Atomic Reference Counted), which uses atomic operations for the counter. This separation keeps Rc efficient for single-threaded code while making the thread-safety requirements explicit in the type system.
See: Companion Code Section 5
Section 6: The SafeObject Pattern - Consuming Self¶
The SafeObject struct and its consume method demonstrate a powerful pattern for managing resource lifecycles: methods that take ownership of self, preventing further use of the object. This pattern appears throughout Rust's ecosystem for operations that logically "finish" with an object.
The consume method's signature—fn consume(self) -> Vec<i32>—takes self by value, not by reference. This means calling consume moves ownership of the entire SafeObject into the method. The method can then destructure the object and return its internal data. After this call, the SafeObject no longer exists as far as the type system is concerned.
The safe_object_usage function shows this in action. After calling obj.consume(), any attempt to use obj produces a compile error. The object has been consumed—its memory hasn't necessarily been freed yet (the Vec data is returned), but the SafeObject wrapper is gone. You cannot accidentally use the object after it's been logically invalidated.
This pattern is essential for implementing safe abstractions over resources that have cleanup requirements. Think of file handles that must be closed, network connections that must be terminated, or database transactions that must be committed. By consuming self, you make it impossible for callers to use the resource after the cleanup operation.
See: Companion Code Section 6
Section 7: The Drop Trait and Deterministic Destruction¶
The compile_time_safety_demo function showcases the Drop trait, Rust's mechanism for running cleanup code when a value goes out of scope. This is RAII (Resource Acquisition Is Initialization) implemented at the language level, and it's the foundation for Rust's deterministic memory management.
The Resource struct implements Drop with a simple print statement. This lets us observe exactly when the destructor runs. When res goes out of scope at the end of the function, Rust automatically calls drop, which runs our custom cleanup code and then frees the memory.
The deterministic nature of Rust's destruction is crucial for understanding how use-after-free is prevented. Unlike garbage-collected languages where objects might be collected at any time (or never, until memory pressure), Rust values are destroyed at precisely predictable points. When a variable goes out of scope, when a value is explicitly dropped, when ownership is transferred and the new owner doesn't use the value—in all these cases, the destructor runs immediately.
This predictability is what makes the ownership model work. The compiler can reason about when values exist and when they don't because the rules are entirely deterministic. There's no uncertainty about whether a value might still be live somewhere. If the borrow checker says a value is gone, it's gone.
See: Companion Code Section 7
Section 8: Testing Ownership Semantics¶
The test module provides executable verification of the ownership patterns discussed throughout the file. These tests don't just check that code works—they document the expected behavior of Rust's ownership system in a way that can be mechanically verified.
The test_ownership_transfer test demonstrates move semantics. When we write let moved_data = data, ownership transfers from data to moved_data. The original data binding becomes invalid—we can't even mention it in the assert. This test would fail to compile if we tried to use data after the move.
The test_rc_shared_ownership test is particularly illuminating because it uses Rc::strong_count to observe the reference count at runtime. We can see the count start at 1, increase to 2 when we clone, then decrease back to 1 when we drop the clone. This provides concrete evidence of the reference counting mechanism that keeps shared data alive.
What these tests demonstrate is that Rust's safety properties aren't just theoretical—they're testable and observable. You can write code that explores the boundaries of the ownership system and verify that it behaves as documented. This testability builds confidence that the safety guarantees hold in practice, not just in theory.
See: Companion Code Section 8
Section 9: Connecting to the Safety Taxonomy¶
This file connects directly to the taxonomy established in lib.rs. Use-after-free is one of the four categories of memory safety vulnerabilities that the crate addresses, alongside buffer overflows, null pointer dereferences, and data races. Understanding how each category is prevented helps build a complete mental model of Rust's safety guarantees.
The pattern established here—showing vulnerable C code, explaining the vulnerability, then demonstrating Rust's prevention mechanism—recurs throughout the crate. Each vulnerability type has its own prevention mechanism, but they all share a common theme: making invalid states unrepresentable at compile time rather than checking for them at runtime.
What distinguishes use-after-free prevention from buffer overflow prevention is the nature of the compile-time analysis. Buffer overflow prevention relies primarily on bounds checking—runtime checks that Rust inserts automatically. Use-after-free prevention relies primarily on ownership and lifetime analysis—compile-time checks that produce no runtime overhead. Both are forms of safety, but they operate through different mechanisms.
This distinction matters for understanding Rust's performance characteristics. The ownership system adds zero runtime cost while preventing an entire class of memory safety bugs. When you hear that Rust provides "zero-cost abstractions," the ownership system is the primary example. The abstraction of safe memory management costs nothing at runtime because all the checking happens before your program runs.
See: Companion Code Section 9
Key Takeaways¶
-
Use-after-free vulnerabilities in C occur when code accesses memory after it's been freed, leading to undefined behavior and exploitable security bugs that have caused real-world CVEs.
-
Rust's ownership system prevents use-after-free at compile time by tracking when values are dropped and making it a compile error to use them afterward—there's no runtime check, just static analysis.
-
Lifetimes ensure references cannot outlive their referents, preventing dangling references even when data is cleaned up automatically at scope boundaries.
-
The borrowing rules prevent indirect use-after-free by blocking mutation while references exist, ensuring that operations like clearing a vector can't invalidate existing references to its elements.
-
Reference counting with
Rc<T>provides shared ownership for cases where single-owner semantics don't fit, with the memory freed only when all owners are gone. -
Methods that consume
selfexplicitly transfer ownership, making it impossible to use an object after an operation that logically invalidates it—a powerful pattern for resource lifecycle management.
What to Read Next¶
How do these ownership patterns apply when building real programs? Explore examples/use_after_free_prevention.rs to see these concepts used in practice with more complex scenarios.
What's the relationship between use-after-free prevention and the other safety categories? Return to src/lib.rs to review how ownership fits into the overall taxonomy of memory safety vulnerabilities.
How does Rust prevent the next category of memory bugs? Continue to the next lesson, which covers null pointer prevention and the Option<T> type that makes null references impossible.