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

  1. Each value has exactly one owner
  2. When the owner goes out of scope, the value is dropped
  3. 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.