unsafe in Rust: Raw Pointers & Safe Abstractions Guide

Max WellsMax WellsFounder of Rustify

TL;DR: unsafe is a keyword that unlocks five capabilities the borrow checker normally forbids: dereferencing raw pointers, calling unsafe functions, implementing unsafe traits, accessing mutable statics, and accessing union fields. Using unsafe doesn't disable Rust's safety, it narrows the scope of your responsibility. The programmer must manually uphold the invariants the compiler would normally enforce. Well-written Rust wraps unsafe in safe abstractions so callers don't need to reason about it.


What Does unsafe Actually Allow?

unsafe unlocks exactly five things, nothing more. Everything else in Rust remains fully checked even inside unsafe blocks.

// The 5 unsafe superpowers:
 
// 1. Dereference raw pointers
let raw: *const i32 = &42;
unsafe { println!("{}", *raw); }
 
// 2. Call unsafe functions
unsafe fn dangerous() { /* ... */ }
unsafe { dangerous(); }
 
// 3. Implement unsafe traits
unsafe trait MyUnsafeTrait {}
unsafe impl MyUnsafeTrait for u32 {}
 
// 4. Access/modify mutable static variables
static mut COUNTER: u32 = 0;
unsafe { COUNTER += 1; }
 
// 5. Access fields of unions
union IntOrFloat { i: u32, f: f32 }
let u = IntOrFloat { i: 42 };
unsafe { println!("{}", u.i); }

Ownership, lifetimes, and type checking still apply everywhere, including inside unsafe blocks.


What Are Raw Pointers?

Raw pointers (*const T and *mut T) are Rust's equivalent of C pointers, they bypass the borrow checker. They can be null, dangling, or unaligned. Dereferencing them requires unsafe.

fn main() {
    let mut x = 5;
 
    let r1 = &x as *const i32;      // immutable raw pointer
    let r2 = &mut x as *mut i32;    // mutable raw pointer
 
    // Creating raw pointers is safe; dereferencing is not
    unsafe {
        println!("r1 = {}", *r1);
        *r2 = 10;
        println!("r2 = {}", *r2);
    }
 
    // Null raw pointer (never do this without checking)
    let null: *const i32 = std::ptr::null();
    // unsafe { *null } // ← undefined behavior; segfault
}

Raw pointers are mainly used when interfacing with C code (FFI), implementing data structures that require aliasing (like linked lists), or building safe abstractions the compiler can't verify.


How Do You Write a Safe Abstraction Over Unsafe?

The goal is to contain unsafe in a small, auditable block and expose a safe API. The unsafe code is your responsibility; callers get compile-time safety.

use std::slice;
 
// A safe wrapper around unsafe pointer arithmetic
pub fn split_at_midpoint(slice: &[i32]) -> (&[i32], &[i32]) {
    let mid = slice.len() / 2;
    let ptr = slice.as_ptr();
    let len = slice.len();
 
    // SAFETY: `mid` is always <= len, both halves are within bounds,
    // and the lifetime is tied to the input slice.
    unsafe {
        (
            slice::from_raw_parts(ptr, mid),
            slice::from_raw_parts(ptr.add(mid), len - mid),
        )
    }
}
 
fn main() {
    let v = vec![1, 2, 3, 4, 5];
    let (left, right) = split_at_midpoint(&v);
    println!("{:?} {:?}", left, right); // [1, 2] [3, 4, 5]
}

The // SAFETY: comment is a convention, document exactly why your unsafe code is correct. This is what gets audited in security reviews.


When Should You Use unsafe?

Use unsafe for FFI, performance-critical data structures, or when the compiler's rules are too conservative for what you know is correct. Never use it to "make the borrow checker shut up."

Common legitimate uses:

  • FFI, calling C libraries: extern "C" { fn strlen(s: *const u8) -> usize; }
  • Intrinsics ; SIMD, hardware-specific operations
  • Custom allocators, GlobalAlloc requires unsafe
  • Kernel / embedded code, memory-mapped I/O, interrupt handlers
  • Standard library internals, Vec, HashMap, Arc are built on unsafe

When to avoid unsafe:

  • Working around a lifetime error you don't understand
  • Sharing data between threads without synchronization
  • Bypassing bounds checks "for performance" (use get_unchecked only after profiling and verification)

What Is an Unsafe Function vs an Unsafe Block?

An unsafe function (unsafe fn) signals that callers must uphold invariants before calling it. An unsafe block is where you actually invoke unsafe operations.

// unsafe fn; caller must ensure `ptr` is non-null and valid
unsafe fn read_value(ptr: *const i32) -> i32 {
    *ptr // unsafe operation inside unsafe fn
}
 
fn safe_wrapper(val: &i32) -> i32 {
    // SAFETY: `val` is a valid reference; always non-null and aligned
    unsafe { read_value(val as *const i32) }
}

Marking a function unsafe fn doesn't run unsafe code, it just signals a contract the caller must fulfill. The unsafe operations inside still need unsafe {} blocks.


Frequently Asked Questions

Not necessarily. unsafe means you've taken responsibility for upholding invariants the compiler can't verify. Well-audited unsafe code in the standard library (Vec, Arc) is used by millions of programs safely. The question is: can you prove your invariants hold?

In application code, rarely. In library/systems code, occasionally, usually in small, well-contained blocks. The entire Rust standard library has a relatively small surface area of unsafe code wrapping a large safe API.

Undefined behavior (UB) means the compiler makes no guarantees about what your program does, it may crash, corrupt memory, or appear to work while producing wrong results. Data races, null dereferences, and out-of-bounds pointer arithmetic are all UB. unsafe code must never cause UB.

Miri is an interpreter that detects undefined behavior in unsafe Rust code. Run your tests under Miri (cargo miri test) to catch alignment issues, use-after-free, and other UB that the compiler itself won't catch.


Sources


  • Ownership: Unsafe bypasses ownership checks, you must enforce them manually
  • Borrow Checker: The system unsafe temporarily opts out of
  • Send/Sync: Manual unsafe impl Send/Sync is one of the five unsafe superpowers
  • Pin: Pin projection without pin-project requires unsafe code
  • PyO3: Language bindings like PyO3 rely on carefully audited unsafe internals
  • Reference: Raw pointers and references are where many unsafe Rust soundness issues begin

Keep Reading

Ready to Land a $120k+ Rust Job in the US or Europe?