Every unsafe superpower
The five operations unsafe unlocks (raw pointer dereference, unsafe calls, unsafe traits, mutable statics, union fields), the contracts that go with them, and the tools that check your work.
unsafe doesn't switch Rust's checks off. It unlocks five operations
the compiler can't check, and makes you responsible for them. Before
writing it, go down this list.
- Is there a safe way already? Most "I need unsafe" moments are a
standard-library function you haven't found (
split_at_mut,swap,Cell) or a data structure that owns its nodes instead of pointing at them. - Dereference a raw pointer → for FFI and hand-built data
structures. Say in a
// SAFETY:comment why it's non-null, aligned, and points at a live value. - Call an
unsafe fn(including every C function) → read its# Safetysection and show that each condition holds. - Implement an
unsafe trait(Send,Syncby hand) → only when you can prove the type really is safe to share or send. - Touch a
static mutor read aunionfield → almost never: an atomic,Mutex,OnceLockor an enum does it safely.
Then wrap each piece in a safe function whose checks make it valid for every input, run the tests under Miri, and have someone review it.
What unsafe means
Section
What unsafe means
A promise from you, not a switch that turns checks off.
Formal. Inside an unsafe block (or an unsafe impl), Rust permits
exactly five operations it otherwise forbids: dereferencing a raw
pointer, calling an unsafe function (including foreign functions),
implementing an unsafe trait, accessing or modifying a mutable
static, and reading a field of a union. Every other rule still
applies: types are checked, references are borrow-checked, and lifetimes
are enforced. What changes is who is responsible: the compiler can no
longer prove the five operations are valid, so the programmer must,
and undefined behavior results if they are wrong.
In plain English. unsafe doesn't switch the safety checks off. It
unlocks five specific tools the compiler can't check, and it means "I
have checked these myself." If you're wrong, the program can break in
ways Rust normally rules out.
Example. let p = &x as *const i32; is safe; unsafe { *p } is the
unsafe part, correct only because x is still alive and aligned.
Why this rule exists. Some things (hardware access, foreign code, low-level data structures) can't be checked by the compiler; confining them to small marked blocks tells reviewers exactly where to look.
Exactly five operations, and every other rule still applies.
Section
Undefined behaviour
What happens if the promise is wrong, and why it can look fine for years.
The five powers
Section
1. Dereference a raw pointer
Creating one is safe; reading through one is not.
Section
2. Call an unsafe function
The # Safety section is the contract; the caller must meet it.
The three rarer powers, and the safe replacements for each.
The compiler can't see C's rules, so each call site is yours to justify.
Using them responsibly
A small unsafe core behind a function no caller can misuse.
Term
soundness
Example
fn get(v: &[u8], i: usize) -> u8 { unsafe { *v.get_unchecked(i) } } is unsound: get(&[], 5) reads memory it doesn't own. Adding assert!(i < v.len()) makes it sound.Formal. An extern "C" function must not let a Rust panic unwind out
of it into foreign code: since Rust 1.81 such an unwind aborts the
process. Functions called from C should catch panics at the boundary
with std::panic::catch_unwind and translate them into an error value
the caller understands (an error code, a null pointer, an out-parameter).
The extern "C-unwind" ABI exists for the rare cases where unwinding
across the boundary is intended and both sides support it.
In plain English. C has no idea what a Rust panic is. If one escapes into C, the whole program stops. So catch it at the door and hand C an error code it does understand.
Example. #[unsafe(no_mangle)] pub extern "C" fn parse(s: *const c_char) -> i32 { match std::panic::catch_unwind(|| ...) { Ok(n) => n, Err(_) => -1 } }.
Why this rule exists. Unwinding through frames compiled by another language skips their cleanup and corrupts their state; aborting is the only safe default.
One more rule at the C boundary; a panic must be caught before it gets there.
Checking the work
Section
Checking unsafe code
Miri, sanitizers, and the review checklist.
The same questions asked of someone else's code, and of the crates you depend on.
Formal. Safe Rust guarantees the absence of undefined behavior:
no use-after-free, double free, out-of-bounds access, data races, or
reads of uninitialized memory in code without unsafe. It guarantees
nothing about a program's logic: injection (SQL, command, path),
broken authentication or authorization, leaked secrets, unbounded
resource use (denial of service), unsafe deserialization of trusted
fields, vulnerable dependencies, and bugs inside unsafe code are all
still possible and must be prevented by design, review, and testing.
In plain English. Rust removes one big family of bugs, the ones where a program scribbles on memory it shouldn't. A Rust program can still do exactly the wrong thing, carefully and correctly: let the wrong person in, run a query an attacker wrote, or run out of memory because someone sent a huge request.
Example. An axum handler that formats a query string into SQL is memory-safe and still vulnerable to SQL injection; binding the value fixes the security bug, not a memory bug.
Why this rule exists. Treating "written in Rust" as "secure" skips the threat modeling every service needs.
Safe Rust rules out memory bugs, not logic bugs or leaked secrets.