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]