Unsafe Rust and raw pointers

Rust · Expert · 20 min

The standard library, FFI and high-performance data structures rest on unsafe code; using it soundly is an expert skill.

unsafe does not turn off the borrow checker. It unlocks five extra operations whose safety the compiler cannot verify:

  • dereferencing a raw pointer (*const T, *mut T),
  • calling an unsafe fn (including foreign functions),
  • reading or writing a static mut,
  • implementing an unsafe trait such as Send or Sync,
  • reading fields of a union.

An unsafe block is your promise that you have upheld the invariants the compiler normally checks. If you are wrong, the result is undefined behaviour (UB): not just a crash, but permission for the optimiser to assume impossible things and miscompile surrounding safe code. Examples of UB: creating a dangling or misaligned reference, two live &mut to the same data, reading uninitialised memory, a bool that is not 0 or 1, data races.

Raw pointers may be null, dangling or aliased; creating them is safe, only dereferencing is unsafe. Use &raw const x / &raw mut x (stable since 1.82) to get a pointer without first creating a reference, which matters for unaligned or uninitialised fields.

The goal is a safe abstraction: a small unsafe core behind a safe API whose every possible use is sound, like Vec or split_at_mut. Soundness is a property of the module, since safe code in the same module can break invariants that the unsafe code relies on. Document each block with a // SAFETY: comment saying why the requirements hold, and each unsafe fn with a # Safety doc section listing caller obligations.

The 2024 edition tightens the rules: operations inside an unsafe fn need their own unsafe {} block (the unsafe_op_in_unsafe_fn lint warns by default), extern blocks must be written unsafe extern, and attributes such as no_mangle become #[unsafe(no_mangle)].

Test unsafe code under Miri (cargo +nightly miri test), an interpreter that detects many kinds of UB.

Example

use std::slice::from_raw_parts_mut;
fn split_at_mut<T>(s: &mut [T], mid: usize) -> (&mut [T], &mut [T]) {
    let (ptr, len) = (s.as_mut_ptr(), s.len());
    assert!(mid <= len, "mid out of bounds"); // the safe API enforces the invariant
    // SAFETY: mid <= len, so [0, mid) and [mid, len) are in bounds and disjoint,
    // and both borrow from `s`, which outlives the returned slices.
    unsafe { (from_raw_parts_mut(ptr, mid), from_raw_parts_mut(ptr.add(mid), len - mid)) }
}

/// # Safety
/// `p` must be non-null, aligned and point to an initialised `u32`.
unsafe fn read_u32(p: *const u32) -> u32 {
    unsafe { *p } // 2024: an explicit unsafe block even inside an unsafe fn
}

fn main() {
    let mut data = [1, 2, 3, 4, 5];
    let (a, b) = split_at_mut(&mut data, 2);
    a[0] = 10; b[0] = 30;
    println!("{data:?}"); // [10, 2, 30, 4, 5]
    let x = 7u32;
    let p = &raw const x;                   // raw pointer without an intermediate reference
    println!("{}", unsafe { read_u32(p) }); // SAFETY: p points to a live u32 -> 7
}

Remember

  • unsafe enables five operations; it does not disable borrow checking
  • Wrap unsafe code in safe APIs that are sound for every possible caller
  • Write // SAFETY: comments and test with Miri
  • 2024 edition: unsafe blocks inside unsafe fn, unsafe extern, #[unsafe(no_mangle)]

Watch out for

  • Assuming code that seems to work is fine; UB can appear only under optimisation or on another compiler version.

Check yourself

  1. Which of these requires an unsafe block?
  2. A safe function wraps an unsafe block but can cause UB with some valid inputs. What is it called?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: doc.rust-lang.org/nomicon/meet-safe-and-unsafe.html

Unsafe Rust and raw pointers · Rust · Topolearn