Skip to content

rust-memory-safety-examples src/use_after_free_prevention.rs: Code Companion

Reference code for the Ownership and Lifetimes Mastery lecture. Sections correspond to the lecture document.


Section 1: The Vulnerability We're Preventing

// C code demonstrating the use-after-free vulnerability
// This is the pattern Rust's ownership system prevents

int* vulnerable_use_after_free() {
    int* ptr = malloc(sizeof(int));  // Allocate heap memory
    *ptr = 42;                        // Store a value
    free(ptr);                        // Memory freed - ptr is now dangling
    return ptr;                       // DANGER: returning invalid pointer!
}

void exploit() {
    int* p = vulnerable_use_after_free();
    printf("%d\n", *p);  // USE-AFTER-FREE: undefined behavior
    // Memory might still contain 42... or anything else
    // Attacker could control what's now in that memory location
}

This C code compiles without warnings but contains a critical security vulnerability. The pointer ptr becomes invalid after free(), yet nothing stops us from returning and dereferencing it. Rust makes this pattern impossible to express.


Section 2: Ownership as the First Line of Defense

pub fn ownership_prevents_uaf() {
    let data = Box::new(42);  // Box<i32>: heap-allocated integer
    let value = *data;        // Copy the i32 value (i32 implements Copy)

    drop(data);  // Explicitly free the heap memory

    // This would NOT compile:
    // println!("{}", *data);  // Error: use of moved value `data`

    println!("Copied value: {}", value);  // Safe: value is independent copy
}

The drop() function takes ownership of data, consuming it. After this point, the compiler marks data as invalid. The *data dereference copies the integer because i32: Copy—primitive types are bitwise copied rather than moved.


Section 3: Lifetimes Preventing Dangling References

pub fn lifetime_prevents_dangling_ref() {
    let _reference: Option<&String> = None;  // Reference lives in outer scope

    {
        let _data = String::from("temporary");  // String lives in inner scope

        // This would NOT compile:
        // reference = Some(&_data);
        // Error: `_data` does not live long enough
        //        borrowed value does not live long enough
    }
    // _data is dropped here - its memory freed

    // If the assignment above compiled, _reference would be dangling here
}

The lifetime of _data is limited to the inner block (the curly braces). The compiler performs lifetime analysis and sees that _reference would outlive what it references. This is a compile-time check with zero runtime cost.


Section 4: Borrowing Rules and Aliasing Control

pub fn borrowing_prevents_uaf() {
    let mut data = vec![1, 2, 3];  // Vec<i32> with heap-allocated storage

    let reference = &data[0];  // Immutable borrow of first element

    // This would NOT compile while `reference` exists:
    // data.clear();
    // Error: cannot borrow `data` as mutable because it is also
    //        borrowed as immutable

    println!("First element: {}", reference);  // Last use of `reference`
    // Non-Lexical Lifetimes: borrow ends here, not at scope end

    data.clear();  // Now mutation is allowed
    println!("Vector cleared");
}

The clear() method requires &mut self, but an immutable borrow (reference) is active. Rust's rule: you can have either one mutable reference OR any number of immutable references, never both. The borrow of reference ends after its last use, enabling the subsequent mutation.


Section 5: Reference Counting for Shared Ownership

use std::rc::Rc;

pub fn shared_ownership_safe() {
    let data = Rc::new(vec![1, 2, 3]);  // Rc<Vec<i32>>: reference-counted
    let clone1 = Rc::clone(&data);       // Increments ref count, doesn't clone data
    let clone2 = Rc::clone(&data);       // Ref count now 3

    println!("Data from clone1: {:?}", clone1);
    println!("Data from clone2: {:?}", clone2);

    // Memory freed only when ALL Rc handles are dropped
    drop(clone1);  // Ref count: 2
    drop(clone2);  // Ref count: 1
    drop(data);    // Ref count: 0 → memory freed
}

Rc::clone() is cheap—it increments an integer counter, not a deep copy. The underlying Vec is deallocated only when strong_count reaches zero. Note: Rc is not thread-safe; use Arc for multi-threaded scenarios.


Section 6: Consuming Self for Ownership Transfer

pub struct SafeObject {
    data: Vec<i32>,
}

impl SafeObject {
    pub fn new(data: Vec<i32>) -> Self {
        Self { data }
    }

    // Borrows self immutably - object still usable after call
    pub fn get_data(&self) -> &[i32] {
        &self.data
    }

    // Takes ownership of self - object consumed, cannot be used again
    pub fn consume(self) -> Vec<i32> {
        self.data  // Moves data out of the struct
    }
}

pub fn safe_object_usage() {
    let obj = SafeObject::new(vec![1, 2, 3]);
    let data_ref = obj.get_data();  // Borrows obj
    println!("Data: {:?}", data_ref);

    let owned_data = obj.consume();  // Moves obj - takes ownership

    // This would NOT compile:
    // obj.get_data();  // Error: value used after move

    println!("Owned data: {:?}", owned_data);
}

The method signature tells the whole story: &self borrows, self consumes. When consume(self) takes ownership, the caller loses access to obj. This pattern is common for builder types and resource cleanup.


Section 7: Drop Trait and Deterministic Cleanup

pub fn compile_time_safety_demo() {
    struct Resource {
        id: i32,
    }

    impl Drop for Resource {
        fn drop(&mut self) {
            println!("Resource {} freed", self.id);
            // Custom cleanup logic runs here
        }
    }

    let res = Resource { id: 1 };
    println!("Resource created: {}", res.id);

    // `res` automatically dropped at scope end
    // Drop::drop() called deterministically - not garbage collected
}
// Output:
// Resource created: 1
// Resource {} freed: 1

The Drop trait provides a destructor that runs when a value goes out of scope. Unlike garbage collection, this is deterministic—you know exactly when cleanup occurs. The compiler inserts the drop call automatically; you cannot call drop() method directly (use std::mem::drop() to drop early).


Quick Reference

Ownership Pattern Signature Effect
Take ownership fn foo(x: T) Caller loses access to x
Borrow immutably fn foo(x: &T) Caller retains access, cannot mutate
Borrow mutably fn foo(x: &mut T) Caller retains access, exclusive mutation
Return owned fn foo() -> T Caller receives ownership
Return reference fn foo(&self) -> &T Lifetime tied to self

Smart Pointer Comparison

Type Ownership Thread-Safe Use Case
Box<T> Single owner N/A (moved) Heap allocation, recursive types
Rc<T> Shared, counted No Single-threaded shared ownership
Arc<T> Shared, counted Yes Multi-threaded shared ownership

Key Compiler Errors

// "use of moved value"
let a = Box::new(1);
let b = a;
println!("{}", a);  // Error: a was moved to b

// "does not live long enough"  
let r;
{ let x = 5; r = &x; }  // Error: x dropped while r borrows it

// "cannot borrow as mutable because also borrowed as immutable"
let mut v = vec![1];
let first = &v[0];
v.push(2);  // Error: push needs &mut, but first holds &