| |

Rust 90 🦀 Unsafe Rust Capabilities and the unsafe Keyword

⏱️ Estimated read: 16 min | |

unsafe is a promise, not a permission. The keyword tells the compiler that the programmer has verified a set of conditions that the compiler cannot verify, and the compiler responds by allowing a small set of operations it otherwise forbids. It does not disable the borrow checker, it does not turn off type checking, and it does not make the code faster by itself. It unlocks five specific capabilities—dereferencing raw pointers, calling unsafe functions, accessing mutable statics, implementing unsafe traits, and accessing union fields—and the programmer assumes responsibility for the invariants that make those capabilities sound . Every safe function that calls an unsafe block is a contract: the caller of the safe function does not need to uphold anything, because the safe function’s implementation has already upheld the unsafe preconditions.

The distinction between safety and soundness is the foundation of every unsafe code review. A function is safe if it cannot cause undefined behavior when called from safe code. A function is sound if it maintains that guarantee for every possible input. An unsafe block that dereferences a null pointer is unsafe and unsound. An unsafe block that dereferences a pointer checked for null immediately before is unsafe but sound. The compiler cannot tell the difference; the programmer must. The unsafe keyword is the mechanism for marking the boundary where the programmer takes over, and the documentation of the safety invariants is the mechanism for making that boundary reviewable.

This chapter covers the five unsafe capabilities and the operations they unlock, the safety invariants that make each capability sound, the unsafe fn versus unsafe block distinction and the unsafe_op_in_unsafe_fn lint, the SAFETY comment convention that makes invariants reviewable, the common soundness bugs that unsafe code introduces, and the tools—Miri, cargo miri, and the unsafe code guidelines—that verify the invariants the compiler cannot. It assumes familiarity with Rust’s ownership model and references.

Key point: unsafe unlocks five capabilities and nothing else. It does not disable the borrow checker, the type checker, or the trait system. Every unsafe block must document the invariants it assumes, and every unsafe fn must document the preconditions its callers must uphold. The compiler trusts the programmer; the programmer must earn that trust with reasoning that a reviewer can verify.


Why unsafe exists as a scoped escape hatch

The compiler cannot verify every invariant. A Vec‘s internal buffer is a raw pointer, and the compiler cannot prove that the pointer is valid, aligned, and points to initialized memory of the correct length. The Vec implementation uses unsafe to dereference the pointer, and it maintains the invariant through the safe API: push reallocates when the capacity is exceeded, pop decrements the length, and Drop deallocates the buffer. The unsafe block is small, the invariants are documented, and the safe API is the contract that callers rely on.

Safe abstractions need unsafe implementations. Every collection, every smart pointer, every thread synchronization primitive, and every FFI boundary in the standard library is built on unsafe code. The Rc type uses Cell for its reference count, which is an unsafe interior mutability primitive. The Mutex uses a platform-specific lock primitive, which is an unsafe FFI call. The safe API is what makes these types usable without the caller needing to understand the underlying unsafe code.

The alternative is a slower, more constrained language. A language without unsafe would either forbid low-level operations entirely or require runtime checks for every pointer dereference. Rust’s approach is to allow the operations in a scoped block, require the programmer to document the invariants, and use the type system to ensure that safe code cannot violate them. The unsafe keyword is the seam between the two worlds.

The trade-off. Unsafe code is harder to review, harder to test, and harder to maintain than safe code. A bug in a safe function produces a panic or a wrong result; a bug in an unsafe function produces undefined behavior, which may manifest as a wrong result, a crash, or a silent memory corruption that only appears in production. The discipline is to minimize the unsafe surface, isolate it behind a safe API, and verify it with Miri and careful review.


a. The five unsafe capabilities

The Rust Reference enumerates the operations that require unsafe. There are exactly five, and they are the entire scope of the keyword .

Dereferencing a raw pointer. A *const T or *mut T can be created in safe code, but dereferencing it requires unsafe. The pointer may be null, dangling, unaligned, or pointing to uninitialized memory. The unsafe block asserts that the pointer is valid for the access it is used for.

// Creating a raw pointer is safe.
let x = 42;
let ptr = &x as *const i32;

// Dereferencing requires unsafe.
let value = unsafe { *ptr };
assert_eq!(value, 42);

Calling an unsafe function or method. An unsafe fn has preconditions that the caller must uphold. The unsafe block at the call site is the caller’s assertion that the preconditions are met. The std::slice::from_raw_parts function is the canonical example: it takes a pointer and a length, and the caller must guarantee that the pointer is valid, aligned, and points to len consecutive initialized values .

let slice = unsafe { std::slice::from_raw_parts(ptr, len) };

Accessing or modifying a mutable static variable. A static mut is a global mutable variable with no synchronization. Reading or writing it from multiple threads is a data race. Rust 2024 edition marks static mut references as errors; the recommended replacements are AtomicT for primitives and Mutex<T> or RwLock<T> for other types .

static mut COUNTER: u32 = 0;

// Unsafe: no synchronization.
unsafe { COUNTER += 1; }

Implementing an unsafe trait. The Send and Sync traits are unsafe because the compiler cannot verify that a type is safe to send across threads or share between them. Implementing them manually is a promise that the type upholds the thread-safety invariants. The unsafe impl Send for MyType {} is the programmer’s assertion that the type’s fields are safe to move across threads.

Accessing a field of a union. A union can hold one of several types, and the compiler does not know which variant is active. Reading a field requires unsafe because the field may not be the one that was last written. Unions are primarily used for FFI and for low-level memory layout optimization.

union IntOrFloat {
    i: u32,
    f: f32,
}

let u = IntOrFloat { f: 1.0 };
let value = unsafe { u.i }; // Reading the wrong variant is UB.

b. unsafe fn versus unsafe block, and the SAFETY comment

An unsafe fn is a function with preconditions. Calling it requires an unsafe block, and the caller must uphold the preconditions. The function body itself is not automatically unsafe—since Rust 2024, the unsafe_op_in_unsafe_fn lint warns when an unsafe fn performs unsafe operations without an explicit unsafe block. The fix is to wrap the unsafe operations in a block and document the invariant .

// Unsafe function with documented precondition.
/// # Safety
/// `ptr` must be valid, aligned, and point to `len` initialized `u8` values.
unsafe fn read_bytes(ptr: *const u8, len: usize) -> Vec<u8> {
    // Explicit unsafe block inside the unsafe fn.
    unsafe {
        std::slice::from_raw_parts(ptr, len).to_vec()
    }
}

The SAFETY comment convention is the standard for documenting why an unsafe block is sound. The comment explains the invariant, references the documentation that establishes it, and is placed immediately above the unsafe block. The convention is used throughout the standard library and is enforced by code review in most Rust projects.

// SAFETY: The pointer was checked for null on the line above,
// and the length is the capacity of the allocation.
let slice = unsafe { std::slice::from_raw_parts(ptr, len) };

c. Soundness, Miri, and the common failure modes

Soundness is the property that safe code cannot cause undefined behavior. An unsafe block is sound if it upholds the invariants that make its operations defined. The common failure modes are the ones that Miri detects: out-of-bounds access, use-after-free, uninitialized memory reads, misaligned pointers, and data races .

Miri is an interpreter for Rust’s mid-level intermediate representation that detects undefined behavior in unsafe code. It is installed with rustup component add miri and run with cargo +nightly miri test. It catches the bugs that the compiler cannot: it tracks the provenance of every pointer, the initialization state of every byte, and the synchronization of every memory access . A test suite that passes under Miri is a strong signal that the unsafe code is sound, though it is not a proof—Miri explores one execution path at a time and cannot cover every possible input.

The common soundness bugs in unsafe code are:

  • Out-of-bounds access. A raw pointer dereference that goes past the end of the allocation. Miri catches this.
  • Use-after-free. A pointer that is used after the allocation it points to has been freed. Miri catches this.
  • Uninitialized memory. Reading a value that has not been initialized, especially a MaybeUninit that was not written. Miri catches this.
  • Misaligned pointer. Creating a reference from a pointer that is not aligned for the type. Miri catches this.
  • Data race. Two threads accessing the same memory without synchronization, at least one of them writing. Miri catches this.
  • Violating aliasing rules. A &mut T and a &T to the same memory existing simultaneously. Miri’s Stacked Borrows model catches this.

The unsafe keyword is not a performance optimization. It is a tool for expressing operations that the compiler cannot verify. The performance benefit comes from the operations themselves—raw pointer dereference is faster than bounds-checked indexing—not from the keyword. Code that is marked unsafe but performs only safe operations is a red flag in review: it suggests the programmer misunderstood what the keyword does.


Complete Example Session

// ============================================
// PART 1: RAW POINTER DEREFERENCE
// ============================================
fn raw_pointer_deref() {
    let x = 42;
    let ptr = &x as *const i32;

    // SAFETY: ptr points to a valid, initialized i32.
    let value = unsafe { *ptr };
    assert_eq!(value, 42);
}

// ============================================
// PART 2: UNSAFE FUNCTION WITH SAFETY DOC
// ============================================
/// # Safety
/// `ptr` must be valid for reads of `len` bytes.
unsafe fn slice_from_raw(ptr: *const u8, len: usize) -> &'static [u8] {
    // SAFETY: Caller guarantees ptr is valid for len bytes.
    unsafe { std::slice::from_raw_parts(ptr, len) }
}

// ============================================
// PART 3: UNSAFE TRAIT IMPLEMENTATION
// ============================================
struct MyWrapper(*mut u8);

// SAFETY: The wrapper is not shared across threads in practice,
// but this impl is intentionally unsound for demonstration.
// A real implementation would verify the field's thread-safety.
unsafe impl Send for MyWrapper {}

// ============================================
// PART 4: MUTABLE STATIC
// ============================================
static mut COUNTER: u32 = 0;

fn increment_counter() {
    // SAFETY: Single-threaded test; no concurrent access.
    unsafe { COUNTER += 1; }
}

// Better: use AtomicU32 for thread-safe static.
use std::sync::atomic::{AtomicU32, Ordering};
static SAFE_COUNTER: AtomicU32 = AtomicU32::new(0);

fn increment_safe() {
    SAFE_COUNTER.fetch_add(1, Ordering::Relaxed);
}

// ============================================
// PART 5: UNION FIELD ACCESS
// ============================================
union FloatBits {
    f: f32,
    bits: u32,
}

fn float_to_bits(f: f32) -> u32 {
    let u = FloatBits { f };
    // SAFETY: The union was initialized with f, and u32
    // has no invalid bit patterns, so reading bits is defined.
    unsafe { u.bits }
}

// ============================================
// PART 6: UNSAFE OP IN UNSAFE FN (RUST 2024)
// ============================================
// Without unsafe_op_in_unsafe_fn, this compiles:
// unsafe fn bad(ptr: *const i32) -> i32 { *ptr }
//
// With the lint, it requires an explicit block:
unsafe fn good(ptr: *const i32) -> i32 {
    // SAFETY: Caller guarantees ptr is valid.
    unsafe { *ptr }
}

// ============================================
// PART 7: SAFETY COMMENT CONVENTION
// ============================================
// The comment explains WHY the unsafe block is sound.
// It references the precondition or the check.
//
// SAFETY: The pointer was validated by the preceding null check,
// and the allocation is still alive because the owner is in scope.
// let value = unsafe { *ptr };

// ============================================
// PART 8: MIRI
// ============================================
// Install: rustup component add miri
// Run:     cargo +nightly miri test
//
// Miri detects:
// - Out-of-bounds access
// - Use-after-free
// - Uninitialized memory
// - Misaligned pointers
// - Data races
// - Aliasing violations (Stacked Borrows)

// ============================================
// PART 9: COMMON SOUNDNESS BUGS
// ============================================
// 1. Deref after free: ptr points to dropped allocation.
// 2. Reading MaybeUninit without writing.
// 3. Creating &mut from a shared raw pointer.
// 4. Assuming alignment without checking.
// 5. Sending non-Send type across threads.
// 6. Reading the wrong union variant.

// ============================================
// PART 10: THE COMPLETE FLOW
// ============================================
// 1. unsafe unlocks 5 capabilities.
// 2. Each requires an invariant.
// 3. SAFETY comment documents the invariant.
// 4. unsafe fn documents preconditions for callers.
// 5. Miri verifies the invariants at runtime.
// 6. Minimize unsafe surface; isolate behind safe API.

Quick Reference

The Five Unsafe Capabilities

CapabilityOperationInvariant
Raw pointer deref*ptrPointer valid, aligned, initialized
Unsafe fn callfoo()Preconditions documented
Mutable staticstatic mut XNo concurrent access
Unsafe trait implunsafe impl SendThread-safety guarantees
Union field accessu.fieldActive variant is the field’s type

unsafe fn vs unsafe block

ConstructMeaningCaller Responsibility
unsafe fnPreconditions existCaller must uphold
unsafe { }Invariants verified by authorNone; author verified

Safety Documentation

ConventionLocationPurpose
# Safety doc sectionOn unsafe fnPreconditions for callers
// SAFETY: commentAbove unsafe { }Why the block is sound

Miri

CommandPurpose
rustup component add miriInstall Miri
cargo +nightly miri testRun tests under Miri
MIRIFLAGS="-Zmiri-strict-provenance"Stricter checks

Common Soundness Bugs

BugMiri DetectsFix
Use-after-freeYesCheck lifetime
Out-of-boundsYesCheck length
Uninitialized readYesInitialize with MaybeUninit
Misaligned pointerYesCheck alignment
Data raceYesSynchronize with atomics/mutex
Aliasing violationYesFollow Stacked Borrows

Best Practices

✅ Do This:

// Document the invariant above every unsafe block.           // ✅
// SAFETY: ptr is non-null and valid for len bytes.
let s = unsafe { std::slice::from_raw_parts(ptr, len) };

// Document preconditions on unsafe fn.                       // ✅
/// # Safety
/// `ptr` must be valid for `len` bytes.
unsafe fn read(ptr: *const u8, len: usize) -> &[u8] { ... }

// Wrap unsafe operations in explicit blocks.                 // ✅
unsafe { *ptr }

// Use atomics instead of static mut.                         // ✅
static COUNTER: AtomicU32 = AtomicU32::new(0);

// Run Miri on the test suite.                                // ✅
cargo +nightly miri test

❌ Don’t Do This:

// Assume unsafe makes code faster.                           // ❌
// It unlocks capabilities; performance is the operation's.

// Use unsafe without a SAFETY comment.                       // ❌
// Reviewers cannot verify the invariant.

// Implement Send for a type with raw pointers.               // ❌
// Unless you have proven the fields are thread-safe.

// Access static mut from multiple threads.                   // ❌
// Data race; use Mutex or atomic.

// Read a MaybeUninit without writing first.                  // ❌
// Undefined behavior; Miri catches it.

Common Pitfalls

PitfallWhy It HappensFix
Use-after-freePointer outlives allocationCheck lifetimes; use PhantomData
Data race on static mutNo synchronizationUse AtomicT or Mutex
Aliasing violation&mut and & coexistFollow Stacked Borrows
Miri passes but code is unsoundMiri explores one pathReview invariants manually
unsafe impl Send without proofType has non-Send fieldVerify every field is Send

Real-World Examples

1. Raw Pointer Deref

let ptr = &x as *const i32;
let value = unsafe { *ptr };

2. Slice from Raw Parts

let slice = unsafe { std::slice::from_raw_parts(ptr, len) };

3. Unsafe fn with Safety Doc

/// # Safety
/// `ptr` must be valid for `len` bytes.
unsafe fn read(ptr: *const u8, len: usize) -> &[u8] { ... }

4. SAFETY Comment

// SAFETY: ptr was checked for null on the previous line.
let value = unsafe { *ptr };

5. Atomic Static

static COUNTER: AtomicU32 = AtomicU32::new(0);

6. Unsafe Trait Impl

unsafe impl Send for MyType {}

7. Union Field Access

let bits = unsafe { u.bits };

8. Miri Command

cargo +nightly miri test

9. unsafe_op_in_unsafe_fn Block

unsafe fn read(ptr: *const i32) -> i32 { unsafe { *ptr } }

10. MaybeUninit Initialization

let mut buf: MaybeUninit<[u8; 1024]> = MaybeUninit::uninit();
unsafe { buf.as_mut_ptr().write([0u8; 1024]) };

Visual

The Five Capabilities

┌──────────────────────────────────────────────┐
│  unsafe { } UNLOCKS                          │
│                                              │
│  1. Dereference raw pointer                  │
│  2. Call unsafe fn                           │
│  3. Access mutable static                    │
│  4. Implement unsafe trait                   │
│  5. Access union field                       │
│                                              │
│  Does NOT:                                   │
│  - Disable borrow checker                    │
│  - Disable type checking                     │
│  - Disable trait system                      │
│  - Make code faster by itself                │
│                                              │
└──────────────────────────────────────────────┘

Safe vs Unsafe Boundary

┌──────────────────────────────────────────────┐
│  SAFE API                                    │
│  fn push(&mut self, value: T)                │
│       │                                      │
│       │ upholds invariants                   │
│       ▼                                      │
│  UNSAFE BLOCK                                │
│    write to raw buffer                       │
│    increment length                          │
│       │                                      │
│       │ invariant: buffer valid,             │
│       │ length <= capacity                   │
│       ▼                                      │
│  SAFE GUARANTEE                              │
│    caller cannot trigger UB                  │
│                                              │
│  The safe API is the contract.               │
│  The unsafe block is the implementation.     │
│                                              │
└──────────────────────────────────────────────┘

Miri Detection

┌──────────────────────────────────────────────┐
│  MIRI                                        │
│                                              │
│  Interprets MIR (mid-level IR).              │
│  Tracks:                                     │
│  - Pointer provenance                        │
│  - Byte initialization                       │
│  - Memory access synchronization             │
│  - Aliasing (Stacked Borrows)                │
│                                              │
│  Catches:                                    │
│  - Out-of-bounds                             │
│  - Use-after-free                            │
│  - Uninitialized read                        │
│  - Misaligned pointer                        │
│  - Data race                                 │
│                                              │
│  Does not prove soundness.                   │
│  Explores one path per run.                  │
│                                              │
└──────────────────────────────────────────────┘

Soundness Decision

┌──────────────────────────────────────────────┐
│  IS THIS UNSAFE BLOCK SOUND?                 │
│                                              │
│  What invariant does the operation require?  │
│       │                                      │
│       ▼                                      │
│  Does the code establish that invariant?     │
│       │                                      │
│       ├── Yes → sound                        │
│       └── No  → unsound (UB)                 │
│                                              │
│  Can a caller of the safe API violate it?    │
│       │                                      │
│       ├── No  → API is sound                 │
│       └── Yes → API is unsound               │
│                                              │
│  Document the invariant in a SAFETY comment. │
│                                              │
└──────────────────────────────────────────────┘

Summary

ItemValue
unsafe capabilitiesFive: raw ptr deref, unsafe fn, static mut, unsafe trait, union
unsafe fnFunction with preconditions; callers must uphold
unsafe { }Block; author asserts invariants
Safety doc# Safety on unsafe fn, // SAFETY: above block
static mut replacementAtomicT, Mutex<T>, RwLock<T>
unsafe_op_in_unsafe_fnLint requiring explicit block in unsafe fn
MiriInterpreter that detects UB in unsafe code
SoundnessSafe code cannot cause UB
Stacked BorrowsAliasing model Miri enforces

Key takeaways:

  • unsafe unlocks exactly five capabilities. It does not disable the borrow checker, the type checker, or the trait system. It allows raw pointer dereference, unsafe function calls, mutable static access, unsafe trait implementations, and union field access—and nothing else .
  • unsafe fn is a precondition contract. The caller must uphold the preconditions documented in the # Safety section. The unsafe_op_in_unsafe_fn lint requires the function body to wrap its own unsafe operations in explicit blocks .
  • The SAFETY comment makes invariants reviewable. Every unsafe block must document why it is sound. A block without a SAFETY comment is a block that a reviewer cannot verify.
  • Soundness is the property that safe code cannot cause UB. An unsafe block is sound if it upholds the invariant of its operation. A safe API is sound if every possible input from safe code maintains the invariants of the unsafe implementation behind it.
  • Miri detects the bugs the compiler cannot. Out-of-bounds access, use-after-free, uninitialized reads, misaligned pointers, data races, and aliasing violations are all caught by running the test suite under cargo +nightly miri test .
  • static mut is a data race waiting to happen. The Rust 2024 edition marks references to static mut as errors. Use AtomicT for primitives and Mutex<T> or RwLock<T> for other types .
  • unsafe is not a performance optimization. The performance comes from the operations—raw pointer dereference is faster than bounds-checked indexing—not from the keyword. Code that is marked unsafe but performs only safe operations is a red flag in review.

Remember: unsafe is a contract between the programmer and the compiler. The compiler cannot verify the invariants of raw pointer dereference, unsafe function calls, mutable statics, unsafe trait implementations, or union field access, so it trusts the programmer to uphold them. The programmer earns that trust by documenting the invariant in a SAFETY comment, by keeping the unsafe surface small, and by verifying the code with Miri. The safe API is the contract that callers rely on; the unsafe block is the implementation that upholds it. A bug in safe code is a wrong result; a bug in unsafe code is undefined behavior, and undefined behavior is the one thing Rust promises never to produce from safe code.

Was this helpful?


Stop using slow, ad-bloated tool sites! 🤮

🔎 Search “KandZ Tools” on Google to use many professional utilities for free.

KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)

⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free

🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!