Expert · Testing, tooling and unsafe Rust · topic 42 of 50 · 30 min
Unsafe Rust: what it is and is not
Explanation
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:
- Dereference a raw pointer (*const T or *mut T).
- Call an unsafe function, including functions from C through FFI.
- Access or modify a mutable static variable.
- Implement an unsafe trait (such as Send or Sync by hand).
- Access the fields of a union (a C-style type that stores several kinds of data in the same memory).
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
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
}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.
/// 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) }
}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.
unsafe extern "C" {
fn abs(input: i32) -> i32; // from the C standard library
}
fn main() {
let n = unsafe { abs(-7) };
println!("{n}"); // 7
}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 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.
// 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
}Practice (0/3 done)
Which of these does unsafe allow?
What should be written above every unsafe block in good Rust code?
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
Read more (free)
Related help
Before this, learn
This topic builds on these. If something here feels new, open them first.
Related topics
- Integration tests and doc testsTesting, tooling and unsafe Rust · same module
- Unit testsTesting, tooling and unsafe Rust · same module
- Buffer overflows and safe string handlingC Programming (from zero to systems) · Strings in C
- Undefined behaviourC Programming (from zero to systems) · Bits, undefined behaviour and safety
- What Rust is and why learn itGetting started with Rust
- The borrow checker in practiceBorrowing, slices and lifetimes
Questions people ask about this
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.
Why is overwriting the return address dangerous?Related
Answer: It can redirect execution to an attacker's choice
Classic stack smashing.
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.
What does the C standard require when undefined behaviour occurs?Related
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
- This is the last topic of “Testing, tooling and unsafe Rust”. The module exam comes next.
- Module 10 exam: Testing, tooling and unsafe Rust · 12 questions · 25 min, without help. 4 topics of this module still to finish first.
- Then the next module, Real projects: CLI, web and WebAssembly. 50 topics left in the course before the final exam.