Unsafe Rust

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