25.1What unsafe actually means
For twenty-four chapters the compiler has guaranteed your programs are memory-safe: no use-after-free, no data races, no dangling pointers, all caught before the code runs. That guarantee is Rust's whole reason for being. This chapter is about the small, deliberate hole in it: the unsafe keyword, which lets you do a handful of things the compiler can't verify. The single most important idea, the one to hold onto through the whole chapter, is that unsafe does not turn off safety. It's not an off-switch. It's you telling the compiler "I'm taking responsibility for this part, because I can prove it's correct and you can't." Unsafe is a contract, not a license to be reckless.
Why unsafe has to exist
It can feel like a betrayal that safe Rust has an escape hatch at all. But there are two unavoidable reasons it must.
First, the borrow checker is conservative: it rejects anything it can't prove safe, which means it also rejects some things that are safe but that it can't see the proof for. A handful of legitimate patterns, certain data structures, certain optimizations, are correct but beyond what static analysis can confirm. Without an escape hatch, those would be impossible to write in Rust at all.
Second, the computer underneath is fundamentally unsafe. The operating system, the hardware, and every library written in C expose raw, unchecked interfaces. To talk to any of them, your file system, the network stack, a graphics card, a C library, something must step outside Rust's guarantees and interface with that unchecked world. unsafe is the doorway. Rust's standard library itself is built on unsafe internally: Vec, String, every collection, manage raw memory through unsafe code, wrapped in safe APIs you've used all along without a thought.
The five superpowers
unsafe doesn't unlock arbitrary chaos. It enables exactly five additional operations, and nothing else. Everything else stays exactly as checked as always. The five:
- Dereference a raw pointer. Raw pointers (next lesson) can be created in safe code but only followed inside
unsafe. - Call an
unsafefunction. Functions markedunsafecarry preconditions the caller must uphold; calling one requiresunsafe. - Access or modify a mutable
static. A mutable global is a data race waiting to happen, so touching one needsunsafe. (This is thestaticaddress loose end from lesson 5.1, now fully explained: a mutable static is exactly the case that needs unsafe.) - Implement an
unsafetrait. Some traits (likeSend/Sync, chapter 22) carry guarantees the compiler can't check, so implementing them by hand isunsafe. - Access fields of a
union. A rarely-used C-interop construct where the compiler can't track which field is valid.
That's the entire list. The most important thing to notice is what's missing from it.
What stays checked inside unsafe
Here is the misconception to kill right now: people imagine unsafe { ... } switches off the borrow checker. It does not. Inside an unsafe block, the borrow checker is still running, ownership is still enforced, types are still checked, lifetimes still apply. All your normal safety is exactly as present as outside. unsafe adds those five abilities on top; it subtracts nothing.
fn main() {
unsafe {
let s = String::from("hello");
let r = &s;
// The borrow checker is fully active here:
// drop(s); // would still be an error: r still borrows s
println!("{r}");
}
}hello
So an unsafe block isn't a lawless zone. It's a small region where you've gained five specific powers and kept every other guarantee. This is why the right mental model is "five extra tools," not "safety off." The vast majority of your code inside an unsafe block is still checked exactly as rigorously as the rest of your program.
Key insight
unsafe doesn't disable Rust's safety checks; it grants five specific abilities the checker can't verify, while leaving everything else fully checked. The borrow checker, ownership, types, and lifetimes are all still active inside an unsafe block. What unsafe really means is "the compiler can't prove this part is sound, so I'm vouching for it instead." The responsibility moves from the compiler to you, for those five operations only, and that's the entire transaction.
The safety-comment culture
Because unsafe shifts the burden of proof onto you, the community has a strong, near-universal convention: every unsafe block gets a comment explaining why it's actually safe, what invariant the author is relying on that the compiler couldn't see. These are called safety comments, conventionally written as // SAFETY: ...:
let numbers = vec![1, 2, 3, 4];
let ptr = numbers.as_ptr();
// SAFETY: index 2 is within bounds (the vec has 4 elements),
// and `numbers` is alive for the whole unsafe block, so the
// pointer is valid to dereference here.
let third = unsafe { *ptr.add(2) };
println!("{third}");3
The // SAFETY: comment is the contract written down: it states the conditions under which this unsafe is correct, so a reviewer (or future you) can check that those conditions actually hold. This convention is load-bearing. Unsafe code that's correct but undocumented is a landmine, because the next person can't tell what they must preserve. The safety comment turns "trust me" into "here's exactly why, verify it." Treating unsafe as a contract that must be justified in writing is the difference between disciplined systems programming and the reckless pointer-juggling unsafe is often imagined to be.
Best practice
Use unsafe as rarely as possible, keep each unsafe block as small as possible (just the operations that truly need it, nothing more), and write a // SAFETY: comment on every one explaining why the code upholds the invariants the compiler couldn't check. Then wrap the unsafe in a safe API so callers never see it (the subject of lesson 25.3). The goal is always to confine unsafety to a tiny, audited, well-justified core, not to spread it around.
Quiz time
Question #1
Does unsafe turn off the borrow checker and Rust's safety guarantees? Explain.
Show solution
No. unsafe grants five specific extra abilities (dereferencing raw pointers, calling unsafe functions, accessing mutable statics, implementing unsafe traits, accessing union fields) but disables nothing. Inside an unsafe block the borrow checker, ownership, type checking, and lifetimes are all still fully active. unsafe means "I take responsibility for these five operations the compiler can't verify," not "safety off."
Question #2
Why does Rust need an unsafe escape hatch at all?
Show solution
Two reasons. (1) The borrow checker is conservative, it rejects anything it can't prove safe, so some genuinely-safe patterns (certain data structures and optimizations) can't be expressed in safe Rust and need an escape hatch. (2) The underlying system (OS, hardware, C libraries) exposes raw, unchecked interfaces, so interfacing with them requires stepping outside Rust's guarantees. Even the standard library (Vec, String) is built on unsafe internally, wrapped in safe APIs.
Question #3
What is a "safety comment," and why is the convention important?
Show solution
A safety comment (conventionally // SAFETY: ...) is a written explanation, on every unsafe block, of why the code is actually sound, the invariants the author relies on that the compiler couldn't verify. It's important because unsafe shifts the burden of proof from compiler to programmer, so the reasoning must be recorded for reviewers and future maintainers to check (and preserve). Correct-but-undocumented unsafe is a landmine; the safety comment turns "trust me" into a verifiable contract.
The first of the five superpowers is the one the others build on: raw pointers. The next lesson (25.2) introduces them, the *const T and *mut T you can create safely but only dereference inside unsafe.