Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Chapter 25: Unsafe Rust

Rust’s core strength lies in its safety guarantees, enforced through compile-time analysis and runtime checks. These mechanisms prevent common programming errors such as null pointer dereferences, buffer overflows, and data races, which frequently plague languages like C and C++. However, the compiler’s safety analysis is inherently conservative; it may reject code that is actually safe but whose safety cannot be proven automatically. Additionally, certain necessary tasks, like direct hardware manipulation or interfacing with code written in other languages (e.g., C libraries via FFI), inherently fall outside the scope of Rust’s verifiable safety model.

To address these scenarios, Rust provides the unsafe keyword. Using unsafe does not switch to a different language but rather enables a specific set of operations forbidden in safe Rust. It acts as a declaration by the programmer: “I have manually verified that the code within this block adheres to Rust’s safety rules, even though the compiler cannot prove it.” This mechanism is crucial. Many fundamental components of the standard library, such as the memory management within Vec<T> or low-level synchronization primitives, rely on unsafe internally, carefully wrapped within safe APIs. This pattern—encapsulating unsafety—is fundamental to building complex systems in Rust without sacrificing overall safety.


25.1 The Unsafe Superset

Safe Rust operates under strict rules (ownership, borrowing, lifetimes, type safety) to prevent undefined behavior (UB). Unsafe Rust provides access to five additional capabilities, sometimes called “unsafe superpowers,” that bypass certain checks:

  1. Dereferencing raw pointers (*const T and *mut T).
  2. Calling unsafe functions (including external functions declared via FFI).
  3. Accessing or modifying static mut variables.
  4. Implementing unsafe traits.
  5. Accessing fields of unions.

Crucially, entering an unsafe context does not disable all of Rust’s safety mechanisms. The borrow checker still operates, ownership rules apply, and type checking is still performed. The unsafe keyword only permits these five specific actions within an unsafe block or function. The responsibility shifts to the programmer to ensure these actions do not violate Rust’s memory safety invariants (e.g., avoiding data races, dangling pointers, invalid pointer arithmetic).

25.1.1 Why Unsafe Rust is Necessary

Despite Rust’s emphasis on safety, the unsafe mechanism is essential for its role as a systems programming language:

  • Hardware Interaction: Direct memory-mapped I/O, register manipulation, or executing specific CPU instructions often requires bypassing safe abstractions.
  • Foreign Function Interface (FFI): Interacting with libraries written in C or other languages involves calling code that Rust’s compiler cannot analyze or verify.
  • Low-Level Data Structures: Implementing certain efficient data structures (e.g., some variants of linked lists, custom allocators, lock-free structures) may require pointer manipulations that are difficult or impossible to express within safe Rust’s constraints.
  • Performance Optimization: In specific, performance-critical sections, manual memory management or pointer operations might offer optimizations beyond what the compiler or safe abstractions provide, although this is less common than the other reasons.

In these situations, the compiler cannot guarantee safety, so the unsafe keyword marks the boundaries where the programmer asserts the code’s correctness regarding Rust’s safety rules.


25.2 Unsafe Blocks and Functions

Operations designated as unsafe can only be performed within contexts explicitly marked by the unsafe keyword.

25.2.1 Unsafe Blocks

An unsafe { ... } block isolates a segment of code containing one or more unsafe operations. This is the most common way to introduce unsafety. It signals that the code within the block might perform actions requiring manual safety verification.

A frequent use case is dereferencing raw pointers. While creating, passing, or comparing raw pointers is safe, reading from or writing to the memory they point to (*ptr) requires an unsafe block. This is because the compiler cannot guarantee that the pointer is valid (i.e., pointing to allocated, initialized, and properly aligned memory of the correct type).

fn main() {
    let mut num: i32 = 42;
    // Creating a raw pointer from a valid reference is safe.
    let r_ptr: *mut i32 = &mut num;

    // Dereferencing the raw pointer requires an unsafe block.
    unsafe {
        println!("Value before: {}", *r_ptr);
        // Modify the value through the raw pointer.
        *r_ptr = 99;
        println!("Value after: {}", *r_ptr);
    }
    // The original variable reflects the change.
    println!("Final value of num: {}", num); // num is now 99
}

In this example, the operation is safe because r_ptr originates from a valid mutable reference &mut num. The unsafe block serves as an annotation that the programmer, not the compiler, is responsible for ensuring this validity.

25.2.2 Unsafe Functions

A function can be declared as unsafe fn if calling it requires the caller to satisfy certain preconditions (invariants) that the compiler cannot enforce through the type system or borrow checker alone. Such functions can perform unsafe operations internally without needing additional unsafe blocks for those specific operations.

However, calling an unsafe fn is itself an unsafe operation and must occur within an unsafe block or another unsafe fn.

// This function is unsafe because dereferencing `ptr` is only valid
// if the caller guarantees `ptr` points to valid, initialized memory.
unsafe fn read_from_pointer(ptr: *const i32) -> i32 {
    unsafe {//Explicit unsafe block for the dereference (recommended by Rust 2024 lint)
        *ptr
    }
}

fn main() {
    let x = 42;
    let ptr = &x as *const i32;

    // Calling an unsafe function requires an unsafe block.
    let value = unsafe {
        read_from_pointer(ptr)
    };
    println!("Value read via unsafe fn: {}", value);
}

The unsafe keyword on the function signature acts as a contract: “Warning: This function relies on preconditions not checked by the compiler. Incorrect usage can lead to undefined behavior. Ensure you meet its documented requirements before calling.”

25.2.3 unsafe fn and Explicit unsafe Blocks in Rust 2024

In previous editions, an unsafe fn implicitly permitted unsafe operations within its body without additional unsafe { ... } blocks. The unsafe keyword on the function served two roles: declaring that calling the function requires unsafe, and allowing unsafe operations inside.

With the Rust 2024 Edition, the unsafe_op_in_unsafe_fn lint now warns by default if unsafe operations are performed directly within an unsafe fn without being enclosed in an explicit unsafe { ... } block. This change helps protect against accidental unsafe usage and encourages minimizing the scope of unsafe operations, making it clearer exactly where the programmer is taking responsibility.

Consider this example:

// An unsafe function that performs an unchecked slice access.
// The `unsafe` keyword on the function means callers need an `unsafe` block.
unsafe fn get_unchecked_val<T>(slice: &[T], index: usize) -> &T {
  // In Rust 2024, the `unsafe_op_in_unsafe_fn` lint will now warn
  // if `slice.get_unchecked(index)` is not wrapped in an `unsafe` block here.
  unsafe { // Explicit unsafe block for the unchecked access.
    slice.get_unchecked(index)
  }
}

fn main() {
    let data = vec![10, 20, 30];
    let index = 1;

    let value = unsafe {
        // Calling the `unsafe fn` requires an `unsafe` block.
        get_unchecked_val(&data, index)
    };
    println!("Value at index {}: {}", index, value); // Outputs 20

    // Attempting to use a potentially invalid index:
    let out_of_bounds_index = 5;
    // unsafe {
    //     // This call will likely lead to Undefined Behavior if actually run,
    //     // as `get_unchecked_val` doesn't check `index`.
    //     let _ = get_unchecked_val(&data, out_of_bounds_index);
    // }
}

This change means that while an unsafe fn allows unsafe operations, it is now best practice (and warned against if not followed) to still use explicit unsafe { ... } blocks within unsafe fn bodies to precisely demarcate the code sections where the safety invariants must be manually upheld.

25.2.4 Choosing between unsafe fn and unsafe Block

Choosing between an unsafe fn and an unsafe block inside a safe function depends on where the responsibility for safety lies:

  • Use unsafe fn when the function has preconditions that the caller must fulfill to ensure safety. Violating these preconditions, even if the function call type-checks, could lead to UB. Safety depends on the caller’s context.
  • Use an unsafe block inside a safe function (fn) when the function itself can guarantee that its internal unsafe operations are performed correctly, provided the function is called with arguments valid according to its safe signature. Safety is maintained by the function’s implementation.

Best Practice: Encapsulate unsafe operations within unsafe blocks inside safe functions whenever feasible. This minimizes the surface area of unsafety and presents a safe interface to the rest of the codebase. Reserve unsafe fn for interfaces where safety fundamentally depends on guarantees provided by the caller, often seen in FFI or low-level abstractions.


25.3 Raw Pointers: *const T and *mut T

Analogous to C pointers, Rust provides two raw pointer types:

  • *const T: A raw pointer to data of type T, indicating the pointer itself does not grant permission to mutate the data through it. Roughly corresponds to C’s const T*.
  • *mut T: A raw pointer to data of type T, indicating the pointer may be used to mutate the data. Roughly corresponds to C’s T*.

The const or mut primarily signifies the intended use and type system interaction, not necessarily the absolute immutability of the underlying memory (e.g., memory behind a *const T might still be mutated through other means, like an UnsafeCell or another *mut T, if done carefully).

Raw pointers differ significantly from Rust’s references (&T, &mut T):

  • They can be null.
  • They are not guaranteed to point to valid memory (could be dangling or uninitialized).
  • They do not have compiler-enforced lifetime constraints.
  • They can alias (e.g., multiple *mut T can point to the same location), but using them must still respect Rust’s aliasing rules to avoid UB (discussed below).
  • They require explicit dereferencing using the * operator, which is an unsafe operation.
  • They do not implement automatic dereferencing.

25.3.1 Creating and Using Raw Pointers

Creating raw pointers is safe; the unsafety primarily lies in dereferencing them. Raw pointers can be obtained in several ways:

  • From references: References can be explicitly cast to raw pointers. For a variable x of type T, &x as *const T creates a const pointer, and &mut x as *mut T creates a mutable pointer. This explicit cast is a common way to convert a reference when its lifetime and validity are known but a raw pointer is needed (e.g., for FFI or certain low-level operations).

  • Using Raw Borrow Operators (&raw const and &raw mut): Introduced in Rust 1.82, these operators allow for the direct creation of raw pointers (*const T and *mut T respectively) from a “place” (a memory location, such as a variable or a field of a struct).

    The key advantage of &raw const expr and &raw mut expr is that they do not first create an intermediate Rust reference (&T or &mut T). This is crucial because Rust references come with strict guarantees: they must always point to valid, initialized, and properly aligned memory. If you were to create a reference to memory that does not uphold these invariants (e.g., an unaligned field in a #[repr(packed)] struct, or uninitialized memory), even if immediately cast to a raw pointer, it could trigger Undefined Behavior (UB) due to the invalid reference creation itself.

    For C-programmers, these operators provide a direct analogy to C’s address-of operator (&) applied to variables, allowing you to obtain a raw memory address without the implicit safety checks associated with Rust references. This is particularly useful in unsafe blocks for low-level memory operations, FFI, or when interacting with memory layout that Rust’s reference system might otherwise consider invalid.

  • From data structures: Many standard library types that manage contiguous data, such as slices ([T]), Vec<T>, and String, provide methods like as_ptr() (to get a *const T) and as_mut_ptr() (to get a *mut T from a mutable instance). For example, my_slice.as_ptr() returns a *const T to the beginning of the slice’s data. These methods are the idiomatic way to obtain pointers to the internal buffer of such types, as shown in the pointer arithmetic examples later.

  • From memory addresses: An integer representing a memory address can be cast to a raw pointer (e.g., address_usize as *const T). This is highly platform-dependent and typically used for memory-mapped I/O or interacting with hardware. The programmer must ensure the address is valid, properly aligned, and points to memory of type T.

  • Null pointers: The std::ptr module provides null() to create a *const T null pointer and null_mut() for a *mut T null pointer (e.g., let p_null: *const T = std::ptr::null();).

  • From FFI calls: Functions defined in unsafe extern blocks (especially C functions) may return raw pointers.

Passing and storing raw pointers is generally safe. Comparing raw pointers for equality (e.g., using std::ptr::eq or the == operator) is also safe and compares their addresses. Ordinal comparison (<, >, etc.) on raw pointers is defined and performs a byte-wise comparison of the addresses; however, the resulting order may be inconsistent or platform-dependent if the pointers do not originate from, or point within, the same allocated object.

#[repr(packed)] // For demonstration: a struct with potentially unaligned fields.
struct PacketHeader {
    version: u8,
    flags: u8,
    // data_length might be unaligned if accessed as u16 from specific byte offsets.
    data_length: u16,
}

fn main() {
    let mut data = 10;

    // Safe: Create raw pointers from references using explicit casts.
    let p_const: *const i32 = &data as *const i32;
    let p_mut: *mut i32 = &mut data as *mut i32;

    println!("--- Creating Pointers from References ---");
    println!("Address from const reference: {:p}", p_const);
    println!("Address from mut reference: {:p}", p_mut);
    println!("Value from const reference: {}", unsafe { *p_const });
    println!("Value from mut reference: {}", unsafe { *p_mut });

    // Safe: Create a raw pointer from an address (usize). Caution: validity unknown.
    let address = 0x1234_5678_usize;
    let p_addr: *const i32 = address as *const i32;
    println!("\n--- Creating Pointer from Raw Address ---");
    println!("Address from integer literal: {:p}", p_addr);

    // Safe: Create and store a null pointer.
    let null_ptr: *const i32 = std::ptr::null();
    println!("\n--- Creating Null Pointer ---");
    println!("Null pointer address:    {:p}", null_ptr);

    // # Using Raw Borrow Operators (&raw const, &raw mut) since Rust 1.82 #
    println!("\n--- Using Raw Borrow Operators (&raw const, &raw mut) ---");
    let mut header = PacketHeader {
        version: 1,
        flags: 0b1010_1010,
        data_length: 512,
    };

    // For C programmers, this is akin to `&header.data_length;`
    // In Rust, using `&header.data_length` directly would be UB if `data_length`
    // is unaligned due to `#[repr(packed)]` and the CPU requires alignment for u16
    // access. The `&raw const` operator avoids this UB by not creating an
    // intermediate Rust reference.
    let raw_len_ptr: *const u16 = &raw const header.data_length;
    println!("Address of data_length (raw const): {:p}", raw_len_ptr);
    unsafe {
    // Accessing the value through the raw pointer.
    // The safety contract requires ensuring the pointer is valid and aligned.
    // Given this specific scenario with #[repr(packed)] it must be handled carefully.
        println!("Value of data_length (raw const): {}", *raw_len_ptr);
    }
    
    // We can also get a mutable raw pointer.
    let raw_flags_ptr: *mut u8 = &raw mut header.flags;
    println!("Address of flags (raw mut): {:p}", raw_flags_ptr);
    unsafe {
        println!("Original flags: {:#b}", *raw_flags_ptr);
        *raw_flags_ptr = 0b0101_0101; // Mutate through the raw pointer
        println!("Modified flags: {:#b}", *raw_flags_ptr);
    }
    println!("Header flags after modification: {:#b}", header.flags);

    // Obtaining a pointer from a slice using as_ptr() is shown in section 25.3.2.
    // For example:
    // let numbers = [1, 2, 3];
    // let slice_ptr: *const i32 = numbers.as_ptr();
    // println!("Address from slice.as_ptr(): {:p}", slice_ptr);
}

You might wonder if 0 as *const T could also be used to create a null pointer, similar to how (T*)0 is used in C. Indeed, Rust defines that casting a literal 0 to a pointer type produces a null pointer, and the std::ptr::null() function is documented as being equivalent to 0 as *const T for creating a pointer to address zero. However, using std::ptr::null() and std::ptr::null_mut() is generally preferred in Rust. These functions clearly convey the intent to create a null pointer and promote consistency by using the dedicated API from the std::ptr module, which centralizes pointer-related utilities. While currently resulting in the same zero-address pointer on common platforms, the explicit functions are the idiomatic choice.

Dereferencing a raw pointer (*p) to access the pointed-to data is unsafe, requiring an unsafe block, because the pointer’s validity (i.e., being non-null, aligned, pointing to initialized and valid memory of the correct type, and not dangling) cannot be guaranteed by the compiler.

fn main() {
    let mut num = 5;
    let p_const = &num as *const i32;
    let p_mut = &mut num as *mut i32;

    // Unsafe: Dereferencing requires an unsafe block.
    unsafe {
        println!("Reading via *const T: {}", *p_const);

        // Writing requires a *mut T.
        *p_mut = 10;
        println!("Reading via *mut T after write: {}", *p_mut);
    }
    println!("Final value of num: {}", num); // num is now 10

    // Example: Dereferencing an arbitrary address is highly likely UB.
    let invalid_addr = 0x1 as *const i32;
    // unsafe { println!("{}", *invalid_addr); } // Likely crash or incorrect behavior
}

Important Note for C/C++ Programmers: Although raw pointers seem to bypass Rust’s borrowing rules (e.g., allowing multiple *mut T to the same data), Rust still imposes strict aliasing rules, even within unsafe code. The exact rules are formalized by models like Stacked Borrows or Tree Borrows (these models are still evolving). Violating these rules—for instance, writing through a *mut T while a shared reference &T to the same location exists and is considered “live”—is undefined behavior. This is stricter than C’s aliasing rules in some respects. Tools like Miri are invaluable for detecting such violations.

25.3.2 Pointer Arithmetic

Raw pointers support arithmetic to calculate new pointer addresses based on existing ones. Common methods include add(count) for moving forward (with count as usize), sub(count) for moving backward (with count as usize), and offset(count) which takes a signed isize for count and can move in either direction. The offset method is often preferred for its versatility in handling both positive and negative displacements with a single method, as it directly accepts an isize argument. These operations adjust the pointer address by count * std::mem::size_of::<T>() bytes, similar to C pointer arithmetic.

All these fundamental pointer arithmetic methods (add, sub, offset) are unsafe functions. This is because even though the address calculations themselves typically handle overflow by wrapping (producing some address value rather than panicking the way standard integer + might in debug mode), the resulting pointer might be misaligned, point outside allocated memory regions, or be otherwise invalid to dereference. The unsafe contract means the caller is responsible for ensuring that the arguments (like count) are valid in the current context and that any subsequent use of the calculated pointer, especially dereferencing, is safe.

fn main() {
    let numbers = [10i32, 20, 30, 40, 50];
    let start_ptr: *const i32 = numbers.as_ptr(); // Pointer to the first element.

    unsafe {
        // Using offset(): move pointer to the third element (index 2).
        // 'offset' takes an isize, so it can be positive or negative.
        let third_elem_ptr = start_ptr.offset(2);
        println!("Third element (via offset(2)): {}", *third_elem_ptr); // Outputs 30

        // Using add(): move pointer to the second element (index 1).
        // 'add' takes a usize, for forward movement.
        let second_elem_ptr = start_ptr.add(1);
        println!("Second element (via add(1)): {}", *second_elem_ptr); // Outputs 20

        // Using offset() for backward movement.
        let first_elem_again_ptr = third_elem_ptr.offset(-2);
        println!("First element (via offset(-2)): {}", *first_elem_again_ptr); // 10

        // Calculating the difference between pointers (in units of T).
        // 'offset_from' is also an unsafe method.
        let diff = third_elem_ptr.offset_from(start_ptr);
        println!("Offset difference (third_elem_ptr from start_ptr): {}", diff); // 2

        // Creating a pointer outside the bounds is possible with these methods.
        // Dereferencing it is Undefined Behavior.
        // let invalid_ptr = start_ptr.offset(10); // Points beyond the allocation
        // println!("{}", *invalid_ptr); // Undefined Behavior!
    }
}

Pointer arithmetic should be used with extreme caution. Ensure that any pointer you dereference remains within the bounds of a single valid memory allocation and is properly aligned. Safer alternatives, like slice indexing (numbers[i]) or iterators, should always be preferred when applicable.

For scenarios where pointer arithmetic might overflow and wrapping behavior is explicitly desired for the address calculation itself, Rust provides wrapping_add(count), wrapping_sub(count), and wrapping_offset(count). Unlike their non-wrapping counterparts, these wrapping_* methods are safe to call (they are not unsafe fn). This is because they guarantee that the pointer address calculation itself will wrap on overflow (consistent with twos-complement arithmetic) instead of panicking or causing other undefined behavior from the arithmetic operation itself. This can be useful in certain low-level algorithms where pointer values are treated more like integers that are allowed to wrap around the address space.

However, it’s crucial to remember that while calling these wrapping_* methods is safe, dereferencing the resulting pointer still requires an unsafe block and is only permissible if the pointer is valid (non-null, aligned, pointing to accessible and correctly typed memory, etc.). Using a wrapped pointer that is invalid for dereferencing will lead to undefined behavior.

25.3.3 Fat Pointers

Raw pointers to Dynamically Sized Types (DSTs), such as slices ([T]) or trait objects (dyn Trait), are “fat pointers.” They consist of two components: the pointer to the data and associated metadata.

  • *const [T], *mut [T]: Contain the address of the first element and the number of elements (length).
  • *const dyn Trait, *mut dyn Trait: Contain the address of the object data and the address of its virtual method table (vtable).

Converting between thin pointers (*const T) and fat pointers usually requires specific functions like std::slice::from_raw_parts or std::slice::from_raw_parts_mut, which are often unsafe.


25.4 Interfacing with C Code (FFI)

A primary motivation for unsafe is the Foreign Function Interface (FFI), enabling Rust code to call functions written in C (or other languages exposing a C-compatible Application Binary Interface, ABI) and allowing C code to call Rust functions.

When interfacing with C, it’s crucial to use C-compatible types in Rust declarations. The sizes of C types like int or long can vary across different platforms and architectures. Rust’s fixed-size types like i32 or i64 might not always match. To handle this correctly, the libc crate provides type aliases that correspond to C types for the specific target platform. For example, libc::c_int represents C’s int, libc::c_double represents C’s double, and so on. Using these types in your extern "C" declarations is best practice for portable FFI.

25.4.1 unsafe extern blocks in Rust 2024

To call a C function from Rust, you first declare its signature within an extern "C" block. The "C" ABI specification ensures that Rust uses the correct calling conventions (argument passing, return value handling) expected by C code.

Starting with the Rust 2024 Edition, extern blocks must now be explicitly marked with the unsafe keyword: unsafe extern "C" { ... }. This change emphasizes that the declarations within the extern block carry safety obligations. The Rust compiler cannot verify the correctness of foreign function signatures or global static definitions. If these definitions are incorrect, it can lead to undefined behavior when interacting with the external C code.

Within an unsafe extern block, individual items can still be marked as safe fn or unsafe fn to indicate whether calling that specific function (or accessing that static) requires an unsafe block. If neither safe nor unsafe is specified, it defaults to unsafe fn.

// First, add `libc` to your Cargo.toml dependencies:
// [dependencies]
// libc = "0.2" # Or the latest version

// Import the C-compatible types from the libc crate.
use libc::{c_int, c_double};

// Assume linkage with the standard C math library (libm) or C standard library.
// This might happen automatically via libc or require explicit linking
// depending on the platform and build configuration (e.g., using #[link(name = "m")]).

unsafe extern "C" {
    // sqrt (from libm) may be called with any `f64` input, and its safety is solely
    // dependent on its arguments matching the C signature. We mark it `safe fn` to
    // indicate that its *call* is not inherently unsafe, given valid arguments.
    pub safe fn sqrt(x: c_double) -> c_double;

    // strlen (from libc) requires a valid pointer, which the Rust compiler cannot
    // verify. Therefore, calling `strlen` is an unsafe operation.
    pub unsafe fn strlen(p: *const std::ffi::c_char) -> usize;

    // free (from libc) is not marked, so it defaults to unsafe fn.
    // Calling it requires an unsafe block, and you must ensure `p` is a valid pointer.
    pub fn free(p: *mut core::ffi::c_void);

    // Declaring a static variable from C. Accessing it from Rust requires
    // an unsafe block and is unsafe, similar to Rust's `static mut`.
    pub safe static IMPORTANT_BYTES: [u8; 256];
}

fn main() {
    // Rust-side types
    let float_num_rs: f64 = 16.0;

    // Calling external functions declared in an `extern` block.
    // `sqrt` is marked `safe fn`, so no unsafe block needed for its call.
    let sqrt_result_rs = sqrt(float_num_rs as c_double);
    println!("C sqrt({}) = {}", float_num_rs, sqrt_result_rs);

    // `strlen` is marked `unsafe fn`, so its call requires an unsafe block.
    let c_string = "Hello from Rust!\0"; // C strings are null-terminated
    let len = unsafe {
        strlen(c_string.as_ptr() as *const std::ffi::c_char)
    };
    println!("C strlen(\"{}\") = {}", c_string, len);

    // Accessing an external static also requires an unsafe block.
    let first_byte = unsafe { IMPORTANT_BYTES[0] };
    println!("First byte of IMPORTANT_BYTES: {}", first_byte);
}

Why is calling foreign functions unsafe (or why is the extern block unsafe)?

  1. External Code Verification: Rust’s compiler cannot analyze the source code of the C function to verify its memory safety, thread safety, or adherence to any implicit contracts. The C function might contain bugs, access invalid memory, or cause data races.
  2. Signature Mismatch: An error in the Rust extern block declaration (e.g., wrong argument types like using i32 when C’s int is i16 on a given platform, incorrect return type, different number of arguments compared to the actual C function) can lead to stack corruption, misinterpretation of data, and other forms of undefined behavior. Using types from libc helps mitigate mismatches related to type sizes.

Best Practice: Wrap unsafe FFI calls within safe Rust functions. These wrappers can handle type conversions (like casting i32 to libc::c_int), enforce preconditions, check return values for errors (if applicable according to the C API’s conventions), and provide an idiomatic Rust interface.

// Ensure libc is a dependency and import c_int
use libc::c_int;

// Declare the external C function within an unsafe extern "C" block.
unsafe extern "C" { pub safe fn abs(input: c_int) -> c_int; }

// Safe wrapper function encapsulating the unsafe call.
// This wrapper uses i32 for its public Rust API for convenience.
fn safe_abs(input: i32) -> i32 {
    // Since `abs` is marked `safe fn` within the `extern` block,
    // its call does *not* require an `unsafe` block here.
    let c_input = input as c_int;
    let c_result = abs(c_input);
    c_result as i32
    // This simplified wrapper assumes that the range of values for `input` (i32)
    // is appropriate for C's `abs(int)` and that the result also fits in an `i32`.
    // On platforms where `libc::c_int` is `i32` (common), this is a direct mapping.
    // If `libc::c_int` were narrower than `i32` (e.g., 16-bit), the `as c_int` cast
    // would truncate, and the `as i32` cast for the result would sign-extend.
    // For `abs`, this is often acceptable, but for other functions, more careful
    // conversion (e.g., using `try_into()` or range checks) might be necessary.
}

fn main() {
    println!("Absolute value via safe wrapper: {}", safe_abs(-5)); // Outputs 5
}

This encapsulation contains the unsafety, making the rest of the Rust code interact with a safe API. The use of libc types in the extern "C" block significantly improves the portability and correctness of the FFI declarations.


25.5 Unsafe Attributes in Rust 2024

Certain attributes influence the symbol names and linking behavior of items. Because the set of symbols across all linked libraries is a global namespace, symbol name collisions can cause issues, typically leading to undefined behavior. While Rust’s name mangling usually ensures uniqueness, attributes like #[no_mangle], #[export_name], and #[link_section] can override this.

Starting with the Rust 2024 Edition, these attributes must now be explicitly marked as unsafe(...) to highlight that the programmer is responsible for upholding soundness requirements. For example, #[no_mangle] becomes #[unsafe(no_mangle)].

// Example from the Edition Guide:
// SAFETY: There should only be a single definition of the loop symbol.
#[unsafe(export_name="loop")]
pub fn arduino_loop() {
    // ... code that might be called from Arduino firmware or C
    println!("Arduino loop running!");
}

fn main() {
    // The `arduino_loop` function can now be called from C code,
    // where it will appear as the symbol "loop".
    // We cannot directly call `arduino_loop` from safe Rust here due to
    // `export_name` making it not accessible via its Rust name.
    // This example is illustrative of the safety requirement.
    arduino_loop();
    // Call the function as normal from Rust, after marking its export unsafe
}

Marking these attributes unsafe emphasizes that the programmer must ensure the correct usage of symbols to prevent collisions or other linking-related undefined behavior.


25.6 Accessing and Modifying Mutable Static Variables

Rust supports global variables declared with the static keyword. By default, static variables are immutable and must be initialized with constant expressions. To allow mutable global state, Rust provides static mut.

Rust 2024 Edition includes a deny-by-default error for creating references to static mut data. While raw pointers (*const T, *mut T) to static mut are still allowed (within unsafe blocks), creating shared (&T) or mutable (&mut T) Rust references to static mut is now an error. This is because static mut variables are inherently difficult to use safely, especially across threads, and creating Rust references to them (which imply strict aliasing and safety guarantees) often leads to unsoundness.

// Mutable static variable. Initialization must be a constant expression.
static mut GLOBAL_COUNTER: u32 = 0;

fn increment_global_counter() {
    // Accessing (reading or writing) a `static mut` is unsafe.
    unsafe {
        GLOBAL_COUNTER += 1;
    }
}

fn read_global_counter() -> u32 {
    // Reading is also unsafe.
    unsafe {
        GLOBAL_COUNTER
    }
}

fn main() {
    increment_global_counter();
    increment_global_counter();
    println!("Counter value: {}", read_global_counter()); // Outputs 2

    // # In Rust 2024 Edition, this would be a compile-time error:
    // let ref_to_counter: &u32 = &GLOBAL_COUNTER;
    // let mut_ref_to_counter: &mut u32 = &mut GLOBAL_COUNTER;
}

Accessing static mut variables is unsafe primarily because it introduces the risk of data races. If multiple threads access the same static mut variable concurrently, and at least one access is a write, without proper synchronization, the behavior is undefined. Rust’s compile-time safety guarantees cannot prevent data races involving static mut.

Comparison to C: This is directly analogous to mutable global variables in C, which are similarly susceptible to race conditions in multithreaded programs unless protected by external synchronization mechanisms (like mutexes).

Best Practice: Avoid static mut whenever possible. For mutable shared state, use safe concurrency primitives provided by the standard library:

  • std::sync::Mutex<T> or std::sync::RwLock<T>: Wrap the data in a lock to ensure exclusive access.
  • std::sync::atomic types (e.g., AtomicU32, AtomicBool, AtomicPtr): Provide atomic operations for lock-free updates on primitive types.
use std::sync::atomic::{AtomicU32, Ordering};

// Safe global counter using AtomicU32.
static SAFE_COUNTER: AtomicU32 = AtomicU32::new(0);

fn increment_safe_counter() {
    // fetch_add provides atomic increment. No `unsafe` needed.
    // Ordering specifies memory ordering constraints for concurrent access.
    SAFE_COUNTER.fetch_add(1, Ordering::SeqCst);
}

fn read_safe_counter() -> u32 {
    // load provides atomic read. No `unsafe` needed.
    SAFE_COUNTER.load(Ordering::SeqCst)
}

fn main() {
    increment_safe_counter();
    increment_safe_counter();
    println!("Safe counter value: {}", read_safe_counter()); // Outputs 2
}

These alternatives provide safe APIs for managing shared mutable state, leveraging Rust’s safety features even in concurrent contexts.


25.7 Implementing Unsafe Traits

A trait can be declared as unsafe trait if implementing it requires the type to uphold specific invariants or properties that the Rust compiler cannot statically verify. These invariants often relate to low-level details like memory layout, thread safety guarantees, or interaction patterns with unsafe code.

// Hypothetical example: A trait indicating a type can be safely zero-initialized.
// (The standard library has `MaybeUninit<T>` for related concepts).
unsafe trait Pod { // Plain Old Data
    // Implementing this trait asserts that a byte pattern of all zeros
    // represents a valid instance of the type.
    // Incorrectly implementing this could lead to UB if zero-initialization
    // is used based on this trait implementation.
}

struct MyStruct {
    a: u32,
    b: bool,
}

// The `unsafe impl` signifies that the programmer guarantees MyStruct
// conforms to the Pod contract.
unsafe impl Pod for MyStruct {
    // No methods required; the guarantee is encoded in the implementation itself.
}

Implementing an unsafe trait is an unsafe operation (requires unsafe impl). This is because other code (potentially safe code) might rely on the invariants promised by the trait implementation. A faulty implementation could violate these assumptions, leading to undefined behavior throughout the program.

The standard library’s marker traits Send and Sync are related. While they are automatically implemented by the compiler for many types, implementing them manually (which is sometimes necessary, e.g., for types containing raw pointers) requires unsafe impl because the programmer must guarantee thread safety properties that the compiler cannot infer.


25.8 Accessing Fields of Unions

Rust includes union types, similar to C unions, allowing different fields to share the same memory location. Unlike Rust’s enums, unions are untagged; there is no built-in mechanism to track which field currently holds valid data.

// A union that can store either an integer or a floating-point number.
union IntOrFloat {
    i: i32,
    f: f32,
}

fn main() {
    // Initialize the union, specifying one field.
    let mut u = IntOrFloat { i: 10 };

    // Accessing union fields (read or write) is unsafe.
    unsafe {
        // Write to the integer field.
        u.i = 20;
        println!("Union as integer: {}", u.i); // OK: Reading the field we just wrote.

        // Write to the float field. This overwrites the memory occupied by `i`.
        u.f = 3.14;
        println!("Union as float: {}", u.f); // OK: Reading the field we just wrote.

        // Reading `i` after writing `f` reads the raw bytes of the float
        // interpreted as an integer. This is usually logically incorrect
        // and can be undefined behavior depending on the types and values involved.
        // The specific bit pattern of 3.14f32 might happen to be a valid i32,
        // but this is not guaranteed and relies on implementation details.
        println!("Union as integer after float write: {}", u.i);
    }
}

Accessing any field of a union is unsafe. The compiler cannot guarantee that the field being accessed corresponds to the type of data last written to that memory location. Reading the bits of one type (f32 in the example) as if they were another type (i32) can lead to incorrect program logic or, depending on the types involved (e.g., types with validity invariants like bool or references), undefined behavior. The programmer is responsible for tracking which field is currently active and valid. Unions are typically used in specific low-level scenarios like FFI or implementing space-efficient data structures.


25.9 Advanced Unsafe Operations

Beyond the primary capabilities, unsafe enables other powerful, but dangerous, low-level operations.

25.9.1 std::mem::transmute

The function std::mem::transmute<T, U>(value: T) -> U reinterprets the raw memory bits of a value of type T as a value of type U. This function is extremely unsafe.

Requirements for transmute:

  • Types T and U must have the same size in memory.
  • The bit pattern of the input value must be a valid bit pattern for the output type U. (e.g., transmuting 0x03u8 to bool is likely UB, as valid bool bit patterns are typically only 0 or 1).
fn main() {
    let float_value: f32 = -1.0; // Example float

    // Unsafe: Reinterpret f32 bits as u32. Requires types of same size.
    let int_bits: u32 = unsafe {
        // f32 and u32 are both 4 bytes.
        std::mem::transmute::<f32, u32>(float_value)
    };
    // This shows the IEEE 754 representation of the float.
    println!("f32: {}, its bits as u32: 0x{:08X}", float_value, int_bits);

    // Unsafe: Reinterpret u32 bits back to f32.
    let float_again: f32 = unsafe {
        std::mem::transmute::<u32, f32>(int_bits)
    };
    println!("u32 bits: 0x{:08X}, interpreted back as f32: {}", int_bits, float_again);
}

Misusing transmute is a very easy way to cause undefined behavior. It should be avoided unless absolutely necessary. Safer alternatives often exist, such as the from_bits and to_bits methods available on floating-point types (f32::to_bits, f32::from_bits) for inspecting their binary representation.

25.9.2 Inline Assembly (asm!)

For ultimate low-level control, Rust allows embedding assembly code directly into functions using the asm! macro (or global_asm! for defining global assembly symbols). Using inline assembly requires an unsafe block because the compiler cannot verify the correctness or safety implications of the raw assembly instructions.

use std::arch::asm;

fn add_with_assembly(a: u64, b: u64) -> u64 {
    let result: u64;
    // Example for x86_64 architecture using Intel syntax.
    // Other architectures would require different assembly code.
    #[cfg(target_arch = "x86_64")]
    {
        unsafe {
            asm!(
                "mov rax, {0}", // Move first input operand into RAX register
                "add rax, {1}", // Add second input operand to RAX
                // Result is implicitly in RAX for this example
                in(reg) a,       // Input operand 'a' (let compiler choose register)
                in(reg) b,       // Input operand 'b' (let compiler choose register)
                lateout("rax") result,
                // Output operand 'result' taken from RAX register
                options(nostack, pure, nomem)
                // Compiler hints: no stack usage, pure function, no memory access
            );
        }
    }
    // Fallback for non-x86_64 architectures.
    #[cfg(not(target_arch = "x86_64"))]
    {
        println!("Inline assembly example skipped (not on x86_64).
        Performing fallback.");
        result = a + b; // Simple fallback calculation
    }
    result
}

fn main() {
    let x: u64 = 10;
    let y: u64 = 20;
    let sum = add_with_assembly(x, y);
    println!("{} + {} = {}", x, y, sum); // Outputs 30
}

Inline assembly is architecture-specific, complex, and highly error-prone. Incorrect register usage, violating calling conventions, or unexpected side effects can easily lead to crashes or subtle bugs. It is typically reserved for niche use cases like accessing special CPU features, fine-tuning performance in critical loops, or interfacing directly with hardware where no Rust or FFI abstraction exists. Encapsulating assembly within a safe, well-tested function is strongly recommended.


25.10 Verifying Unsafe Code: Miri

Since the compiler’s guarantees do not extend into unsafe blocks, verifying the correctness of unsafe code is crucial. Miri is an experimental interpreter for Rust’s Mid-level Intermediate Representation (MIR). It executes Rust code (including unsafe blocks) and dynamically checks for certain types of undefined behavior.

Miri can detect violations such as:

  • Memory leaks (if enabled).
  • Out-of-bounds memory access (pointers and slices).
  • Use of uninitialized memory.
  • Use-after-free (accessing deallocated memory).
  • Invalid pointer alignment.
  • Violations of pointer aliasing rules (Stacked Borrows/Tree Borrows).
  • Invalid values for types with specific constraints (e.g., using a value other than 0 or 1 for bool, invalid enum discriminants).
  • Invalid transmute operations.
  • Data races (Miri has limited data race detection capabilities, primarily in single-threaded contexts by checking memory model violations).

25.10.1 Using Miri

Miri can be installed as a rustup component and run via Cargo:

  1. Install Miri:
    rustup component add miri
    
  2. Run Miri on your project’s tests:
    cargo miri test
    
  3. Run Miri on a specific binary target:
    cargo miri run --bin your_binary_name
    

If Miri encounters undefined behavior during execution, it will terminate the program and report an error detailing the violation type and location.

25.10.2 Example: Dangling Pointer Detection

Consider code that incorrectly returns a pointer to a stack variable:

fn create_dangling_pointer() -> *const i32 {
    let local_var = 100;
    let ptr = &local_var as *const i32;
    ptr // Return pointer to `local_var`
} // `local_var` goes out of scope here; its stack memory is now invalid.

fn main() {
    let dangling_ptr = create_dangling_pointer();

    // Unsafe: Dereferencing a dangling pointer is Undefined Behavior!
    unsafe {
        // Miri will likely detect this access as invalid.
        // Normal compilation may crash, print garbage, or appear to work by chance.
        println!("Attempting to read dangling pointer: {}", *dangling_ptr);
    }
}

Running this code using cargo miri run should trigger a Miri error report upon reaching the *dangling_ptr dereference, indicating an access to invalid memory (specifically, memory previously allocated on the stack frame of create_dangling_pointer, which no longer exists). Miri helps catch such errors that might otherwise go unnoticed in standard testing.


25.11 Summary

Unsafe Rust is a necessary component of the language, providing the means to perform operations that are beyond the scope of the compiler’s static safety verification. It unlocks capabilities essential for systems programming, such as hardware interaction, FFI, low-level optimizations, and the implementation of foundational data structures.

Key points to remember:

  • The unsafe keyword enables five specific capabilities otherwise forbidden in safe Rust.
  • unsafe does not disable the borrow checker or other fundamental Rust safety rules like type checking. It only permits the five specified “superpowers.”
  • Programmers using unsafe take responsibility for manually upholding Rust’s safety invariants for the operations performed within unsafe contexts.
  • Use unsafe { ... } blocks to isolate specific unsafe operations within a function.
  • Use unsafe fn when a function requires the caller to guarantee certain preconditions for safe execution.
  • Rust 2024 Edition Changes:
    • unsafe extern "C" { ... } blocks are now required for FFI declarations.
    • Attributes like #[no_mangle], #[export_name], and #[link_section] must be marked #[unsafe(...)].
    • The unsafe_op_in_unsafe_fn lint now warns by default, encouraging explicit unsafe { ... } blocks even within unsafe fn bodies.
    • Creating Rust references (&T, &mut T) to static mut variables is now a deny-by-default error.
  • Raw pointers (*const T, *mut T) offer C-like pointer flexibility but require manual verification for validity, alignment, and aliasing rules before dereferencing or performing arithmetic.
  • FFI (unsafe extern "C") allows interaction with external code but is unsafe because Rust cannot verify the external code or the declared function signatures.
  • static mut provides mutable global variables but is inherently unsafe due to data race risks; prefer thread-safe alternatives like Mutex or atomics.
  • Accessing union fields is unsafe as the compiler doesn’t track the active field.
  • Implementing unsafe trait requires unsafe impl as the programmer must guarantee adherence to the trait’s safety contract.
  • Advanced features like std::mem::transmute and asm! are powerful but extremely dangerous and should be used sparingly and with great care.
  • Minimize Unsafe Code: Keep unsafe blocks as small and localized as possible.
  • Encapsulate Unsafety: Whenever feasible, wrap unsafe operations within safe abstraction layers (safe functions or methods).
  • Document Assumptions: Clearly document the invariants and safety conditions that must hold for any unsafe block or unsafe fn to be correct.
  • Verify Thoroughly: Use tools like Miri, code review, and rigorous testing (including fuzzing) to validate the correctness of unsafe code sections.

Unsafe Rust is a tool to be used judiciously. When employed carefully and correctly, it allows Rust to achieve the low-level control and performance characteristics required for systems programming, while the majority of the codebase benefits from the strong safety guarantees of safe Rust.

25.11.1 Further Reading

  • The Rustonomicon: The official guide to Unsafe Rust, delving into memory layout, undefined behavior, FFI details, concurrency, and more. Essential reading for serious unsafe usage.
  • Rust Standard Library Documentation: Key modules include std::ptr (raw pointers), std::mem (memory operations like transmute, size_of), std::ffi (foreign function interface), std::sync::atomic (atomic types), and std::arch (platform-specific intrinsics and assembly).
  • Rust Atomics and Locks by Mara Bos: An in-depth exploration of low-level concurrency primitives in Rust, heavily featuring unsafe code and concepts.