Unsafe Code Policy¶
Moon enforces a strict gate on unsafe blocks. This document complements the
"Unsafe Code" section in CLAUDE.md with concrete review and
merge requirements.
Why this matters¶
unsafe is the audit surface where the borrow checker stops protecting us. A
single unsound block can produce data races, use-after-free, or torn-page
corruption that no test will catch until production. We pay a higher review
cost on unsafe to keep that risk bounded.
Hard rules¶
-
No new
unsafeblock lands without explicit human approval in the PR. This includesunsafe impl Send/Sync,unsafe fn, and trivial libc syscall wrappers. AI assistants and automated refactors must surface every new unsafe block to the reviewer. -
Every
unsafeblock must have a// SAFETY:comment that names: - The exact precondition(s) being upheld.
- Where the precondition comes from (caller contract, type invariant, hardware guarantee, etc.).
-
Why violating it would be UB, in one sentence.
-
Prefer the safe alternative when the cost is < 100 ns on the hot path.
parking_lot::MutexandRwLockare cheap enough to replaceUnsafeCellin almost every case.get_uncheckedshould be replaced withdebug_assert!+ indexed access unless a benchmark proves otherwise. -
Encapsulate
unsafebehind a safe public API. Apub fnwhose body containsunsafeand whose precondition is "caller must X" is a footgun. Make itunsafe fnso the caller has to opt in. -
Field drop order matters for mmap/FD/raw-pointer types. When a struct holds a resource whose lifetime depends on another field (e.g.,
Mmap+SegmentHandle), document the field ordering invariant in the struct doc comment and add a// MUST be the last fieldcomment on the keepalive.
Review checklist (for PRs touching unsafe)¶
- Each new
unsafeblock has a// SAFETY:comment. - Each
unsafe impl Send/Syncis justified by either: (a) the type is genuinely thread-safe by construction, or (b) a runtime invariant is enforced by the type system (e.g.,!Syncnewtype,thread_local!, or compile-time feature gate). Hand-wavy "we only call this from one thread" is not acceptable unless the PR description names the specific runtime feature gate that enforces it. - All raw pointer arithmetic (
ptr.add,ptr.offset) is preceded by adebug_assert!proving the result is in-bounds, OR the SAFETY comment derives the bound from caller-visible preconditions. - No
unsafeis used purely to suppress borrow checker errors. Fix the ownership model instead. - PR description includes "Unsafe added: N blocks" in the summary, with a one-line justification per block.
Auditing existing unsafe¶
Run the project's unsafe-audit skill / cargo-geiger periodically:
# Count unsafe blocks added on the current branch vs main
git diff main -- 'src/**/*.rs' | grep -cE '^\+.*\bunsafe\b'
# Inventory all unsafe blocks
grep -rn 'unsafe' src/ --include='*.rs' | grep -v '// SAFETY'
Any block missing a SAFETY comment is a bug — file an issue.
Approved patterns¶
These are pre-vetted and don't require fresh justification, just the SAFETY comment:
libc::close(fd)inDropfor an owned FD._mm_prefetch(cannot fault on x86_64).slice::from_raw_parts(self.ptr, self.len)whereselfowns the allocation andlenis a struct invariant.is_x86_feature_detected!-gated SIMD intrinsics.- AArch64 NEON intrinsics from
core::arch::aarch64under a#[cfg(target_arch = "aarch64")]gate. NEON is mandatory in ARMv8-A, so no runtime detection is needed; theunsafewrapper is a Rust formality. The SAFETY comment must still justify pointer validity, alignment requirements (notevld1q_u8has none, unlike SSE2's_mm_load_si128), and any shift-by-constant assumptions. MmapOptions::new().map(&file)over a sealed-after-rename file with a refcount-protected directory handle (seevector::persistence::warm_segment::WarmSegmentFilesfor the canonical pattern).
Forbidden without explicit design review¶
transmutebetween non-trivially-equivalent types.unsafe impl Send/Syncon types containingUnsafeCellor raw pointers without aMutex/atomic enforcement.get_unchecked/get_unchecked_mutwithout a benchmark showing > 5% speedup over[idx].mem::uninitialized/MaybeUninit::assume_initwithout zero-init proof.- Holding
*mut Tacross anawaitpoint.