DevRef / Rust / Memory Safety
Ownership and the Borrow Checker Explained
Rust eliminates an entire class of memory bugs at compile time with no runtime overhead. The ownership system is not a restriction — it is a precise language for expressing intent about who owns and who borrows data.
The ownership rules
Three rules govern all values in Rust. First: every value has exactly one owner. Second: when the owner goes out of scope, the value is dropped. Third: there can be either one mutable reference or any number of immutable references to a value at any given time, but not both simultaneously.
These rules sound constraining. In practice they describe what a correct program already does — Rust just makes the implicit explicit and enforces it at compile time rather than at runtime or in a post-mortem debugger.
Move semantics by default
Assignment in Rust moves ownership. This is different from languages where assignment copies a reference (Java, Python) or copies a value (C by default for structs).
fn main() {
let s1 = String::from("hello");
let s2 = s1; // s1 is moved — ownership transfers to s2
// println!("{}", s1); // compile error: value moved
println!("{}", s2); // fine
}
Types that implement the Copy trait (integers, booleans, chars, fixed-size arrays of Copy types) are copied on assignment rather than moved. This is sound because they live entirely on the stack and copying them is cheap and unambiguous.
Borrowing and references
A reference lets you use a value without taking ownership. Creating a reference is called borrowing. References are written with &; mutable references with &mut.
fn length(s: &String) -> usize {
s.len()
// s goes out of scope here but we don't drop it — we don't own it
}
fn main() {
let s = String::from("hello");
let len = length(&s);
println!("{} has {} chars", s, len); // s still valid here
}
The borrow checker enforces that references never outlive the value they refer to. This eliminates dangling pointers entirely. If you have a reference to a value, that value will exist for at least as long as the reference.
Lifetimes
Lifetimes are the compiler's way of naming the scope for which a reference is valid. Most of the time they are inferred. When a function takes multiple references and returns one, the compiler needs to know which input lifetime the output is tied to.
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() { x } else { y }
}
The annotation 'a says: the returned reference will live at least as long as the shorter of x and y. The caller is bound by this — they cannot use the returned reference after either input goes out of scope.
Why this matters in practice
Rust's borrow checker catches use-after-free, double-free, and data races at compile time. These are the bug classes that represent the majority of security vulnerabilities in systems software written in C and C++. The checker imposes a discipline that produces the same outcome that careful manual memory management would — but enforced mechanically, on every build, without exception.