Chapter 37: Unsafe Rust, Power and Responsibility
Prerequisites
You will understand
- The 5 unsafe superpowers and nothing more
- Safe abstraction over unsafe implementation
- Sound wrappers: prove preconditions, expose safe API
Reading time
unsafe to call into C code. This chapter's sound-wrapper patterns are essential for building safe abstractions over foreign libraries.Preview Ch 38 →The Five Unsafe Capabilities
Small Unsafe Core, Safe Public API
Step 1 - The Problem
Safe Rust is intentionally incomplete as a systems implementation language.
Some tasks require operations the compiler cannot fully verify:
- implementing containers like
Vec<T> - FFI with foreign memory rules
- intrusive or self-referential structures
- lock-free structures
- raw OS or hardware interaction
If Rust simply banned these, the language could not implement its own standard library. If Rust allowed them without structure, it would lose its safety claim.
Step 2 - Rust’s Design Decision
Rust isolates these operations behind unsafe.
unsafe is not “turn off the borrow checker.” It is “I am performing one of a small set of operations whose safety depends on extra invariants the compiler cannot prove.”
Rust accepted:
- a visible escape hatch
- manual reasoning burden for low-level code
- audit requirements at abstraction boundaries
Rust refused:
- making low-level systems work impossible
- letting unchecked code silently infect the entire language
Step 3 - The Mental Model
Plain English rule: unsafe means “the compiler is trusting me to uphold additional safety rules here.”
The goal is not to write “unsafe code.” The goal is to write safe abstractions that contain small, justified islands of unsafe implementation.
Step 4 - Minimal Code Example
#![allow(unused)]
fn main() {
pub fn get_unchecked_safe<T>(slice: &[T], index: usize) -> Option<&T> {
if index < slice.len() {
unsafe { Some(slice.get_unchecked(index)) }
} else {
None
}
}
}
Step 5 - Line-by-Line Compiler Walkthrough
- The public function is safe to call.
- It checks
index < slice.len(). - Inside the
unsafeblock, it callsget_unchecked, which requires the caller to guarantee the index is in bounds. - The preceding
ifestablishes that guarantee. - The function returns a safe reference because the unsafe precondition has been discharged locally.
This is the essence of sound unsafe Rust:
- identify the unsafe precondition
- prove it in a smaller local context
- expose only the safe result
Step 6 - Three-Level Explanation
unsafe means Rust cannot check everything for you in this block, so you must be extra precise.
Use unsafe sparingly and isolate it. Most application code should not need it. When unsafe is necessary:
- keep the block small
- document the preconditions with
# Safety - test and fuzz the safe wrapper
- make invariants obvious to reviewers
Unsafe Rust still enforces most of the language:
- types still exist
- lifetimes still exist
- moves still exist
- aliasing rules still matter
The five unsafe superpowers are specific:
- dereferencing raw pointers
- calling unsafe functions
- accessing mutable statics
- implementing unsafe traits
- accessing union fields
Everything else must still be correct under Rust’s semantic model. Undefined behavior is the risk when your manual reasoning is wrong.
Common UB Shapes
Important undefined-behavior risks include:
- dereferencing dangling or misaligned pointers
- violating aliasing assumptions through raw-pointer misuse
- reading uninitialized memory
- using invalid enum discriminants
- double-dropping or forgetting required drops
- creating references to memory that does not satisfy reference rules
ManuallyDrop<T> and MaybeUninit<T> exist because low-level code sometimes must control initialization and destruction explicitly. They are not performance toys. They are contract-carrying tools for representation-sensitive code.
Safety Contracts and # Safety
Unsafe APIs should document:
- required pointer validity
- alignment expectations
- aliasing constraints
- initialization state
- ownership and drop responsibilities
Example:
#![allow(unused)]
fn main() {
/// # Safety
/// Caller must ensure `ptr` is non-null, aligned, and points to a live `T`.
unsafe fn as_ref<'a, T>(ptr: *const T) -> &'a T {
&*ptr
}
}
The caller owns the obligation. The callee documents it. A safe wrapper may discharge it and hide the unsafe function from public callers.
Miri and the Audit Mindset
Miri is valuable because it executes Rust under an interpreter that can catch classes of undefined behavior invisible to ordinary tests.
The audit mindset for unsafe code:
- what invariant is this block relying on?
- where is that invariant established?
- can a future refactor accidentally violate it?
- is the safe API narrower than the unsafe machinery beneath it?
Step 7 - Common Misconceptions
Wrong model 1: “unsafe means Rust stops checking everything.”
Correction: it enables only specific operations whose safety must be justified manually.
Wrong model 2: “Unsafe code is fine if tests pass.”
Correction: UB can stay dormant across enormous test suites.
Wrong model 3: “Using unsafe makes code faster.”
Correction: only if it enables a design or optimization the safe version could not express. Unsafe itself is not an optimization flag.
Wrong model 4: “Small unsafe blocks are automatically safe.”
Correction: small scope helps review, but soundness still depends on invariants being correct.
Step 8 - Real-World Pattern
Production Rust uses unsafe primarily in:
- collections and allocators
- synchronization internals
- FFI layers
- performance-sensitive parsing or buffer code
The important pattern is that the public API is usually safe. Unsafe lives in implementation modules with heavy comments, tests, and strict invariants.
Step 9 - Practice Block
Code Exercise
Write a safe wrapper around slice::get_unchecked that returns Option<&T>, then explain exactly which unsafe precondition your wrapper discharged.
Code Reading Drill
Read this signature and list every obligation:
#![allow(unused)]
fn main() {
unsafe fn from_raw_parts<'a>(ptr: *const u8, len: usize) -> &'a [u8]
}
Spot the Bug
What is wrong here?
#![allow(unused)]
fn main() {
unsafe {
let x = 5u32;
let ptr = &x as *const u32 as *const u8;
let y = *(ptr as *const u64);
println!("{y}");
}
}
Refactoring Drill
Take a large unsafe function and redesign it into:
- one public safe wrapper
- one or more private unsafe helpers
- explicit documented invariants
Compiler Error Interpretation
If a type requires unsafe impl Send, translate that as: “I am asserting a thread-safety property the compiler cannot prove automatically, so this is a soundness boundary.”
Step 10 - Contribution Connection
After this chapter, you can review:
- safety comments and contracts
- wrappers around raw-pointer APIs
- unsafe trait impls
- container and buffer internals
Approachable first PRs include:
- tightening safety docs
- shrinking unsafe regions
- replacing unnecessary unsafe with safe std APIs where equivalent
In Plain English
Unsafe Rust exists because some low-level jobs cannot be fully checked by the compiler. Rust’s deal is that these dangerous operations must be small, visible, and justified, so the rest of the code can stay safe. That matters because systems software still needs raw power, but raw power without boundaries becomes unmaintainable fast.
What Invariant Is Rust Protecting Here?
Any value or reference created through unsafe code must still satisfy Rust’s normal aliasing, lifetime, initialization, and ownership rules, even if the compiler could not verify them directly.
If You Remember Only 3 Things
unsafeis about additional obligations, not fewer semantics.- The real goal is safe abstractions over unsafe implementation details.
- Documented invariants are part of the code, not optional prose.
Memory Hook
Unsafe Rust is not taking the guardrails off the road. It is opening a maintenance hatch under the road and saying: if you go down there, you are now responsible for the support beams.
Flashcard Deck
| Question | Answer |
|---|---|
What does unsafe actually mean? | The compiler is trusting you to uphold extra safety invariants for specific operations. |
| Name the five unsafe superpowers. | Raw pointer dereference, unsafe fn call, mutable static access, unsafe trait impl, union field access. |
| What should a safe wrapper around unsafe code do? | Discharge the unsafe preconditions internally and expose only a sound safe API. |
| Does unsafe disable the borrow checker? | No. Most Rust semantics still apply. |
| Why are tests alone insufficient for unsafe code? | Undefined behavior can remain latent and nondeterministic. |
What is ManuallyDrop for? | Controlling destruction explicitly in low-level code. |
What is MaybeUninit for? | Representing memory that may not yet hold a fully initialized value. |
What should # Safety docs describe? | The precise obligations callers must uphold. |
Chapter Cheat Sheet
| Need | Tool or practice | Why |
|---|---|---|
| Raw memory not fully initialized yet | MaybeUninit<T> | avoid UB from pretending it is initialized |
| Delay or control destruction | ManuallyDrop<T> | explicit drop management |
| Sound low-level boundary | safe wrapper over unsafe core | narrow public risk surface |
| Review unsafe code | invariant checklist | soundness depends on it |
| Catch UB during testing | Miri | interpreter-based checks |