Unsafe Documentation Safety Comments That Reviewers Trust
Document every unsafe block with a SAFETY comment and every unsafe fn with a Safety section. Learn what a reviewer expects in them and how Clippy enforces the habit.

An unsafe block without a comment is a claim with no evidence. The compiler will not read your reasoning, but your reviewer will, and so will you in a year. The convention in the Rust ecosystem is small and specific, and following it is the fastest route to unsafe code that other people will merge. It builds on unsafe Rust explained and is checked by Clippy.
Rust 1.90, edition 2024.
Two places, two audiences
For the caller of an unsafe fn, the doc comment states the contract:
/// Copies `len` bytes from `src` to `dst`.
///
/// # Safety
/// - `src` must be valid for reads of `len` bytes.
/// - `dst` must be valid for writes of `len` bytes.
/// - The two regions must not overlap.
pub unsafe fn copy_bytes(src: *const u8, dst: *mut u8, len: usize) {
// SAFETY: the caller upholds the three conditions above.
unsafe { std::ptr::copy_nonoverlapping(src, dst, len) }
}For the reader of an unsafe block, the // SAFETY: comment is the proof that this call meets the contract of what it calls. Each line of the callee's # Safety section should have a matching justification.
A worked proof
pub struct Ring { buf: Box<[u8]>, head: usize }
impl Ring {
pub fn peek(&self) -> u8 {
// SAFETY: `head` is always less than `buf.len()`; `new` sets it to 0 for a
// non-empty buffer and `advance` wraps it with `% buf.len()`.
unsafe { *self.buf.get_unchecked(self.head) }
}
}The comment names the invariant (head < buf.len()) and where it is established. If someone adds a method that sets head carelessly, the comment tells them what they would break. Compare a weak one:
// SAFETY: this is fine
unsafe { *self.buf.get_unchecked(self.head) }It cannot be reviewed, because it claims nothing that can be checked.
What to put in a proof
| Category | Question the comment answers |
|---|---|
| Bounds | Why is the index or length in range? |
| Initialization | Why is this memory initialized? |
| Alignment | Why is the pointer aligned for the type? |
| Aliasing | Why is no other reference using this memory? |
| Lifetime | Why is the pointee alive for the whole use? |
| Threading | Why is no other thread accessing it? |
Enforce with lints
[lints.clippy]
undocumented_unsafe_blocks = "warn"
missing_safety_doc = "warn"
multiple_unsafe_ops_per_block = "warn"The third lint nudges toward one unsafe operation per block, which keeps each comment honest. See lints in Cargo.toml for the table syntax. unsafe_op_in_unsafe_fn is already denied in edition 2024.
Review habits
A reviewer reads the comment first, checks each fact against the code, then checks that the facts imply the callee's requirements. If the proof needs to read a distant function, move the invariant into a type (a private field with a constructor that validates it), so the block can rely on the type.
When comments are not enough
Comments do not stop mistakes; tests and tools do. Pair them with Miri. The convention is described in the standard library's own style and in the Rustonomicon's chapter on safe and unsafe.


