Skip to content
All insights

Academy

Safe Pointers by Default: How References, Option, and the Unsafe Boundary Replace C's Pointer Chaos

In C, pointer chaos causes 70% of security bugs. Rust replaces raw pointers with references, Option, and strict unsafe boundaries for memory safety.

6 min read

Safe Pointers by Default: How References, Option, and the Unsafe Boundary Replace C's Pointer Chaos
On this page

In C, everything is a pointer. Function arguments? Pointers. Strings? Pointers to char arrays. Arrays? Pointers in disguise. Dynamic memory? Pointers from malloc. Data structures? Pointers connecting nodes. The entire language is essentially an abstraction layer over memory addresses, and the programmer is responsible for ensuring every single one of those addresses is valid, non-null, properly aligned, not dangling, not aliased in conflicting ways, and eventually freed exactly once.

The result is a language that's incredibly powerful and incredibly dangerous. Pointer bugs—null dereferences, dangling pointers, buffer overflows, use-after-free, double-free—account for roughly 70% of security vulnerabilities in C/C++ software, according to both Microsoft and Google. These aren't bugs caused by incompetent programmers—they're caused by the fundamental impossibility of manually tracking pointer validity across millions of lines of code.

Rust's answer is elegant: replace raw pointers with references—pointers that carry compile-time guarantees about validity, aliasing, and lifetime. A reference (&T or &mut T) is a pointer that the compiler proves is non-null, non-dangling, properly aligned, and used according to the borrowing rules. You get the performance of pointers (references compile to the same machine code as C pointers) with the safety of managed languages (the compiler prevents invalid access).

For the rare cases where you genuinely need raw pointer semantics—FFI with C libraries, implementing custom data structures, accessing hardware registers—Rust provides *const T and *mut T. But these exist behind the unsafe keyword, clearly marked as "the compiler can't verify this, you're on your own."

At RantAI, references are our daily tools; raw pointers appear only in our FFI layer. This article, drawn from Chapter 10 of our guide "The Rust Programming Language," covers the pointer spectrum from safe references to raw pointers.

The Safety Spectrum#

Rust
// SAFE: References — compiler-verified pointers
let x = 42;
let r: &i32 = &x;           // Immutable reference: guaranteed valid, non-null
let mut y = 42;
let mr: &mut i32 = &mut y;   // Mutable reference: guaranteed exclusive access

// UNSAFE: Raw pointers — no compiler verification
let rp: *const i32 = &x;     // Raw immutable pointer
let mp: *mut i32 = &mut y;    // Raw mutable pointer
// Dereferencing raw pointers requires unsafe:
unsafe {
    println!("Raw: {}", *rp);
    *mp = 100;
}

References and raw pointers compile to identical machine code—both are memory addresses. The difference is entirely at compile time: the compiler verifies references are valid; it doesn't verify raw pointers.

References: The 99% Case#

Rust
fn sum(data: &[f64]) -> f64 {
    data.iter().sum()
}

fn normalize(data: &mut [f64]) {
    let total: f64 = data.iter().sum();
    for x in data.iter_mut() {
        *x /= total;
    }
}

fn main() {
    let mut values = vec![1.0, 2.0, 3.0, 4.0];
    println!("Sum: {}", sum(&values));       // Immutable borrow
    normalize(&mut values);                   // Mutable borrow
    println!("Normalized: {:?}", values);     // [0.1, 0.2, 0.3, 0.4]
}

References give you everything pointers give you—indirection, pass-by-reference, shared access to data—without the risks. The compiler guarantees:

  • Non-null: References can never be null. Use Option<&T> for "maybe no value."

  • Valid: References always point to live, initialized data.

  • Aligned: References always point to properly aligned data.

  • Non-aliasing: Mutable references have exclusive access; immutable references don't coexist with mutable ones.

References as Struct Fields#

Rust
struct Parser<'a> {
    source: &'a str,
    position: usize,
}

impl<'a> Parser<'a> {
    fn new(source: &'a str) -> Self {
        Parser { source, position: 0 }
    }
    
    fn peek(&self) -> Option<char> {
        self.source[self.position..].chars().next()
    }
    
    fn advance(&mut self) {
        if let Some(ch) = self.peek() {
            self.position += ch.len_utf8();
        }
    }
}

The lifetime 'a guarantees that the Parser cannot outlive the source string it references. Zero-copy parsing with compile-time safety.

Raw Pointers: The 1% Case#

Rust
// FFI: Interfacing with C libraries
extern "C" {
    fn strlen(s: *const std::ffi::c_char) -> usize;
}

fn c_string_length(s: &str) -> usize {
    let c_str = std::ffi::CString::new(s).unwrap();
    unsafe { strlen(c_str.as_ptr()) }
}

// Custom data structures: Implementing a linked list
struct Node<T> {
    data: T,
    next: *mut Node<T>,  // Raw pointer for self-referential structure
}

Raw pointers are necessary for FFI (C functions expect raw pointers), implementing low-level data structures (doubly-linked lists, graphs with cycles), and memory-mapped I/O (hardware register access). Every use requires unsafe, making these locations auditable.

The unsafe Contract#

Rust
// GOOD: Minimal unsafe, wrapped in safe interface
pub fn safe_wrapper(data: &[i32]) -> i32 {
    assert!(!data.is_empty());
    unsafe {
        // SAFETY: We verified data is non-empty, so the pointer is valid
        *data.as_ptr()
    }
}

// BAD: Large unsafe block, unclear invariants
unsafe {
    // Hundreds of lines of unverified code...
}

The best practice: minimize unsafe blocks, wrap them in safe interfaces, and document the safety invariants with // SAFETY: comments.

Null Pointers: Option<&T> Instead of NULL#

Rust
// C style: null pointer
// int* find(int* arr, int len, int target) {
//     for (int i = 0; i < len; i++)
//         if (arr[i] == target) return &arr[i];
//     return NULL;  // Caller might forget to check!
// }

// Rust style: Option<&T>
fn find(data: &[i32], target: i32) -> Option<&i32> {
    data.iter().find(|&&x| x == target)
}

fn main() {
    let numbers = vec![1, 2, 3, 4, 5];
    match find(&numbers, 3) {
        Some(val) => println!("Found: {}", val),
        None => println!("Not found"),
    }
    // Can't forget to check — the compiler requires handling both cases
}

Option<&T> is Rust's replacement for nullable pointers. It's the same size as a raw pointer (the compiler optimizes None to the null bit pattern), but the type system forces you to handle the absent case. No null pointer dereferences. Ever.

Broader Implications: Safety Without Sacrifice#

At RantAI, our codebase is 99.5% safe Rust and 0.5% unsafe—entirely in our C FFI layer and a custom allocator. The safe 99.5% has zero pointer-related bugs by construction. The unsafe 0.5% is carefully audited, wrapped in safe interfaces, and tested extensively. This ratio—maximal safety surface, minimal unsafe surface—is the practical result of Rust's pointer design.

Practical Applications & Strategic Takeaways#

For newcomers: Use references (&T, &mut T) for everything. You'll write thousands of lines of Rust before you need a raw pointer.

For C veterans: Rust references are C pointers with compile-time verification. Same performance. Same machine code. Different safety guarantees.

For library authors: Wrap unsafe raw pointer operations in safe public APIs. Your users get safety; you handle the complexity internally.

Our Commitment to Open Knowledge#

RantAI is committed to open education. Pointers and references are covered in Chapter 10 of our guide, "The Rust Programming Language," freely available online.

Explore these concepts further: https://trpl.rantai.dev

Support Our Mission & Get Your Handbook#

What percentage of your Rust code uses unsafe? Have you found ways to eliminate raw pointers you once thought were necessary? Share below!

#RustLang #Pointers #References #MemorySafety #RantAI #LearnRust #Unsafe #FFI #NullSafety #SystemsProgramming

Next article

The Pointer Hierarchy: References, Smart Pointers, and Raw Pointers — Choosing the Right Abstraction for Every Layer