August 3, 20255 min

How does Rust prevent dangling pointer at compile time?

m
mayo

A dangling pointer occurs when a pointer references memory that has already been freed, leading to undefined behavior like crashes or security vulnerabilities. In languages like C/C++, this is a common issue:

int* create_int() {
    int x = 5;  // `x` lives on the stack
    return &x;  // Returns a pointer to `x`...
}  // `x` is destroyed here (dangling pointer returned!)

Step by step, here is what the stack does in that C function — and why the returned pointer is already garbage before the caller ever reads it:

1. frame pushed caller frame int* p; not set yet create_int frame x = 5 lives here 2. return &x caller frame p = address of x create_int frame x = 5 still alive 3. frame popped p still holds the address nothing marked it invalid reclaimed slot reused by the next call C happily compiles this: the pointer outlives the frame it points into. Rust rejects step 2 outright, so step 3 can never happen.

Rust eliminates dangling pointers at compile time using its ownership model and lifetime system, ensuring memory safety without runtime overhead.

Rejected fn dangling() { let s = ..; } s dropped at end of scope return &s compile error: dangling reference Accepted fn valid<'a>(s: &'a String) input lifetime 'a borrowed -> &'a String { s } output tied to caller's data The borrow checker rejects references that outlive their data's scope

How Rust Prevents Dangling Pointers

Rust uses two key mechanisms to prevent dangling pointers:

1. Ownership + Borrowing Rules

  • Rule: References (&T or &mut T) must not outlive the data they point to.
  • Enforced by: The borrow checker, which tracks variable scopes and ensures references remain valid.

Example: Rejected at Compile Time:

fn dangling() -> &String {  // Missing lifetime specifier!
    let s = String::from("hello");
    &s  // ERROR: `s` dies at end of function
}       // Compiler: "returns a reference to dropped data"

Fixed with Lifetimes (Explicit Guarantee):

fn valid_reference<'a>(s: &'a String) -> &'a String {
    s  // OK: Returned reference tied to input's lifetime
}

2. Lifetime Annotations

  • Rust requires explicit lifetime declarations ('a) when references cross scope boundaries.
  • The compiler ensures all references obey their assigned lifetimes, preventing references to freed memory.

Example: Struct with Reference:

struct Book<'a> {  // Must declare lifetime
    title: &'a str  // Reference must live as long as `Book`
}

fn main() {
    let title = String::from("Rust");
    let book = Book { title: &title };
    // `book.title` cannot outlive `title`
}

Why This Matters

Language Dangling Pointer Risk Safety Mechanism
C/C++ High (manual memory mgmt) None (programmer's responsibility)
Rust Zero Compile-time checks (ownership + lifetimes)

Key Takeaways

✅ Rust’s compiler guarantees:

  • No references to freed memory.
  • No undefined behavior from dangling pointers.
  • Safety without runtime overhead.

Real-World Impact: Crates like hyper (HTTP) and tokio (async) rely on these guarantees for secure, performant code.