Unsafe Rust
|
This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production. The reference material consulted while preparing these pages is listed in the section bibliography. |
This page is written against Rust 1.99.0 and the 2024 edition, checked on 2026-10-06. Safe Rust’s guarantees — no
dangling pointers, no data races, no out-of-bounds access — rest on rules the compiler can check. Some correct programs
cannot be expressed within those rules: talking to hardware or C libraries, implementing Vec or Mutex themselves,
squeezing out a bounds check. unsafe lets you do the few extra things such code needs, and makes you responsible for
upholding the invariants the compiler can no longer check.
The five unsafe superpowers
Inside an unsafe block (or an unsafe fn body, see below) you may:
-
dereference a raw pointer;
-
call an
unsafefunction or method (including foreign functions); -
access or modify a mutable
staticvariable; -
implement an
unsafetrait (such asSend,SyncorGlobalAlloc); -
access the fields of a
union.
Doing any of these outside unsafe is error E0133.
Source: The Book: Unsafe Rust
What unsafe does — and does not — turn off
unsafe does not switch off the borrow checker, type checking or any other check: references inside an unsafe block
are checked exactly as outside it. It only unlocks the five operations above. The Rustonomicon’s way of saying this:
unsafe code is code where the programmer promises that certain conditions hold. A bug in unsafe code can cause
undefined behaviour (UB), after which the program may do anything — including appearing to work.
The boundary that matters is the module: an unsafe block often relies on invariants established by safe code
elsewhere in the same module (such as Vec's len <= capacity), so the whole module must be reviewed together.
Raw pointers
*const T and *mut T are pointers without the guarantees of references: they may be null, dangling or unaligned,
may alias freely, and carry no lifetime. Creating one is safe; dereferencing one is not.
use std::ptr::{self, NonNull};
fn main() {
let mut copies: u32 = 3;
let r: *const u32 = &raw const copies; // 1.82: create a raw pointer without an intermediate reference
let w: *mut u32 = &raw mut copies;
// SAFETY: `w` points to a live, aligned u32 and no reference to `copies` is in use.
unsafe {
*w += 1;
println!("{}", *r); // 4
}
let null: *const u32 = ptr::null();
println!("{}", null.is_null()); // safe to create and test
let mut titles = vec!["Dune", "Emma"];
let p = titles.as_mut_ptr();
// SAFETY: index 1 is within the Vec's length; nothing else borrows `titles` meanwhile.
unsafe { *p.add(1) = "Kindred" };
println!("{titles:?}");
let nn: NonNull<u32> = NonNull::from(&mut copies); // never null: Option<NonNull<T>> is pointer-sized
// SAFETY: `nn` was created from a valid &mut and `copies` is still alive.
println!("{}", unsafe { *nn.as_ptr() });
}
Source: std: pointer primitive
&raw const place and &raw mut place (1.82) create a raw pointer directly, which matters for fields of
#[repr(packed)] structs and uninitialised memory, where creating a reference first (&place as *const _) would itself
be UB. NonNull<T> is a non-null *mut T that is covariant in T; it is what most collections store. Pointer
arithmetic uses add, sub, offset and byte_add, all unsafe because the result must stay within the same
allocation.
Strict provenance (1.84)
A pointer is more than an address: it also carries provenance, the permission to access a particular allocation.
Casting a pointer to an integer and back (ptr as usize as *const T) loses track of that permission, which makes some
optimisations and tools (Miri, CHERI hardware) unable to reason about the program. Rust 1.84 stabilised the strict
provenance APIs that keep provenance explicit: ptr.addr() reads the address without exposing it, ptr.with_addr(a)
and ptr.map_addr(f) build a pointer with a new address but the same provenance, and ptr::without_provenance makes an
invalid sentinel pointer. When you genuinely need integer round-trips (FFI handles), use the exposed provenance APIs
expose_provenance and with_exposed_provenance.
fn main() {
let data = [10u64, 20, 30];
let p: *const u64 = data.as_ptr();
// Tag the low bit of an aligned pointer (pointer tagging), keeping provenance:
let tagged = p.map_addr(|a| a | 1);
let untagged = tagged.map_addr(|a| a & !1);
// SAFETY: `untagged` has the same address and provenance as `p`, which points into `data`.
println!("{} tag={}", unsafe { *untagged.add(1) }, tagged.addr() & 1); // 20 tag=1
}
Source: std::ptr: Provenance
unsafe fn, unsafe blocks and the 2024 lint
An unsafe fn is a function with preconditions the caller must satisfy; calling it requires unsafe. Its
documentation must say what they are in a # Safety section. In the 2024 edition, the body of an unsafe fn is no
longer an implicit unsafe block: the unsafe_op_in_unsafe_fn lint warns by default, so each unsafe operation inside
needs its own unsafe { } block with a // SAFETY: comment explaining why that particular operation is sound.
/// Returns the element at `index` without bounds checking.
///
/// # Safety
///
/// `index` must be less than `items.len()`.
unsafe fn get_unchecked_title<'a>(items: &[&'a str], index: usize) -> &'a str {
// SAFETY: the caller guarantees `index < items.len()` (see # Safety above).
unsafe { items.get_unchecked(index) }
}
fn first_title<'a>(items: &[&'a str]) -> Option<&'a str> {
if items.is_empty() {
return None;
}
// SAFETY: we just checked that index 0 is in bounds.
Some(unsafe { get_unchecked_title(items, 0) })
}
fn main() {
println!("{:?}", first_title(&["Dune", "Emma"]));
}
Unsafe traits and unsafe attributes
An unsafe trait has invariants that implementors must uphold; implementing it requires unsafe impl. Send and
Sync are the common examples (see Shared
State, Send/Sync & Atomics); GlobalAlloc and bytemuck::Pod are others.
Some attributes can also cause UB if misused — for example two functions exporting the same symbol name. Since the
2024 edition they must be written as unsafe attributes: #[unsafe(no_mangle)], #[unsafe(export_name = "...")] and
#[unsafe(link_section = "...")]. Likewise extern blocks must be declared unsafe extern "C" { ... } (see
FFI & C Interop).
Undefined behaviour
The Reference keeps the list of behaviour considered undefined. The main items:
-
data races;
-
accessing (loading from or storing to) a dangling or misaligned place, or out of bounds of its allocation;
-
breaking the aliasing rules: mutating through a pointer while a
&Tto the same data is live, or accessing data while a&mut Tto it is live (except through that&mut); -
mutating immutable data (anything reachable through a shared reference, outside
UnsafeCell, or aconst); -
calling a function with the wrong ABI, or unwinding out of a function whose ABI does not allow it;
-
producing an invalid value, even without using it: a
boolother than 0/1, acharoutside the Unicode scalar range, an enum with an invalid discriminant, a null or dangling reference orBox, a!, uninitialised integers, floats or pointers, astrthat is not UTF-8 when the type requires it; -
executing code compiled with target features the CPU does not support;
-
incorrect use of compiler intrinsics and
unreachable_unchecked.
Things that are not UB in Rust, though they may be bugs: deadlocks, memory leaks, integer overflow (it panics or wraps predictably), exiting without running destructors, and logic errors in safe code.
SAFETY comments and safe abstractions
The idiom that makes unsafe code reviewable: keep unsafe blocks as small as possible, put a // SAFETY: comment on
every one stating which invariant makes it sound (Clippy’s undocumented_unsafe_blocks lint can require it), and wrap
the unsafe code in a safe API whose types make misuse impossible. The standard library’s split_at_mut is the classic
example — safe callers cannot create overlapping mutable slices, even though the implementation uses raw pointers:
/// Splits a mutable slice into two non-overlapping halves at `mid`.
///
/// # Panics
///
/// Panics if `mid > slice.len()`.
pub fn split_halves<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; [0, mid) and [mid, len) do not overlap and both
// lie within the original slice, whose exclusive borrow is held for the returned 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 shelf = [1, 2, 3, 4, 5];
let (left, right) = split_halves(&mut shelf, 2);
left[0] = 10;
right[0] = 30;
println!("{shelf:?}"); // [10, 2, 30, 4, 5]
}
The community Rust Design Patterns recommends confining unsafe code to small modules with safe interfaces ("Contain
unsafety in small modules"), and many crates add #![forbid(unsafe_code)] (or unsafe_code = "forbid" in [lints])
everywhere else.
MaybeUninit and transmute
std::mem::MaybeUninit<T> holds memory that may not yet contain a valid T — the safe way to work with uninitialised
memory (mem::uninitialized() is deprecated because it produced invalid values). Write each element, then
assume_init once everything is initialised:
use std::mem::MaybeUninit;
fn squares<const N: usize>() -> [u64; N] {
let mut out: [MaybeUninit<u64>; N] = [const { MaybeUninit::uninit() }; N];
for (i, slot) in out.iter_mut().enumerate() {
slot.write((i as u64) * (i as u64));
}
// SAFETY: every element was written in the loop above.
out.map(|x| unsafe { x.assume_init() })
}
fn main() {
println!("{:?}", squares::<5>()); // [0, 1, 4, 9, 16]
let bits = 1.5f32.to_bits(); // the safe alternative to transmute::<f32, u32>
println!("{bits:#x} {}", f32::from_bits(bits));
}
Source: std::mem::MaybeUninit
std::mem::transmute::<A, B> reinterprets the bits of an A as a B of the same size. It is extremely easy to misuse
(lifetimes, invalid values, layout of repr(Rust) types), and almost every legitimate use has a safer replacement:
to_bits/from_bits, to_le_bytes/from_le_bytes, as casts for pointers, bytemuck/zerocopy for plain-old-data
conversions. The Rustonomicon’s "Transmutes" chapter lists the pitfalls.
Source: The Rustonomicon: Transmutes
Alternatives to static mut
static mut is a mutable global accessible from any thread without synchronisation. In the 2024 edition, taking a
reference to one is rejected (see Variables,
Mutability & Constants). Use instead: an atomic (static COUNT: AtomicU64), a Mutex/RwLock in a static, a
OnceLock/LazyLock, or thread_local!. If a static mut is unavoidable (FFI, embedded interrupt handlers), access it
only through &raw const/&raw mut pointers inside unsafe blocks with a // SAFETY: comment.
Miri and sanitizers
Testing unsafe code requires tools that detect UB, because UB often does not crash:
-
Miri (
cargo +nightly miri test) interprets the program and reports UB precisely: aliasing violations under the Stacked or Tree Borrows models, uninitialised reads, out-of-bounds and use-after-free accesses, data races, invalid values and leaks. See Debugging & Diagnostics. -
Sanitizers (nightly:
RUSTFLAGS="-Zsanitizer=address" cargo +nightly test --target x86_64-unknown-linux-gnu) instrument native code: AddressSanitizer, ThreadSanitizer, MemorySanitizer and LeakSanitizer. They also work across FFI boundaries, where Miri cannot go. -
Fuzzing with
cargo fuzzfinds the inputs that trigger the bug in the first place.
env::set_var and other newly unsafe functions
In the 2024 edition, std::env::set_var and std::env::remove_var are unsafe: on most platforms, modifying the
environment while another thread reads it (C libraries do, for example through getaddrinfo) is a data race in the C
runtime. Call them only before spawning threads, and prefer passing configuration explicitly:
fn main() {
// SAFETY: called at the very start of main, before any other thread exists.
unsafe {
std::env::set_var("SHELF_LOG", "debug");
}
println!("{:?}", std::env::var("SHELF_LOG"));
}
std::os::unix::process::CommandExt::before_exec became unsafe for the same kind of reason; pre_exec is its unsafe
replacement.
Where the books are outdated
Mara & Holmes (Refactoring to Rust, chapters 3-4) introduce raw pointers and unsafe for FFI using the 2018 edition:
bare extern "C" {} blocks, #[no_mangle], and unsafe fn bodies that implicitly allow unsafe operations. In the 2024
edition those are unsafe extern "C" {}, #[unsafe(no_mangle)] and explicit unsafe {} blocks inside unsafe fn; the
&raw syntax (1.82) and strict provenance APIs (1.84) are also newer than their examples.
References
-
The Rustonomicon — Meet Safe and Unsafe, What Unsafe Can Do, Working with Unsafe, Uninitialized Memory, Transmutes
-
std::ptr (provenance, NonNull)
-
The Edition Guide: unsafe_op_in_unsafe_fn, Unsafe attributes, Newly unsafe functions
-
Rust Design Patterns: Contain unsafety in small modules (community)
-
Lily Mara and Joel Holmes, Refactoring to Rust (Manning, 2025)