Rust Systems Programming Memory

Rust Memory Safety: Ownership and Borrowing Explained

AM
Alex Mercer
Systems Developer
Nov 15, 2025
22 min read

What You'll Learn

How Rust solves the billion-dollar mistake (Null Pointers and memory leaks) at compile time without a Garbage Collector, using its unique system of Ownership and Borrowing.

The Problem with C/C++ and Java

Memory management is hard. Historically, languages forced you to choose between two evils:

  • Manual Memory Management (C/C++): The programmer must explicitly malloc() and free() memory. This leads to horrific bugs: double frees, memory leaks, and use-after-free vulnerabilities.
  • Garbage Collection (Java/Go/Python): A background process pauses execution to scan for unused memory and clean it up. This leads to unpredictable performance spikes and high memory overhead.

Rust offers a third way: Ownership. Memory is managed through a system of rules checked at compile time. No garbage collector, no manual freeing, and 100% memory safety.

1. The Rules of Ownership

Rust's compiler enforces three strict rules:

  1. Each value in Rust has a variable that's called its owner.
  2. There can only be one owner at a time.
  3. When the owner goes out of scope, the value will be dropped (memory freed automatically).
rust — The Move Semantic
fn main() {
    let s1 = String::from("hello"); // s1 owns the memory on the heap
    
    let s2 = s1; // Ownership is MOVED to s2. 
    
    // println!("{}", s1); 
    // ❌ ERROR: "value borrowed here after move"
    // s1 is completely invalid now. This prevents double-free errors!
    
    println!("{}", s2); // ✅ Works perfectly
} // s2 goes out of scope, memory is freed here.

2. Borrowing (References)

Moving ownership every time you pass a variable to a function would be exhausting. Instead, Rust allows you to borrow values using references (&).

rust — Borrowing
fn main() {
    let s1 = String::from("hello");

    // We pass a reference (&s1) instead of moving ownership
    let len = calculate_length(&s1);

    // ✅ We can still use s1 here because we only borrowed it!
    println!("The length of '{}' is {}.", s1, len);
}

// Function takes a reference to a String, not ownership
fn calculate_length(s: &String) -> usize {
    s.len()
} // s goes out of scope, but since it doesn't own the data, nothing is freed.

3. The Borrow Checker Rules

To prevent Data Races (where two pointers access data simultaneously and one modifies it), the compiler's Borrow Checker enforces a strict rule at compile time:

You can have exactly ONE mutable reference OR ANY NUMBER of immutable references, but NOT BOTH at the same time.

rust — Data Race Prevention
fn main() {
    let mut s = String::from("hello");

    let r1 = &mut s;
    // let r2 = &mut s; 
    // ❌ ERROR: cannot borrow `s` as mutable more than once at a time

    println!("{}", r1);
    
    // Note: If you wanted multiple readers, that's fine!
    let v = String::from("world");
    let read1 = &v;
    let read2 = &v; // ✅ Multiple immutable borrows are safe!
    println!("{} {}", read1, read2);
}

Fighting the Borrow Checker

New Rust developers often feel like they are fighting the compiler. If your code won't compile because of lifetimes or borrowing rules, the compiler is probably saving you from a crash or security vulnerability in production. Embrace it!

Keep Reading

D
DevOps

Docker Networking Demystified: Bridge, Host & Overlay

8 min read Read More
C
Cloud

AWS IAM Roles vs Users vs Policies

10 min read Read More
P
Programming

Understanding Python's GIL & Multiprocessing

14 min read Read More