Reading Rust

Part 5 · Unsafe & real code

`unsafe` does not turn off the borrow checker

It unlocks exactly five abilities and disables nothing. The widespread belief that it is an escape hatch from the borrow checker is the fastest way to sound like you have only read about Rust.

40 minrefutation textPOEbeacon training
What this lesson is trying to break (3)
  • `unsafe` disables the borrow checker.
  • unsafe code is bad code, and a crate containing it is suspect.
  • If it compiles inside `unsafe`, it works.
Unlearn thiscarried over from every article that calls unsafe an escape hatch
You think

unsafe is the trapdoor. When the borrow checker refuses something you know is fine, you wrap it in unsafe and the compiler stops complaining — Rust’s equivalent of any in TypeScript or @SuppressWarnings in Java.

Wrong here

unsafe disables nothing. The borrow checker runs. The type checker runs. Lifetimes are checked. Send and Sync are enforced. Wrapping a borrow error in unsafe changes nothing at all — you get the identical error.

Instead

unsafe adds five abilities that are otherwise forbidden, and transfers responsibility for a specific set of invariants from the compiler to you. It is not “turn off checking”; it is “I am now allowed to do these five things, and I am asserting I have checked what the compiler cannot”.

The five abilities

Inside an unsafe block, and only there, you may:

  1. Dereference a raw pointer (*const T, *mut T)
  2. Call an unsafe function or method — including every extern "C" FFI call
  3. Access or modify a mutable static
  4. Implement an unsafe trait (Send, Sync, and others)
  5. Access the fields of a union

That is the complete list. Nothing about borrows, moves, lifetimes or types changes.

Predict firstcommit in writing, then look
fn main() {
    let mut v = vec![1, 2, 3];

    unsafe {
        let first = &v[0];
        v.push(4);
        println!("{first}");
    }
}

Undefined behaviour

The stakes are higher than “might crash”. UB means the compiler is permitted to assume the situation never happens, and to optimise on that basis. A program with UB has no defined meaning at all — it may work for a year and then break when you upgrade the compiler or add an unrelated function.

The main sources in Rust:

  • Dereferencing a dangling or unaligned pointer
  • Producing two &mut to the same data (aliasing violation), even transiently
  • Data races
  • Reading uninitialised memory
  • Producing an invalid value for a type — a bool that is not 0 or 1, an enum with an out-of-range discriminant, a &T that is null
  • Breaking an invariant a safe API relies on (a String containing invalid UTF-8)

That fifth one is worth pausing on. unsafe code must uphold every type’s validity invariant, including the ones the compiler enforces elsewhere. A String with invalid UTF-8 constructed via from_utf8_unchecked is UB, and the crash will happen somewhere else entirely.

The point: safe abstractions

Here is the reframing that makes unsafe make sense.

Vec is implemented with raw pointers, manual allocation, and ptr::copy_nonoverlapping. String too. Rc, Arc, Mutex, HashMap, slice::split_at_mut — all unsafe inside.

And all of them are impossible to misuse from outside. That is the design:

Confine unsafe behind an API whose safe surface cannot be used incorrectly.

So “this crate contains unsafe” is not a red flag by itself. Any crate doing low-level work, FFI, or high-performance data structures will contain some. The questions worth asking are better ones:

  • Is the unsafe confined to a small, reviewed module?
  • Does each block have a // SAFETY: comment giving the argument?
  • Does the crate run under Miri in CI?
  • Is the safe API genuinely unmisusable, or does it merely document requirements?
// SAFETY: `idx < self.len` was checked on the line above, and `self.ptr`
// is valid for `self.len` elements by the type invariant.
unsafe { &*self.ptr.add(idx) }

That comment is the proof obligation, written down. Its absence in a review is a legitimate and specific thing to raise — far more useful than “why is there unsafe here?”

Beaconsee this → think this, without decoding it

#![forbid(unsafe_code)]

A crate-level promise that there is no unsafe anywhere in this crate. forbid (unlike deny) cannot be overridden by an inner allow.

Many web frameworks, parsers and utility crates carry it as a selling point. When you see it at the top of lib.rs, you can skip an entire category of review concern — though note it says nothing about the crate’s dependencies.

Edition 2024 changes

Two changes worth knowing, since they show up in newer code and in migration discussions.

The body of an unsafe fn is no longer implicitly an unsafe block. You now write explicit unsafe {} blocks inside it. The reasoning is good: an unsafe fn declares “callers must uphold preconditions”, which is a different statement from “every line in here may do unsafe things”. Conflating them hid real bugs.

// Edition 2024
unsafe fn thing(p: *const u8) -> u8 {
    // SAFETY: caller guarantees p is valid and aligned
    unsafe { *p }
}

extern blocks must be marked unsafe extern, and some attributes are now unsafe(...) — for example #[unsafe(no_mangle)]. Both make the danger visible at the declaration rather than implicit.

Miri

cargo +nightly miri test runs your tests in an interpreter that detects UB — out-of-bounds access, use-after-free, alignment violations, aliasing violations that ordinary testing will never surface.

It is slow, it cannot run FFI, and it does not cover every case. It is still by some distance the most useful tool for validating unsafe code, and serious crates run it in CI.

As a tech lead, this is the practical takeaway: if you are evaluating a dependency that contains meaningful unsafe, checking whether it runs Miri in CI tells you more than reading the code will.

Explain it to yourselfwrite it — thinking it does not count

A team member proposes using unsafe to avoid a .clone() in a hot path.

Write your response. It should: not reject the idea outright, name what would have to be true for it to be acceptable, and specify what you would require before merging.

What you should now be able to say

  • unsafe unlocks five abilities and disables nothing — the borrow checker still runs.
  • UB is not “might crash” — it means the program has no defined meaning, and the compiler optimises on the assumption it cannot happen.
  • The purpose of unsafe is to build safe abstractions. Vec and String are unsafe inside and unmisusable outside.
  • // SAFETY: comments are the written proof obligation.
  • Miri detects UB at run time; CI usage is a good dependency signal.
  • Edition 2024 requires explicit unsafe blocks inside unsafe fn.

Next: 5.2 — The idiom catalogue.

In the wildgo read the real thing now, while it is fresh
  • Vec::push — the unsafe underneathSearch for `pub fn push`. The safest, most-used container in Rust is built on raw pointers and manual allocation. Note the SAFETY comments — that is the standard this lesson describes.
  • MiriAn interpreter that detects undefined behaviour at run time. `cargo +nightly miri test`. If you ever review unsafe code, asking whether it runs under Miri is the single highest-value question you can ask.
Scheduled for recall4 items added to your review queue

These are not shown to you now on purpose. They will surface in Review tomorrow, mixed in among items from other parts of the course — not grouped by topic. Sorting out which idea applies is the part that transfers.