Rust Unsafe Rust Cheat Sheet
The five unsafe superpowers, raw pointers, FFI, and the invariants you must uphold manually when the borrow checker steps aside.
The Five unsafe Superpowers
unsafe only enables these; it does NOT disable the borrow checker elsewhere.
- dereference raw pointers- *const T / *mut T can be dereferenced
- call unsafe functions- including FFI functions and unsafe trait methods
- access/modify mutable statics- static mut (increasingly discouraged, use Atomic* or Cell types)
- implement unsafe traits- e.g. Send, Sync when the compiler can't verify them
- access union fields- reading a union field requires unsafe
Raw Pointers
Unlike references, raw pointers can be null, dangling, or unaligned — creating them is safe, dereferencing is not.
let mut x = 5;let r1 = &x as *const i32; // creating a raw pointer is safelet r2 = &mut x as *mut i32;unsafe { println!("{}", *r1); // dereferencing requires unsafe *r2 = 10;}// From an arbitrary address (dangerous, only do this with a real reason)let address = 0x012345usize;let p = address as *const i32;// unsafe { *p } // undefined behavior unless you KNOW this address is valid
FFI (Foreign Function Interface)
Calling into C, and exposing Rust functions to C.
// Declaring external C functionsextern "C" { fn abs(input: i32) -> i32;}fn call_c() { unsafe { println!("abs(-3) = {}", abs(-3)); }}// Exposing a Rust function to C (no name mangling, C calling convention)#[no_mangle]pub extern "C" fn rust_add(a: i32, b: i32) -> i32 { a + b}
Safe Abstractions Over unsafe
The standard pattern: contain unsafe inside a module, expose a safe API.
pub struct Wrapper<T> { ptr: *mut T, len: usize,}impl<T> Wrapper<T> { pub fn get(&self, index: usize) -> Option<&T> { if index >= self.len { return None; // bounds check happens in safe code } // SAFETY: index < self.len, ptr was allocated for `len` elements // and is non-null (checked at construction). unsafe { Some(&*self.ptr.add(index)) } }}
MaybeUninit for Deferred Initialization
MaybeUninit<T> lets you hold uninitialized memory without triggering UB from reading it, the modern replacement for mem::uninitialized().
use std::mem::MaybeUninit;fn build_array() -> [i32; 5] { let mut arr: [MaybeUninit<i32>; 5] = unsafe { MaybeUninit::uninit().assume_init() // OK: array-of-MaybeUninit needs no init }; for (i, slot) in arr.iter_mut().enumerate() { slot.write(i as i32 * 10); } // SAFETY: every element was written above, so transmuting to [i32; 5] is sound. unsafe { std::mem::transmute::<_, [i32; 5]>(arr) }}// Since 1.63: array::each_ref / MaybeUninit::array_assume_init helpers// exist to avoid the transmute dance above in newer code.
transmute: The Sharpest Tool Available
transmute reinterprets bits and skips every safety check — size mismatches are a compile error, but validity is entirely your responsibility.
// Sizes must match exactly (compile error otherwise)let bits: u32 = unsafe { std::mem::transmute(1.5f32) };// DANGEROUS: transmuting arbitrary bytes into an enum/bool can produce// an invalid value, which is instant undefined behavior even if unused.// unsafe { std::mem::transmute::<u8, bool>(2) } // UB: bool must be 0 or 1// Prefer safe, checked alternatives whenever one exists:let f = f32::from_bits(0x3fc00000); // safe, no validity requirementlet maybe_char = char::from_u32(0x1F600); // safe, returns Optionlet ptr_addr = some_ptr as *const u8 as usize; // `as` casts, not transmute
Manually Implementing Send/Sync
Types built on raw pointers don't get Send/Sync auto-derived; you must assert the invariant yourself and justify it.
struct SharedBuffer { ptr: *mut u8, len: usize,}// SAFETY: SharedBuffer never exposes aliased mutable access across threads;// all access goes through an internal Mutex guarding `ptr`, so it's sound// to send the pointer between threads and to share &SharedBuffer.unsafe impl Send for SharedBuffer {}unsafe impl Sync for SharedBuffer {}// Conversely, to OPT OUT of an auto-trait (e.g. force !Send on a type that// would otherwise qualify), wrap a PhantomData<*const ()> field:use std::marker::PhantomData;struct NotSendMarker(PhantomData<*const ()>);
A Minimal Manual Vec via NonNull + alloc
The canonical exercise for understanding what Vec<T> does under the hood: raw allocation, growth, and drop.
use std::alloc::{self, Layout};use std::ptr::NonNull;struct MiniVec<T> { ptr: NonNull<T>, len: usize, cap: usize,}impl<T> MiniVec<T> { fn push(&mut self, value: T) { if self.len == self.cap { let new_cap = if self.cap == 0 { 4 } else { self.cap * 2 }; let layout = Layout::array::<T>(new_cap).unwrap(); let new_ptr = unsafe { if self.cap == 0 { alloc::alloc(layout) } else { let old_layout = Layout::array::<T>(self.cap).unwrap(); alloc::realloc(self.ptr.as_ptr() as *mut u8, old_layout, layout.size()) } }; self.ptr = NonNull::new(new_ptr as *mut T).unwrap_or_else(|| alloc::handle_alloc_error(layout)); self.cap = new_cap; } // SAFETY: len < cap guaranteed by the growth check above. unsafe { self.ptr.as_ptr().add(self.len).write(value); } self.len += 1; }}impl<T> Drop for MiniVec<T> { fn drop(&mut self) { if self.cap != 0 { unsafe { for i in 0..self.len { std::ptr::drop_in_place(self.ptr.as_ptr().add(i)); } let layout = Layout::array::<T>(self.cap).unwrap(); alloc::dealloc(self.ptr.as_ptr() as *mut u8, layout); } } }}
Ways to Trigger Undefined Behavior
The invariants unsafe code must uphold manually — violating any of these is UB even if the program 'seems to work'.
- aliased mutable references- two &mut T (or &mut + &T) pointing at overlapping memory at the same time
- data races- unsynchronized concurrent access where at least one side writes
- invalid values- e.g. a bool byte that isn't 0/1, a non-UTF-8 str, a null NonNull<T>
- unaligned access- dereferencing a pointer not aligned to T's required alignment (use read_unaligned)
- dangling pointer deref- reading/writing through a pointer whose pointee was freed or never allocated
- breaking library invariants- e.g. mutating a HashMap key's Hash-relevant fields through unsafe interior access
- unwinding across FFI- letting a Rust panic unwind through an extern "C" boundary (undefined pre-1.71, abort-by-default after)
Every unsafe block should have a `// SAFETY:` comment directly above it explaining WHY the invariants hold (bounds, alignment, non-null, no aliasing) — this is community convention (and a Clippy lint, undocumented_unsafe_blocks) precisely because unsafe code is only as trustworthy as the reasoning behind it.