Safety Documentation
Safety documentation is the formal convention in Rust for documenting the two sides of every unsafe contract:
/// # Safetydoc comments onunsafe fnandunsafe traitdeclarations specify the exact preconditions that callers or implementors must satisfy.// SAFETY:inline comments immediately above everyunsafe { ... }block andunsafe implexplain 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.