Skip to content

Expert · Testing, tooling and unsafe Rust · topic 42 of 50 · 30 min

Unsafe Rust: what it is and is not

Explanation

unsafe marks code where you, not the compiler, promise that memory rules hold. It unlocks five extra abilities, such as dereferencing raw pointers and calling C functions, but it does NOT turn off the borrow checker or type checks. Good Rust keeps unsafe tiny, explains each block with a // SAFETY: comment, wraps it in a safe API, and tests it with tools like Miri.

Why does unsafe exist?

The compiler's checks are conservative: they reject some programs that are actually correct, because it cannot prove them safe. And some jobs are impossible without lower-level access: talking to hardware, calling C libraries, or building fast data structures like Vec itself. unsafe is the escape hatch for these cases. Most Rust programmers rarely write it, but every Rust programmer benefits from understanding it.

The five extra abilities

Inside an unsafe block or unsafe function you may do exactly five extra things:

  1. Dereference a raw pointer (*const T or *mut T).
  2. Call an unsafe function, including functions from C through FFI.
  3. Access or modify a mutable static variable.
  4. Implement an unsafe trait (such as Send or Sync by hand).
  5. Access the fields of a union (a C-style type that stores several kinds of data in the same memory).
Careful: unsafe does NOT turn off the borrow checker, type checking or bounds checks on normal indexing. References inside unsafe still follow the borrowing rules. It only allows the five actions above, and with them, the responsibility for not causing undefined behaviour.

Undefined behaviour (UB) means the program breaks a rule the compiler relies on, such as reading freed memory or creating two &mut to the same value. After UB, anything can happen: crashes, wrong results, or security holes, sometimes only in release builds. The whole point of safe Rust is that UB is impossible; inside unsafe, avoiding it is your job.

Raw pointers

rust
let mut x = 10;
let p1 = &x as *const i32;     // creating raw pointers is safe
let p2 = &mut x as *mut i32;

unsafe {
    *p2 += 5;                   // dereferencing them is not
    println!("{}", *p1);        // 15
}
Raw pointers may be null or dangling and are not tracked by the borrow checker; that is why reading through them needs unsafe.

Safe abstractions

The key idea: wrap a small unsafe block inside a function whose public API is safe, and make sure no caller can trigger UB through it. The standard library does this everywhere. For example, slice::split_at_mut returns two non-overlapping &mut halves of one slice; the borrow checker cannot see that they do not overlap, so the implementation uses unsafe internally, but callers use it safely.

rust
/// Returns the item at index i without bounds checks.
///
/// # Safety
/// The caller must ensure i < v.len().
unsafe fn get_fast(v: &[u32], i: usize) -> u32 {
    // SAFETY: the caller promised i is in bounds.
    unsafe { *v.get_unchecked(i) }
}

fn sum_first_three(v: &[u32]) -> u32 {
    assert!(v.len() >= 3);
    // SAFETY: we just checked that indexes 0, 1 and 2 exist.
    unsafe { get_fast(v, 0) + get_fast(v, 1) + get_fast(v, 2) }
}
Document the rules in a # Safety section and justify every unsafe block with a // SAFETY: comment. sum_first_three is a safe function: it checks before calling.

FFI: calling C code

FFI (foreign function interface) lets Rust call functions written in C, and C call Rust. The compiler cannot check C code, so calls are unsafe. In the 2024 edition, extern blocks themselves must be written unsafe extern, to make clear that the declarations are promises you are making.

rust
unsafe extern "C" {
    fn abs(input: i32) -> i32;     // from the C standard library
}

fn main() {
    let n = unsafe { abs(-7) };
    println!("{n}");               // 7
}
Real projects usually generate bindings with tools such as bindgen and wrap them in a safe Rust API.

Tools and habits

  • Keep unsafe blocks as small as possible, ideally a line or two.
  • Write a // SAFETY: comment above each block explaining why it is sound.
  • Run tests under Miri (cargo +nightly miri test), an interpreter that detects many kinds of undefined behaviour.
  • Prefer a well-tested crate that already wraps the unsafe code.
  • Some teams forbid unsafe in application code with #![forbid(unsafe_code)] at the top of the crate.

Common misunderstandings

  • "unsafe means the code is dangerous." It means the compiler cannot check it; it can still be correct.
  • "unsafe turns off the borrow checker." It does not.
  • "Writing unsafe makes code faster." Rarely; safe Rust is usually just as fast. Measure first.
  • "Using a crate with unsafe inside makes my code unsafe." Not if the crate's safe API is sound, as with std.

Need help?

Stuck on a word or an idea? Search this course, or every course, and jump straight to the explanation or a worked question.

Try:

Watch the lesson

Unsafe Rust: what it is and is not · 1/7

unsafe = you promise

unsafe marks code where you, not the compiler, promise the memory rules hold.

Prefer a video by a teacher? Find free videos on “unsafe rust explained raw pointers ffi safety” on YouTube ↗

In real life: Reusing a C library for image compression

A photo-sharing app's backend needs a fast, mature C library for image compression. The team writes a small Rust crate that declares the C functions in an unsafe extern block and wraps them in safe functions taking &[u8] and returning Result<Vec<u8>, Error>. All the unsafe code lives in one file with SAFETY comments; the rest of the backend only sees the safe API.

rust
// The pattern: a tiny unsafe core behind a safe function.
fn checked_abs(n: i32) -> Option<i32> {
    if n == i32::MIN {
        return None;            // abs(i32::MIN) overflows in C: guard first
    }
    // SAFETY: abs is a pure C function; the overflow case is excluded above.
    Some(unsafe { abs(n) })
}

unsafe extern "C" {
    fn abs(input: i32) -> i32;
}

fn main() {
    println!("{:?} {:?}", checked_abs(-42), checked_abs(i32::MIN));   // Some(42) None
}
The safe wrapper checks the input so no caller can trigger the bad case.

Practice (0/3 done)

Task 1 · Question

Which of these does unsafe allow?

Task 2 · Question

What should be written above every unsafe block in good Rust code?

Task 3 · Practice

Read the documentation of slice::split_at_mut in the standard library, including its source (there is a "source" link on doc.rust-lang.org). Write 5 to 8 lines explaining: why the borrow checker cannot accept the safe version, what the unsafe code relies on, and why callers are still safe.

Quick quiz

Question 1

1.Does unsafe turn off the borrow checker?

Question 2

2.What is undefined behaviour?

Question 3

3.What is a "safe abstraction"?

Question 4

4.Which tool runs your tests and detects many kinds of undefined behaviour?

Question 5

5.How must extern blocks be written in the 2024 edition?

Read more (free)

Open a question to see the answer and why. The first ones are this topic's own quiz, so try the quiz above before you peek.

Does unsafe turn off the borrow checker?

Answer: No; it only allows five specific extra actions

References are still checked inside unsafe.

What is undefined behaviour?

Answer: Breaking a rule the compiler relies on, after which anything can happen

UB can cause crashes, wrong results or security holes.

What is a "safe abstraction"?

Answer: A safe public API that wraps a small, carefully checked unsafe core

std is full of safe abstractions over unsafe internals.

Which tool runs your tests and detects many kinds of undefined behaviour?

Answer: Miri

Miri interprets the program and checks memory rules.

How must extern blocks be written in the 2024 edition?

Answer: unsafe extern "C" { }

Edition 2024 requires the unsafe keyword on extern blocks.

Why does Rust forbid a &mut while & references exist?Related

Answer: So readers never see the value change under them (no data races)

Exclusive access for writers prevents data races and similar bugs.

From References and borrowing

Why is overwriting the return address dangerous?Related

Answer: It can redirect execution to an attacker's choice

Classic stack smashing.

From Buffer overflows and safe string handling

How does Rust decide when to free heap memory?Related

Answer: Ownership rules let the compiler insert the free at the right point

Rust frees a value when its owner goes out of scope, decided at compile time.

From Memory 101: the stack and the heap

What does the C standard require when undefined behaviour occurs?Related

Answer: Nothing at all

No requirements: anything may happen.

From Undefined behaviour

Pass the quiz (60% or more) or mark this topic as done when you are ready.

Your progress is saved on this device only.

Next steps

  1. This is the last topic of “Testing, tooling and unsafe Rust”. The module exam comes next.
  2. Module 10 exam: Testing, tooling and unsafe Rust · 12 questions · 25 min, without help. 4 topics of this module still to finish first.
  3. Then the next module, Real projects: CLI, web and WebAssembly. 50 topics left in the course before the final exam.

← Previous: Tooling: rustfmt, Clippy, docs and editions