unsafe
Goal of This Episode
Understand what unsafe means, what it lets you do, and what to watch out for when writing unsafe code.
Concept
Why unsafe Exists
Rust’s safety guarantees rest on certain assumptions — for example, that a &mut T is always exclusive, or that a reference always points to valid data. The compiler checks those assumptions for you.
But some operations are impossible for the compiler to verify. Rust doesn’t forbid you from doing them — it asks you to explicitly say “I take responsibility for this part.” That’s unsafe.
Rust’s Safety Guarantees
Safe Rust guarantees the following things can never happen, no matter how your code is written:
- No access to memory that has already been freed.
- No data races (multiple
Threads reading and writing simultaneously with at least one writer). - No dangling references.
- No value getting
dropped twice. - No reads of uninitialized memory.
- No type confusion (e.g. reading the bytes of an
isizeas a pointer).
Violate any one of these and you have undefined behavior (UB for short). It is far worse than “the result is unpredictable”: the compiler optimizes on the assumption that these things never happen, so once one does, it isn’t only that line that breaks — the whole program loses every guarantee, and it may compute nonsense, crash, or go wrong somewhere entirely unrelated.
The responsibility of unsafe code is this: even while bypassing the compiler’s checks, it must ensure that all of these guarantees still hold.
unsafe Blocks
Wrap code needing unsafe operations in unsafe { }. unsafe is not “turn off all checks” — borrowing rules and type checking still apply inside an unsafe block. It merely unlocks a few specific extra operations.
The Five unsafe Operations
- Dereferencing raw pointers (
*const T,*mut T) - Calling
unsafefunctions - Manually implementing an
unsafe trait - Accessing
static mutvariables - Accessing a
union’s fields
Raw Pointers
A raw pointer is a pointer with no borrow-rule protection. Creating one doesn’t require unsafe; using it (dereferencing) does:
fn main() {
let x = 42;
let ptr: *const i32 = &raw const x; // creating: no unsafe needed
let value = unsafe { *ptr }; // dereferencing: unsafe needed
println!("{}", value); // 42
}
You can also convert a reference into a raw pointer with as:
fn main() {
let x = 42;
let ptr = &x as *const i32; // &i32 to *const i32
}
But &raw const x and &raw mut x are better — they take a raw pointer straight from the variable without creating a reference first. Sometimes merely creating the reference can itself break the rules (e.g. taking & of uninitialized memory); &raw sidesteps that problem.
unsafe fn
If a function’s safety must be guaranteed by its caller, mark it unsafe fn:
unsafe fn dangerous(ptr: *const i32) -> i32 {
unsafe { *ptr }
}
fn main() {
let x = 42;
let value = unsafe { dangerous(&raw const x) };
}
Note: since the Rust 2024 edition, unsafe operations require an unsafe { } block even inside an unsafe fn — so every unsafe operation is explicitly marked.
unsafe trait
Some traits can only be implemented correctly by satisfying conditions the compiler can’t check automatically:
unsafe trait MyGuarantee {
fn check(&self) -> bool;
}
unsafe impl MyGuarantee for i32 {
fn check(&self) -> bool { *self >= 0 }
}
fn main() {}
An unsafe trait means: “implementing this trait requires satisfying conditions the compiler cannot check.” You implement it with unsafe impl, signaling that you guarantee those conditions hold.
Send and Sync are unsafe traits — the compiler implements them automatically when appropriate, but if you implement them manually, you must guarantee Thread safety yourself.
Note: calling an unsafe trait’s methods doesn’t require unsafe — the danger lies in the implementation, not the use.
The unsafe Boundary
unsafe code must guarantee: no matter what safe code calls it, it can never cause undefined behavior.
Take the standard library’s Vec: it uses unsafe internally to manage memory, but exposes a safe API. No matter how you use Vec’s safe API, you cannot trigger undefined behavior.
Guidelines for Writing unsafe Code
- Keep
unsafeblocks as small as possible — wrap only the lines that truly need it. - Write
// SAFETY:comments — explain why thisunsafeoperation is correct. - Mind the borrowing rules — even with raw pointers, rules like “
&mutmust be exclusive” still hold semantically. - Uphold type invariants — e.g. a
Stringis always valid UTF-8; aboolis always 0 or 1. - Consider panic safety — if the
unsafeblock contains operations that can panic, make sure the data structure remains valid after a panic. - Test with Miri —
cargo +nightly miri testcan detect manyunsafeproblems.
Common Uses
- Implementing data structures (linked lists, the internals of
Vec) - Interoperating with C
- Performance-critical sections
Example Code
fn main() {
// raw pointers
let mut x = 42;
let ptr_const: *const i32 = &raw const x;
let ptr_mut: *mut i32 = &raw mut x;
unsafe {
println!("read: {}", *ptr_const);
*ptr_mut = 100;
println!("after write: {}", *ptr_mut);
}
// unsafe fn
unsafe fn add_one(ptr: *mut i32) {
unsafe { *ptr += 1; }
}
let mut val = 10;
// SAFETY: ptr points to a valid, initialized i32, and no other references exist
unsafe { add_one(&raw mut val); }
println!("val = {}", val);
}
Recap
unsafelets you do things the compiler can’t verify, but it doesn’t turn off all checks.- The five
unsafeoperations: dereferencing raw pointers, callingunsafe fns, implementingunsafe traits, accessingstatic mut, accessingunionfields. - Raw pointers
*const T/*mut T: pointers without borrow-rule protection, not guaranteed to point to valid data. Creating them needs nounsafe; dereferencing does. &raw const x/&raw mut x: take a raw pointer directly, without going through a reference.- Since the 2024 edition,
unsafe fnbodies also requireunsafe { }blocks. - With
unsafe traits the danger is in implementing, not using (calling methods needs nounsafe). - Undefined behavior (UB): violating safe Rust’s guarantees. The compiler optimizes on the assumption that it never happens, so once it does, the whole program loses its guarantees.
- The
unsafeboundary: no matter what safe code calls it, it must never cause undefined behavior.