← Back to blog Khalil Drissi

Memory safety with Rust ownership and borrowing

Listen to article
0:00

After years of chasing dangling pointers and double-frees in C, Rust felt like someone had finally written down the rules I was holding in my head and made the compiler enforce them. Rust gives you manual control over memory with no garbage collector, yet it statically prevents the entire class of memory bugs that plague C. The mechanism is ownership and borrowing, and it is simpler than its reputation suggests.

The three ownership rules

Everything in Rust starts from three rules the compiler enforces:

That last rule is the quiet genius. There is no free to call and no garbage collector to run. The compiler knows exactly where each value’s scope ends and inserts the cleanup for you. This is the same scope-based lifetime you get from the stack, which I described in stack vs heap: how memory actually works, except Rust extends it to heap data too.

Moves, not copies

Because there is only one owner, assigning a heap value to another variable moves ownership rather than copying the data. The old variable becomes invalid, and the compiler will reject any use of it.

let s1 = String::from("hello");
let s2 = s1;            // ownership moves from s1 to s2
// println!("{}", s1);  // compile error: s1 was moved
println!("{}", s2);     // fine, s2 owns the data now

This single rule eliminates double-free at compile time. In C, two pointers to the same heap block both think they should free it. In Rust, only one variable owns the data, so it gets freed exactly once. The whole category of bug is gone before the program runs.

Borrowing instead of moving

Moving everywhere would be painful, so Rust lets you borrow a value by taking a reference. A reference is a pointer that does not own what it points to. The borrow checker enforces one more set of rules to keep references safe:

fn main() {
    let mut data = vec![1, 2, 3];
    let r1 = &data;        // immutable borrow
    let r2 = &data;        // another one, fine
    println!("{} {}", r1[0], r2[0]);

    let m = &mut data;     // mutable borrow, allowed now r1/r2 are done
    m.push(4);
}

This “one writer or many readers” rule is what prevents data races and use-after-free. You cannot hold a reference into a vector while another piece of code reorganizes it, because that would require a mutable and an immutable borrow simultaneously. The compiler refuses to build it.

Lifetimes make dangling impossible

The borrow checker also tracks how long each reference lives and guarantees a reference never outlives the data it points to. The classic dangling pointer from C, returning a reference to a local, simply does not compile.

fn dangle() -> &String {
    let s = String::from("oops");
    &s    // error: s is dropped here, the reference would dangle
}

If you have read how pointers really work, you know this is exactly the bug that produces silent corruption in C. Rust turns it into a compile error with a clear message.

The escape hatch and why it matters

Rust is not naive about the fact that some code genuinely needs to do unsafe things: dereference raw pointers, call into C, talk to hardware. It does not pretend these never happen. Instead it walls them off behind the unsafe keyword. Inside an unsafe block you get the raw pointer operations that C gives you everywhere, but the block is a visible marker. When something does go wrong with memory, you have a small audited surface to inspect instead of the whole program. The standard library is built on these blocks, carefully reviewed, so the safe code on top inherits the guarantees. That layering is the practical reason Rust scales: most code stays safe, and the unsafe parts are few and labeled.

What you give up and what you get

The cost is real: the borrow checker rejects programs that would actually be fine, and you spend time restructuring code to satisfy it. That learning curve is the famous “fighting the borrow checker” phase. What you get in return is C-level performance and control with none of the memory unsafety, verified before the program ever runs. After living in both worlds, I think the tradeoff is worth it for anything where correctness matters. Rust did not invent these rules. It just made the compiler the one who remembers them so I do not have to.

Comments
Leave a comment