Rust-skills rust-unsafe

Unsafe code and FFI expert covering raw pointers (*mut, *const), FFI patterns, transmute, union, #[repr(C)], SAFETY comments, soundness rules, and undefined behavior prevention.

install
source · Clone the upstream repo
git clone https://github.com/huiali/rust-skills
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/huiali/rust-skills "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/rust-unsafe" ~/.claude/skills/huiali-rust-skills-rust-unsafe-0e9689 && rm -rf "$T"
manifest: skills/rust-unsafe/SKILL.md
source content

When Unsafe is Justified

Use CaseExampleJustified?
FFI calls to C
extern "C" { fn libc_malloc(size: usize) -> *mut c_void; }
✅ Yes
Low-level abstractionsInternal implementation of
Vec
,
Arc
✅ Yes
Performance optimization (measured)Hot path with proven bottleneck⚠️ Verify first
Escaping borrow checkerDon't know why you need it❌ No

SAFETY Comment Requirements

Every unsafe block must include a SAFETY comment:

// SAFETY: ptr must be non-null and properly aligned.
// This function is only called after a null check.
unsafe { *ptr = value; }

/// # Safety
///
/// * `ptr` must be properly aligned and not null
/// * `ptr` must point to initialized memory of type T
/// * The memory must not be accessed after this function returns
pub unsafe fn write(ptr: *mut T, value: &T) { ... }

Solution Patterns

Pattern 1: FFI with Safe Wrapper

use std::ffi::{CStr, CString};
use std::os::raw::c_char;

extern "C" {
    fn c_function(s: *const c_char) -> i32;
}

// ✅ Safe wrapper
pub fn safe_c_function(s: &str) -> Result<i32, Box<dyn Error>> {
    let c_str = CString::new(s)?;
    // SAFETY: c_str is a valid null-terminated string created from Rust data.
    // The pointer is valid for the duration of this call.
    let result = unsafe { c_function(c_str.as_ptr()) };
    Ok(result)
}

Pattern 2: Raw Pointer with Validation

use std::ptr::NonNull;

struct Buffer {
    ptr: NonNull<u8>,
    len: usize,
}

impl Buffer {
    pub fn write(&mut self, index: usize, value: u8) -> Result<(), String> {
        if index >= self.len {
            return Err("index out of bounds".to_string());
        }

        // SAFETY: We've checked index is within bounds.
        // ptr is NonNull and points to valid memory.
        unsafe {
            self.ptr.as_ptr().add(index).write(value);
        }
        Ok(())
    }
}

Pattern 3: Uninitialized Memory

use std::mem::MaybeUninit;

// ✅ Safe uninitialized memory handling
fn create_buffer(size: usize) -> Vec<u8> {
    let mut buffer: Vec<MaybeUninit<u8>> = Vec::with_capacity(size);

    for i in 0..size {
        buffer.push(MaybeUninit::new(0));
    }

    // SAFETY: All elements have been initialized to 0.
    unsafe { std::mem::transmute(buffer) }
}

// ❌ Avoid: deprecated pattern
fn bad_buffer(size: usize) -> Vec<u8> {
    let mut v = Vec::with_capacity(size);
    unsafe { v.set_len(size); }  // UB if not initialized!
    v
}

Pattern 4: Repr(C) for FFI

#[repr(C)]
pub struct Point {
    pub x: f64,
    pub y: f64,
}

#[repr(C)]
pub enum Status {
    Success = 0,
    Error = 1,
}

// SAFETY: Layout matches C struct exactly
extern "C" {
    fn process_point(p: *const Point) -> Status;
}

47 Unsafe Rules Reference

General Principles (3 rules)

RuleDescription
G-01Don't use unsafe to escape compiler safety checks
G-02Don't blindly use unsafe for performance
G-03Don't create "Unsafe" aliases for types/methods

Memory Layout (6 rules)

RuleDescription
M-01Choose appropriate memory layout for struct/tuple/enum
M-02Don't modify memory variables of other processes
M-03Don't let String/Vec auto-deallocate memory from other processes
M-04Prefer reentrant versions of C-API or syscalls
M-05Use third-party crates for bit fields
M-06Use
MaybeUninit<T>
for uninitialized memory

Raw Pointers (6 rules)

RuleDescription
P-01Don't share raw pointers across threads
P-02Prefer
NonNull<T>
over
*mut T
P-03Use
PhantomData<T>
to mark variance and ownership
P-04Don't dereference pointers cast to misaligned types
P-05Don't manually convert immutable pointers to mutable
P-06Use
ptr::cast
instead of
as
for pointer casts

Unions (2 rules)

RuleDescription
U-01Avoid unions except for C interop
U-02Don't use union variants with different lifetimes

FFI (18 rules)

RuleDescription
F-01Avoid passing strings directly to C
F-02Carefully read
std::ffi
types documentation
F-03Implement Drop for wrapped C pointers
F-04Handle panics across FFI boundaries
F-05Use portable type aliases from
std
or
libc
F-06Ensure C-ABI string compatibility
F-07Don't implement Drop for types passed to extern code
F-08Handle errors properly in FFI
F-09Use references instead of raw pointers in safe wrappers
F-10Exported functions must be thread-safe
F-11Be careful with references to
repr(packed)
fields
F-12Document invariant assumptions for C parameters
F-13Ensure consistent data layout for custom types
F-14FFI types should have stable layout
F-15Validate robustness of external values
F-16Separate data and code for C closures
F-17Use opaque types instead of
c_void
F-18Avoid passing trait objects to C

Safe Abstractions (11 rules)

RuleDescription
S-01Be aware of memory safety issues from panics
S-02Unsafe code authors must verify safety invariants
S-03Don't expose uninitialized memory in public APIs
S-04Avoid double-free from panics
S-05Consider safety when manually implementing Auto Traits
S-06Don't expose raw pointers in public APIs
S-07Provide safe alternatives for performance
S-08Returning mutable reference from immutable parameter is wrong
S-09Add SAFETY comment before each unsafe block
S-10Add Safety section to public unsafe function docs
S-11Use
assert!
instead of
debug_assert!
in unsafe functions

I/O Safety (1 rule)

RuleDescription
I-01Ensure I/O safety when using raw handles

Workflow

Step 1: Question the Need

Do I really need unsafe?
  → Can I use safe abstractions?
  → Is this for FFI? (justified)
  → Is this for measured performance? (profile first)
  → Am I fighting the borrow checker? (redesign instead)

Step 2: Write SAFETY Comments

For every unsafe block:
1. Document preconditions
2. Explain why they hold
3. Reference invariants maintained

For public unsafe functions:
1. Add /// # Safety section
2. List all requirements
3. Document consequences of violations

Step 3: Validate with Tools

# Detect undefined behavior
cargo +nightly miri test

# Memory leak detection
valgrind ./target/release/program

# Data race detection
RUST_BACKTRACE=1 cargo test --features tsan

Step 4: Build Safe Wrappers

Raw unsafe code
  ↓
Safe private functions (validate inputs)
  ↓
Safe public API (no unsafe visible)

Common Errors and Fixes

ErrorFix
Null pointer dereferenceCheck for null before dereferencing
Use after freeEnsure lifetime validity
Data raceAdd synchronization
Alignment violationUse
#[repr(C)]
, check alignment
Invalid bit patternUse
MaybeUninit
Missing SAFETY commentAdd comment

Deprecated Patterns

DeprecatedModern Alternative
mem::uninitialized()
MaybeUninit<T>
mem::zeroed()
(for reference types)
MaybeUninit<T>
Raw pointer arithmetic
NonNull<T>
,
ptr::add
CString::new().unwrap().as_ptr()
Store
CString
first
static mut
AtomicT
or
Mutex
Manual extern declarations
bindgen

FFI Tools

DirectionTool
C → Rust
bindgen
Rust → C
cbindgen
Python
PyO3
Node.js
napi-rs
C++
cxx

Review Checklist

When reviewing unsafe code:

  • Unsafe usage is justified (FFI, low-level abstraction, measured perf)
  • Every unsafe block has SAFETY comment
  • Public unsafe functions document Safety requirements
  • Raw pointers are validated before dereferencing
  • No raw pointer sharing across threads without sync
  • FFI boundaries properly handle panics
  • Memory layout explicitly specified for FFI types
  • Uninitialized memory uses
    MaybeUninit
  • Safe public API wraps unsafe internals
  • Tested with Miri for undefined behavior

Verification Commands

# Check for undefined behavior
cargo +nightly miri test

# Run with address sanitizer
RUSTFLAGS="-Z sanitizer=address" cargo +nightly test

# Check FFI bindings
cargo check --features ffi

# Verify memory safety
valgrind --leak-check=full ./target/release/program

# Documentation check
cargo doc --no-deps --open

Common Pitfalls

1. Dangling Pointers

Symptom: Use-after-free, segfault

// ❌ Bad: pointer outlives data
fn bad() -> *const i32 {
    let x = 42;
    &x as *const i32  // Dangling!
}

// ✅ Good: proper lifetime management
fn good(x: &i32) -> *const i32 {
    x as *const i32  // Lifetime tied to input
}

2. Uninitialized Memory

Symptom: Undefined behavior, random values

// ❌ Bad: reading uninitialized memory
let x: i32;
unsafe { println!("{}", x); }  // UB!

// ✅ Good: use MaybeUninit
let mut x = MaybeUninit::<i32>::uninit();
x.write(42);
let x = unsafe { x.assume_init() };  // Safe

3. Invalid Repr

Symptom: FFI crashes, data corruption

// ❌ Bad: default repr with FFI
struct Point { x: f64, y: f64 }
extern "C" { fn use_point(p: Point); }

// ✅ Good: explicit C layout
#[repr(C)]
struct Point { x: f64, y: f64 }
extern "C" { fn use_point(p: Point); }

Related Skills

  • rust-ownership - Lifetime and borrowing fundamentals
  • rust-ffi - Advanced FFI patterns
  • rust-performance - When unsafe optimization is justified
  • rust-coding - SAFETY comment conventions
  • rust-concurrency - Thread-safe unsafe patterns

Localized Reference

  • Chinese version: SKILL_ZH.md - 完整中文版本,包含所有内容