Unsafe Rust

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production.

The reference material consulted while preparing these pages is listed in the section bibliography.

This page is written against Rust 1.99.0 and the 2024 edition, checked on 2026-10-06. Safe Rust’s guarantees — no dangling pointers, no data races, no out-of-bounds access — rest on rules the compiler can check. Some correct programs cannot be expressed within those rules: talking to hardware or C libraries, implementing Vec or Mutex themselves, squeezing out a bounds check. unsafe lets you do the few extra things such code needs, and makes you responsible for upholding the invariants the compiler can no longer check.

The five unsafe superpowers

Inside an unsafe block (or an unsafe fn body, see below) you may:

  1. dereference a raw pointer;

  2. call an unsafe function or method (including foreign functions);

  3. access or modify a mutable static variable;

  4. implement an unsafe trait (such as Send, Sync or GlobalAlloc);

  5. access the fields of a union.

Doing any of these outside unsafe is error E0133.

What unsafe does — and does not — turn off

unsafe does not switch off the borrow checker, type checking or any other check: references inside an unsafe block are checked exactly as outside it. It only unlocks the five operations above. The Rustonomicon’s way of saying this: unsafe code is code where the programmer promises that certain conditions hold. A bug in unsafe code can cause undefined behaviour (UB), after which the program may do anything — including appearing to work.

The boundary that matters is the module: an unsafe block often relies on invariants established by safe code elsewhere in the same module (such as Vec's len <= capacity), so the whole module must be reviewed together.

Raw pointers

*const T and *mut T are pointers without the guarantees of references: they may be null, dangling or unaligned, may alias freely, and carry no lifetime. Creating one is safe; dereferencing one is not.

use std::ptr::{self, NonNull};

fn main() {
    let mut copies: u32 = 3;
    let r: *const u32 = &raw const copies;   // 1.82: create a raw pointer without an intermediate reference
    let w: *mut u32 = &raw mut copies;
    // SAFETY: `w` points to a live, aligned u32 and no reference to `copies` is in use.
    unsafe {
        *w += 1;
        println!("{}", *r);                     // 4
    }

    let null: *const u32 = ptr::null();
    println!("{}", null.is_null());              // safe to create and test

    let mut titles = vec!["Dune", "Emma"];
    let p = titles.as_mut_ptr();
    // SAFETY: index 1 is within the Vec's length; nothing else borrows `titles` meanwhile.
    unsafe { *p.add(1) = "Kindred" };
    println!("{titles:?}");

    let nn: NonNull<u32> = NonNull::from(&mut copies); // never null: Option<NonNull<T>> is pointer-sized
    // SAFETY: `nn` was created from a valid &mut and `copies` is still alive.
    println!("{}", unsafe { *nn.as_ptr() });
}

&raw const place and &raw mut place (1.82) create a raw pointer directly, which matters for fields of #[repr(packed)] structs and uninitialised memory, where creating a reference first (&place as *const _) would itself be UB. NonNull<T> is a non-null *mut T that is covariant in T; it is what most collections store. Pointer arithmetic uses add, sub, offset and byte_add, all unsafe because the result must stay within the same allocation.

Strict provenance (1.84)

A pointer is more than an address: it also carries provenance, the permission to access a particular allocation. Casting a pointer to an integer and back (ptr as usize as *const T) loses track of that permission, which makes some optimisations and tools (Miri, CHERI hardware) unable to reason about the program. Rust 1.84 stabilised the strict provenance APIs that keep provenance explicit: ptr.addr() reads the address without exposing it, ptr.with_addr(a) and ptr.map_addr(f) build a pointer with a new address but the same provenance, and ptr::without_provenance makes an invalid sentinel pointer. When you genuinely need integer round-trips (FFI handles), use the exposed provenance APIs expose_provenance and with_exposed_provenance.

fn main() {
    let data = [10u64, 20, 30];
    let p: *const u64 = data.as_ptr();
    // Tag the low bit of an aligned pointer (pointer tagging), keeping provenance:
    let tagged = p.map_addr(|a| a | 1);
    let untagged = tagged.map_addr(|a| a & !1);
    // SAFETY: `untagged` has the same address and provenance as `p`, which points into `data`.
    println!("{} tag={}", unsafe { *untagged.add(1) }, tagged.addr() & 1); // 20 tag=1
}

unsafe fn, unsafe blocks and the 2024 lint

An unsafe fn is a function with preconditions the caller must satisfy; calling it requires unsafe. Its documentation must say what they are in a # Safety section. In the 2024 edition, the body of an unsafe fn is no longer an implicit unsafe block: the unsafe_op_in_unsafe_fn lint warns by default, so each unsafe operation inside needs its own unsafe { } block with a // SAFETY: comment explaining why that particular operation is sound.

/// Returns the element at `index` without bounds checking.
///
/// # Safety
///
/// `index` must be less than `items.len()`.
unsafe fn get_unchecked_title<'a>(items: &[&'a str], index: usize) -> &'a str {
    // SAFETY: the caller guarantees `index < items.len()` (see # Safety above).
    unsafe { items.get_unchecked(index) }
}

fn first_title<'a>(items: &[&'a str]) -> Option<&'a str> {
    if items.is_empty() {
        return None;
    }
    // SAFETY: we just checked that index 0 is in bounds.
    Some(unsafe { get_unchecked_title(items, 0) })
}

fn main() {
    println!("{:?}", first_title(&["Dune", "Emma"]));
}

Unsafe traits and unsafe attributes

An unsafe trait has invariants that implementors must uphold; implementing it requires unsafe impl. Send and Sync are the common examples (see Shared State, Send/Sync & Atomics); GlobalAlloc and bytemuck::Pod are others.

Some attributes can also cause UB if misused — for example two functions exporting the same symbol name. Since the 2024 edition they must be written as unsafe attributes: #[unsafe(no_mangle)], #[unsafe(export_name = "...")] and #[unsafe(link_section = "...")]. Likewise extern blocks must be declared unsafe extern "C" { ... } (see FFI & C Interop).

Undefined behaviour

The Reference keeps the list of behaviour considered undefined. The main items:

  • data races;

  • accessing (loading from or storing to) a dangling or misaligned place, or out of bounds of its allocation;

  • breaking the aliasing rules: mutating through a pointer while a &T to the same data is live, or accessing data while a &mut T to it is live (except through that &mut);

  • mutating immutable data (anything reachable through a shared reference, outside UnsafeCell, or a const);

  • calling a function with the wrong ABI, or unwinding out of a function whose ABI does not allow it;

  • producing an invalid value, even without using it: a bool other than 0/1, a char outside the Unicode scalar range, an enum with an invalid discriminant, a null or dangling reference or Box, a !, uninitialised integers, floats or pointers, a str that is not UTF-8 when the type requires it;

  • executing code compiled with target features the CPU does not support;

  • incorrect use of compiler intrinsics and unreachable_unchecked.

Things that are not UB in Rust, though they may be bugs: deadlocks, memory leaks, integer overflow (it panics or wraps predictably), exiting without running destructors, and logic errors in safe code.

SAFETY comments and safe abstractions

The idiom that makes unsafe code reviewable: keep unsafe blocks as small as possible, put a // SAFETY: comment on every one stating which invariant makes it sound (Clippy’s undocumented_unsafe_blocks lint can require it), and wrap the unsafe code in a safe API whose types make misuse impossible. The standard library’s split_at_mut is the classic example — safe callers cannot create overlapping mutable slices, even though the implementation uses raw pointers:

/// Splits a mutable slice into two non-overlapping halves at `mid`.
///
/// # Panics
///
/// Panics if `mid > slice.len()`.
pub fn split_halves<T>(slice: &mut [T], mid: usize) -> (&mut [T], &mut [T]) {
    let len = slice.len();
    assert!(mid <= len, "mid out of bounds");
    let ptr = slice.as_mut_ptr();
    // SAFETY: `ptr` is valid for `len` elements; [0, mid) and [mid, len) do not overlap and both
    // lie within the original slice, whose exclusive borrow is held for the returned lifetime.
    unsafe {
        (
            std::slice::from_raw_parts_mut(ptr, mid),
            std::slice::from_raw_parts_mut(ptr.add(mid), len - mid),
        )
    }
}

fn main() {
    let mut shelf = [1, 2, 3, 4, 5];
    let (left, right) = split_halves(&mut shelf, 2);
    left[0] = 10;
    right[0] = 30;
    println!("{shelf:?}"); // [10, 2, 30, 4, 5]
}

The community Rust Design Patterns recommends confining unsafe code to small modules with safe interfaces ("Contain unsafety in small modules"), and many crates add #![forbid(unsafe_code)] (or unsafe_code = "forbid" in [lints]) everywhere else.

MaybeUninit and transmute

std::mem::MaybeUninit<T> holds memory that may not yet contain a valid T — the safe way to work with uninitialised memory (mem::uninitialized() is deprecated because it produced invalid values). Write each element, then assume_init once everything is initialised:

use std::mem::MaybeUninit;

fn squares<const N: usize>() -> [u64; N] {
    let mut out: [MaybeUninit<u64>; N] = [const { MaybeUninit::uninit() }; N];
    for (i, slot) in out.iter_mut().enumerate() {
        slot.write((i as u64) * (i as u64));
    }
    // SAFETY: every element was written in the loop above.
    out.map(|x| unsafe { x.assume_init() })
}

fn main() {
    println!("{:?}", squares::<5>()); // [0, 1, 4, 9, 16]
    let bits = 1.5f32.to_bits();      // the safe alternative to transmute::<f32, u32>
    println!("{bits:#x} {}", f32::from_bits(bits));
}

std::mem::transmute::<A, B> reinterprets the bits of an A as a B of the same size. It is extremely easy to misuse (lifetimes, invalid values, layout of repr(Rust) types), and almost every legitimate use has a safer replacement: to_bits/from_bits, to_le_bytes/from_le_bytes, as casts for pointers, bytemuck/zerocopy for plain-old-data conversions. The Rustonomicon’s "Transmutes" chapter lists the pitfalls.

Alternatives to static mut

static mut is a mutable global accessible from any thread without synchronisation. In the 2024 edition, taking a reference to one is rejected (see Variables, Mutability & Constants). Use instead: an atomic (static COUNT: AtomicU64), a Mutex/RwLock in a static, a OnceLock/LazyLock, or thread_local!. If a static mut is unavoidable (FFI, embedded interrupt handlers), access it only through &raw const/&raw mut pointers inside unsafe blocks with a // SAFETY: comment.

Miri and sanitizers

Testing unsafe code requires tools that detect UB, because UB often does not crash:

  • Miri (cargo +nightly miri test) interprets the program and reports UB precisely: aliasing violations under the Stacked or Tree Borrows models, uninitialised reads, out-of-bounds and use-after-free accesses, data races, invalid values and leaks. See Debugging & Diagnostics.

  • Sanitizers (nightly: RUSTFLAGS="-Zsanitizer=address" cargo +nightly test --target x86_64-unknown-linux-gnu) instrument native code: AddressSanitizer, ThreadSanitizer, MemorySanitizer and LeakSanitizer. They also work across FFI boundaries, where Miri cannot go.

  • Fuzzing with cargo fuzz finds the inputs that trigger the bug in the first place.

env::set_var and other newly unsafe functions

In the 2024 edition, std::env::set_var and std::env::remove_var are unsafe: on most platforms, modifying the environment while another thread reads it (C libraries do, for example through getaddrinfo) is a data race in the C runtime. Call them only before spawning threads, and prefer passing configuration explicitly:

fn main() {
    // SAFETY: called at the very start of main, before any other thread exists.
    unsafe {
        std::env::set_var("SHELF_LOG", "debug");
    }
    println!("{:?}", std::env::var("SHELF_LOG"));
}

std::os::unix::process::CommandExt::before_exec became unsafe for the same kind of reason; pre_exec is its unsafe replacement.

Where the books are outdated

Mara & Holmes (Refactoring to Rust, chapters 3-4) introduce raw pointers and unsafe for FFI using the 2018 edition: bare extern "C" {} blocks, #[no_mangle], and unsafe fn bodies that implicitly allow unsafe operations. In the 2024 edition those are unsafe extern "C" {}, #[unsafe(no_mangle)] and explicit unsafe {} blocks inside unsafe fn; the &raw syntax (1.82) and strict provenance APIs (1.84) are also newer than their examples.