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.mdsource content
When Unsafe is Justified
| Use Case | Example | Justified? |
|---|---|---|
| FFI calls to C | | ✅ Yes |
| Low-level abstractions | Internal implementation of , | ✅ Yes |
| Performance optimization (measured) | Hot path with proven bottleneck | ⚠️ Verify first |
| Escaping borrow checker | Don'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)
| Rule | Description |
|---|---|
| G-01 | Don't use unsafe to escape compiler safety checks |
| G-02 | Don't blindly use unsafe for performance |
| G-03 | Don't create "Unsafe" aliases for types/methods |
Memory Layout (6 rules)
| Rule | Description |
|---|---|
| M-01 | Choose appropriate memory layout for struct/tuple/enum |
| M-02 | Don't modify memory variables of other processes |
| M-03 | Don't let String/Vec auto-deallocate memory from other processes |
| M-04 | Prefer reentrant versions of C-API or syscalls |
| M-05 | Use third-party crates for bit fields |
| M-06 | Use for uninitialized memory |
Raw Pointers (6 rules)
| Rule | Description |
|---|---|
| P-01 | Don't share raw pointers across threads |
| P-02 | Prefer over |
| P-03 | Use to mark variance and ownership |
| P-04 | Don't dereference pointers cast to misaligned types |
| P-05 | Don't manually convert immutable pointers to mutable |
| P-06 | Use instead of for pointer casts |
Unions (2 rules)
| Rule | Description |
|---|---|
| U-01 | Avoid unions except for C interop |
| U-02 | Don't use union variants with different lifetimes |
FFI (18 rules)
| Rule | Description |
|---|---|
| F-01 | Avoid passing strings directly to C |
| F-02 | Carefully read types documentation |
| F-03 | Implement Drop for wrapped C pointers |
| F-04 | Handle panics across FFI boundaries |
| F-05 | Use portable type aliases from or |
| F-06 | Ensure C-ABI string compatibility |
| F-07 | Don't implement Drop for types passed to extern code |
| F-08 | Handle errors properly in FFI |
| F-09 | Use references instead of raw pointers in safe wrappers |
| F-10 | Exported functions must be thread-safe |
| F-11 | Be careful with references to fields |
| F-12 | Document invariant assumptions for C parameters |
| F-13 | Ensure consistent data layout for custom types |
| F-14 | FFI types should have stable layout |
| F-15 | Validate robustness of external values |
| F-16 | Separate data and code for C closures |
| F-17 | Use opaque types instead of |
| F-18 | Avoid passing trait objects to C |
Safe Abstractions (11 rules)
| Rule | Description |
|---|---|
| S-01 | Be aware of memory safety issues from panics |
| S-02 | Unsafe code authors must verify safety invariants |
| S-03 | Don't expose uninitialized memory in public APIs |
| S-04 | Avoid double-free from panics |
| S-05 | Consider safety when manually implementing Auto Traits |
| S-06 | Don't expose raw pointers in public APIs |
| S-07 | Provide safe alternatives for performance |
| S-08 | Returning mutable reference from immutable parameter is wrong |
| S-09 | Add SAFETY comment before each unsafe block |
| S-10 | Add Safety section to public unsafe function docs |
| S-11 | Use instead of in unsafe functions |
I/O Safety (1 rule)
| Rule | Description |
|---|---|
| I-01 | Ensure 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
| Error | Fix |
|---|---|
| Null pointer dereference | Check for null before dereferencing |
| Use after free | Ensure lifetime validity |
| Data race | Add synchronization |
| Alignment violation | Use , check alignment |
| Invalid bit pattern | Use |
| Missing SAFETY comment | Add comment |
Deprecated Patterns
| Deprecated | Modern Alternative |
|---|---|
| |
(for reference types) | |
| Raw pointer arithmetic | , |
| Store first |
| or |
| Manual extern declarations | |
FFI Tools
| Direction | Tool |
|---|---|
| C → Rust | |
| Rust → C | |
| Python | |
| Node.js | |
| C++ | |
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 - 完整中文版本,包含所有内容