Memory safety bugs—use-after-free, double-free, buffer overflows—cause 70% of security vulnerabilities in systems software according to Microsoft. Rust eliminates these bugs entirely through its ownership system, without the runtime cost of garbage collection.
The Three Rules of Ownership
- Each value has exactly one owner
- When the owner goes out of scope, the value is dropped
- You can have either one mutable reference OR any number of immutable references
These rules, enforced at compile time, prevent entire categories of bugs.
Ownership in Action
1
2
3
4
5
6
7
| fn main() {
let s1 = String::from("hello"); // s1 owns the String
let s2 = s1; // ownership moves to s2
// println!("{}", s1); // ERROR: s1 no longer valid
println!("{}", s2); // OK: s2 owns it now
}
|
When s2 goes out of scope, it drops the String. If s1 were still valid, we’d have a double-free bug. Rust prevents this at compile time.
Borrowing: References Without Ownership
Instead of transferring ownership, you can borrow:
1
2
3
4
5
6
7
8
9
| fn calculate_length(s: &String) -> usize {
s.len()
} // s goes out of scope, but since it doesn't own the String, nothing happens
fn main() {
let s = String::from("hello");
let len = calculate_length(&s); // borrow s
println!("The length of '{}' is {}", s, len); // s still valid!
}
|
Mutable Borrowing
You can have one mutable borrow at a time:
1
2
3
4
5
6
7
8
9
| fn main() {
let mut s = String::from("hello");
let r1 = &mut s;
// let r2 = &mut s; // ERROR: can't borrow mutably twice
r1.push_str(" world");
println!("{}", r1);
}
|
This prevents data races at compile time. Two mutable references to the same data cannot exist simultaneously.
The Borrow Checker Prevents Data Races
1
2
3
4
5
6
7
8
9
10
11
| fn main() {
let mut data = vec![1, 2, 3];
// This would be a data race in C++
let ref1 = &data[0];
// data.push(4); // ERROR: can't mutate while borrowed
println!("{}", ref1);
// After ref1 is no longer used, we can mutate
data.push(4); // OK now
}
|
Lifetimes: Preventing Dangling References
Lifetimes ensure references don’t outlive their data:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() { x } else { y }
}
fn main() {
let s1 = String::from("long string");
let result;
{
let s2 = String::from("short");
result = longest(&s1, &s2);
println!("Longest: {}", result); // OK: both s1 and s2 alive
}
// println!("{}", result); // ERROR: result might reference s2
}
|
Interior Mutability: Controlled Escape Hatches
Sometimes you need mutation through an immutable reference. Rust provides safe patterns:
RefCell: Runtime Borrow Checking
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
| use std::cell::RefCell;
fn main() {
let data = RefCell::new(5);
// Multiple immutable borrows OK
let r1 = data.borrow();
let r2 = data.borrow();
println!("{} {}", r1, r2);
drop(r1);
drop(r2);
// Mutable borrow
*data.borrow_mut() += 1;
// Panics at runtime if rules violated
// let r1 = data.borrow();
// let r2 = data.borrow_mut(); // PANIC: already borrowed
}
|
Mutex: Thread-Safe Mutation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| use std::sync::Mutex;
use std::thread;
fn main() {
let counter = Mutex::new(0);
let mut handles = vec![];
for _ in 0..10 {
let handle = thread::scope(|_| {
let mut num = counter.lock().unwrap();
*num += 1;
});
}
println!("Counter: {}", *counter.lock().unwrap());
}
|
Smart Pointers
Box: Heap Allocation
1
2
3
4
| fn main() {
let b = Box::new(5); // 5 is stored on heap
println!("{}", b);
} // Box and its data are freed
|
Rc: Reference Counting
1
2
3
4
5
6
7
8
9
10
| use std::rc::Rc;
fn main() {
let data = Rc::new(vec![1, 2, 3]);
let a = Rc::clone(&data); // Increment count
let b = Rc::clone(&data); // Increment count
println!("Count: {}", Rc::strong_count(&data)); // 3
} // All Rcs dropped, data freed
|
Arc: Thread-Safe Reference Counting
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| use std::sync::Arc;
use std::thread;
fn main() {
let data = Arc::new(vec![1, 2, 3]);
let handles: Vec<_> = (0..3).map(|i| {
let data = Arc::clone(&data);
thread::spawn(move || {
println!("Thread {}: {:?}", i, data);
})
}).collect();
for h in handles {
h.join().unwrap();
}
}
|
Real-World Pattern: Builder with Ownership
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
| pub struct Config {
host: String,
port: u16,
timeout: u64,
}
pub struct ConfigBuilder {
host: Option<String>,
port: Option<u16>,
timeout: Option<u64>,
}
impl ConfigBuilder {
pub fn new() -> Self {
Self { host: None, port: None, timeout: None }
}
pub fn host(mut self, host: impl Into<String>) -> Self {
self.host = Some(host.into());
self
}
pub fn port(mut self, port: u16) -> Self {
self.port = Some(port);
self
}
pub fn timeout(mut self, timeout: u64) -> Self {
self.timeout = Some(timeout);
self
}
pub fn build(self) -> Result<Config, &'static str> {
Ok(Config {
host: self.host.ok_or("host is required")?,
port: self.port.unwrap_or(8080),
timeout: self.timeout.unwrap_or(30),
})
}
}
fn main() {
let config = ConfigBuilder::new()
.host("localhost")
.port(3000)
.build()
.unwrap();
}
|
Unsafe: When You Know Better
Sometimes you need to bypass the borrow checker:
1
2
3
4
5
6
7
8
9
10
11
12
13
| fn main() {
let mut num = 5;
let r1 = &num as *const i32;
let r2 = &mut num as *mut i32;
// Raw pointer operations require unsafe
unsafe {
println!("r1: {}", *r1);
*r2 = 10;
println!("r2: {}", *r2);
}
}
|
Use unsafe sparingly and only when you can guarantee safety invariants.
Conclusion
Rust’s ownership model provides:
- Zero-cost memory safety: No garbage collector overhead
- Compile-time guarantees: Bugs caught before runtime
- Fearless concurrency: Data races impossible
- Explicit resource management: No hidden allocations
At Sajima Solutions, we leverage Rust’s safety guarantees to build systems that are both fast and secure. Contact us to learn how memory-safe systems can benefit your organization.