| name | rust-unsafe |
| description | | Use when this capability is needed. |
Quick Navigation
Unsafe Rust Programming Guide
Guidelines for maintaining soundness, preventing Undefined Behavior (UB), and proving safety boundaries.
Core Safety Constraints
1. Mandatory Safety Explanations
Every single unsafe block or function MUST have a preceding // SAFETY: comment explaining exactly why it is safe and how invariants are preserved.
let my_vec = vec![1, 2, 3];
let ptr = my_vec.as_ptr();
unsafe {
let first = *ptr;
assert_eq!(first, 1);
}
2. Soundness Boundaries
Unsafe blocks must build a safe abstraction layer. A safe public function must not trigger Undefined Behavior for any possible inputs.
pub fn get_value_unchecked(slice: &[i32], offset: usize) -> i32 {
unsafe { *slice.as_ptr().add(offset) }
}
pub unsafe fn get_value_unchecked_safe(slice: &[i32], offset: usize) -> i32 {
unsafe { *slice.as_ptr().add(offset) }
}
3. Strict Pointer Aliasing Rules
Never violate Rust's aliasing rules (one mutable reference XOR multiple immutable references) even when using raw pointers.
- Converting raw pointers directly to
&mut T while other references exist is Undefined Behavior.
- Avoid using
std::mem::transmute if you can convert pointers or references via as.
let mut val = 42;
let r1 = &mut val as *mut i32;
let ref_mut = unsafe { &mut *r1 };
*ref_mut = 100;
4. FFI Safety
When exposing or calling external C functions, always handle null pointers, ensure string slices are null-terminated, and maintain correct layout representations using #[repr(C)].
extern "C" {
fn c_process_data(data: *const u8, len: libc::size_t);
}
pub fn process_data(data: &[u8]) {
unsafe {
c_process_data(data.as_ptr(), data.len() as libc::size_t);
}
}
MaybeUninit Patterns
Use MaybeUninit for uninitialized memory instead of mem::zeroed():
use std::mem::MaybeUninit;
pub struct RingBuffer<T> {
buffer: Box<[MaybeUninit<T>]>,
head: usize,
tail: usize,
capacity: usize,
}
impl<T> RingBuffer<T> {
pub fn with_capacity(cap: usize) -> Self {
let mut buffer = Vec::with_capacity(cap);
unsafe { buffer.set_len(cap); }
Self {
buffer: buffer.into_boxed_slice(),
head: 0,
tail: 0,
capacity: cap,
}
}
pub fn push(&mut self, value: T) -> Result<(), T> {
if self.len() == self.capacity {
return Err(value);
}
self.buffer[self.tail] = MaybeUninit::new(value);
self.tail = (self.tail + 1) % self.capacity;
(())
}
(& ) <T> {
.head == .tail {
;
}
= { .buffer[.head].() };
.head = (.head + ) % .capacity;
(value)
}
}
<T> <T> {
(& ) {
(_) = .() {}
}
}
Custom Vec Implementation
A minimal unsafe-backed Vec showing key safety invariants:
use std::alloc::{alloc, dealloc, handle_alloc_error, Layout};
use std::ptr::NonNull;
pub struct RawVec<T> {
ptr: NonNull<T>,
cap: usize,
}
impl<T> RawVec<T> {
pub fn with_capacity(cap: usize) -> Self {
if cap == 0 {
return Self { ptr: NonNull::dangling(), cap: 0 };
}
let layout = Layout::array::<T>(cap).unwrap();
let ptr = unsafe { alloc(layout) as *mut T };
let ptr = NonNull::new(ptr).unwrap_or_else(|| handle_alloc_error(layout));
Self { ptr, cap }
}
pub unsafe fn ptr(&self) -> *mut T { self.ptr.as_ptr() }
pub (&) { .cap }
}
<T> <T> {
(& ) {
.cap > {
= Layout::array::<T>(.cap).();
{ (.ptr.() * , layout); }
}
}
}
Pin and Self-Referential Structs
use std::pin::Pin;
use std::marker::PhantomPinned;
pub struct SelfReferential {
data: i32,
ptr: *const i32,
_pin: PhantomPinned,
}
impl SelfReferential {
pub fn new(data: i32) -> Pin<Box<Self>> {
let mut boxed = Box::new(Self {
data,
ptr: std::ptr::null(),
_pin: PhantomPinned,
});
let ptr = &boxed.data as *const i32;
boxed.ptr = ptr;
Pin::new(boxed)
}
pub fn get_data(self: Pin<&Self>) -> i32 {
unsafe { *self.ptr }
}
}
Transmute Alternatives
Try to avoid transmute. Prefer safer alternatives:
let bytes: [u8; 4] = std::mem::transmute(1234u32);
let bytes: [u8; 4] = 1234u32.to_ne_bytes();
let bytes: &[u8] = bytemuck::cast_slice(&[1.0f32, 2.0f32]);
Union Access
#[repr(C)]
pub union IntOrFloat {
pub int: i32,
pub float: f32,
}
pub struct TypedValue {
value: IntOrFloat,
is_int: bool,
}
impl TypedValue {
pub fn new_int(val: i32) -> Self {
Self { value: IntOrFloat { int: val }, is_int: true }
}
pub fn as_int(&self) -> Option<i32> {
if self.is_int {
Some(unsafe { self.value.int })
} else {
None
}
}
}
Automated Soundness Checks: Miri
Always validate unsafe code using Miri, Rust's Undefined Behavior interpreter.
cargo miri test
cargo miri test -- test_unsafe
#[cfg(miri)]
#[test]
fn test_ring_buffer_miri() {
let mut buf = RingBuffer::with_capacity(4);
buf.push(1).unwrap();
buf.push(2).unwrap();
assert_eq!(buf.pop(), Some(1));
assert_eq!(buf.pop(), Some(2));
assert_eq!(buf.pop(), None);
}
Unsafe Review Workflow
- Identify the safe abstraction boundary.
- List every invariant unsafe code relies on.
- Ensure safe callers cannot violate those invariants.
- Add
// SAFETY: comments at each unsafe block.
- Run tests under Miri when feasible.
- Minimize unsafe surface area.
- Document what must hold for each unsafe operation.
Pointer Rules
Raw pointers can be null, dangling, unaligned, or aliased. Convert to references only after proving validity.
pub fn get(slice: &[u8], index: usize) -> Option<u8> {
if index < slice.len() {
Some(unsafe { *slice.as_ptr().add(index) })
} else {
None
}
}
FFI Boundaries
- Use
#[repr(C)] for C-facing structs.
- Never let Rust panics cross
extern "C" boundaries.
- Document ownership transfer for every pointer.
- Accept null only when the API explicitly supports it.
- Provide matching allocation/free functions when Rust owns memory.
- Use
extern "C" for callbacks passed to C.
- Use
catch_unwind to prevent panics from crossing FFI boundaries.
use std::panic::catch_unwind;
pub extern "C" fn callback(data: *const libc::c_char) -> i32 {
let result = catch_unwind(|| {
let c_str = unsafe { std::ffi::CStr::from_ptr(data) };
let s = c_str.to_str().unwrap_or("");
process(s)
});
match result {
Ok(val) => val,
Err(_) => -1,
}
}
Unsafe Traits
Unsafe traits mean implementors must uphold invariants the compiler cannot verify.
pub unsafe trait ByteSource {
fn ptr(&self) -> *const u8;
fn len(&self) -> usize;
}
Concurrency Hazards
Manually implementing Send or Sync is a major soundness claim. Check interior mutability, aliasing, thread-affinity, and foreign handles.
struct MyRc<T> {
inner: *mut Inner<T>,
}
unsafe impl<T: Send> Send for MyRc<T> {}
unsafe impl<T: Sync> Sync for MyRc<T> {}
Anti-Patterns
let val: u64 = transmute(float_val);
let val: u64 = float_val.to_bits();
let ptr = &mut val as *mut i32;
let r1 = unsafe { &mut *ptr };
let r2 = unsafe { &mut *ptr };
pub fn read_at(slice: &[u8], offset: usize) -> u8 {
unsafe { *slice.as_ptr().add(offset) }
}
pub fn read_at(slice: &[u8], offset: usize) -> Option<u8> {
if offset < slice.len() {
Some(unsafe { *slice.as_ptr().add(offset) })
} else {
None
}
}
- Missing safety docs on
unsafe fn.
- Treating Miri success as a proof of correctness (Miri doesn't catch logic bugs).
mem::zeroed() where MaybeUninit should be used.
- Forgetting to drop elements when implementing a custom collection.
Review Prompt
For unsafe Rust, require invariants, safety comments, safe-boundary proof, Miri coverage, FFI ownership docs, and justification for each unsafe operation.
Production Readiness Checklist
- Inputs are validated at boundaries.
- Errors preserve enough context for debugging.
- Expensive work is measured or bounded.
- Examples avoid hidden global state.
- Security and privacy assumptions are stated.
- Tests cover edge cases and failure paths.
- All unsafe blocks have
// SAFETY: comments.
- Safe public API cannot trigger UB for any input.
- Miri tests cover the unsafe code paths.
- FFI boundary has panic guards and ownership documentation.
- Custom collections implement Drop correctly for all elements.
References
Source: adxptived/Rust-Skills — distributed by TomeVault.