This is a reference page. Do not memorise it; bookmark it.
The honest answer to “how do I know my unsafe code is correct?” is: you check it against this list. Every item in this track is a specific instance of one of these bullets, and the later problems will name them by number rather than re-deriving the argument.
One caveat first, and it matters. The Reference’s list is normative but explicitly non-exhaustive and evolving. The opsem team is still specifying parts of it. Treat it as the best current statement of the rules, not as a closed set — and treat anything it does not clearly permit as forbidden.
The list
1. Data races
Two threads accessing the same memory, at least one writing, with no synchronisation.
static mut COUNTER: u64 = 0;
// two threads: unsafe { COUNTER += 1 } <- UB
Not “the count comes out wrong”. Undefined. Use an atomic or a lock. Note the contrast with a race condition (item 17.1), which is safe.
2. Accessing a dangling or misaligned place
let p: *const u64;
{ let x = 5u64; p = &raw const x; } // x is dead here
unsafe { *p } // UB: dangling
let bytes = [0u8; 16];
let p = bytes.as_ptr().wrapping_add(1).cast::<u64>();
unsafe { *p } // UB: misaligned
“Dangling” has a precise definition, below.
3. Place projections violating in-bounds arithmetic
Computing ptr.add(n) where the result leaves the allocation. The undefined behaviour is at the arithmetic, not at the dereference — item 17.7 is entirely about this, because it is the most counterintuitive rule in the topic.
4. Breaking the pointer aliasing rules
Three sub-rules, and they are the ones you will break most often:
-
&Tmust not be mutated while it is live — except through anUnsafeCell; -
&mut Tmust not be read or written by anything not derived from it; -
Box<T>behaves like&'static mut T— which is why holding a raw pointer into aBoxacross a move of theBoxis a trap.
Item 17.13 develops this and item 17.14 explains the two competing formal models.
5. Mutating immutable bytes
let x = 5i32;
let p = &raw const x as *mut i32;
unsafe { *p = 6 }; // UB
This includes const-promoted expressions and immutable statics. rustc’s deny-by-default invalid_reference_casting catches the obvious shape and tells you the answer: use an UnsafeCell.
6. Invoking undefined behaviour via intrinsics
Every core::intrinsics function has preconditions. unreachable_unchecked() reached is the canonical example — and is exactly what assume compiles into.
7. Executing code for unsupported target features
Calling an AVX-512 function on a machine without AVX-512. This is why #[target_feature] functions are unsafe to call from functions without those features (superpower #6 in item 17.2).
8. Calling with the wrong ABI, or unwinding past a frame that forbids it
Declaring a C function as extern "C" fn(i32) when it actually takes a f64. And unwinding across an extern "C" boundary, which is why extern "C-unwind" exists.
9. Producing an invalid value
The big one. Table below.
10. Incorrect inline assembly
asm! blocks that violate their own constraints — clobbering a register you did not declare, or falling out of a noreturn block.
11. Violating runtime assumptions
Standard-library preconditions that are documented rather than checked: String::from_utf8_unchecked with non-UTF-8, slice::get_unchecked out of range, NonZero::new_unchecked(0).
The invalid-value table, in full
Producing a value that violates its type’s validity invariant is instant undefined behaviour the moment the value exists — assigned to a place, read from one, passed as an argument, returned. Not when you look at it. When it exists.
| type | valid values |
|---|---|
bool |
exactly 0 or 1 |
char |
<= 0x10FFFF and not a surrogate (0xD800..=0xDFFF) |
! |
none — a value of type ! must never exist |
| integers, floats, raw pointers | any bit pattern, but must be initialised |
| enums | must hold a valid discriminant |
&T, &mut T, Box<T> |
aligned, non-null, non-dangling, pointing at a valid T |
dyn Trait |
metadata must be a real vtable pointer for that trait |
| slices |
length metadata must not imply a size over isize::MAX |
NonNull<T>, NonZero<T> |
custom valid ranges — never null, never zero |
💡let x: u32 = unsafe { mem::uninitialized() }; — you never read x. Integers accept every bit pattern. Is this undefined behaviour?
click to reveal
Yes, and this is the bullet people find hardest to accept.
“Any bit pattern is valid” and “must be initialised” are different requirements, and integers are subject to both. Uninitialised memory is not “some arbitrary value” — it is formally not a value at all. The compiler models it as a distinct thing, and two reads of the same uninitialised byte may legitimately yield different results, which is exactly why it cannot be treated as “whatever happened to be there”.
rustc’s invalid_value lint (warn-by-default) fires on mem::uninitialized::<u32>() for precisely this reason. The correct spelling of “I have some bytes that are not yet a u32“ is MaybeUninit<u32> — item 17.23.
Compare mem::zeroed::<T>(), which is a different question: all-zeroes is a real bit pattern, so mem::zeroed::<u32>() is fine. It is instant UB for &T (null), for NonZero (zero), and for any struct containing a bool only if the zero pattern would be invalid — which for bool it is not, since 0 is false. The trap is NonZero and references.
Two definitions that resolve most confusion
“Dangling” means not all the bytes are inside one live allocation. Note the “all” and note the “one”. A pointer that reaches from the end of allocation A into allocation B is dangling even though both are live. And a zero-sized pointer is never dangling, whatever its address — which is why NonNull::dangling() is a legal thing to hold and why Vec<()> works.
“Based on a misaligned pointer” only causes undefined behaviour on a load or a store. Computing a misaligned address is fine. That is exactly why &raw const (*p).val on a repr(packed) field is legal while &(*p).val is not: the first computes an address, the second produces a reference, and a misaligned reference violates rule 9 the instant it exists.
💡Why is a misaligned raw pointer fine but a misaligned reference instant undefined behaviour, when neither has been dereferenced? click to reveal
Because a raw pointer has no validity invariant beyond “it is a number with provenance”, and a reference has a long one.
*const u32 promises nothing. You may point it at address 3, store it in a struct, pass it around, compare it. The obligation attaches at the load or store.
&u32 promises: non-null, aligned to 4, pointing at an initialised u32, and not aliased by a live &mut. Those promises are what let the compiler hoist loads, cache values in registers and reorder across calls — so they must hold from the moment the reference exists, not from the moment it is used. There is no window in which you “have” an invalid reference and have not yet done anything wrong.
This single asymmetry is why &raw exists as an operator (item 17.6) and why the standard survival tactic in unsafe code is “keep it in raw pointers and mint references late” (item 17.14).
Const contexts have extra rules
Compile-time evaluation is stricter than runtime, not looser. const contexts have additional provenance rules the runtime does not — for instance, a const cannot produce a value containing a pointer to a runtime allocation, and the interpreter rejects a great deal that would merely be undefined at runtime. If a const fn compiles, it has passed a stronger check than the same code would face at runtime.
Lints that catch specific bullets
These are all on by default at deny, and each one exists because a whole class of real bug had exactly one syntactic shape:
| lint | catches |
|---|---|
clippy::uninit_assumed_init |
MaybeUninit::uninit().assume_init() |
clippy::uninit_vec |
with_capacity then set_len |
clippy::wrong_transmute |
transmutes whose types cannot be related |
clippy::transmuting_null |
transmuting a null pointer to a reference |
clippy::transmute_null_to_fn |
… to a function pointer |
clippy::eager_transmute |
a transmute evaluated before its guard |
rustc::invalid_reference_casting |
&T as *const T as *mut T, then writing |
rustc::mutable_transmutes |
transmute::<&T, &mut T> |
rustc::invalid_from_utf8_unchecked |
from_utf8_unchecked on a bad literal |
And one that is merely a warning but worth heeding: clippy::invalid_null_arguments.
A lint firing means you have definitely got it wrong. A lint not firing means nothing at all — these catch syntactic shapes, not semantic errors. That asymmetry is the whole reason this list exists as prose.
How to use this page
When you write an unsafe block, the safety comment you owe is an argument that none of the eleven bullets applies. In practice three or four are relevant and the rest are obviously satisfied — but “obviously” should mean you checked, not that you did not think about it.
When something breaks in a way that makes no sense — works in debug, fails in release; breaks after an unrelated edit; the debugger shows impossible values — come back here and go down the list.