Skip to content

Instantly share code, notes, and snippets.

@geocine
Last active August 30, 2025 14:04
Show Gist options
  • Save geocine/e4e9a6dea1b533d704e58bf84cf6fad9 to your computer and use it in GitHub Desktop.
Save geocine/e4e9a6dea1b533d704e58bf84cf6fad9 to your computer and use it in GitHub Desktop.
Understanding the Rust Borrow Checker and Use-After-Free

Understanding the Rust Borrow Checker and Use-After-Free

As someone familiar with JavaScript, Python, C#, and Go, you're about to encounter Rust's unique approach to memory safety through its borrow checker. Understanding this concept requires first grasping what problems it solves, particularly the dangerous "use-after-free" vulnerability.

What is Use-After-Free?

Use-after-free (UAF) is a critical memory safety vulnerability that occurs when a program continues to use a memory location after it has been freed or deallocated. This creates what's called a dangling pointer - a pointer that still references a memory address that no longer contains valid data. 1234

Here's a simple example in C that demonstrates the problem:

int* ptr = malloc(sizeof(int));
*ptr = 42;
free(ptr);        // Memory is freed
printf("%d", *ptr); // Use-after-free! Accessing freed memory

The consequences can be severe: 51

  • Memory corruption and crashes
  • Data breaches (attackers can manipulate freed memory)
  • Arbitrary code execution (attackers inject malicious code)
  • Security vulnerabilities

In languages you're familiar with, this problem is handled differently:

  • JavaScript and Python: Garbage collection prevents this entirely
  • C#: Garbage collection in managed code (unsafe code can still have issues)
  • Go: Garbage collection handles memory automatically

Memory Management: The Landscape

To understand why Rust's approach is revolutionary, let's compare memory management strategies: 67

Manual Memory Management (C/C++)

  • Programmer explicitly allocates (malloc, new) and frees (free, delete) memory
  • Pros: Complete control, predictable performance
  • Cons: Error-prone, vulnerable to use-after-free, memory leaks, double-free

Garbage Collection (JavaScript, Python, C#, Go)

  • Runtime automatically tracks object references and frees unused memory
  • Pros: Memory safe, eliminates use-after-free and memory leaks
  • Cons: Performance overhead, unpredictable pauses, higher memory usage 89

Rust's Ownership Model

  • Compile-time memory safety without garbage collection
  • Pros: Memory safe, zero-cost abstraction, predictable performance
  • Cons: Learning curve, "fighting the borrow checker"

Stack vs Heap Memory

Before diving into ownership, you need to understand where data lives: 1011

Stack Memory

  • Fast: Allocation is just moving a pointer
  • Limited: Fixed size (typically ~8MB)
  • Automatic: Cleaned up when variables go out of scope
  • LIFO: Last-in, first-out like a stack of plates
fn main() {
    let x = 5;         // Stored on stack
    let y = 10;        // Stored on stack
} // x and y automatically cleaned up

Heap Memory

  • Flexible: Dynamic allocation, arbitrary sizes
  • Slower: Requires finding free space
  • Manual: Must be explicitly managed
  • Fragmented: Can develop holes over time
fn main() {
    let x = Box::new(5); // 5 stored on heap, pointer on stack
} // Heap memory automatically freed when x goes out of scope

Rust's Ownership Rules

Rust's memory safety is built on three fundamental rules: 121314

  1. Each value has exactly one owner
  2. There can only be one owner at a time
  3. When the owner goes out of scope, the value is dropped

These rules are enforced at compile time, meaning memory safety bugs are caught before your code ever runs.

The Borrow Checker Explained

The borrow checker is Rust's compile-time guardian that enforces ownership rules. Think of it as a strict librarian who tracks every book (memory) and ensures: 151612

  • Only one person can write in a book at a time
  • Multiple people can read a book simultaneously
  • Nobody can access a book after it's been returned (freed)

Core Borrowing Rules

The borrow checker enforces these rules: 1715

At any given time, you can have either:

  • One mutable reference (exclusive write access)
  • Any number of immutable references (shared read access)

Plus:

  • References must always be valid (no dangling pointers)
  • You cannot use a value after it has been moved

Comparing to Your Familiar Languages

Let's see how this differs from languages you know:

Python Example (Dangerous if it were C)

a = [1, 2, 3]
b = a          # Both point to same memory
a.append(4)    # Modifies behind b's back
print(b)       # [1, 2, 3, 4] - "unexpected" change

In Python, this works because garbage collection prevents memory issues. But imagine if a could be freed while b still referenced it!

Rust Equivalent

let mut a = vec![1, 2, 3];
let b = &mut a;        // Mutable borrow of a
// a.push(4);          // ERROR! Cannot use a while b borrows it
b.push(4);             // OK: Use b to modify
println!("{:?}", b);   // [1, 2, 3, 4]
// Now b goes out of scope, a can be used again

The borrow checker prevents the "modification behind your back" problem by ensuring exclusive access. 15

How Rust Prevents Use-After-Free

The borrow checker prevents use-after-free through several mechanisms:

1. Ownership Transfer (Move Semantics)

let v = vec![1, 2, 3];
take_ownership(v); // v is "moved" into the function
// println!("{:?}", v); // ERROR! v no longer accessible

2. Borrowing Instead of Moving

let v = vec![1, 2, 3];
borrow_value(&v);  // Only borrow, don't take ownership
println!("{:?}", v); // OK! v is still accessible

3. Lifetime Checking

fn dangling_reference() -> &i32 {
    let x = 5;
    &x  // ERROR! Cannot return reference to local variable
}   // x is dropped here, making the reference invalid

The Mental Model: Coming from GC Languages

If you're coming from JavaScript, Python, C#, or Go, here's the key mental shift:

In GC Languages:

  • Create objects freely
  • Runtime tracks and cleans up automatically
  • Some performance cost and unpredictability
  • Memory safety guaranteed

In Rust:

  • Think about who "owns" each piece of data
  • Compiler ensures memory safety at compile time
  • Zero runtime cost for memory management
  • Learning curve to satisfy the borrow checker

Practical Benefits for Your Background

Given your experience with JS, Python, C#, and Go:

Performance Gains

  • No GC pauses: Unlike Go and C#, Rust has predictable performance 9
  • Lower memory usage: No GC overhead, more efficient than Node.js 18
  • Zero-cost abstractions: High-level features with C-like performance

Safety Guarantees

  • Eliminate entire bug classes: No use-after-free, double-free, or memory leaks
  • Thread safety: The type system prevents data races
  • Compile-time guarantees: Catch bugs before deployment

Real-World Impact

  • Discord switched from Go to Rust for performance-critical services due to GC latency 8
  • Memory usage comparisons show Rust using significantly less memory than GC languages 18
  • Security benefits: Eliminates ~70% of security vulnerabilities related to memory safety

Getting Started: Fighting the Borrow Checker

Initially, the borrow checker can feel frustrating. Common beginner experiences: 19

// This won't compile
let mut vec = vec![1, 2, 3];
let first = &vec[0];
vec.push(4); // ERROR! Cannot modify vec while first borrows it

But this "fight" teaches you to write better, safer code. The borrow checker is teaching you patterns that prevent bugs that could cause security vulnerabilities in other languages.

Summary

The Rust borrow checker is a compile-time memory safety system that prevents use-after-free and other memory bugs without the performance cost of garbage collection. Coming from GC languages, you'll need to think more about ownership and lifetimes, but in exchange, you get:

  • Memory safety without runtime overhead
  • Predictable performance without GC pauses
  • Elimination of entire bug classes that plague other systems languages

The learning curve is real, but the payoff is significant: you can write systems-level code with the safety guarantees you're used to in high-level languages, but with the performance characteristics of C and C++.


References

Footnotes

  1. What Is a Use-After-Free Bug? | Paubox 2

  2. CWE-416: Use After Free - Backslash Security

  3. Dangling, Void, Null, Wild Pointers - GeeksforGeeks

  4. Dangling Pointer in Programming - GeeksforGeeks

  5. Use-after-free vulnerability: what it is and how to fix it

  6. Understanding Garbage Collection - Aerospike

  7. Garbage collection (computer science) - Wikipedia

  8. Rust vs. Go for Backend Services - mbinjamil.dev 2

  9. Rust vs. Go: The Performance Debate - Bitfield Consulting 2

  10. Memory Management in Rust: Stack vs. Heap - dev.to

  11. Stack vs. Heap in Rust - Koding Korp

  12. The Borrow Checker - Rustc Dev Guide 2

  13. Rust Ownership 101 - Hashnode

  14. Understanding Ownership - The Rust Programming Language

  15. The Intuition Behind Rust's Borrowing Rules and Ownership - Raining Computers Blog 2 3

  16. Rust Lifetimes Simplified Part 1: The Borrow Checker - Dev Genius

  17. What is the biggest difference between garbage collection and ownership? - Rust Users Forum

  18. Memory consumption of async Rust services - pkolaczk's blog 2

  19. Rust's Most FEARED Concept (Explained with a Simple Analogy) - YouTube

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment