Skip to content
Free public beta until December 4, 2026: every course is open, no card needed. After that, the first chapter of each course stays free and full access is a one-time $39 ($19 for up to 100 founding members chosen for helpful feedback). Your progress is kept. How the beta works

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 # Safety section and show that each condition holds.
  • Implement an unsafe trait (Send, Sync by hand) → only when you can prove the type really is safe to share or send.
  • Touch a static mut or read a union field → almost never: an atomic, Mutex, OnceLock or 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

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.

What happens if the promise is wrong, and why it can look fine for years.

The five powers

Creating one is safe; reading through one is not.

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.

The guarantee that no safe code can use an API to cause undefined behavior.

Example

A safe 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

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.