Auditing Unsafe Code
Auditing unsafe code is the systematic review process of verifying that every unsafe block, unsafe fn, and unsafe impl in a crate or dependency tree upholds Rust's memory, aliasing, lifetime, and concurrency invariants under all possible safe inputs—including edge cases like zero-sized types, empty slices, integer overflow, and panicking user callbacks.
The Unsafe Audit Checklist
When reviewing any module containing unsafe, verify these seven failure modes:
| # | Audit Category | Questions to Verify |
|---|---|---|
| 1 | Pointer validity & alignment | Can ptr ever be null (len == 0 slices must still use NonNull::dangling(), never null())? Is ptr aligned to align_of::<T>()? |
| 2 | Initialization & bit validity | Are all bytes read or turned into &T / &mut T guaranteed initialized? Is *ptr = val ever called on uninitialized memory instead of ptr.write(val)? |
| 3 | Aliasing & lifetimes | Could an unbounded lifetime 'a be synthesized from a raw pointer (&*ptr) that outlives its owner? Could two &mut references overlap? |
| 4 | Panic / unwind safety | If a user-supplied closure (FnMut), Clone::clone(), Ord::cmp(), or Drop::drop() panics mid-operation, will the struct's Drop implementation double-free or read uninitialized elements? |
| 5 | Zero-Sized Types (ZSTs) | What happens when size_of::<T>() == 0? (alloc(Layout::new::<T>()) is UB when size is 0! Pointer arithmetic ptr.add(n) is a no-op when size_of::<T>() == 0!) |
| 6 | Allocation & capacity math | Can cap * size_of::<T>() overflow isize::MAX? Does dealloc(ptr, layout) pass the exact same Layout that was passed to alloc? |
| 7 | Untrusted safe traits | Does memory safety depend on a safe trait (Ord, Eq, Hash, Deref) behaving properly? |
Classic Audit Finding: Unwind Double-Free Bug and Fix
Consider a method that moves an element out of a buffer, calls a user closure f(&mut elem), and writes it back. If f panics before the element is written back, and self.len was not adjusted first, the container's Drop implementation will drop the moved slot a second time (double free):
Unsound Sequence on Panic:
1. `let val = ptr::read(slot);` (Bitwise copy created; `self.len` still includes `slot`!)
2. `user_closure(&val);` (Panics! Unwinding begins!)
3. `val` is dropped on stack (Drop #1)
4. `Container::drop` runs (Drops `slot` again because `self.len` was not decremented -> Drop #2 = UB!)
Decrementing self.len before calling any potentially panicking code (and only restoring it after the slot is re-initialized) makes the operation unwind-safe:
use std::panic;
use std::ptr;
struct SingleSlot<T> {
value: std::mem::MaybeUninit<T>,
initialized: bool,
}
impl<T> SingleSlot<T> {
fn new(val: T) -> Self {
Self {
value: std::mem::MaybeUninit::new(val),
initialized: true,
}
}
fn transform_unwind_safe(&mut self, f: impl FnOnce(T) -> T) {
if !self.initialized {
return;
}
// 1. Mark slot as uninitialized BEFORE reading or calling `f`:
self.initialized = false;
// SAFETY: `self.initialized` was `true`, so `self.value` is initialized.
let current = unsafe { self.value.assume_init_read() };
// 2. If `f(current)` panics here, `current` is dropped once by `f`, and
// `SingleSlot::drop` sees `self.initialized == false` (no double-drop!).
let updated = f(current);
// 3. Write the updated value back and mark initialized:
self.value.write(updated);
self.initialized = true;
}
}
impl<T> Drop for SingleSlot<T> {
fn drop(&mut self) {
if self.initialized {
// SAFETY: `self.initialized` is true.
unsafe {
ptr::drop_in_place(self.value.as_mut_ptr());
}
}
}
}
fn main() {
let mut slot = SingleSlot::new(String::from("safe"));
let _ = panic::catch_unwind(panic::AssertUnwindSafe(|| {
slot.transform_unwind_safe(|_s| panic!("user closure panicked!"));
}));
println!("slot.initialized after panic: {}", slot.initialized);
}
slot.initialized after panic: false
Ecosystem Tools for Auditing Unsafe Usage
| Tool / Command | Purpose |
|---|---|
cargo geiger |
Scans a crate and its entire dependency tree, reporting the count of unsafe functions, expressions, traits, and impls per package |
cargo vet |
Supply-chain audit tool (used by Mozilla, Google, and Bytecode Alliance) that tracks human code reviews of third-party crates, including safe-to-deploy criteria |
cargo clippy |
Flags missing safety docs, transmutation bugs, unaligned pointers, and invalid uninit patterns |
cargo miri test |
Interprets MIR to detect actual undefined behavior dynamically during tests |