The unsafe keyword grants access to five capabilities that the compiler cannot verify:
Dereference raw pointers — *const T and *mut TCall unsafe functions — fn foo() { unsafe { ... } }Access mutable statics — static mut X: i32Implement unsafe traits — unsafe impl Send for T {}Access union fields . Unions require unsafe for field accessunsafe does not disable the borrow checker. It does not bypass Rust”s safety guarantees — it Allows you to do things that the compiler cannot prove are safe. You are responsible for maintaining All invariants manually.
Raw pointers are like C pointers — they can be null, dangling, misaligned, or aliased. The compiler Does not check them:
let raw_const : *const i32 = & x;
let raw_mut : *mut i32 = &mut y;
println! ( "const: {}" , * raw_const);
println! ( "mut: {}" , * raw_mut);
assert_eq! ( * raw_mut, 43 );
Raw pointers can be created in safe code — only dereferencing them requires unsafe:
let ptr : *const i32 = & x; // safe
let ptr_mut : *mut i32 = &mut x; // safe
let null : *const i32 = std :: ptr :: null (); // safe
// *ptr // only dereferencing is unsafe
let mut values = [ 1 i32 , 2 , 3 , 4 , 5 ];
let ptr : *mut i32 = values . as_mut_ptr ();
// Offset — returns pointer to ptr + count
assert_eq! (values[ 0 ], 100 );
// Read-add-write in one operation
let old = ptr . replace ( 200 );
Convert raw pointers to optional references:
let ptr : *const i32 = & x;
let reference : Option < & i32 > = unsafe { ptr . as_ref () };
assert_eq! (reference, Some ( & 42 ));
let null : *const i32 = std :: ptr :: null ();
let reference : Option < & i32 > = unsafe { null . as_ref () };
assert_eq! (reference, None );
as_ref() returns None for null pointers, preventing undefined behavior from null dereferences.
let mut arr = [ 10 i32 , 20 , 30 , 40 , 50 ];
let ptr = arr . as_mut_ptr ();
assert_eq! (arr, [ 20 , 40 , 60 , 80 , 100 ]);
The resulting pointer is not dereferenced. `ptr.add(len)` is UB if the pointer does not point to an Allocation of at least `len` elements.Functions marked unsafe declare that the caller must uphold certain invariants:
// implementation that requires caller to maintain invariants
unsafe blocks delimit the region where unsafe operations are permitted. The compiler checks that All unsafe operations occur within an unsafe block:
fn split_at_mut (values : &mut [ i32 ], mid : usize ) -> ( &mut [ i32 ], &mut [ i32 ]) {
let ptr = values . as_mut_ptr ();
std :: slice :: from_raw_parts_mut (ptr, mid),
std :: slice :: from_raw_parts_mut (ptr . add (mid), len - mid),
This function cannot be written in safe Rust because it creates two mutable references to Overlapping regions. The unsafe block is justified because we guarantee the two slices do not Overlap (the split point ensures this).
The best practice is to wrap unsafe code in safe functions with documented invariants:
fn push ( &mut self , value : T ) {
if self . len == self . cap {
std :: ptr :: write ( self . ptr . add ( self . len), value);
The caller of push does not need unsafe — the safety invariant is maintained internally.
The Send and Sync traits are auto-implemented by the compiler. Manually implementing them is unsafe because you are asserting thread safety that the compiler cannot verify:
unsafe impl Send for MyType {}
unsafe impl Sync for MyType {}
Only do this when you can rigorously prove thread safety. This requires that the raw Pointer is only accessed through a synchronization mechanism (mutex, atomic, etc.) that the compiler Cannot see.Implementing the global allocator requires unsafe:
use std :: alloc :: { GlobalAlloc , Layout , System };
unsafe impl GlobalAlloc for MyAllocator {
unsafe fn alloc ( & self , layout : Layout ) -> *mut u8 {
unsafe fn dealloc ( & self , ptr : *mut u8 , layout : Layout ) {
System . dealloc (ptr, layout)
static ALLOCATOR : MyAllocator = MyAllocator ;
fn abs (input : i32 ) -> i32 ;
fn malloc (size : usize ) -> *mut u8 ;
fn printf (format : *const i8 , ... ) -> i32 ;
pub extern "C" fn rust_add (a : i32 , b : i32 ) -> i32 {
pub extern "C" fn rust_greet (name : *const i8 ) {
let name_str = std :: ffi :: CStr :: from_ptr (name);
println! ( "hello, {}" , name_str . to_str () . unwrap ());
use std :: ffi :: { CString , CStr };
fn rust_to_c (s : & str ) -> CString {
CString :: new (s) . expect ( "CString::new failed — contains null byte" )
fn c_to_rust <' a >(s : & ' a CStr ) -> & ' a str {
s . to_str () . expect ( "invalid UTF-8" )
let c_string = rust_to_c ( "hello" );
let rust_str = c_to_rust (c_string . as_c_str ());
assert_eq! (rust_str, "hello" );
cbindgen generates C header files from Rust FFI declarations:
[ package . metadata . cbindgen ]
cbindgen --config cbindgen.toml --crate my_lib --output my_lib.h
Rust extern "C" functions must not panic across the FFI boundary C strings are null-terminated; Rust strings are not. Use CString/CStr C does not have Move semantics. Rust values passed to C must be Copy or leaked C does not have destructors. Resources allocated by Rust and passed to C must be freed manually or through a callback The ABI must match — "C" is the most portable, but platform-specific ABIs exist Unsafe code is sound if it maintains the following invariants:
No null dereferences . Raw pointers are checked for null before dereferencingNo dangling pointers . Pointers reference valid memoryNo aliasing violations . No &T and &mut T to the same data simultaneouslyNo out-of-bounds access . Pointer arithmetic stays within allocation boundsNo data races . Concurrent access is properly synchronizedNo use-after-free . Memory is not accessed after being deallocatedWrap unsafe code in safe abstractions with documented preconditions:
struct BoundedSlice <' a , T > {
_marker : std :: marker :: PhantomData < & ' a T >,
impl <' a , T > BoundedSlice <' a , T > {
fn new (data : & ' a [ T ]) -> Self {
_marker : std :: marker :: PhantomData ,
fn get ( & self , index : usize ) -> Option < & ' a T > {
unsafe { Some ( &* self . ptr . add (index)) }
The caller never uses unsafe — the safe abstraction enforces bounds checking.
Understanding how Vec works internally is essential for writing unsafe code correctly:
┌──────────────────────────────────────────────┐
│ ┌──────────┬────────┬──────────┐ │
│ │ ptr │ len │ capacity │ │
│ └────┼─────┴────────┴──────────┘ │
│ ┌───┬───┬───┬───┬───┬───┬───┬───┐ │
│ │ 0 │ 1 │ 2 │ 3 │ │ │ │ │ │
│ └───┴───┴───┴───┴───┴───┴───┴───┘ │
│ ←──────── capacity ──────────────→ │
└──────────────────────────────────────────────┘
Arena allocation allocates from a contiguous memory region. All allocations are freed at once when The arena is dropped. This eliminates per-allocation deallocation overhead and ensures all References have the same lifetime:
fn new (capacity : usize ) -> Self {
fn alloc < T >( &mut self , value : T ) -> &mut T {
let align = std :: mem :: align_of :: < T >();
let size = std :: mem :: size_of :: < T >();
let offset = ( self . len + align - 1 ) & ! (align - 1 );
assert! (offset + size <= self . data . len (), "arena exhausted" );
let ptr = self . data . as_mut_ptr () . add (offset) as *mut T ;
self . len = offset + size;
use std :: collections :: HashMap ;
strings : RefCell < HashMap < & ' a str , & ' a str >>,
storage : RefCell < Vec < String >>,
strings : RefCell :: new ( HashMap :: new ()),
storage : RefCell :: new ( Vec :: new ()),
fn intern ( & self , s : & str ) -> & str {
if let Some ( & cached) = self . strings . borrow () . get (s) {
let owned = s . to_string ();
let leaked : & ' a str = unsafe {
let ptr = owned . as_ptr ();
std :: str :: from_utf8_unchecked ( std :: slice :: from_raw_parts (ptr, len))
self . storage . borrow_mut () . push (leaked . to_string ());
self . strings . borrow_mut () . insert (leaked, leaked);
A lifetime tied to the arena, which is correct as long as the arena outlives all interned References. If the arena is dropped while interned references exist, they become dangling.Undefined behavior (UB) means the compiler is free to assume the undefined operation never happens And can optimize based on that assumption. UB in Rust includes:
Dereferencing a null pointer Dereferencing a dangling pointer (use-after-free) Creating two mutable references to the same data Reading uninitialized memory (except for MaybeUninit) Out-of-bounds array/vector access (via unchecked indexing) Calling extern "C" functions that violate their documented requirements Integer overflow in release mode (wraps, but is defined behavior) Data races (concurrent unsynchronized access) miri is an interpreter for Rust’s mid-level IR (MIR) that detects undefined behavior:
miri detects:
Use of uninitialized memory Out-of-bounds access Invalid memory alignment Data races Null pointer dereferences Invalid enum discriminants Test the safe abstraction, not the unsafe internals:
fn test_bounded_slice_valid_index () {
let slice = BoundedSlice :: new ( & data);
assert_eq! (slice . get ( 0 ), Some ( & 10 ));
assert_eq! (slice . get ( 2 ), Some ( & 30 ));
fn test_bounded_slice_out_of_bounds () {
let slice = BoundedSlice :: new ( & data);
assert_eq! (slice . get ( 3 ), None );
assert_eq! (slice . get ( 100 ), None );
let mut arena = Arena :: new ( 1024 );
let b = arena . alloc ( "hello" );
Run all tests under miri to catch undefined behavior that unit tests might miss.
Fuzz testing with cargo-fuzz or proptest is especially valuable for unsafe code because it Exercises edge cases that hand-written tests may miss.
FFI . Calling C functions or exposing Rust functions to CPerformance-critical code . After profiling shows a bottleneckImplementing safe abstractions — Vec``String``Box are all implemented with unsafeInterfacing with hardware . Memory-mapped I/O, raw device accessCustom allocators . Implementing GlobalAllocBypassing the borrow checker . If the borrow checker rejects your code, redesign the data flow. unsafe to bypass borrow checking almost always introduces soundness bugs.Premature optimization . Benchmark first, use unsafe only when profiling shows it is necessary.Raw pointers for convenience . Use references and smart pointers unless you have a specific reason for raw pointers.Dereferencing null pointers. Always check ptr.is_null() before dereferencing. Use ptr.as_ref() which returns Option<&T> and handles null safely.
Use-after-free through raw pointers. A raw pointer may outlive the data it points to. The compiler does not track this — you must ensure the pointer’s lifetime does not exceed the data’s lifetime.
Aliasing violations. Creating &T and &mut T to the same data is UB, even through raw pointers. The unsafe block does not exempt you from Rust’s aliasing rules.
Uninitialized memory. MaybeUninit<T> is the correct way to work with uninitialized memory. Reading from uninitialized memory is UB, even for u8.
Panic across FFI. A Rust panic unwinding across a C callback is UB. Use std::panic::catch_unwind at FFI boundaries or compile with panic = "abort".
Not using miri. Any code using unsafe should be tested with miri to catch undefined behavior that may not manifest in normal testing.
Over-large unsafe blocks. Keep unsafe blocks as small as possible. Each block should contain exactly the operations that require unsafeWith clear comments explaining why they are safe.
Assuming layout. Unless #[repr(C)] is specified, the compiler may reorder struct fields and add padding. Do not rely on field order or offset calculations without explicit repr.
Thread safety assertions without proof. Manually implementing Send or Sync without a rigorous proof of thread safety is a common source of data races. Document the proof.
Ignoring #[no_mangle] for FFI. Without #[no_mangle]Rust mangles function names, making them inaccessible from C. Always use #[no_mangle] on extern "C" functions that C code calls.
graph TD
A[Writing unsafe code] --> B{Is this the minimal unsafe surface?}
B -->|No| C[Reduce unsafe scope]
B -->|Yes| D{Are invariants documented?}
D -->|No| E[Document preconditions and invariants]
D -->|Yes| F{Is memory validity guaranteed?}
F -->|No| G[Add null checks, bounds checks]
F -->|Yes| H{No aliasing violations?}
H -->|No| I[Ensure exclusive access where needed]
H -->|Yes| J{No UB from data races?}
J -->|No| K[Add synchronization]
J -->|Yes| L[Tested with miri?}
L -->|No| M[Run cargo +nightly miri test]
L -->|Yes| N[Fuzz tested?}
N -->|No| O[Add fuzz testing]
N -->|Yes| P[Code is sound] MaybeUninit<T> is the correct way to work with uninitialized memory. It prevents reading Uninitialized values and is the foundation for manually constructing types without calling their Constructors:
use std :: mem :: MaybeUninit ;
let mut data = MaybeUninit :: <[ u8 ; 1024]> :: uninit ();
data . as_mut_ptr () . write_bytes ( 0 , 1 );
data : data . assume_init (),
Byte of the `MaybeUninit` has been written to before calling `assume_init()`. Use `write_bytes` Individual `write()` calls, or `ptr::copy_nonoverlapping` to initialize the memory.Creating an array of non-Copy types without calling Default:
use std :: mem :: MaybeUninit ;
fn create_array < T >(count : usize , init : impl Fn ( usize ) -> T ) -> Vec < T > {
let mut items : Vec < MaybeUninit < T >> = Vec :: with_capacity (count);
items . push ( MaybeUninit :: new ( init (i)));
let items = std :: mem :: transmute :: < Vec < MaybeUninit < T >>, Vec < T >>(items);
let items = create_array ( 10 , | i | format! ( "item_{}" , i));
assert_eq! (items . len (), 10 );
assert_eq! (items[ 5 ], "item_5" );
When you allocate memory manually, you must deallocate it. Implement Drop to ensure cleanup:
use std :: alloc :: {alloc, dealloc, Layout };
fn new (size : usize ) -> Self {
let layout = Layout :: array :: < u8 >(size) . unwrap ();
let ptr = unsafe { alloc (layout) };
std :: alloc :: handle_alloc_error (layout);
ManualBuffer { ptr, layout }
fn as_slice ( & self ) -> & [ u8 ] {
unsafe { std :: slice :: from_raw_parts ( self . ptr, self . layout . size ()) }
impl Drop for ManualBuffer {
dealloc ( self . ptr, self . layout);
std::mem::transmute reinterprets the bits of one type as another. It is extremely dangerous and Should be avoided when alternatives exist:
// Dangerous — use only when you understand the exact bit layout
let bytes : [ u8 ; 4 ] = unsafe { std :: mem :: transmute (a) };
assert_eq! (bytes, [ 0x78 , 0x56 , 0x34 , 0x12 ]); // little-endian
Prefer safe alternatives:
// Safe alternative: use to_be_bytes / to_le_bytes
let bytes = a . to_le_bytes ();
assert_eq! (bytes, [ 0x78 , 0x56 , 0x34 , 0x12 ]);
// Safe alternative: use bytemuck or zerocopy crates
// These provide checked transmute operations
When implementing custom data structures, you may need to access struct fields through raw pointers:
fn append ( &mut self , value : i32 ) {
let new_node = Box :: into_raw ( Box :: new ( Node {
next : std :: ptr :: null_mut (),
let mut current = self as *mut Node ;
while ! ( * current) . next . is_null () {
current = ( * current) . next;
( * current) . next = new_node;
let mut current = self . next;
while ! current . is_null () {
let boxed = Box :: from_raw (current);
Vec::set_len() is unsafe because it changes the logical length without initializing the elements:
use std :: mem :: MaybeUninit ;
fn uninitialized_vec (len : usize ) -> Vec < u8 > {
let mut v = Vec :: with_capacity (len);
The elements from 0..len are uninitialized. Reading them is UB. Use MaybeUninit instead.
Creating a slice from a raw pointer and length is unsafe because you must guarantee that:
The pointer is valid for len elements The pointer is properly aligned The memory is not mutated by anything else during the slice’s lifetime fn split_at_mid (data : &mut [ i32 ], mid : usize ) -> ( &mut [ i32 ], &mut [ i32 ]) {
let ptr = data . as_mut_ptr ();
slice :: from_raw_parts_mut (ptr, mid),
slice :: from_raw_parts_mut (ptr . add (mid), len - mid),
Convert a Vec<u8> to a String without validating UTF-8:
fn from_ascii (s : & [ u8 ]) -> String {
assert! (s . iter () . all ( |& b | b < 128 ), "not ASCII" );
unsafe { String :: from_utf8_unchecked (s . to_vec ()) }
Only use this when you can prove the bytes are valid UTF-8. The assertion above checks ASCII (which Is a subset of UTF-8), so the conversion is safe.
The cxx crate provides safe C++ interoperability:
fn my_function (obj : & MyClass ) -> i32 ;
fn rust_callback (value : i32 );
fn rust_callback (value : i32 ) {
println! ( "callback from C++: {}" , value);
bindgen generates Rust bindings from C header files:
bindgen mylib.h -o bindings.rs
Every unsafe block should have a comment explaining:
What the unsafe code does Why it is safe (which invariants are upheld) What would break if the invariants were violated // SAFETY: `ptr` was allocated with `alloc(layout)` in `new()` and has not been freed.
// The layout is the same as used for allocation, so deallocating with the same layout
// is correct. The pointer is non-null because we checked in `new()`.
dealloc ( self . ptr, self . layout);
When reviewing code that uses unsafeVerify:
The unsafe surface is as small as possible Every unsafe block has a SAFETY comment All pointer operations are bounds-checked No aliasing violations (no &T and &mut T to the same data) All memory is properly initialized before use All memory is properly deallocated (no leaks) No data races in concurrent code FFI boundaries handle panics correctly The code has been tested with miri The code has been fuzz tested for edge cases This topic covers the biological principles of unsafe rust, including key concepts, experimental evidence, and real-world applications.
Key concepts include:
key biological principles and concepts experimental methods and data analysis applications of biology in medicine and industry ethical considerations in biological research the relationship between structure and function Success requires the ability to recall specific factual content, apply knowledge to novel scenarios, and evaluate experimental evidence critically.
Worked examples demonstrating the application of key concepts are covered in the detailed sub-pages linked above.
## Intuition
Unsafe Rust lets you bypass the borrow checker’s guarantees when you can prove safety manually. Raw pointers, unsafe function calls, and trait implementations require unsafe blocks. FFI (Foreign Function Interface) uses unsafe to call C code. The key insight is that unsafe does not disable the type system; it adds five additional capabilities that the compiler cannot verify automatically. Properly encapsulated unsafe code behind safe abstractions maintains the overall safety guarantee.
[[rust/02-ownership-borrowing/ownership]] - What unsafe bypasses [[rust/05-traits-generics/traits-and-generics]] - Unsafe trait implementations [[rust/06-concurrency/concurrency]] - Unsafe Send and Sync implementations [[rust/07-cargo-ecosystem/cargo-and-ecosystem]] - FFI and system programming patterns