TL;DR:
unsafeis a keyword that unlocks five capabilities the borrow checker normally forbids: dereferencing raw pointers, calling unsafe functions, implementing unsafe traits, accessing mutable statics, and accessing union fields. Usingunsafedoesn't disable Rust's safety, it narrows the scope of your responsibility. The programmer must manually uphold the invariants the compiler would normally enforce. Well-written Rust wrapsunsafein safe abstractions so callers don't need to reason about it.
What Does unsafe Actually Allow?
unsafe unlocks exactly five things, nothing more. Everything else in Rust remains fully checked even inside unsafe blocks.
// The 5 unsafe superpowers:
// 1. Dereference raw pointers
let raw: *const i32 = &42;
unsafe { println!("{}", *raw); }
// 2. Call unsafe functions
unsafe fn dangerous() { /* ... */ }
unsafe { dangerous(); }
// 3. Implement unsafe traits
unsafe trait MyUnsafeTrait {}
unsafe impl MyUnsafeTrait for u32 {}
// 4. Access/modify mutable static variables
static mut COUNTER: u32 = 0;
unsafe { COUNTER += 1; }
// 5. Access fields of unions
union IntOrFloat { i: u32, f: f32 }
let u = IntOrFloat { i: 42 };
unsafe { println!("{}", u.i); }Ownership, lifetimes, and type checking still apply everywhere, including inside unsafe blocks.
What Are Raw Pointers?
Raw pointers (*const T and *mut T) are Rust's equivalent of C pointers, they bypass the borrow checker. They can be null, dangling, or unaligned. Dereferencing them requires unsafe.
fn main() {
let mut x = 5;
let r1 = &x as *const i32; // immutable raw pointer
let r2 = &mut x as *mut i32; // mutable raw pointer
// Creating raw pointers is safe; dereferencing is not
unsafe {
println!("r1 = {}", *r1);
*r2 = 10;
println!("r2 = {}", *r2);
}
// Null raw pointer (never do this without checking)
let null: *const i32 = std::ptr::null();
// unsafe { *null } // ← undefined behavior; segfault
}Raw pointers are mainly used when interfacing with C code (FFI), implementing data structures that require aliasing (like linked lists), or building safe abstractions the compiler can't verify.
How Do You Write a Safe Abstraction Over Unsafe?
The goal is to contain unsafe in a small, auditable block and expose a safe API. The unsafe code is your responsibility; callers get compile-time safety.
use std::slice;
// A safe wrapper around unsafe pointer arithmetic
pub fn split_at_midpoint(slice: &[i32]) -> (&[i32], &[i32]) {
let mid = slice.len() / 2;
let ptr = slice.as_ptr();
let len = slice.len();
// SAFETY: `mid` is always <= len, both halves are within bounds,
// and the lifetime is tied to the input slice.
unsafe {
(
slice::from_raw_parts(ptr, mid),
slice::from_raw_parts(ptr.add(mid), len - mid),
)
}
}
fn main() {
let v = vec![1, 2, 3, 4, 5];
let (left, right) = split_at_midpoint(&v);
println!("{:?} {:?}", left, right); // [1, 2] [3, 4, 5]
}The // SAFETY: comment is a convention, document exactly why your unsafe code is correct. This is what gets audited in security reviews.
When Should You Use unsafe?
Use unsafe for FFI, performance-critical data structures, or when the compiler's rules are too conservative for what you know is correct. Never use it to "make the borrow checker shut up."
Common legitimate uses:
- FFI, calling C libraries:
extern "C" { fn strlen(s: *const u8) -> usize; } - Intrinsics ; SIMD, hardware-specific operations
- Custom allocators,
GlobalAllocrequires unsafe - Kernel / embedded code, memory-mapped I/O, interrupt handlers
- Standard library internals,
Vec,HashMap,Arcare built on unsafe
When to avoid unsafe:
- Working around a lifetime error you don't understand
- Sharing data between threads without synchronization
- Bypassing bounds checks "for performance" (use
get_uncheckedonly after profiling and verification)
What Is an Unsafe Function vs an Unsafe Block?
An unsafe function (unsafe fn) signals that callers must uphold invariants before calling it. An unsafe block is where you actually invoke unsafe operations.
// unsafe fn; caller must ensure `ptr` is non-null and valid
unsafe fn read_value(ptr: *const i32) -> i32 {
*ptr // unsafe operation inside unsafe fn
}
fn safe_wrapper(val: &i32) -> i32 {
// SAFETY: `val` is a valid reference; always non-null and aligned
unsafe { read_value(val as *const i32) }
}Marking a function unsafe fn doesn't run unsafe code, it just signals a contract the caller must fulfill. The unsafe operations inside still need unsafe {} blocks.
Frequently Asked Questions
Not necessarily. unsafe means you've taken responsibility for upholding invariants the compiler can't verify. Well-audited unsafe code in the standard library (Vec, Arc) is used by millions of programs safely. The question is: can you prove your invariants hold?
In application code, rarely. In library/systems code, occasionally, usually in small, well-contained blocks. The entire Rust standard library has a relatively small surface area of unsafe code wrapping a large safe API.
Undefined behavior (UB) means the compiler makes no guarantees about what your program does, it may crash, corrupt memory, or appear to work while producing wrong results. Data races, null dereferences, and out-of-bounds pointer arithmetic are all UB. unsafe code must never cause UB.
Miri is an interpreter that detects undefined behavior in unsafe Rust code. Run your tests under Miri (cargo miri test) to catch alignment issues, use-after-free, and other UB that the compiler itself won't catch.
Sources
- The Rust Book ; Unsafe Rust
- The Rustonomicon: The dark arts of unsafe Rust
- Miri ; Rust UB detector
Related Glossary Terms
- Ownership: Unsafe bypasses ownership checks, you must enforce them manually
- Borrow Checker: The system
unsafetemporarily opts out of - Send/Sync: Manual
unsafe impl Send/Syncis one of the five unsafe superpowers - Pin: Pin projection without
pin-projectrequires unsafe code - PyO3: Language bindings like PyO3 rely on carefully audited unsafe internals
- Reference: Raw pointers and references are where many unsafe Rust soundness issues begin
Keep Reading
- Rust in the Linux Kernel: unsafe Rust in kernel driver code
- Rust Memory Safety: NSA and CISA: unsafe is the escape hatch; understanding its cost explains safe Rust's value
- Rust vs C++: C++ has no safe/unsafe distinction; this is Rust's key architectural advantage
