agentleFS
dawidpereiradawidpereira/rust-skills/skills/rust-unsafe/SKILL.md

rust-unsafe

Skill0 starsChanged 8 months agoMITView on GitHub

Unsafe Rust and FFI patterns. Use when writing or reviewing unsafe blocks, doing FFI/extern calls, working with raw pointers, transmute, MaybeUninit, or #[repr]. Covers SAFETY documentation requirements and review checklists. Also use when auditing unsafe code or deciding whether unsafe is actually needed.

3 other projects use a version of this.

Adopted by

Most recently taken up by

The file

We split this skill into the rules it gives an agent

5 rules. 0 are common, 2 are distinctive, and 3 are very specific to this project.

  • 0

    Common

    Most of the 3 edited versions we read keep these.

  • 2

    Distinctive

    This author's own choices. Worth a look.

  • 3

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Distinctive:

    Check if a safe abstraction already exists in std or a well-audited crate.

    line 18
  2. Distinctive:

    Check if the borrow checker complaint reveals a design problem, not a limitation.

    line 20
  3. Very specific:

    If unsafe is truly needed, identify every invariant the compiler can no longer enforce.

    line 22
  4. Very specific:

    If you cannot precisely state the invariants, you do not understand the code well enough to use unsafe.

    line 25
  5. Very specific:

    Before approving any unsafe block, verify each applicable item:

    line 49

“Common” is measured: kept by at least 6 in 10 of the 3 edited versions of this file we could read. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md

Unsafe Rust & FFI

Core Question

Can this be done without unsafe? If not, what invariants must be upheld?

Most Rust code never needs unsafe. Before reaching for it:

  • Check if a safe abstraction already exists in std or a well-audited crate.
  • Check if the borrow checker complaint reveals a design problem, not a limitation.
  • If unsafe is truly needed, identify every invariant the compiler can no longer enforce.

If you cannot precisely state the invariants, you do not understand the code well enough to use unsafe.


Quick Decisions

ScenarioUnsafe Needed?Use Instead
Bounds-checked indexing is too slowMaybeProve bounds first, then get_unchecked
Calling a C library functionYesWrap in safe API with validity checks
Sharing data between threadsNoArc<Mutex<T>>, crossbeam, channels
Reinterpreting bytes as a typeUsuallybytemuck, zerocopy, or TryFrom
Global mutable stateNoOnceLock, AtomicT, Mutex
Implementing a custom collectionYesEncapsulate unsafe behind safe public API
Bypassing the borrow checkerNoFix the ownership design
Self-referential structNopin-project, ouroboros, or redesign
SIMD intrinsicsYesUse std::simd (portable) when possible
Inline assemblyYesWrap with safe function, document constraints

Review Checklist

Before approving any unsafe block, verify each applicable item:

CategoryCheck
Pointer validityNon-null, points to allocated memory, not yet freed
AlignmentPointer aligned to align_of::<T>()
InitializationMemory contains a valid value of type T
AliasingNo &mut and & to same memory simultaneously
LifetimePointed-to data outlives the reference created from it
Send/SyncManual impls are sound — type is actually thread-safe
Uninitialized memoryOnly accessed through MaybeUninit, never read before init
FFI signaturesMatch the C header exactly (types, calling convention, nullability)
DropNo double-free, no use-after-free, destructors run correctly
Size/layouttransmute source and target have identical size and valid bit patterns

Required Documentation

Every unsafe block: // SAFETY: comment

pub fn get(&self, index: usize) -> Option<&T> {
    if index < self.len {
        // SAFETY: bounds check above guarantees index < len,
        // so this access is within the allocated region.
        Some(unsafe { &*self.ptr.as_ptr().add(index) })
    } else {
        None
    }
}

Every unsafe function: /// # Safety doc section

/// Constructs a `Name` from raw UTF-8 bytes without validation.
///
/// # Safety
///
/// - `bytes` must contain valid UTF-8.
/// - `bytes` must not be empty.
/// - The caller retains no mutable reference to `bytes` after this call.
pub unsafe fn from_utf8_unchecked(bytes: Vec<u8>) -> Name {
    Name(String::from_utf8_unchecked(bytes))
}

Every unsafe trait impl: // SAFETY: comment above impl

// SAFETY: All fields of Buffer are Send-safe (Vec<u8> and usize).
// The raw pointer field is only dereferenced while &self is held.
unsafe impl Send for Buffer {}

Enforce with: #![warn(clippy::undocumented_unsafe_blocks)]


Deprecated → Better

Deprecated / RiskyUse InsteadWhy
mem::uninitialized()MaybeUninit<T>UB if T has invalid bit patterns
mem::zeroed() for refs/boolsMaybeUninit<T>Null ref and 0u8 bool are instant UB
static mutAtomicT, Mutex, OnceLockData races are UB even on single core
transmute for byte reinterpretationfrom_ne_bytes, bytemuck::castChecked size/alignment, no surprise UB
transmute for enum conversionTryFrom implInvalid discriminant is UB
Raw pointer arithmetic (ptr + n)ptr::add(n), ptr::offset(n)Overflow and alignment are checked in debug
CString::new(s).unwrap().as_ptr()Bind CString to a variable firstTemporary is dropped, pointer dangles
Manual extern "C" declarationsbindgenAvoids signature mismatches

FFI Patterns

repr attributes

AttributeUse When
#[repr(C)]Struct passed to/from C code
#[repr(transparent)]Newtype wrapper used in FFI (same ABI as inner type)
#[repr(u8)] / #[repr(c_int)]Enum matching a C enum's underlying type
#[repr(packed)]Matching a packed C struct (avoid — causes unaligned access)

Safe wrapper pattern

mod ffi {
    use std::os::raw::c_int;

    extern "C" {
        pub fn compress(src: *const u8, src_len: usize,
                        dst: *mut u8, dst_len: *mut usize) -> c_int;
    }
}

pub fn compress(input: &[u8]) -> Result<Vec<u8>, CompressError> {
    let mut buf = vec![0u8; max_compressed_size(input.len())];
    let mut out_len = buf.len();

    // SAFETY: input and buf are valid slices, out_len points to a valid usize.
    // compress() writes at most out_len bytes to dst.
    let rc = unsafe {
        ffi::compress(
            input.as_ptr(), input.len(),
            buf.as_mut_ptr(), &mut out_len,
        )
    };

    if rc != 0 {
        return Err(CompressError(rc));
    }
    buf.truncate(out_len);
    Ok(buf)
}

Usage Scenarios

Scenario 1: "I need to call a C library from Rust." → Use bindgen to generate bindings. Wrap every FFI call in a safe function that validates inputs and checks return codes. Use #[repr(C)] on structs shared across the boundary. Use #[repr(transparent)] for newtype wrappers of C types. Bind CString to a variable before calling .as_ptr().

Scenario 2: "I'm reviewing code that uses unsafe — what do I check?" → Load references/checklist.md. For each unsafe block: verify a // SAFETY: comment exists and is accurate, check every item in the review checklist above, and confirm there is no safe alternative. For unsafe fn, verify the # Safety doc section lists all caller obligations.

Scenario 3: "The borrow checker won't let me do X — should I use unsafe?" → Almost certainly not. Unsafe does not disable the borrow checker's rules — it merely hides UB. Redesign ownership (see rust-ownership), use RefCell for interior mutability, or use Pin for self-referential types.


Reference Files

FileRead When
references/checklist.mdReviewing or auditing unsafe code, FFI bindings, or unsafe trait impls

Cross-References

WhenCheck
Ownership questions in unsafe coderust-ownership → Quick Decisions
Send/Sync trait bounds for async + unsaferust-async → Quick Decisions
Newtype wrappers for FFI typesrust-types → Quick Decisions
Documenting # Safety sectionsrust-api → Quick Decisions
Clippy unsafe lintsrust-quality → Quick Decisions

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

No reviews yet

People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.

  • It worked
  • Partly
  • It didn't
What did you use it for? e.g. Reviewing pull requests in a Rails app

Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.