Frontend Labs
⋯

33. unsafe Rust: the five superpowers and the contract they carry

33.1 What unsafe unlocks

An unsafe { } block allows exactly five extra operations:

  1. Dereferencing a raw pointer (*const T, *mut T).
  2. Calling an unsafe fn, including every FFI function.
  3. Reading or writing a static mut.
  4. Implementing an unsafe trait (such as Send or Sync).
  5. Reading a field of a union.

It does not turn off the borrow checker or type checking for ordinary references. It means: "I, the programmer, have checked the preconditions the compiler cannot check here." If you are wrong, the result is undefined behavior, the same category of bug as in the C guide §28.

33.2 Safe abstractions over unsafe code

The standard library is full of unsafe, wrapped in APIs that cannot be misused. Here is the real idea behind split_at_mut, which the borrow checker cannot verify on its own because it cannot tell that the two halves do not overlap:

fn split_at_mut<T>(s: &mut [T], mid: usize) -> (&mut [T], &mut [T]) {
    let len = s.len();
    assert!(mid <= len, "mid out of bounds");   // the check that makes the unsafe below sound
    let ptr = s.as_mut_ptr();
    // SAFETY: `mid <= len`, so both ranges are inside the original allocation,
    // and [0, mid) and [mid, len) do not overlap, so the two &mut never alias.
    unsafe {
        (
            std::slice::from_raw_parts_mut(ptr, mid),
            std::slice::from_raw_parts_mut(ptr.add(mid), len - mid),
        )
    }
}

fn main() {
    let mut v = [1, 2, 3, 4, 5];
    let (a, b) = split_at_mut(&mut v, 2);
    a[0] = 10;
    b[0] = 30;
    println!("{v:?}"); // [10, 2, 30, 4, 5]
}

The pattern is: check the invariant in safe code, write a // SAFETY: comment that justifies each unsafe operation, and expose a safe function. Callers cannot cause UB however they call it. Clippy's undocumented_unsafe_blocks lint enforces the comment.

33.3 UB in Rust is a short list, but it is real

  • Dereferencing a dangling, null, or misaligned pointer.
  • Breaking the aliasing rules: creating a &mut T while another reference to the same data is in use, even through raw pointers.
  • Producing an invalid value: a bool that is not 0 or 1, a char outside Unicode, a null &T, a str that is not UTF-8, an enum with an invalid tag.
  • Data races, which are possible only through unsafe or a wrong unsafe impl Send.
  • Calling a foreign function with the wrong signature.

33.4 Miri: an interpreter that detects UB

rustup +nightly component add miri
cargo +nightly miri test        # runs your tests in an interpreter that checks every memory access

Miri catches use-after-free, out-of-bounds access, aliasing violations, and memory leaks in code with unsafe. Think of it as AddressSanitizer plus UBSan (C §14) with knowledge of Rust's aliasing model. Run it in CI for every crate that contains unsafe.

War story. A popular crate's unsafe impl Send on a type containing an Rc passed review and tests for months. When users began moving it between Tokio worker threads, the non-atomic reference count raced, and objects were occasionally freed twice. The RustSec advisory database has many entries like this, all in unsafe code. Safe Rust cannot express this bug, so reviewing a crate's unsafe blocks covers the part of the code where it can occur. cargo geiger counts the unsafe code in your dependency tree.


Generated from real-life-rust-guide.md · 8 Oct 2026 16:36