Unsafe Rust â When and How to Use It
Unsafe Rust lets you perform operations the compiler cannot verify, such as dereferencing raw pointers and calling foreign functions, while requiring you to uphold four core safety guarantees yourself.
What You'll Learn
In this tutorial, you'll learn when and how to use Rust unsafe code, the four unsafe superpowers, raw pointer manipulation, calling C functions via FFI, implementing unsafe traits, and how to safely encapsulate unsafe operations.
Why It Matters
Systems programming often requires low-level operations: direct memory access, hardware registers, external C libraries. Rust's safe subset cannot express these. Unsafe Rust bridges this gap while maintaining the safety guarantees of the surrounding safe code when used correctly.
Real-World Use
The Linux kernel's Rust bindings use unsafe to interact with C APIs. Embedded drivers use unsafe for memory-mapped I/O. Allocators like jemalloc use unsafe for heap management. Durga Antivirus Pro uses unsafe for zero-copy memory scanning of raw disk sectors.
flowchart TD
SAFE[Safe Rust Code] --> UNSAFE[Unsafe Block]
UNSAFE --> OP1[Dereference raw pointer]
UNSAFE --> OP2[Call unsafe function]
UNSAFE --> OP3[Access/modify static mut]
UNSAFE --> OP4[Implement unsafe trait]
OP1 --> INV1{Invariants upheld?}
INV1 -->|Yes| RESULT[Correct behavior]
INV1 -->|No| UB[Undefined behavior]
Prerequisites: Rust Ownership, Borrowing, and Lifetimes.
The Four Unsafe Superpowers
Unsafe code gives you four capabilities that safe Rust cannot:
- Dereference a raw pointer (
*const T,*mut T) - Call an unsafe function or method (including FFI)
- Access or modify a mutable static variable
- Implement an unsafe trait
fn main() {
let mut num = 42;
// Raw pointer from reference
let r1 = &num as *const i32;
let r2 = &mut num as *mut i32;
// Unsafe block to dereference
unsafe {
println!("r1 points to: {}", *r1);
*r2 = 100;
println!("r2 wrote: {}", *r1);
}
println!("num is now: {}", num);
}
Expected output:
r1 points to: 42
r2 wrote: 100
num is now: 100
Raw Pointers vs References
Raw pointers differ from references: they can be null, they can alias, and they ignore borrow checker rules.
fn main() {
let values = vec![10, 20, 30, 40, 50];
let ptr = values.as_ptr();
unsafe {
// Pointer arithmetic
for i in 0..5 {
let val = *ptr.add(i);
println!("values[{}] = {}", i, val);
}
// Offset from end
let last = *ptr.add(4);
println!("Last: {}", last);
}
}
Expected output:
values[0] = 10
values[1] = 20
values[2] = 30
values[3] = 40
values[4] = 50
Last: 50
Mutable Static Variables
Safe Rust cannot access mutable statics due to data race risk.
static mut COUNTER: u32 = 0;
fn increment_counter() {
unsafe {
COUNTER += 1;
}
}
fn read_counter() -> u32 {
unsafe { COUNTER }
}
fn main() {
increment_counter();
increment_counter();
increment_counter();
println!("Counter: {}", read_counter());
}
Expected output:
Counter: 3
Calling C Functions (FFI)
Unsafe is required for calling functions from C libraries.
extern "C" {
fn abs(input: i32) -> i32;
fn strlen(s: *const u8) -> usize;
}
fn main() {
unsafe {
println!("abs(-5) = {}", abs(-5));
println!("abs(10) = {}", abs(10));
let s = "Hello, FFI!\0";
let len = strlen(s.as_ptr());
println!("strlen: {}", len);
}
}
Expected output:
abs(-5) = 5
abs(10) = 10
strlen: 11
Implementing Unsafe Traits
Some traits are unsafe because implementing them incorrectly leads to undefined behavior.
use std::mem;
// Custom allocator trait (simplified)
unsafe trait MyAllocator {
fn allocate(&self, size: usize) -> *mut u8;
fn deallocate(&self, ptr: *mut u8, size: usize);
}
struct SimpleAllocator;
unsafe impl MyAllocator for SimpleAllocator {
fn allocate(&self, size: usize) -> *mut u8 {
let layout = std::alloc::Layout::from_size_align(size, 1).unwrap();
unsafe { std::alloc::alloc(layout) }
}
fn deallocate(&self, ptr: *mut u8, size: usize) {
if !ptr.is_null() {
let layout = std::alloc::Layout::from_size_align(size, 1).unwrap();
unsafe { std::alloc::dealloc(ptr, layout) }
}
}
}
fn main() {
let allocator = SimpleAllocator;
unsafe {
let ptr = allocator.allocate(64);
if !ptr.is_null() {
*ptr = 42;
println!("Allocated: {}", *ptr);
allocator.deallocate(ptr, 64);
}
}
}
Expected output:
Allocated: 42
Encapsulating Unsafe Safely
The key pattern: write unsafe code inside a safe function that verifies invariants.
/// Safe abstraction over unsafe pointer arithmetic
fn split_at<T>(slice: &[T], mid: usize) -> Option<(&[T], &[T])> {
if mid > slice.len() {
return None;
}
unsafe {
let ptr = slice.as_ptr();
// Safe because mid <= len and pointer arithmetic is valid
let left = std::slice::from_raw_parts(ptr, mid);
let right = std::slice::from_raw_parts(ptr.add(mid), slice.len() - mid);
Some((left, right))
}
}
fn main() {
let data = [1, 2, 3, 4, 5, 6];
if let Some((left, right)) = split_at(&data, 3) {
println!("Left: {:?}", left);
println!("Right: {:?}", right);
}
}
Expected output:
Left: [1, 2, 3]
Right: [4, 5, 6]
Unsafe and Security
Unsafe code is the primary source of security vulnerabilities in Rust programs. Every unsafe block should be:
- Minimal: Use the smallest possible unsafe block
- Audited: Review for pointer validity, alignment, and aliasing
- Encapsulated: Hidden behind safe APIs that verify invariants
- Documented: Include safety comments explaining preconditions
Durga Antivirus Pro's disk scanning engine uses unsafe only for raw sector access, with all pointer operations verified against sector boundaries before dereferencing.
Common Mistakes
1. Dangling Pointers from Dropped Values
Storing a raw pointer to a value that goes out of scope creates a dangling pointer. Ensure the pointed-to value outlives all raw pointers.
2. Misaligned Accesses
Creating a pointer to an unaligned address causes undefined behavior on many architectures. Always verify alignment.
3. Violating Pointer Aliasing Rules
Mutable and immutable references cannot overlap. Raw pointers can, but creating overlapping references from them violates Rust's aliasing rules.
4. Calling Unsafe Functions Without Reading Docs
Every unsafe function has safety preconditions. Read and verify them before calling. Ignoring preconditions leads to undefined behavior.
5. Using transmute Incorrectly
std::mem::transmute reinterprets bytes. Using it between types of different sizes or invalid bit patterns causes UB.
Practice Questions
1. What are the four things you can do in unsafe Rust? Dereference raw pointers, call unsafe functions, access mutable statics, and implement unsafe traits.
*2. What is the difference between const T and &T? A raw pointer can be null, can be created from arbitrary addresses, and ignores borrow checker rules. A reference must always be valid and follows borrowing rules.
3. Why should unsafe be encapsulated in safe functions? To provide a safe API that verifies preconditions, preventing callers from accidentally invoking undefined behavior. The unsafe details are hidden behind a safe interface.
4. What happens if you violate unsafe invariants? Undefined behavior. The program may crash, produce wrong results, or appear to work until a seemingly unrelated change triggers UB.
5. Challenge: Write a safe wrapper around std::mem::transmute that only allows transmuting between types of the same size, returning an error otherwise.
Mini Project: Safe Arena Allocator
use std::cell::RefCell;
struct Arena {
memory: RefCell<Vec<u8>>,
capacity: usize,
}
impl Arena {
fn new(capacity: usize) -> Self {
Arena {
memory: RefCell::new(Vec::with_capacity(capacity)),
capacity,
}
}
fn allocate<T>(&self, value: T) -> Option<&mut T> {
let size = std::mem::size_of::<T>();
let align = std::mem::align_of::<T>();
let mut mem = self.memory.borrow_mut();
let start = mem.len();
let aligned = (start + align - 1) & !(align - 1);
let padding = aligned - start;
let new_len = aligned + size;
if new_len > self.capacity {
return None;
}
mem.resize(new_len, 0);
unsafe {
let ptr = mem.as_mut_ptr().add(aligned) as *mut T;
ptr.write(value);
Some(&mut *ptr)
}
}
}
fn main() {
let arena = Arena::new(1024);
let x = arena.allocate(42).unwrap();
let y = arena.allocate(String::from("hello")).unwrap();
println!("x: {}, y: {}", x, y);
*x = 100;
println!("x modified: {}", x);
}
FAQ
Related Concepts
What's Next
Learn Rust FFI for calling C code, and Embedded Rust for microcontroller programming where unsafe is often required.
Built by the developers of Doda Browser, DodaZIP, and Durga Antivirus Pro.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro