33. unsafe Rust: the five superpowers and the contract they carry
33.1 What unsafe unlocks
An unsafe { } block allows exactly five extra operations:
- Dereferencing a raw pointer (
*const T,*mut T). - Calling an
unsafe fn, including every FFI function. - Reading or writing a
static mut. - Implementing an
unsafe trait(such asSendorSync). - Reading a field of a
union.
It does not turn off the borrow checker or type checking for ordinary references. It means: "I, the programmer, have checked the preconditions the compiler cannot check here." If you are wrong, the result is undefined behavior, the same category of bug as in the C guide §28.
33.2 Safe abstractions over unsafe code
The standard library is full of unsafe, wrapped in APIs that cannot be
misused. Here is the real idea behind split_at_mut, which the borrow
checker cannot verify on its own because it cannot tell that the two halves
do not overlap:
fn split_at_mut<T>(s: &mut [T], mid: usize) -> (&mut [T], &mut [T]) {
let len = s.len();
assert!(mid <= len, "mid out of bounds"); // the check that makes the unsafe below sound
let ptr = s.as_mut_ptr();
// SAFETY: `mid <= len`, so both ranges are inside the original allocation,
// and [0, mid) and [mid, len) do not 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),
)
}
}
fn main() {
let mut v = [1, 2, 3, 4, 5];
let (a, b) = split_at_mut(&mut v, 2);
a[0] = 10;
b[0] = 30;
println!("{v:?}"); // [10, 2, 30, 4, 5]
}
The pattern is: check the invariant in safe code, write a // SAFETY:
comment that justifies each unsafe operation, and expose a safe function.
Callers cannot cause UB however they call it. Clippy's
undocumented_unsafe_blocks lint enforces the comment.
33.3 UB in Rust is a short list, but it is real
- Dereferencing a dangling, null, or misaligned pointer.
- Breaking the aliasing rules: creating a
&mut Twhile another reference to the same data is in use, even through raw pointers. - Producing an invalid value: a
boolthat is not 0 or 1, acharoutside Unicode, a null&T, astrthat is not UTF-8, an enum with an invalid tag. - Data races, which are possible only through
unsafeor a wrongunsafe impl Send. - Calling a foreign function with the wrong signature.
33.4 Miri: an interpreter that detects UB
rustup +nightly component add miri
cargo +nightly miri test # runs your tests in an interpreter that checks every memory access
Miri catches use-after-free, out-of-bounds access, aliasing violations, and
memory leaks in code with unsafe. Think of it as AddressSanitizer plus
UBSan (C §14)
with knowledge of Rust's aliasing model. Run it in CI for every crate that
contains unsafe.
War story. A popular crate's unsafe impl Send on a type containing an
Rc passed review and tests for months. When users began moving it between
Tokio worker threads, the non-atomic reference count raced, and objects were
occasionally freed twice. The RustSec advisory database has many entries
like this, all in unsafe code. Safe Rust cannot express this bug, so
reviewing a crate's unsafe blocks covers the part of the code where it
can occur. cargo geiger counts the unsafe code in your dependency tree.