
Rust Unsafe
- 326 installs
- 155 repo stars
- Updated June 27, 2026
- mohitmishra786/low-level-dev-skills
Implement and review unsafe Rust for FFI, allocators, and zero-copy buffers while documenting invariants required for safe public APIs.
About
Provides patterns for writing, encapsulating, and testing unsafe Rust in backend and FFI layers, emphasizing documented invariants, layout correctness, and safe public wrappers around performance-critical internals.
- unsafe fn contract documentation
- raw pointer and lifetime rules
- FFI repr(C) layout checks
- Send/Sync justification patterns
- Miri-ready invariant tests
Rust Unsafe by the numbers
- 326 all-time installs (skills.sh)
- +25 installs in the week ending Aug 4, 2026 (Skillselion tracking)
- Ranked #36 of 121 Rust skills by installs in the Skillselion catalog
- Data as of Aug 4, 2026 (Skillselion catalog sync)
npx skills add https://github.com/mohitmishra786/low-level-dev-skills --skill rust-unsafeAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 326 |
|---|---|
| repo stars | ★ 155 |
| Last updated | June 27, 2026 |
| Repository | mohitmishra786/low-level-dev-skills ↗ |
What it does
Implement and review unsafe Rust for FFI, allocators, and zero-copy buffers while documenting invariants required for safe public APIs.
Files
Rust unsafe
Purpose
Guide agents through writing, reviewing, and reasoning about unsafe Rust: what operations require unsafe, how to write safe abstractions, audit patterns, common pitfalls, and when to reach for unsafe.
Triggers
- "When do I need to use unsafe in Rust?"
- "How do I write a safe abstraction over unsafe code?"
- "How do I audit an unsafe block?"
- "What are the rules for raw pointers in Rust?"
- "What does transmute do and when is it safe?"
- "How do I implement UnsafeCell correctly?"
Workflow
1. The five unsafe superpowers
unsafe grants exactly five capabilities not available in safe Rust:
1. Dereference raw pointers (*const T, *mut T) 2. Call unsafe functions (including extern "C" functions) 3. Access or modify mutable static variables 4. Implement unsafe traits (Send, Sync) 5. Access fields of unions
Everything else in Rust — including memory allocation, borrowing, closures — follows safe rules even inside unsafe blocks.
2. Raw pointers
// Creating raw pointers (safe — no dereference yet)
let x = 42u32;
let ptr: *const u32 = &x;
let mut_ptr: *mut u32 = &mut some_val as *mut u32;
// Null pointer
let null: *const u32 = std::ptr::null();
let null_mut: *mut u32 = std::ptr::null_mut();
// Dereference (unsafe)
let val = unsafe { *ptr };
// Null check
if !ptr.is_null() {
let val = unsafe { *ptr };
}
// Offset (safe to compute, unsafe to dereference)
let arr = [1u32, 2, 3, 4, 5];
let p = arr.as_ptr();
let third = unsafe { *p.add(2) }; // arr[2]
let also_third = unsafe { *p.offset(2) };
// Slice from raw parts
let slice: &[u32] = unsafe {
std::slice::from_raw_parts(p, arr.len())
};Rules for sound raw pointer dereference:
- Pointer must be non-null
- Pointer must be aligned for
T - Memory must be initialized for
T - Must not violate aliasing rules (only one
&mutto a location) - Memory must be valid for the lifetime of the reference
3. unsafe functions and traits
// Declare unsafe function (callers must uphold invariants)
/// # Safety
/// `ptr` must be non-null and aligned to `T`, and point to initialized data.
/// The caller must ensure no other mutable reference to the same location exists.
unsafe fn read_ptr<T>(ptr: *const T) -> T {
ptr.read() // ptr::read is unsafe
}
// Call unsafe function
let val = unsafe { read_ptr(some_ptr) };
// Unsafe trait — implementor must uphold safety invariants
unsafe trait MyUnsafeTrait {
fn operation(&self);
}
// Implementing an unsafe trait is unsafe
unsafe impl MyUnsafeTrait for MyType {
fn operation(&self) { /* must uphold the trait's invariants */ }
}
// Send and Sync
// Send: type can be moved to another thread
// Sync: type can be shared between threads (&T is Send)
unsafe impl Send for MyType {}
unsafe impl Sync for MyType {}4. Safe abstractions over unsafe
// The golden rule: unsafe blocks should be small, isolated, and
// wrapped in a safe API that maintains the invariant
pub struct MyVec<T> {
ptr: *mut T,
len: usize,
cap: usize,
}
impl<T> MyVec<T> {
pub fn new() -> Self {
MyVec { ptr: std::ptr::NonNull::dangling().as_ptr(), len: 0, cap: 0 }
}
// Safe public API
pub fn get(&self, index: usize) -> Option<&T> {
if index < self.len {
// Safety: index < len guarantees ptr+index is in bounds and initialized
Some(unsafe { &*self.ptr.add(index) })
} else {
None
}
}
// # Safety comment documents the invariant
pub fn push(&mut self, val: T) {
if self.len == self.cap {
self.grow();
}
// Safety: len < cap after grow(), so ptr+len is in bounds
unsafe { self.ptr.add(self.len).write(val) };
self.len += 1;
}
}
// Implement Drop to clean up
impl<T> Drop for MyVec<T> {
fn drop(&mut self) {
// Safety: ptr was allocated with this layout, and all elements are initialized
unsafe {
std::ptr::drop_in_place(std::slice::from_raw_parts_mut(self.ptr, self.len));
std::alloc::dealloc(self.ptr as *mut u8,
std::alloc::Layout::array::<T>(self.cap).unwrap());
}
}
}5. transmute
// transmute: reinterpret bits of one type as another
// Both types must have the same size
// Safe uses:
let x: u32 = 0x3f800000;
let f: f32 = unsafe { std::mem::transmute(x) }; // bits → float
// Transmute slice pointer (sound if types have same size/align)
let bytes: &[u8] = &[0x00, 0x00, 0x80, 0x3f];
let floats: &[f32] = unsafe {
std::slice::from_raw_parts(bytes.as_ptr() as *const f32, 1)
};
// Prefer safe alternatives when available:
let f = f32::from_bits(x); // instead of transmute for float bits
let n = u32::from_ne_bytes(bytes); // instead of transmute for byte arraysCommon transmute pitfalls:
- Wrong sizes (compile error, but check for generic types)
- Creating invalid enum values
- Creating references with wrong lifetimes
6. UnsafeCell — interior mutability
use std::cell::UnsafeCell;
// UnsafeCell is the only way to mutate through a shared reference
struct MyCell<T> {
value: UnsafeCell<T>,
}
impl<T: Copy> MyCell<T> {
fn new(val: T) -> Self {
MyCell { value: UnsafeCell::new(val) }
}
fn get(&self) -> T {
// Safety: single-threaded, no concurrent mutation
unsafe { *self.value.get() }
}
fn set(&self, val: T) {
// Safety: single-threaded, no outstanding references
unsafe { *self.value.get() = val }
}
}7. Unsafe audit checklist
When reviewing an unsafe block:
- [ ] Is there a
// Safety:comment explaining the invariant? - [ ] Is the raw pointer non-null?
- [ ] Is the raw pointer correctly aligned for the target type?
- [ ] Is the memory initialized?
- [ ] Is the lifetime of the reference valid?
- [ ] Are aliasing rules respected (no simultaneous
&and&mut)? - [ ] For
extern "C": are C invariants documented and verified? - [ ] For
Send/Syncimpl: is thread safety actually guaranteed? - [ ] Is the unsafe block as small as possible?
- [ ] Is there a test under Miri for the unsafe code?
8. When to use unsafe
Before reaching for unsafe, check:
├── Does std have a safe API? (Vec, Box, Arc — usually yes)
├── Does a crate handle it? (memmap2, nix, windows-sys)
├── Can you restructure to avoid it?
└── Is the performance gain measured and significant?
Legitimate uses:
├── FFI to C libraries (extern "C")
├── OS-level APIs (syscalls, mmap, ioctl)
├── Performance-critical data structures (custom allocators, SoA)
├── Hardware access (embedded, drivers)
└── Implementing safe abstractions (the standard library itself)For unsafe patterns and audit examples, see references/unsafe-patterns.md.
Related skills
- Use
skills/rust/rust-sanitizers-miri— Miri is the essential tool for testing unsafe code - Use
skills/rust/rust-ffifor unsafe patterns in FFI contexts - Use
skills/rust/rust-debuggingfor debugging panics in unsafe code - Use
skills/low-level-programming/memory-modelfor aliasing and memory ordering in unsafe
Rust unsafe Patterns Reference
The Unsafe Contract
Every unsafe block has an implicit contract: the programmer claims all safety invariants are upheld. Document them:
// Always document with a Safety comment
// # Safety
// - `ptr` must be non-null
// - `ptr` must be aligned to `align_of::<T>()`
// - `ptr` must point to `len` initialized values of type `T`
// - The memory must remain valid and not be mutated for the lifetime of the returned slice
unsafe fn raw_slice<'a, T>(ptr: *const T, len: usize) -> &'a [T] {
std::slice::from_raw_parts(ptr, len)
}Raw Pointer Patterns
NonNull — non-null raw pointer wrapper
use std::ptr::NonNull;
struct Node<T> {
data: T,
next: Option<NonNull<Node<T>>>,
}
// Create NonNull
let boxed = Box::new(42u32);
let nn: NonNull<u32> = NonNull::new(Box::into_raw(boxed)).unwrap();
// Dereference (unsafe)
let val = unsafe { nn.as_ref() };
// Convert back to Box (takes ownership, will drop)
let boxed_again = unsafe { Box::from_raw(nn.as_ptr()) };Pointer arithmetic
let arr = [1u32, 2, 3, 4, 5];
let ptr = arr.as_ptr();
// Offset by count (wrapping_add is safe to call, dereference still unsafe)
let p3 = ptr.wrapping_add(2); // no UB even if out of bounds (don't deref though)
let third = unsafe { *ptr.add(2) }; // add: UB if out of bounds even without deref
// Distance between pointers
let end = unsafe { ptr.add(arr.len()) };
let count = unsafe { end.offset_from(ptr) }; // count == 5Read/Write without creating references
// ptr::read: copies T out without binding lifetime
let val: u32 = unsafe { std::ptr::read(ptr) };
// ptr::write: writes T without dropping old value
unsafe { std::ptr::write(mut_ptr, new_val) };
// ptr::copy: memcpy (may overlap for copy_nonoverlapping)
unsafe { std::ptr::copy_nonoverlapping(src, dst, count) };
unsafe { std::ptr::copy(src, dst, count) }; // overlapping OK
// ptr::drop_in_place: run destructor without freeing memory
unsafe { std::ptr::drop_in_place(ptr) };Safe Abstraction Patterns
Invariant-based safety
pub struct AlignedBuffer {
ptr: NonNull<u8>,
len: usize,
align: usize,
}
impl AlignedBuffer {
pub fn new(len: usize, align: usize) -> Option<Self> {
let layout = std::alloc::Layout::from_size_align(len, align).ok()?;
// Safety: layout.size() > 0 (checked by from_size_align)
let ptr = unsafe { std::alloc::alloc(layout) };
let ptr = NonNull::new(ptr)?; // null check
Some(Self { ptr, len, align })
}
pub fn as_slice(&self) -> &[u8] {
// Safety: ptr is non-null, aligned, and points to len initialized bytes
// Invariant maintained by constructor and no unsafe mutations
unsafe { std::slice::from_raw_parts(self.ptr.as_ptr(), self.len) }
}
}Pin and self-referential structs
use std::pin::Pin;
// Pin prevents moving the value (required for self-referential structs and async)
struct SelfRef {
data: String,
data_ref: *const String, // points into self
}
impl SelfRef {
fn new(s: String) -> Pin<Box<Self>> {
let mut boxed = Box::pin(SelfRef { data: s, data_ref: std::ptr::null() });
// Safety: we're setting data_ref to point to our own data field
// Pin guarantees the Box won't move, so data_ref remains valid
let ptr = &boxed.data as *const String;
unsafe { boxed.as_mut().get_unchecked_mut().data_ref = ptr; }
boxed
}
}Transmute Safety Table
| From | To | Safe? | Alternative |
|---|---|---|---|
u32 | f32 | ✓ (same size) | f32::from_bits(u) |
[u8; 4] | u32 | ✓ | u32::from_ne_bytes(arr) |
&T | *const T | ✓ | ptr as *const T |
*mut T | *const T | ✓ | ptr as *const T |
&'a T | &'b T (longer) | ✗ | Restructure lifetimes |
Box<T> | *mut T | ✓ | Box::into_raw(b) |
u8 | bool | ✗ unless 0/1 | Match on value |
u8 | MyEnum | ✗ unless valid tag | MyEnum::try_from(u) |
i32 | u32 | ✓ | i as u32 |
Vec<T> | Vec<U> | ✗ | Manual conversion |
Miri Testing for Unsafe
# Run unsafe tests under Miri
cargo +nightly miri test
# With stricter provenance checking
MIRIFLAGS="-Zmiri-strict-provenance" cargo +nightly miri test
# Isolate a specific test
cargo +nightly miri test test_my_unsafe_fnPattern for testable unsafe:
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_raw_slice_roundtrip() {
let data = vec![1u32, 2, 3, 4, 5];
let slice = unsafe { raw_slice(data.as_ptr(), data.len()) };
assert_eq!(slice, &[1, 2, 3, 4, 5]);
}
}clippy for Unsafe
# Run all lints including unsafe-related
cargo clippy -- -W clippy::undocumented-unsafe-blocks \
-W clippy::multiple-unsafe-ops-per-block \
-W clippy::transmute-undefined-repr \
-W clippy::ptr-as-ptr
# Deny undocumented unsafe blocks in production code
#![deny(clippy::undocumented_unsafe_blocks)]Stacked Borrows Rules (Miri model)
1. Each borrow creates a new "tag" on the borrow stack 2. &mut T access pops all borrows above it from the stack (invalidates them) 3. &T access is valid as long as the shared reference is on the stack 4. Raw pointer access: tag must still be on the stack at time of access
Violation example:
let mut x = 5u32;
let raw = &mut x as *mut u32;
let shared = &x; // shared borrow pushed onto stack
let _ = unsafe { *raw }; // VIOLATION: raw's &mut tag was invalidated by &x