Unsafe Rust

Undefined Behavior

Undefined behavior (UB) is a state in which a program violates the foundational rules of the Rust abstract machine. Because the compiler and its LLVM backend optimize code under the strict assumption that undefined behavior never occurs, a program that triggers UB has no guaranteed semantics: it may crash, silently corrupt unrelated memory, delete security checks during dead-code elimination, or appear to work in debug builds while failing unpredictably in --release builds.

Complete Catalog of Undefined Behavior in Rust

The Rust Reference defines the operations that constitute immediate undefined behavior:

Category Specific Violation Safe Alternative / Prevention
Data races Two threads accessing the same memory location concurrently where at least one is a write and at least one is non-atomic Use Atomic*, Mutex<T>, or RwLock<T>
Invalid pointer dereference Dereferencing a null, unaligned, dangling, or out-of-bounds pointer Check !ptr.is_null(), use read_unaligned() for packed data, keep allocation alive
Aliasing violations Having an active &mut T overlap with any other live &T or &mut T, or mutating through &T outside UnsafeCell<T> Follow Stacked/Tree Borrows; use &raw mut or UnsafeCell<T>
Invalid primitive bit patterns Constructing a bool not equal to 0 or 1, an invalid char surrogate, a null &T/Box<T>/fn(), an unassigned enum discriminant, or reading uninitialized memory as a typed value Use MaybeUninit<T>, checked constructors (char::from_u32), and avoid arbitrary mem::transmute
Out-of-bounds pointer arithmetic Stepping outside an allocation (beyond one-past-the-end) with ptr.add() or ptr.offset() Bounds-check before .add(), or use .wrapping_add()
Unwinding across FFI Letting a panic unwind across an extern "C" boundary Use catch_unwind at the boundary or extern "C-unwind"
Calling unreachable_unchecked() Reaching std::hint::unreachable_unchecked() at runtime Use safe unreachable!() unless proven unreachable

How Optimizers Exploit Assumed Absence of UB

Unlike an assembly assembler, an optimizing compiler reasons backward from the rule that UB is impossible. For example, because references (&T) can never be null, checking (r as *const i32).is_null() in release mode is optimized directly to false—even if unsafe code illegally constructed a null reference!

Unsafe Code Constructs Illegal Value      LLVM Optimizer Reasoning
------------------------------------      ------------------------
let r: &i32 = unsafe { &*null_ptr }; ---> "A reference `&i32` is never null.
if (r as *const i32).is_null() {           Therefore, `(r as *const i32).is_null()`
    return; // Deleted by optimizer!       is provably `false` -> delete branch!"
}

Avoiding Uninitialized Memory UB with MaybeUninit<T>

Calling unsafe { std::mem::MaybeUninit::<T>::uninit().assume_init() } to create an "empty" array of integers ([u8; 256]) or booleans (bool) is immediate undefined behavior (#[warn(invalid_value)]), because u8 and bool require initialized bits. Instead, initialize arrays safely or populate a [MaybeUninit<T>; N] element by element before calling assume_init:

use std::mem::MaybeUninit;

fn make_squares() -> [u32; 5] {
    // 1. Create an array of `MaybeUninit<u32>` (safe, because `MaybeUninit` allows uninit bytes):
    let mut uninit_arr: [MaybeUninit<u32>; 5] = [const { MaybeUninit::uninit() }; 5];

    // 2. Initialize every element via `.write()`:
    for (i, slot) in uninit_arr.iter_mut().enumerate() {
        let n = (i as u32) + 1;
        slot.write(n * n);
    }

    // 3. Transmute or convert the fully initialized array:
    // SAFETY: Every element `0..5` was initialized in the loop above, and
    // `MaybeUninit<u32>` has the exact same layout as `u32` (`#[repr(transparent)]`).
    unsafe { std::mem::transmute::<[MaybeUninit<u32>; 5], [u32; 5]>(uninit_arr) }
}

fn main() {
    println!("squares: {:?}", make_squares());
}
squares: [1, 4, 9, 16, 25]