Lesson 7 — unsafe Rust
Question
unsafe blocks temporarily remove Rust's safety guarantees. Normally you avoid them. But Reth / Revm hotpaths use unsafe for performance. When, why is it safe, and how do you read it?
Principle (minimum model)
- Five things
unsafeenables. (1) Dereferencing a raw pointer. (2) Mutating a static. (3) Calling anunsafe fn. (4) Implementing anunsafe trait. (5) Accessing union fields. - What Rust's safety guarantees are. Memory safety + no data races + no undefined behaviour, all enforced at compile time via ownership / borrowing / lifetimes.
unsaferemoves that. - The contract pattern. Inside an
unsafeblock, encode "an invariant I checked myself" and trust the compiler to honour it. Break the contract → instant UB. unwrap_unchecked(). Theunsafevariant ofOption::unwrap. UB ifNone, but skips the runtime check. Hotpath speedup when you can proveSomeahead of time.- Revm's
popn_top!macro. Guards withif stack.len() < N { return Err }first, thenunwrap_uncheckedto skip the runtimeSomecheck. The guard proves the invariant. unsafe fnis the advertised version. The function itself is unsafe — the caller is responsible for the contract.pub unsafe fnputs the contract in the doc comment.- Soundness. Unsafe code is sound when it cannot cause UB on any input. Violating soundness = anyone can hit UB with valid input → a real bug.
- How to read it. When you see
unsafe, ask "why is this needed?" and "what invariant is assumed?" Read the surrounding comments / docs — they explain the contract.
Worked example + steps
unsafe Rust
One of two areas where the standard Rust book is thin, and where Revm's interpreter goes deep. This lesson gives you enough vocabulary to read Revm's hot path without flinching at unsafe { ... } blocks. (The other area — macro_rules! — is the next lesson; together they're what you need to read popn_top! and friends.)
What unsafe actually allows
Rust's safety guarantees (no data races, no use-after-free, no out-of-bounds access) are enforced by the compiler — but only over safe Rust. There are five things only unsafe code can do:
- Dereference a raw pointer (
*const Tor*mut T) - Call an
unsafe fn(a function the compiler can't verify) - Access or modify a mutable static variable
- Implement an
unsafe trait(likeSendorSyncmanually) - Access fields of a
union
That's the entire list. Crucially, unsafe doesn't turn off Rust's borrow checker for your local variables, doesn't allow null pointer derefs automatically, and doesn't let you skip type-checking.
The contract — what unsafe does
unsafe is a promise from you to the compiler: "I have manually verified that this code maintains Rust's safety invariants. You can trust me."
If the promise is wrong, you get undefined behavior (UB) — and UB is catastrophic. Once UB happens, the entire program is in an inconsistent state. The compiler may have optimized assuming UB doesn't happen, so the actual runtime behavior can be anything: crashes, wrong results, security holes, plausible-looking-but-wrong output.
There is no "small UB." A program with UB is wrong, full stop.
Why Revm uses unsafe on the hot path
Revm's popn_top! macro from Intermediate lesson 1 contains:
let ([$( $x ),*], $top) = unsafe {
$crate::interpreter_types::StackTr::popn_top(&mut $interpreter.stack)
.unwrap_unchecked()
};
The function being called returns Option<...>. unwrap_unchecked() is the unsafe version of unwrap() — it skips the runtime "is this Some?" check.
Why is this safe here? Because the immediately preceding code is:
if $interpreter.stack.len() < (1 + $crate::_count!($($x)*)) {
return Err(...);
}
So at the moment of unwrap_unchecked(), the stack length is provably sufficient. The author has verified by inspection that popn_top will return Some. Calling .unwrap() would do a redundant runtime check; .unwrap_unchecked() skips it.
The cost saved: one branch per opcode execution. On the hot path of an interpreter that runs billions of times, that's measurable.
The unsafe discipline in Revm/Reth
Idiomatic unsafe use looks like this:
// Comment block explaining the safety invariant
// SAFETY: We just checked stack.len() >= N+1 above, so popn_top
// is guaranteed to return Some.
let result = unsafe { popn_top.unwrap_unchecked() };
Every unsafe block in well-written Rust has a // SAFETY: comment describing why the unsafe is sound. Reviewers look for these; their absence is a code smell.
unsafe fn vs unsafe { ... }
Two related but different concepts:
| Form | Meaning |
|---|---|
unsafe { ... } block | "I'm doing one of the 5 unsafe things, and I've verified the invariants" |
unsafe fn foo(...) | "Calling this function requires unsafe — the caller must verify invariants" |
unwrap_unchecked() is an unsafe fn. To call it, you wrap the call site in unsafe { ... }. That's the contract: the function declares "I have a precondition," the caller declares "I've checked it."
What you do NOT need to know yet
- Manual implementation of
Send/Sync(Reth doesn't really do this) - Inline assembly (almost never)
- FFI to C (only relevant for jemalloc, which is one global setting)
For reading Revm/Reth source, the patterns above (manual safety verification + unwrap_unchecked after a check) are 95% of what you'll see.
What you should walk away with
unsafeallows 5 specific things; it's a contract with the compiler, not a licenseunwrap_unchecked()+ a preceding length/state check is the canonical Revm pattern for skipping redundant runtime checks on the hot path- The
// SAFETY:comment discipline — everyunsafeblock in well-written Rust documents the invariant that justifies it
The next lesson covers macro_rules!, the other half of what you need to read Revm's interpreter source.
Summary (3 lines)
unsafeenables five operations while removing Rust's safety guarantees. The contract pattern encodes a manually-verified invariant for the compiler.- Revm's
popn_top!usesunwrap_uncheckedafter a guard proves the invariant — the guard is the contract. - Read
unsafeby asking "why" and "what invariant" — the comments explain.unsafeis a last resort; soundness is the bar. Next lesson:macro_rules!basics.