Unsafe Rust

Safety Documentation

Safety documentation is the formal convention in Rust for documenting the two sides of every unsafe contract:

  1. /// # Safety doc comments on unsafe fn and unsafe trait declarations specify the exact preconditions that callers or implementors must satisfy.
  2. // SAFETY: inline comments immediately above every unsafe { ... } block and unsafe impl explain why those preconditions hold at that specific site.
Producer (`unsafe fn` / `unsafe trait`)          Consumer (`unsafe {}` / `unsafe impl`)
+-------------------------------------+          +-------------------------------------+
| /// # Safety                        |          | // SAFETY:                          |
| /// - `ptr` must be non-null...     | -------> | // - `ptr` comes from `&mut val`... |
| /// - `idx < len` must hold...      |          | // - `idx` was checked `< len`...   |
| pub unsafe fn write_at(...)         |          | unsafe { write_at(ptr, idx, v); }   |
+-------------------------------------+          +-------------------------------------+

Enforcing Safety Comments with Compiler and Clippy Lints

Rust provides built-in and Clippy lints that reject undocumented unsafe declarations and blocks at compile time:

Lint Tool What It Enforces
#[deny(unsafe_op_in_unsafe_fn)] rustc (default in Rust 2024) Requires explicit unsafe {} blocks inside unsafe fn bodies
#[deny(clippy::missing_safety_doc)] cargo clippy Requires a # Safety section in the doc comment of every pub unsafe fn and pub unsafe trait
#[deny(clippy::undocumented_unsafe_blocks)] cargo clippy Requires a // SAFETY: comment immediately preceding every unsafe { ... } block and unsafe impl
#[forbid(unsafe_code)] rustc Completely bans unsafe in crates or modules that do not need it

Writing Effective # Safety and // SAFETY: Comments

A good # Safety section lists concrete, verifiable predicates on the arguments and program state (not vague warnings like "Use with care"). A good // SAFETY: comment addresses each bullet point of the callee's # Safety section one by one, pointing to the specific line or invariant that guarantees it:

/// Swaps the elements at indices `i` and `j` without bounds checks or aliasing checks.
///
/// # Safety
/// The caller must guarantee all three of the following:
/// 1. `i < slice.len()`
/// 2. `j < slice.len()`
/// 3. `i != j` (the two indices must be strictly distinct so the mutable pointers do not alias).
pub unsafe fn swap_distinct_unchecked<T>(slice: &mut [T], i: usize, j: usize) {
    debug_assert!(i < slice.len() && j < slice.len() && i != j);
    let base = slice.as_mut_ptr();

    // SAFETY:
    // 1 & 2. Caller guarantees `i < slice.len()` and `j < slice.len()`, so both
    //        `.add(i)` and `.add(j)` are in-bounds of the `slice` allocation.
    // 3.     Caller guarantees `i != j`, so `ptr_i` and `ptr_j` are non-overlapping.
    unsafe {
        let ptr_i = base.add(i);
        let ptr_j = base.add(j);
        std::ptr::swap_nonoverlapping(ptr_i, ptr_j, 1);
    }
}

fn reverse_slice<T>(slice: &mut [T]) {
    let len = slice.len();
    for i in 0..(len / 2) {
        let j = len - 1 - i;
        // SAFETY:
        // 1. `i < len / 2 < len`.
        // 2. `j = len - 1 - i < len`.
        // 3. Since `i < len / 2`, `2 * i < len - 1`, which implies `i < len - 1 - i = j`, so `i != j`.
        unsafe {
            swap_distinct_unchecked(slice, i, j);
        }
    }
}

fn main() {
    let mut nums = [1, 2, 3, 4, 5];
    reverse_slice(&mut nums);
    println!("reversed: {:?}", nums);
}
reversed: [5, 4, 3, 2, 1]

Pairing # Safety Contracts with debug_assert!

Whenever an unsafe fn's safety preconditions can be checked in $O(1)$ time (such as !ptr.is_null(), pointer alignment ptr.is_aligned(), i < len, or i != j), placing debug_assert! at the top of the unsafe fn catches contract violations immediately during test runs while compiling to zero instructions in release builds.