Unsafe Rust: what it allows and when to use it
What the unsafe keyword actually permits in Rust, raw pointers, unsafe functions, FFI, writing safe abstractions with SAFETY comments, and when it's justified.
Safe Rust guarantees no dangling pointers, no data races and no use-after-free. Some correct programs can't be proven safe by the compiler, though: talking to C libraries, writing a new data structure, using special CPU instructions. unsafe marks code where you take responsibility for upholding those guarantees.
What unsafe allows (and only this)
Inside an unsafe { } block you may:
- dereference a raw pointer (
*const T,*mut T), - call an
unsafe fn(including C functions via FFI), - read or write a
static mut, - implement an
unsafe trait(likeSendorSync), - access fields of a
union.
That's the complete list. unsafe does not turn off the borrow checker, type checking or bounds checks on normal code. Creating a raw pointer is safe; only using it needs unsafe:
fn main() {
let x = 5;
let p = &x as *const i32;
println!("{}", *p);
}
error[E0133]: dereference of raw pointer is unsafe and requires unsafe block
Undefined behavior
If unsafe code breaks Rust's rules (a dangling or misaligned pointer, two &mut to the same data, an invalid bool or char value, a data race), the program has undefined behavior: the compiler's optimizations assume it can't happen, and anything may result, including bugs that appear far away and only in release builds.
Safe abstractions
The idiomatic use of unsafe is a small block inside a function with a safe signature, where the function's own checks guarantee the unsafe operations are valid. The standard library is full of these: Vec, String and split_at_mut are built on unsafe code that you call safely.
Every unsafe block should have a // SAFETY: comment explaining why it's sound, and every unsafe fn a # Safety doc section listing what callers must guarantee. Clippy can require these (undocumented_unsafe_blocks).
FFI: calling C
Functions from other languages are declared in an extern block. In the 2024 edition the block itself is written unsafe extern "C", because declaring a foreign signature wrongly is itself unsound. Calls to the functions need an unsafe block, since the compiler can't check C code. Libraries usually wrap these calls in safe Rust functions (the -sys crate plus a safe wrapper crate pattern).
When unsafe is justified at work
- FFI: calling C libraries or the OS, or exposing Rust to other languages.
- Performance, only after profiling shows a real need, and after trying safe options (iterators,
chunks_exact, slices instead of indexing) that remove bounds checks anyway. - Low-level data structures that need raw pointers (intrusive lists, lock-free queues, custom allocators).
Otherwise, prefer existing well-reviewed crates. Many companies require extra review for any new unsafe, and #![forbid(unsafe_code)] at the crate root lets a project ban it outright.
Tools
Miri (cargo +nightly miri test) runs your tests in an interpreter that detects many kinds of undefined behavior. Sanitizers (AddressSanitizer, ThreadSanitizer) help with FFI-heavy code.
Common mistakes
static mutfor global state. Use atomics,Mutex, orOnceLock/LazyLock; the 2024 edition rejects references tostatic mutby default (static_mut_refs).- Large unsafe blocks. Keep them to the exact operations that need it, so reviewers know where to look.
- "It works in my test" as proof. Undefined behavior can pass every test. Reason about the rules, and run Miri.
Example
/// Splits a slice into two mutable halves at `mid`, like the standard `split_at_mut`.
fn split_halves(values: &mut [i32], mid: usize) -> (&mut [i32], &mut [i32]) {
let len = values.len();
assert!(mid <= len, "mid {mid} out of range for length {len}");
let ptr = values.as_mut_ptr();
// SAFETY: mid <= len was checked above, so both ranges are inside the slice,
// and [0, mid) and [mid, len) don't overlap, so the two &mut never alias.
unsafe { (std::slice::from_raw_parts_mut(ptr, mid), std::slice::from_raw_parts_mut(ptr.add(mid), len - mid)) }
}
unsafe extern "C" {
// The C standard library's abs(), linked into every Rust program on common platforms.
fn abs(input: i32) -> i32;
}
fn c_abs(x: i32) -> i32 {
// SAFETY: abs has no preconditions for values other than i32::MIN.
if x == i32::MIN { i32::MAX } else { unsafe { abs(x) } }
}
fn main() {
let mut scores = [10, 20, 30, 40, 50];
let (left, right) = split_halves(&mut scores, 2);
left[0] += 1;
right[0] += 100;
println!("{scores:?}");
let x = 42;
let p = &x as *const i32; // creating a raw pointer is safe
// SAFETY: p points to x, which is alive and initialized.
let value = unsafe { *p };
println!("through raw pointer: {value}");
println!("abs from C: {} {}", c_abs(-17), c_abs(5));
}
Output:
[11, 20, 130, 40, 50]
through raw pointer: 42
abs from C: 17 5