Skip to main content
โšก Calmops

Unsafe Rust: When & How to Use It Correctly

Introduction

Unsafe Rust allows you to disable certain safety checks when necessary. It’s not about writing dangerous codeโ€”it’s about writing code whose safety Rust’s compiler cannot verify automatically.

This guide covers when, where, and how to use unsafe Rust correctly.


The Purpose of Unsafe

What Unsafe Does NOT Do

// โŒ MYTH: unsafe disables safety checks entirely
// โœ… TRUTH: unsafe lets you assert safety Rust can't prove

unsafe {
    let x = 5;
    let y = x + 1;  // Still safe - no bounds checking needed
}

// โŒ MYTH: unsafe code is faster
// โœ… TRUTH: unsafe only removes checks Rust couldn't optimize away

What Unsafe Enables

Safe Rust prevents:
โœ… Data races
โœ… Buffer overflows
โœ… Use-after-free
โœ… Null pointer dereferences
โœ… Type confusion

Unsafe allows accessing:
โš ๏ธ  Raw pointers
โš ๏ธ  Calling unsafe functions
โš ๏ธ  Mutating statics
โš ๏ธ  Implementing unsafe traits

Raw Pointers

Creating Raw Pointers

let x = 5;
let raw_ptr: *const i32 = &x as *const i32;
let mut y = 10;
let mutable_raw: *mut i32 = &mut y as *mut i32;

// Raw pointers can be null
let null_ptr: *const i32 = std::ptr::null();
let null_mut: *mut i32 = std::ptr::null_mut();

Dereferencing Raw Pointers

let x = 5;
let raw_ptr: *const i32 = &x as *const i32;

// Dereferencing requires unsafe
unsafe {
    println!("Value: {}", *raw_ptr);  // OK
}

// This is undefined behavior:
let dangling = unsafe {
    let local = 5;
    &local as *const i32  // Pointer to stack variable
};

unsafe {
    println!("{}", *dangling);  // UB: stack memory freed
}

Pointer Arithmetic

let arr = [1, 2, 3, 4, 5];
let ptr = arr.as_ptr();

unsafe {
    println!("{}", *ptr);        // 1
    println!("{}", *ptr.add(2)); // 3
    println!("{}", *ptr.add(4)); // 5
    // println!("{}", *ptr.add(5)); // Out of bounds!
}

Unsafe Functions

Defining Unsafe Functions

// Function that requires careful preconditions
unsafe fn dangerous_function(ptr: *const i32) -> i32 {
    *ptr  // Assumes ptr is valid and aligned
}

fn main() {
    let x = 5;
    let result = unsafe {
        dangerous_function(&x as *const i32)
    };
    println!("{}", result);
}

When to Use Unsafe Functions

// Good: Document the safety preconditions
/// Splits a slice at an index without bounds checking.
/// 
/// # Safety
/// The caller must ensure `index <= len(slice)`
/// Otherwise undefined behavior will occur.
unsafe fn split_at_unchecked<T>(
    slice: &[T], 
    index: usize
) -> (&[T], &[T]) {
    let ptr = slice.as_ptr();
    (
        std::slice::from_raw_parts(ptr, index),
        std::slice::from_raw_parts(ptr.add(index), slice.len() - index),
    )
}

// Safe wrapper
fn split_at_safe<T>(slice: &[T], index: usize) -> (&[T], &[T]) {
    assert!(index <= slice.len(), "index out of bounds");
    unsafe { split_at_unchecked(slice, index) }
}

Foreign Function Interface (FFI)

Calling C from Rust

// Declare C function
extern "C" {
    fn strlen(s: *const u8) -> usize;
    fn printf(fmt: *const u8, ...) -> i32;
}

fn main() {
    unsafe {
        let c_string = b"Hello\0";
        let len = strlen(c_string.as_ptr());
        println!("Length: {}", len);
    }
}

Calling Rust from C

// In Rust library code
#[no_mangle]
pub extern "C" fn add_numbers(a: i32, b: i32) -> i32 {
    a + b
}

// Callable from C:
// int add_numbers(int a, int b);

Safe FFI Wrapper

use std::ffi::CStr;

extern "C" {
    fn strlen(s: *const u8) -> usize;
}

// Safe Rust wrapper
fn rust_strlen(s: &CStr) -> usize {
    unsafe { strlen(s.as_ptr() as *const u8) }
}

fn main() {
    let c_str = CStr::from_bytes_with_nul(b"Hello\0").unwrap();
    let len = rust_strlen(c_str);
    println!("Length: {}", len);
}

Mutable Statics

Interior Mutability with Statics

static COUNTER: std::sync::atomic::AtomicU32 = 
    std::sync::atomic::AtomicU32::new(0);

fn increment_counter() {
    COUNTER.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
}

fn main() {
    increment_counter();
    let value = COUNTER.load(std::sync::atomic::Ordering::SeqCst);
    println!("Counter: {}", value);
}

Mutable Static (Dangerous)

static mut GLOBAL_STATE: i32 = 0;

fn set_global(value: i32) {
    unsafe {
        GLOBAL_STATE = value;
    }
}

fn get_global() -> i32 {
    unsafe {
        GLOBAL_STATE
    }
}

fn main() {
    set_global(42);
    println!("Global: {}", get_global());
}
// โš ๏ธ  Data races possible with threads!

Unsafe Traits

Implementing Unsafe Traits

// A trait that can be unsafe to implement
unsafe trait Send {}

// Only implement if truly safe
struct MyType;
unsafe impl Send for MyType {}

// This won't compile without unsafe:
// impl Send for MyType {}  // ERROR

Common Unsafe Traits

// Types that are safe to send between threads
unsafe trait Send {}

// Types safe to share references across threads
unsafe trait Sync {}

// Usually auto-implemented by the compiler
// But can implement manually for custom types

Real-World Example: Memory Pool

/// Thread-safe memory pool using unsafe
pub struct MemoryPool {
    buffer: Vec<u8>,
    allocations: std::sync::Mutex<Vec<(usize, usize)>>,
}

impl MemoryPool {
    pub fn new(size: usize) -> Self {
        MemoryPool {
            buffer: vec![0; size],
            allocations: std::sync::Mutex::new(Vec::new()),
        }
    }

    pub fn allocate(&self, size: usize) -> Option<*mut [u8]> {
        let mut allocs = self.allocations.lock().unwrap();
        
        let mut offset = 0;
        for (alloc_offset, alloc_size) in allocs.iter() {
            if *alloc_offset - offset >= size {
                // Found gap
                allocs.push((offset, size));
                unsafe {
                    let ptr = self.buffer.as_ptr().add(offset) as *mut u8;
                    return Some(std::slice::from_raw_parts_mut(ptr, size));
                }
            }
            offset = alloc_offset + alloc_size;
        }
        
        None
    }

    pub fn deallocate(&self, offset: usize) {
        let mut allocs = self.allocations.lock().unwrap();
        allocs.retain(|(o, _)| *o != offset);
    }
}

Checking Unsafe Code

Common Mistakes

// โŒ WRONG: Undefined behavior
fn bad_example() {
    let x = 5;
    let ptr = &x as *const i32;
    // x is dropped here
    
    unsafe {
        println!("{}", *ptr);  // UB: use-after-free
    }
}

// โœ… CORRECT: Verify validity
fn good_example() {
    let x = 5;
    let ptr = &x as *const i32;
    
    unsafe {
        println!("{}", *ptr);  // OK: x still in scope
    }
}

Safety Documentation Template

/// # Safety
/// The caller must ensure:
/// - `ptr` is valid and properly aligned
/// - `ptr` points to initialized data
/// - No other mutable references to this data exist
/// - The operation won't cause integer overflow
unsafe fn unsafe_operation(ptr: *const i32) -> i32 {
    *ptr
}

Performance Impact

Unsafe Optimization Example

// Safe version (bounds checked)
fn safe_sum(arr: &[i32]) -> i32 {
    arr.iter().sum()
}

// Unsafe version (no bounds check)
fn unsafe_sum(arr: &[i32]) -> i32 {
    let mut sum = 0;
    let mut ptr = arr.as_ptr();
    let end = unsafe { ptr.add(arr.len()) };
    
    unsafe {
        while ptr != end {
            sum += *ptr;
            ptr = ptr.add(1);
        }
    }
    sum
}

// Benchmark: 99% identical performance after optimization
// Rust's optimizer can often remove redundant bounds checks

Best Practices

Do’s โœ…

โœ… Use unsafe for FFI (C interop)
โœ… Encapsulate unsafe in safe abstractions
โœ… Document all safety requirements
โœ… Use `#[cfg(test)]` to test unsafe code
โœ… Consider using existing safe crates instead
โœ… Minimize unsafe blocks

Don’ts โŒ

โŒ Don't use unsafe to bypass type system
โŒ Don't ignore compiler warnings
โŒ Don't assume performance without benchmarking
โŒ Don't write unsafe without documentation
โŒ Don't leak abstractions (expose raw pointers)
โŒ Don't use unsafe for style

Testing Unsafe Code

Unit Tests for Unsafe

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_safe_wrapper() {
        let arr = [1, 2, 3, 4, 5];
        let (left, right) = split_at_safe(&arr, 2);
        assert_eq!(left, &[1, 2]);
        assert_eq!(right, &[3, 4, 5]);
    }

    #[test]
    #[should_panic]
    fn test_bounds_check() {
        let arr = [1, 2, 3];
        split_at_safe(&arr, 10);  // Should panic
    }
}

Glossary

  • Unsafe: Code block that disables some safety checks
  • Raw Pointer: Direct memory address without ownership
  • FFI: Foreign Function Interface (calling C/other languages)
  • Undefined Behavior: Violation of safety guarantees
  • Precondition: Requirement caller must guarantee

Resources


Comments