Why this matters
Everything so far has been safe Rust: the compiler proves that your program has no dangling pointers, no data races, no out-of-bounds accesses. Some things cannot be proven by the compiler: talking to hardware, calling C libraries, implementing the very collections (Vec, String) that safe code relies on. For those, Rust has unsafe: a way to say "I have checked this myself". Understanding what unsafe does (and does not) do lets you read the standard library, call C code, and recognize when a crate's use of unsafe deserves scrutiny.
Undefined behavior
The reason unsafe exists, and the reason to be careful with it, is undefined behavior (UB). Some operations, such as reading through a dangling pointer, creating two &mut to the same value, reading uninitialized memory, or a data race, are declared by the language to have no defined meaning. The compiler optimizes on the assumption that they never happen. If they do, the program may crash, silently compute nonsense, or appear to work until a compiler upgrade changes the optimization. UB is not "it might crash": it means anything can happen, including the program behaving differently from what the source code says.
Safe Rust guarantees no UB. unsafe code can cause it, and the programmer must prevent it.
The five unsafe superpowers
An unsafe block lets you do five things the compiler otherwise forbids:
- Dereference a raw pointer.
- Call an
unsafefunction (including functions from other languages via FFI). - Access or modify a mutable
staticvariable. - Implement an
unsafetrait (likeSendorSyncby hand). - Access fields of a
union.
That is the complete list. Type checking, ownership, and borrow checking of ordinary references all still apply inside unsafe. The keyword does not turn off the compiler; it marks the places where you are responsible for upholding rules the compiler cannot verify. If a Rust program has UB, the cause is in (or reached from) an unsafe block, which tells you where to look.
Raw pointers
Raw pointers, *const T (read) and *mut T (write), are Rust's equivalent of C pointers. Unlike references, they:
- may be null, dangling, or unaligned,
- are not tracked by the borrow checker (several
*mutto one value may coexist), - do not free anything when dropped.
Creating a raw pointer is safe; only dereferencing one is unsafe:
fn main() {
let mut x = 10;
let p_const = &x as *const i32;
let p_mut = &mut x as *mut i32;
println!("addresses: {:p} {:p}", p_const, p_mut);
unsafe {
*p_mut += 5;
println!("value through p_const: {}", *p_const);
}
let dangling = 0x1234usize as *const i32;
println!("a pointer to nowhere is fine as long as nobody reads it: {:p}", dangling);
}
addresses: 0x7ffdccc1654c 0x7ffdccc1654c
value through p_const: 15
a pointer to nowhere is fine as long as nobody reads it: 0x1234
(Addresses vary.) The first lines are safe; the reads and writes need unsafe. Forgetting the block gives E0133:
fn main() {
let x = 5;
let p = &x as *const i32;
println!("{}", *p);
}
error[E0133]: dereference of raw pointer is unsafe and requires unsafe function or block
--> main.rs:4:20
|
4 | println!("{}", *p);
| ^^ dereference of raw pointer
|
= note: raw pointers may be null, dangling or unaligned; they can violate aliasing rules and cause data races: all of these are undefined behavior
Pointer arithmetic
ptr.add(n) moves a pointer forward by n elements (not bytes), just like p + n in C. It is unsafe because the result must stay inside the same allocation:
fn main() {
let arr = [10u16, 20, 30, 40];
let p = arr.as_ptr();
unsafe {
for i in 0..arr.len() {
let elem = p.add(i);
println!("element {i} at {:p} = {}", elem, *elem);
}
}
}
element 0 at 0x7ffc40fae560 = 10
element 1 at 0x7ffc40fae562 = 20
element 2 at 0x7ffc40fae564 = 30
element 3 at 0x7ffc40fae566 = 40
The addresses go up by 2 bytes each, the size of a u16. Going past the end (p.add(4) and reading it) would be UB: no bounds check protects you here.
unsafe functions and SAFETY comments
A function whose correct use depends on conditions the compiler cannot check is declared unsafe fn; calling it requires an unsafe block, which forces the caller to acknowledge the contract. Convention (enforced by lints in many projects):
- document the conditions in a
# Safetysection of the function's doc comment, - put a
// SAFETY:comment on everyunsafeblock explaining why the conditions hold.
/// Returns the element at `index` without bounds checking.
///
/// # Safety
/// `index` must be less than `slice.len()`.
unsafe fn get_unchecked_demo(slice: &[i32], index: usize) -> i32 {
unsafe { *slice.as_ptr().add(index) }
}
fn main() {
let v = [5, 6, 7];
for i in 0..v.len() {
// SAFETY: i ranges over 0..v.len(), so it is in bounds.
let x = unsafe { get_unchecked_demo(&v, i) };
print!("{x} ");
}
println!();
}
5 6 7
The standard library has exactly such functions (get_unchecked, from_utf8_unchecked): faster, because the caller promises what the checked version would verify.
Mutable statics
A static mut is a global variable that can be changed. Any thread could access it at any time, so every access is unsafe, and in practice you should use an atomic or a Mutex in a normal static instead:
use std::sync::atomic::{AtomicU32, Ordering};
static CALLS: AtomicU32 = AtomicU32::new(0);
fn tracked() {
CALLS.fetch_add(1, Ordering::Relaxed);
}
fn main() {
tracked();
tracked();
println!("called {} times", CALLS.load(Ordering::Relaxed));
}
called 2 times
Safe code, no unsafe needed: prefer it.
Common mistakes
- Treating
unsafeas an escape hatch for borrow errors: it does not disable borrow checking for references, and converting references to raw pointers to dodge the rules usually creates UB (two live&mutto one value). - Large unsafe blocks: keep them minimal, so the part that needs auditing is small.
- No SAFETY comment: future readers (including you) cannot verify the reasoning.
- Assuming "it worked when I ran it" means no UB: UB can stay invisible until a different optimization level or compiler version. Tools like Miri (
cargo miri) detect many kinds of UB at run time.
Recap
- Undefined behavior means the program has no defined meaning; safe Rust rules it out.
unsafeenables five operations: deref raw pointers, call unsafe functions, use mutable statics, implement unsafe traits, access unions. Everything else is still checked.- Raw pointers
*const T/*mut Tmay be null or dangling; creating them is safe, dereferencing is unsafe. ptr.add(n)moves by elements and must stay within the allocation.unsafe fnstates a contract; document it under# Safetyand justify each block with// SAFETY:.
Going further: read the Rust Book chapter 20.1 ("Unsafe Rust") and skim the Rustonomicon (doc.rust-lang.org/nomicon), the official guide to writing unsafe code.