Skip to content
rusthacks.dev
Systems

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.

3 min read
Summarize in AI
Flat illustration for the post “Unsafe Documentation Safety Comments That Reviewers Trust”, composed around unsafe.

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:

rust
/// 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

rust
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:

rust
// 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

CategoryQuestion the comment answers
BoundsWhy is the index or length in range?
InitializationWhy is this memory initialized?
AlignmentWhy is the pointer aligned for the type?
AliasingWhy is no other reference using this memory?
LifetimeWhy is the pointee alive for the whole use?
ThreadingWhy is no other thread accessing it?

Enforce with lints

Cargo.tomlTOML
[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.

Share

Keep reading

Get new posts by email

We send one email when a new post is published. No spam.

Unsubscribe in one click. No spam, ever.