Unsafe Rust Explained:
When and How to Use It

Last updated: April 20, 2025

1. Introduction: Stepping Beyond Safety

Rust's core promise is memory safety, guaranteed at compile time through its ownership and borrowing rules. This prevents entire classes of bugs common in languages like C and C++. However, there are situations where these strict rules are too restrictive or where Rust needs to interact with lower-level systems or non-Rust code.

For these cases, Rust provides the unsafe keyword. It allows you to bypass some of the compiler's safety checks, essentially telling the compiler, "Trust me, I know what I'm doing here, and I guarantee this code is safe." Using unsafe doesn't disable the borrow checker entirely, but it unlocks a few extra "superpowers" that are inherently risky if misused.

This article explains what unsafe Rust is, when you might need it, and how to use it responsibly.

2. What Does unsafe Mean?

Using the unsafe keyword signifies that you, the programmer, are taking responsibility for upholding Rust's memory safety guarantees manually within that specific scope. The compiler cannot verify the safety of operations performed inside an unsafe block or function.

It's crucial to understand that unsafe does not turn off all safety checks. The ownership and borrowing rules still apply within unsafe blocks. It only grants access to a small set of operations that have the *potential* to violate memory safety if not used correctly.

3. When is unsafe Necessary?

You can only perform five specific actions within an unsafe context that are not allowed in safe Rust. These are often called the "unsafe superpowers":

3.1 Dereferencing Raw Pointers

Rust has references (&T, &mut T) which are guaranteed to be valid. It also has raw pointers (*const T, *mut T) which are similar to pointers in C/C++. Raw pointers:

  • Can be null.
  • Are allowed to point to invalid memory or ignore borrowing rules.
  • Are not automatically cleaned up (no RAII).
  • Do not guarantee immutability or exclusivity.

Creating raw pointers is safe, but dereferencing them (reading or writing the data they point to) requires an unsafe block because the compiler cannot guarantee their validity.

fn main() {
    let mut num = 5;

    // Create raw pointers (safe)
    let r1 = &num as *const i32;
    let r2 = &mut num as *mut i32;

    // Dereference raw pointers (unsafe)
    unsafe {
        println!("r1 points to: {}", *r1);
        *r2 = 10; // Write through mutable raw pointer
        println!("num is now: {}", *r1);
    }
}

3.2 Calling Unsafe Functions or Methods (including FFI)

Some functions or methods are marked as unsafe fn because calling them might violate safety invariants if their documented requirements (preconditions) are not met. Calling such functions requires an unsafe block.

The most common use case is the Foreign Function Interface (FFI), used to call functions written in other languages (like C). Since the Rust compiler cannot verify the safety of C code, calls to external C functions are inherently unsafe.

// Assuming a C library with `abs` function linked
extern "C" {
    fn abs(input: i32) -> i32;
}

fn main() {
    let number = -5;
    // Calling the external C function `abs` requires unsafe
    unsafe {
        println!("Absolute value of {} is {}", number, abs(number));
    }
}

See our Rust FFI Guide for more details.

3.3 Accessing or Modifying Mutable Static Variables

Rust allows global static variables, but mutable static variables (static mut) pose a risk of data races if accessed concurrently from multiple threads. Therefore, reading or writing a static mut requires an unsafe block. Using thread-safe wrappers like Mutex or RwLock is generally preferred over static mut.

static mut COUNTER: u32 = 0;

fn increment_counter() {
    // Accessing and modifying static mut requires unsafe
    unsafe {
        COUNTER += 1;
    }
}

fn main() {
    increment_counter();
    unsafe {
        println!("COUNTER: {}", COUNTER); // Reading also requires unsafe
    }
}

3.4 Implementing Unsafe Traits

A trait can be marked as unsafe trait if at least one of its methods requires uphold safety invariants that the compiler cannot verify. Implementing an unsafe trait requires the unsafe keyword on the implementation block (unsafe impl ...).

The standard library traits Send and Sync are examples; they are automatically derived by the compiler for most types, but implementing them manually is unsafe because you must guarantee thread safety properties.

3.5 Accessing Fields of unions

Rust has unions similar to C, where multiple fields share the same memory location. Accessing fields of a union is unsafe because Rust cannot guarantee which field variant is currently active and valid in memory.

4. How to Use unsafe: Blocks and Functions

There are two ways to introduce an unsafe context:

4.1 unsafe Blocks

Wrap the specific unsafe operations within an unsafe { ... } block. This is the preferred way to limit the scope where safety rules are relaxed.

let some_address = 0x12345usize;
let r = some_address as *const i32;

// Keep the unsafe scope as small as possible
unsafe {
    println!("Value at address {:#x}: {}", some_address, *r);
}

4.2 unsafe fn

Mark an entire function as unsafe using unsafe fn my_function() { ... }. This signifies that calling this function itself is an unsafe operation because it might violate memory safety if its preconditions aren't met. The body of an unsafe fn implicitly behaves like an unsafe block, allowing unsafe operations within it.

// This function has preconditions that must be met by the caller
unsafe fn read_at_address(addr: usize) -> i32 {
    let ptr = addr as *const i32;
    // Unsafe operation allowed because we are inside `unsafe fn`
    *ptr
}

fn main() {
    // Calling an `unsafe fn` requires an `unsafe` block
    unsafe {
        let value = read_at_address(0x12345);
        println!("Read value: {}", value);
        // Warning: This is likely to crash if 0x12345 is not a valid address!
    }
}

5. Best Practices: Minimizing and Encapsulating unsafe

While unsafe is necessary sometimes, its use should be minimized.

  • Keep unsafe blocks small: Only wrap the minimal code requiring unsafe operations.
  • Document invariants: Clearly explain *why* the unsafe code is necessary and what conditions must hold for it to be safe.
  • Build safe abstractions: If possible, wrap unsafe code within a safe function or module that provides a safe API, ensuring all safety invariants are upheld internally. The standard library does this extensively (e.g., Vec uses unsafe code internally but provides a safe interface).

6. Conclusion

The unsafe keyword is Rust's escape hatch, allowing interaction with lower levels of the system, FFI, and performance optimizations that the compiler cannot verify. It grants access to potentially dangerous operations like dereferencing raw pointers and calling unsafe functions.

Use unsafe sparingly, justify its use clearly, keep its scope minimal, and strive to build safe abstractions around it. By doing so, you can leverage Rust's full power while still benefiting from its strong safety guarantees in the majority of your codebase.

7. Additional Resources

Related Articles

External Links