Skip to content

← Unsafe and Soundness step 4 of 24

Easy Primitives

Safety comments as a discipline: `// SAFETY:` and `# Safety`

pub unsafe fn copy_run(src: &[u8], from: usize, len: usize) -> Vec<u8>

Copy len bytes out of src, starting at index from, into a fresh Vec<u8>. Build the vector with Vec::with_capacity, fill it with ptr::copy_nonoverlapping, and publish it with Vec::set_len — no extend_from_slice, no to_vec, no indexing.

The interesting part is not the copy. It is that this template makes the safety discipline a compile gate, and the starter fails it four different ways.

Two comments, two different jobs

Every unsafe fn and every unsafe trait gets a # Safety section in its doc comment. It states the obligations the caller must meet. It is a proof obligation, written outward, for a stranger.

Every unsafe {} block and every unsafe impl gets an immediately preceding // SAFETY: comment. It states why those obligations are met here. It is a proof discharge, written inward, for the next person to touch this function — usually you, in eight months.

They should read as such. A good // SAFETY: comment names the specific preconditions that specific earlier lines established:

// SAFETY: `mid <= len` was asserted above, so both ranges lie inside the
// same allocation and are disjoint.

A bad one reassures:

// SAFETY: this is fine, we checked it

The second one is worse than no comment, because it looks like an audit.

::: question Why is “I couldn’t write the safety comment” the most reliable bug signal in unsafe code? Because the comment is the proof, and if the proof does not exist the code is unsound.

Writing a safety comment forces you to enumerate the preconditions of every operation in the block and point at the line that establishes each one. When you sit down to do that and find there is no such line — the check happens in a different function, or it happens after, or it does not happen at all — you have found the bug before it shipped.

This is why experienced reviewers read the safety comments first and the code second. The comment tells them what to verify. Code with no comment tells them nothing, so the only honest review is to re-derive the whole argument from scratch, which nobody does. :::

Three lints, on deliberately, and one authoring gotcha each

clippy::missing_safety_doc — on by default. A pub unsafe fn without a # Safety section is an error. Write what the caller must guarantee: from + len must not overflow and must be <= src.len().

clippy::undocumented_unsafe_blocks — enabled by the template. Every unsafe {} needs a // SAFETY: comment. The gotcha: the comment must be on the line or lines directly preceding the block. A blank line between the comment and the unsafe breaks detection and the lint fires anyway.

clippy::multiple_unsafe_ops_per_block — enabled by the template, and much stricter than it reads. It counts operations, not statements:

unsafe { std::slice::from_raw_parts_mut(ptr.add(mid), n) }

is two unsafe operations — the call to the unsafe function from_raw_parts_mut, and the call to the unsafe method add. That block fails. You must hoist:

// SAFETY: ...
let p = unsafe { ptr.add(mid) };
// SAFETY: ...
let s = unsafe { std::slice::from_raw_parts_mut(p, n) };

This is why the lint is restriction rather than a default: turned on everywhere, every pointer-arithmetic function becomes an exercise in let bindings. It is on here, in the one problem that is about it, so that you feel what one-justification-per-operation actually costs. Later problems in this track leave it off.

clippy::unnecessary_safety_comment is also on, and it grades the reverse direction: a // SAFETY: comment attached to a block or statement that is not unsafe at all. A comment not attached to a real obligation is noise that trains readers to skip safety comments, which is the habit this whole discipline exists to prevent.

::: question let dst = out.as_mut_ptr(); — does that line need a // SAFETY: comment? No, and adding one is an error here.

Vec::as_mut_ptr is a safe function. Creating a raw pointer is always safe — it is just an address. Nothing can go wrong until somebody dereferences it, and that is the operation that needs the justification.

clippy::unnecessary_safety_comment will reject a // SAFETY: on that line. The rule it is enforcing: a safety comment marks a place where a proof obligation was discharged. Sprinkling them on safe code destroys that signal. :::

The three unsafe operations you actually need

ptr.add(from) — offsetting a pointer. Unsafe because computing an out-of-bounds address is undefined behaviour at the arithmetic, before any dereference. Its precondition: the result must stay inside the same allocation, or be exactly one past its end.

ptr::copy_nonoverlapping(src, dst, count) — a memcpy. Its preconditions: both pointers valid for count elements, properly aligned, and the two ranges must not overlap. count is in elements, not bytes. Here the source is inside src‘s allocation and the destination is inside a freshly allocated buffer, so disjointness is free — say so in the comment.

Vec::set_len(len) — publishing initialised memory. Its precondition: len <= capacity and the first len elements are actually initialised. This is the one that repays a careful comment, because it is the line that turns “some bytes I wrote” into “a Vec<u8> the rest of the program will trust”.

Ordering matters, and the harness cannot check it

Set the length after the copy, never before. If you set_len(len) first and then copy, there is a window in which a Vec<u8> exists whose length claims len initialised bytes that are not initialised. For u8 you will almost certainly get away with it; for a type with a destructor a panic in between would run destructors on garbage.

Nothing in this grader will catch that. Every test here passes with the statements in the wrong order. Item 17.12 explains why “briefly invalid” is not a thing, and item 17.21 explains what a panic does to a half-built structure.

Remember the grade is compile + tests + clippy -D warnings.