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

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.