Skip to content
safe-over-unsafe logo

Safe API Design Over Unsafe Rust

safe-over-unsafe

Use when designing safe public APIs that wrap unsafe Rust code, adding unsafe blocks to existing types, reviewing unsafe code for soundness, or creating new types backed by raw pointers, MaybeUninit, or FFI

ahrav/scratch-scanner-rs0installs2stars

SKILL.md

Full skill instructions

Safe API Design Over Unsafe Rust

Module privacy is the soundness boundary — not the unsafe keyword. The unsafe {} block is a syntax marker; the real safety mechanism is private fields behind a safe public API, within a module (or crate) where all code is trusted.

When to Use

  • Designing a new type backed by MaybeUninit, raw pointers, or FFI
  • Adding unsafe blocks to existing code for performance
  • Reviewing unsafe code for soundness
  • Implementing Send/Sync/Drop for types with unsafe internals

Design Checklist

Follow every step. Skipping any step is how soundness bugs happen.

1. Establish the Safety Boundary

  • Private fields, private module, safe public API. Every public method — safe or not — is part of the soundness proof. Changing < to <= in a safe bounds check can make all unsafe blocks unsound.
  • Consider crate-level isolation. Consumer crate uses #![forbid(unsafe_code)]; unsafe lives in a dedicated crate.
  • Enable lints at the crate root:
    #![deny(unsafe_op_in_unsafe_fn)]
    #![deny(clippy::undocumented_unsafe_blocks)]
    

2. Document Invariants at the Type Level

Two kinds of invariants exist — conflating them causes bugs:

  • Validity invariant: Must hold at ALL times. Violation is instant UB even if the value is never "used." (Example: bool must be 0 or 1.)
  • Safety invariant: Must hold at API boundaries. May be temporarily violated internally. (Example: Vec's len <= cap.)

Document both in a module-level //! or struct-level /// doc comment:

/// # Invariants
/// - `len <= CAPACITY` (safety)
/// - Slots `[head..head+len)` (mod cap) are initialized (safety)
/// - Initialized slots contain valid `T` values (validity)

3. Enforce at Compile Time First

Every invariant should be evaluated: can this be a const assertion, a NonZero type, or a typestate transition instead of a runtime check?

const { assert!(N > 0 && N.is_power_of_two()) }; // compile-time

Compile-time enforcement eliminates entire bug classes. Runtime debug_assert! is the fallback — use assert! in public API preconditions that guard unsafe code in the implementation.

4. Write SAFETY Comments as Proof Sketches

Every // SAFETY: comment must argue invariant preservation, not just state a fact. Use this structure:

// SAFETY: [what unsafe operation is being performed]
// Invariant: [which safety invariant holds here — and WHY from the code path]
// Implies: [which validity invariant this satisfies for the unsafe op]
// Preserved: [what invariants remain intact after this operation]

Bad: // SAFETY: index is in bounds Good: // SAFETY: physical = idx & MASK, so physical < CAPACITY (mask clears // high bits). The element is initialized because idx < self.len, and our // safety invariant guarantees slots [head..head+len) are initialized. // This satisfies assume_init_ref's requirement that the value is valid T.

For unsafe fn, use # Safety doc sections documenting caller obligations.

5. Get Variance, PhantomData, and Send/​Sync Right

Incorrect Send/​Sync is the #1 real-world unsafe soundness bug (tokio RUSTSEC-2025-0023, lock_api RUSTSEC-2020-0070, windows RUSTSEC-2022-0008).

SituationAction
Type uses [MaybeUninit<T>; N]Auto-derive is correct. No PhantomData needed.
Type uses raw pointersAdd PhantomData<T> for owned data (covariant, correct Send/​Sync).
Need manual Send/​SyncRed flag. Bound generics: unsafe impl<T: Send + Sync> Sync for MyType<T> {}
Unsure about varianceIf the type owns T, it should be covariant (like Vec). Use PhantomData<T>.

Always prefer auto-derivation. Add compile-time assertions:

const _: () = {
    fn assert_send<T: Send>() { fn requires_send<S: Send>() {} requires_send::<MyType<T>>(); }
    fn assert_sync<T: Sync>() { fn requires_sync<S: Sync>() {} requires_sync::<MyType<T>>(); }
};

6. Handle Panic Safety

If an operation modifies state in multiple steps and any step can panic (e.g., T::drop(), T::clone()), use the decrement-before-drop or guard pattern:

// Decrement-before-drop: len reflects reality even if drop panics
while self.len > 0 {
    self.len -= 1;
    unsafe { self.buf[self.len].assume_init_drop(); }
}

Test with a deliberately-panicking Drop impl via catch_unwind.

7. Defend Against Generic Parameters

Unsafe code cannot trust safe trait implementations. T::drop() can panic. T::clone() can panic. Ord impls can be inconsistent. Write defensively so broken trait impls cause wrong results, never UB.

Verification Layers

All three are required. No single tool is sufficient.

LayerToolWhat It CatchesRequired
Dynamic UBcargo +nightly miri testAliasing, uninit, alignment, use-after-freeEvery test run
Aliasing modelMIRIFLAGS="-Zmiri-tree-borrows" cargo +nightly miri testTree Borrows violationsCI
Reference modelproptest vs known-correct safe impl (e.g., VecDeque)Logic errors, edge casesEvery test run
Drop correctnessDropTracker with Arc<AtomicUsize>Double-drop, leaksEvery test run
Bounded proofKani proof harnessesExhaustive invariant verificationCritical code

Risk Checklist

Before declaring unsafe code "done," verify each:

  • No manual unsafe impl Send/​Sync without restrictive bounds on generics
  • All values crossing safe→unsafe boundary are validated
  • MaybeUninit: using write() for init, tracking init state, never mem::uninitialized
  • Multi-step operations handle panics (guard/​decrement-before-drop pattern)
  • len/capacity updated atomically with the memory operation
  • Public API preconditions use assert! (not just debug_assert!)
  • PhantomData matches ownership semantics (covariant for owned data)
  • Miri passes under both Stacked Borrows and Tree Borrows

Pattern Catalog

PatternWhen to UseExemplar
Private fields + safe APIAlwaysstd Vec
Crate-level #![forbid(unsafe_code)]Multi-crate projectsFuchsia, gossip-rs
Compile-time const assertionsNumeric invariantsRingBuffer power-of-2
MaybeUninit + init trackingUninitialized storagestd MaybeUninit docs
PhantomData for varianceRaw pointer containersRustonomicon Vec
Guard struct for panic safetyMulti-step mutationshashbrown HashMap
Typestate (generic state param)State-dependent operationsserde Serializer
Sealed traitFixed impl setzerocopy
unsafe trait as capability gateExternal impls neededbytemuck Pod
Lifetime-encoded validityTemporal access controlcrossbeam-epoch Guard

References

  1. Rustonomicon: Working with Unsafe
  2. Ralf Jung: The Scope of Unsafe
  3. Unsafe Code Guidelines: Glossary
  4. std::mem::MaybeUninit
  5. Miri
  6. Kani Rust Verifier
  7. Stacked Borrows (POPL 2020)
  8. Tree Borrows
  9. Fuchsia Unsafe Guidelines
  10. Rust API Guidelines
  11. RustSec Advisory Database

More skills from ahrav

execute-review-findings logo
ahrav/scratch-scanner-rs

execute-review-findings

Use when you have code review findings, PR comments, or review reports that need to be systematically addressed — especially when there are multiple findings across different files and severities

2 0
View
deep-research logo
ahrav/scratch-scanner-rs

deep-research

Deep research before design — 3-5 parallel research agents survey papers, production systems, failure modes, and prior art, then a synthesizer compiles evidence, and an integrator maps findings to a concrete codebase plan with citations

2 0
View
perf-regression logo
ahrav/scratch-scanner-rs

perf-regression

Performance regression testing workflow for hot path changes

2 0
View
create-task logo
ahrav/scratch-scanner-rs

create-task

Use when creating any beads task — auto-researches the codebase, links related tasks, and produces a rich self-contained description from a structured template. Accepts minimal intent and outputs a complete task ready for agent implementation.

2 0
View
linux-perf-profile logo
ahrav/scratch-scanner-rs

linux-perf-profile

Deep Linux perf profiling — PMU counters, topdown analysis, flamegraphs, and annotated hotspot drill-down on ARM/Graviton

2 0
View
doc-rigor logo
ahrav/scratch-scanner-rs

doc-rigor

Write-then-verify documentation pipeline. Use when a user asks to

2 0
View
sqlite-review logo
ahrav/scratch-scanner-rs

sqlite-review

Review and tune SQLite schemas, queries, indexes, and pragmas. Connects to the actual database to gather concrete evidence (EXPLAIN QUERY PLAN, page counts, table stats) before recommending changes.

2 0
View
review-dispatch logo
ahrav/scratch-scanner-rs

review-dispatch

Parallel specialist code review — 6 focused agents (correctness, design, performance, safety, docs, complexity) diverge independently, then a single ranker merges findings into an importance-ranked report with confidence scores

2 0
View
sim-run logo
ahrav/scratch-scanner-rs

sim-run

Run deterministic simulation tests with progressive difficulty levels (sunny/stormy/radioactive) inspired by TigerBeetle VOPR — orchestrates seed management, workload selection, and invariant verification

2 0
View
archive-src logo
ahrav/scratch-scanner-rs

archive-src

Use when the user wants a minimal source-only archive for upload or checkpoint. Creates a tar.gz with just src/ and tests/ directories.

2 0
View
security-reviewer logo
ahrav/scratch-scanner-rs

security-reviewer

Audit memory safety and security in unsafe code blocks, buffer handling, and security-sensitive operations

2 0
View
sim-scaffold logo
ahrav/scratch-scanner-rs

sim-scaffold

Scaffold simulation-testable modules with sans-IO pattern, proptest state machine tests, and fault injection points — prevents retrofitting costs by making code DST-ready from the start

2 0
View

Popular AI tools

Kaiber logo
Video

Kaiber

Generate, edit, and beat-sync AI video with leading models in one workspace.

Paid
View
Vimcal logo
Productivity

Vimcal

The world's fastest calendar for remote work

Free
View

Transform Your Design with AI Designer by ImgCreator.ai

Freemium
View
Akool AI logo
Content & writing

Akool AI

Revolutionizing Video Production with AI-Powered Creativity

Paid
View

Extend an image past the frame and let AI fill the new aspect ratio.

Freemium
View
StarByFace logo
Security

StarByFace

Discover your celebrity doppelgänger with StarByFace!

Free
View
C

ChainClarity explains 700+ crypto whitepapers in plain English, with layered summaries, comparisons, research tools, alerts, and a $4.99 Pro plan.

Freemium
View
Opus Clip logo
Coding & apps

Opus Clip

Opus.ai: Revolutionize Your Web Experience

Free
View