Unsafe Rust
Prerequisites: Procedural macros
The borrow checker is conservative: it rejects some programs that are
actually correct. unsafe is how you tell the compiler "I have checked this
myself". It does not turn off the borrow checker or type system; it
unlocks a small set of extra operations, and makes you responsible for
upholding the rules they could break.
The five unsafe superpowers
Inside an unsafe block you may:
- Dereference a raw pointer (
*const T,*mut T). - Call an
unsafe fn(including foreign functions). - Read or write a
static mut. - Implement an
unsafe trait(such asSendorSync). - Read fields of a
union.
Everything else — borrowing rules, bounds checks on normal indexing, type checking — still applies.
Raw pointers
Creating a raw pointer is safe; dereferencing one is not, because the compiler can no longer prove it points to valid, live, properly aligned memory.
fn main() {
let mut x = 10;
let p = &raw mut x; // a *mut i32; creating it is safe
// SAFETY: `p` points to `x`, which is live, aligned and not otherwise
// borrowed while we use the pointer.
unsafe {
*p += 5;
println!("{}", *p);
}
}
Expected output:
15
&raw const x and &raw mut x create pointers without first creating a
reference. Raw pointers can be null, dangle, alias each other and ignore
lifetimes — that freedom is why reading through them is unsafe.
Wrapping unsafe in a safe API
The idiomatic use of unsafe is a small block inside a function whose
safe signature cannot be misused. The standard library's
split_at_mut is the textbook case: two mutable borrows of one slice are
rejected by the borrow checker even though they don't overlap.
fn split_at_mut<T>(slice: &mut [T], mid: usize) -> (&mut [T], &mut [T]) {
let len = slice.len();
assert!(mid <= len, "mid out of bounds");
let ptr = slice.as_mut_ptr();
// SAFETY: `ptr` is valid for `len` elements. The two ranges
// [0, mid) and [mid, len) do not overlap and both lie inside the
// original slice, whose unique borrow we hold for the output lifetime.
unsafe {
(
std::slice::from_raw_parts_mut(ptr, mid),
std::slice::from_raw_parts_mut(ptr.add(mid), len - mid),
)
}
}
fn main() {
let mut v = [1, 2, 3, 4, 5];
let (left, right) = split_at_mut(&mut v, 2);
left[0] = 10;
right[0] = 30;
println!("{v:?}");
}
Expected output:
[10, 2, 30, 4, 5]
The assert! is what makes this function safe: without it, a caller could
pass mid > len and get memory outside the slice from safe code.
unsafe fn and SAFETY comments
Mark a function unsafe fn when callers must uphold a condition that the
function can't check. Document it under a # Safety heading. In the 2024
edition the body of an unsafe fn is not automatically an unsafe block;
unsafe operations inside it need their own unsafe { }, or the
unsafe_op_in_unsafe_fn lint warns.
/// Returns the element at `index` without bounds checking.
///
/// # Safety
///
/// `index` must be less than `values.len()`.
unsafe fn get_unchecked_i32(values: &[i32], index: usize) -> i32 {
// SAFETY: the caller guarantees `index < values.len()`.
unsafe { *values.get_unchecked(index) }
}
fn main() {
let v = [4, 8, 15];
// SAFETY: 2 < v.len() == 3.
let x = unsafe { get_unchecked_i32(&v, 2) };
println!("{x}");
}
Expected output:
15
The convention is strict: every unsafe block gets a // SAFETY: comment
explaining why the operation is sound here, and every unsafe fn gets a
# Safety section. Clippy's undocumented_unsafe_blocks lint can enforce
the first.
Checking your work with Miri
Undefined behaviour often "works" in testing and then fails with a new compiler version or optimisation level. Miri is an interpreter that runs your tests and detects many kinds of UB: out-of-bounds access, use after free, invalid alignment, data races and aliasing violations.
rustup +nightly component add miri
cargo +nightly miri test
Run it in CI for any crate that contains unsafe.
Common mistakes
- Writing the obvious
split_at_mutwith two reborrows:
fn split_at_mut<T>(s: &mut [T], mid: usize) -> (&mut [T], &mut [T]) {
(&mut s[..mid], &mut s[mid..])
}
error[E0499]: cannot borrow `*s` as mutable more than once at a time
Before writing unsafe, check whether std already has what you need — it
does here (slice::split_at_mut).
- Taking references to a
static mut. The 2024 edition rejects it:
error: creating a shared reference to mutable static
Use an atomic, a Mutex, or OnceLock instead.
- Treating
unsafeas "the compiler trusts me, so it's fine". A single violated invariant is undefined behaviour anywhere in the program, not just in that block. - Large
unsafeblocks. Keep them to the few lines that need it so the reasoning in theSAFETYcomment stays checkable. - Unsafe code that relies on a safe function behaving correctly (for
example, a user's
Ordimpl being consistent). Safe code is allowed to be wrong; unsafe code must stay sound anyway.
Practice
Write a safe function swap_first_last(v: &mut [i32]) that swaps the first
and last elements using raw pointers and std::ptr::swap. It must do nothing
for slices shorter than two elements. Add a SAFETY comment.
Show solution
fn swap_first_last(v: &mut [i32]) {
if v.len() < 2 {
return;
}
let ptr = v.as_mut_ptr();
// SAFETY: len >= 2, so index 0 and index len - 1 are both in bounds
// and distinct. We hold the only (mutable) borrow of `v`.
unsafe {
std::ptr::swap(ptr, ptr.add(v.len() - 1));
}
}
fn main() {
let mut a = [1, 2, 3, 4];
swap_first_last(&mut a);
println!("{a:?}");
let mut b = [7];
swap_first_last(&mut b);
println!("{b:?}");
}
This prints [4, 2, 3, 1] and [7]. In real code you would call the safe
v.swap(0, v.len() - 1); the exercise shows how the guard makes the unsafe
call sound.