This is a reference page. You do not need to memorise it; come back to it when you write or review unsafe code.
Undefined behaviour (UB) is behaviour the language gives no meaning to. If a program performs any of the operations listed below, Rust makes no promise about what the whole program does: not only the faulty line, but code before and after it. The compiler optimises on the assumption that these operations never happen, so the result can differ between debug and release builds, between compiler versions, or after an unrelated edit.
unsafe does not relax these rules. It only moves the job of avoiding them from the compiler to you. Unsafe code is called sound if no safe code that uses it can cause undefined behaviour, and unsound if some safe caller can.
The list comes from the Rust Reference. Two caveats from the Reference itself matter:
- The list is not exhaustive, and it may grow or shrink. There is no formal model of exactly what unsafe code may do.
- Some items, such as the aliasing rules, are only outlined, because the precise rules have not been decided.
So treat the list as the current statement of the rules, not as a closed set. If an operation is not clearly permitted, the cautious choice is to avoid it. The numbers below follow the order of the Reference's list so that other pages can point at an item; the Reference itself does not number them.
The list
1. Data races
A data race is two accesses to the same memory from different threads, where at least one access writes, at least one is not atomic, and nothing (a lock, an atomic operation with suitable ordering, a thread join) orders one before the other.
static mut COUNTER: u64 = 0;
fn bump() {
// Not sound: two threads can run this at the same time.
unsafe { COUNTER += 1 }
}
fn main() {
let t = std::thread::spawn(bump);
bump();
t.join().unwrap();
}
This compiles, and the two COUNTER += 1 statements can race. The consequence is not merely a wrong count; it is undefined behaviour. Use an AtomicU64 or a Mutex<u64> instead. Safe Rust cannot create a data race.
A race condition is different: the result depends on timing, but every access is synchronised. For example, two threads that each atomically load a counter and then atomically store old + 1 can lose an update. That is a logic bug, not undefined behaviour. The safe and unsafe Rust article discusses this distinction.
2. Accessing a place that is dangling or based on a misaligned pointer
A place is an expression that names a memory location, such as x, *p or (*p).field. "Accessing" means loading from it or storing to it. Both terms in the heading are defined precisely in "Two definitions that resolve most confusion" below.
A dangling access:
fn main() {
let p: *const u64;
{
let x = 5u64;
p = &raw const x;
} // x no longer exists here
let _v = unsafe { *p }; // UB: p is dangling
}
The borrow checker does not track raw pointers, so this compiles.
A misaligned access:
fn main() {
let words = [0u64; 2];
// words is 8-aligned, so the address one byte past its start is not.
let p = words.as_ptr().cast::<u8>().wrapping_add(1).cast::<u64>();
let _v = unsafe { *p }; // UB: a u64 needs 8-byte alignment
}
When debug assertions are enabled (the default for debug builds), rustc inserts a check before a raw pointer dereference like this one, and the program panics with "misaligned pointer dereference". Optimised builds do not include the check by default, so do not rely on it. To read a value that may be misaligned, use p.read_unaligned().
3. Offsetting place projections that break in-bounds arithmetic
A place projection computes a new place from an old one: a field ((*p).f), a tuple index ((*p).0) or an array or slice index ((*p)[i]). When the base is a pointer, the address computation must follow the same rules as pointer::offset: the result must stay inside the same allocation, or point exactly one byte past its end.
The pointer methods offset, add and sub carry the same requirement in their documented safety conditions. p.add(n) with a result beyond one-past-the-end is undefined behaviour when it is computed, even if the result is never dereferenced. wrapping_add does not have that requirement, which is why example 2 uses it. The pointer arithmetic exercise works through these rules.
4. Breaking the pointer aliasing rules
The Reference states that the exact aliasing rules are not decided yet, and gives this outline:
- While a
&Tis live, the memory it points to must not be mutated, except for data inside anUnsafeCell. - While a
&mut Tis live, the memory it points to must not be read or written through any pointer not derived from it, and no other reference may point to it. Box<T>is treated like&'static mut Tfor these rules.
These rules apply to every reference and Box, including ones stored in private fields of your own types. How long a reference stays live is not fully specified, but some bounds are: it is never live longer than the lifetime the borrow checker gives it; it is live each time it is dereferenced, reborrowed, passed to a function or returned; and a reference (not a Box) passed to a function is live for the whole call, again except for data inside an UnsafeCell.
A common way to break these rules is to keep a raw pointer into a Box's contents, then move or reborrow the Box itself, then use the old raw pointer. Miri, the interpreter that checks unsafe code against candidate aliasing models, reports many such cases. The aliasing article develops the rules and the Stacked Borrows and Tree Borrows article explains the two models Miri implements.
5. Mutating immutable bytes
These bytes are immutable:
- bytes owned by an immutable binding (
let x) or an immutablestatic, unless they are inside anUnsafeCell; - bytes reachable through a constant that the compiler promoted to static memory, such as the target of
&5, and bytes reachable through borrows instaticandconstinitialisers that were extended to'static; - bytes pointed to by a shared reference, including through further references and
Boxes reachable from it.
Any write of one or more bytes to them is undefined behaviour, even if it writes the value that is already there.
fn main() {
let x = 5i32;
let p = &raw const x as *mut i32;
unsafe { *p = 6 }; // UB: x is an immutable binding
}
The deny-by-default rustc lint invalid_reference_casting rejects a closely related shape, &x as *const i32 as *mut i32 followed by a write, with the message "assigning to &T is undefined behavior, consider using an UnsafeCell". The version above, which starts from &raw const, compiles without that lint on Rust 1.98.1. Lints recognise particular shapes of code; they do not prove the absence of this bug. If you need to mutate through a shared path, put the data in an UnsafeCell (or a type built on it, such as Cell or Mutex).
6. Undefined behaviour through compiler intrinsics
Intrinsics are operations built into the compiler. Most of core::intrinsics requires an unstable feature (the few items reachable there on stable, such as transmute, are deprecated re-exports of the std::mem and std::ptr functions), so stable code reaches intrinsics through standard library wrappers that document their preconditions. Two examples:
std::hint::unreachable_unchecked()is undefined behaviour if it is ever executed.std::hint::assert_unchecked(cond)is undefined behaviour ifcondis false.
Both let the optimiser assume something without checking it, so a wrong assumption has no defined outcome.
7. Running code compiled for CPU features the machine lacks
For example, running a function compiled with #[target_feature(enable = "avx512f")] on a CPU without AVX-512 is undefined behaviour, unless the platform explicitly documents it as safe. Since Rust 1.86, such a function may be declared as a safe fn, but calling it from a function that does not enable the same features still requires an unsafe block (error E0133 otherwise). That block is where you argue, for example with is_x86_feature_detected!("avx512f"), that the CPU has the feature. The same applies to a whole program built with -C target-cpu=native and then run on an older CPU. The unsafe superpowers exercise lists this call among the operations that need unsafe.
8. Calling with the wrong ABI, or unwinding where unwinding is not allowed
An ABI (application binary interface) fixes how arguments and return values are passed between caller and callee. Declaring a foreign function with a calling convention other than the one it was compiled with, or with incompatible parameter types (for example i32 where the C function takes a double), makes the two sides disagree about where the arguments are. The standard library's documentation for function pointers lists which signature differences are compatible.
Unwinding is how a panic (or a C++ exception) travels up the stack. Unwinding into a frame that does not allow it is undefined behaviour. The main case is a foreign function that throws or unwinds, called through a declaration or function pointer with a non-unwinding ABI such as "C". If an unwind needs to cross the boundary, declare the function with extern "C-unwind".
The opposite direction is handled for you: since Rust 1.81, a Rust panic that would unwind out of an extern "C" function aborts the process instead, so that case is not undefined behaviour.
9. Producing an invalid value
Each type has a validity invariant: the set of bit patterns that count as a value of that type. Producing a value outside that set is undefined behaviour. The invalid-value table below lists the rules.
10. Incorrect inline assembly
Code in an asm! block must follow the rules for inline assembly. Two examples: changing a register that is not declared as an output or clobber, and, for a block marked options(noreturn), letting execution reach the end of the block.
11. Violating assumptions of the Rust runtime
Most of these assumptions are not written down yet. Two are:
- the rules for unwinding across language boundaries in the Reference's panic chapter;
- a Rust stack frame is never deallocated without running the destructors of the local variables it owns. C's
longjmpjumping over such a frame breaks this.
Library preconditions
Separate from the language's list, many unsafe fns in the standard library document a Safety section. Calling them without meeting it is undefined behaviour by their documentation, even where the current implementation happens to cope. Examples:
String::from_utf8_uncheckedwith bytes that are not valid UTF-8;slice::get_uncheckedwith an index out of range;NonZero::new_unchecked(0).
Some of these immediately hit an item above: NonZero::new_unchecked(0) produces an invalid value. Others break an invariant that later safe code relies on. A String holding invalid UTF-8 is not an invalid value at the language level, but safe string methods are written on the assumption that it is UTF-8, so later safe calls can then cause undefined behaviour. Follow the documented contract rather than the current implementation, which may change.
The invalid-value table
Producing a value happens whenever it is assigned to a place, read from a place, passed to a function or operator, or returned from one. An invalid value is undefined behaviour at that moment, even if no later code inspects it.
| type | rule for a valid value |
|---|---|
bool |
0 (false) or 1 (true) |
char |
at most char::MAX (0x10FFFF) and not a surrogate (0xD800..=0xDFFF) |
! |
has no valid values, so a value of type ! must never exist |
| integers, floats, raw pointers | any bit pattern, but it must be initialised (not read from uninitialised memory) |
fn pointers |
non-null |
str |
treated like [u8]: the bytes must be initialised (UTF-8 is a library rule, see above) |
| enums | a valid discriminant, and every field of that variant valid |
| structs, tuples, arrays | every field or element valid |
| unions | not fully decided; any value that safe code can create is valid |
&T, &mut T, Box<T> |
aligned, non-null, not dangling, and pointing at a valid T (the Reference notes the last point is still debated) |
metadata of wide pointers (references, Box, raw pointers) |
dyn Trait metadata must point to a compiler-generated vtable for that trait (for raw pointers this is still debated); slice and str lengths must be initialised usize values; for references and Box, the total pointed-to size must not exceed isize::MAX |
NonNull<T>, NonZero<T> |
a restricted range: never null, never zero |
Uninitialised memory is invalid for every type in this table that restricts its values. The only places where reading uninitialised memory is allowed are inside unions and in padding bytes (the gaps between fields).
In const contexts (evaluation at compile time) there is one more rule: values of integer-like types must not carry pointer provenance (the record of which allocation a pointer came from). So reinterpreting a pointer's bytes as an integer during const evaluation, for example by reading a pointer through a *const usize, is undefined behaviour there. The compiler reports this case as error E0080, "unable to turn pointer into integer".
Check your understanding
let x: u32 = unsafe { std::mem::uninitialized() }; and x is never read afterwards. Integers accept every bit pattern. Is this undefined behaviour?
Show answer
Hide answer
Yes.
"Any bit pattern is valid" and "must be initialised" are separate requirements, and integers have both. Uninitialised memory is not "some arbitrary number": the compiler may treat it as having no fixed value, and may make different choices about it at different uses. So it cannot be reasoned about as "whatever happened to be in memory".
The line also produces the value: mem::uninitialized() returns it and the let assigns it, and either one is enough, so not reading x later does not help. mem::uninitialized is deprecated for this reason, and rustc's warn-by-default invalid_value lint reports this line with "the type u32 does not permit being left uninitialized" and "integers must be initialized". To hold bytes that are not yet a u32, use MaybeUninit<u32>, covered in the union, transmute and MaybeUninit exercise.
mem::zeroed::<T>() is a different question, because all-zero bytes are an initialised bit pattern. Whether it is valid depends on T: zero is a valid u32, f64, raw pointer, bool (false) and char ('\0'), but not a valid reference, Box, fn pointer, NonNull or NonZero. A struct is valid when zeroed only if zero is valid for every field. The invalid_value lint warns about mem::zeroed for &i32, NonZero<u32> and fn(), among others.
Two definitions that resolve most confusion
Dangling. A pointer or reference is dangling if the bytes it points to (as many as the size of its pointee type) are not all inside one live allocation. Both words matter: a pointer whose range starts in allocation A and runs into allocation B is dangling even if both are live. When the pointee size is zero, the pointer is never dangling, whatever its address. That is why a zero-sized read through NonNull::<()>::dangling() is allowed, and why a Vec<()> can hold elements without allocating. A reference must still be non-null and aligned, even for a zero-sized type.
Based on a misaligned pointer. A place is based on a misaligned pointer if the last * in it dereferences a pointer that is not aligned for the pointer's own type. The requirement comes from the type being dereferenced, not the field being accessed: if p: *const S and S needs 8-byte alignment, (*p).f is based on a misaligned pointer when p is not 8-aligned, even if f is a u8.
Such a place causes undefined behaviour only when it is loaded from or stored to. Taking &raw const or &raw mut of it is allowed. Taking & or &mut of it creates a reference, and that reference must be aligned for the field's type, or it is an invalid value (item 9).
For a field of a #[repr(packed)] struct whose type needs more alignment than the packed struct provides (a u32 field, for example, but not a u8 one), the compiler rejects a reference with error E0793, "reference to field of packed struct is unaligned". The raw form &raw const packed.field is accepted; read through it with read_unaligned.
Check your understanding Why is a misaligned raw pointer fine to hold, while a misaligned reference is undefined behaviour even before anything dereferences it? Show answer Hide answer
Because the two types have different validity invariants.
A *const u32 only has to be initialised. You may point it at address 3, store it, pass it around and compare it. The alignment requirement applies when you load or store through it.
A &u32 must be non-null, aligned to 4 bytes, not dangling and pointing at a valid u32, and the aliasing rules (item 4) also apply to it. The compiler relies on these facts wherever the reference exists, for example to load from it early or keep its value in a register. So they must hold from the moment the reference is produced, not only when it is used.
This difference is why the &raw operator exists (see the &raw const / &raw mut exercise), and why a common approach in unsafe code is to work with raw pointers and create references only at the point where you can show their requirements hold.
Undefined behaviour during const evaluation
When the compiler evaluates a const, a static initialiser or a const fn call at compile time, it interprets the code and reports many kinds of undefined behaviour as errors (usually E0080) instead of compiling them. For example, reading one element past the end of an array in a const initialiser fails with "memory access failed". Two limits apply:
- The checks are not complete; the compiler does not promise to detect every case.
- They only cover code that is actually evaluated at compile time. A
const fnthat compiles may still cause undefined behaviour when it is called at run time with different arguments.
The extra provenance rule for const contexts is described under the invalid-value table.
Lints that catch specific cases
Each lint below recognises one shape of code that is almost always undefined behaviour. The rustc lints are enabled at deny level in every build. The Clippy lints belong to Clippy's correctness group, which is denied by default when you run cargo clippy.
| lint | source | catches |
|---|---|---|
invalid_reference_casting |
rustc | casting &T to *mut T and writing through it |
mutable_transmutes |
rustc | transmute::<&T, &mut T> |
invalid_from_utf8_unchecked |
rustc | str::from_utf8_unchecked on a literal that is not UTF-8 |
invalid_null_arguments |
rustc | passing a known null pointer to a function that requires non-null, such as ptr::copy_nonoverlapping |
deref_nullptr |
rustc | dereferencing ptr::null() |
uninit_assumed_init |
Clippy | MaybeUninit::uninit().assume_init() for a type that must be initialised |
uninit_vec |
Clippy | Vec::with_capacity followed by set_len without initialising the elements |
wrong_transmute |
Clippy | transmutes that cannot be correct, such as f64 to a pointer |
transmuting_null |
Clippy | transmuting a known null pointer into a reference |
transmute_null_to_fn |
Clippy | transmuting a known null pointer into a function pointer |
eager_transmute |
Clippy | a transmute inside then_some(...) that runs even when its guard condition is false |
rustc's invalid_value lint, covered in the question above, is a warning rather than an error.
A lint that fires points at a real problem and deserves a fix, not an #[allow]. A lint that stays silent tells you little: these lints match code shapes, and example 5 shows a write that none of them reports.
How to use this page
The safety comment on an unsafe block is an argument that the operations inside cannot cause any of the eleven items above, plus any library preconditions they rely on. Usually only a few items are relevant to a given block, but you should check which ones rather than assume.
Running tests under Miri (cargo miri test, available on nightly toolchains) detects many of these errors at run time. It only checks the code paths your tests execute and does not detect every kind of undefined behaviour, so it supports the written argument rather than replacing it.
If a program behaves in ways that do not make sense, such as differing between debug and release builds or changing after an unrelated edit, undefined behaviour is one possible cause, and this list is a place to start looking.