Unsafe Rust


Rust's type system provides many guarantees, but sometimes, they make specific solutions hard or impossible.

For that reason, Rust has the concept of "unsafe code".


Unsafe code is allowed to:

  • freely access memory
  • dereference raw pointers
  • call external functions
  • declare values Send and Sync
  • write to unsynced global variables

By definition, these are not unsafe:

  • conversion to raw pointers
  • memory leaks

Making pointers

#![allow(unused_variables)]
fn main() {
    let mut x = 1;
    // The old way
    let p1 = &x as *const i32;
    let p2 = &mut x as *mut i32;
    // Added in 1.51, was unsafe until 1.82
    let p1 = core::ptr::addr_of!(x);
    let p2 = core::ptr::addr_of_mut!(x);
    // As of Rust 1.82, use this instead:
    let p1 = &raw const x;
    let p2 = &raw mut x;    
}

Unsafe code should never:

  • be used to manage memory managed by a different allocator (e.g. construct a std:::vec::Vec from a C++ vector and drop it)
  • cheat on the borrow checker, for example by changing lifetimes or mutability of a type. The most common source of "but I was so sure that works" bugs.

Rust's little secret

When implementing data structures, unsafe isn't unusual.

Safe Rust is the worst language to implement linked lists. There's a full text on this


Unsafe code must always be marked unsafe.

fn main() {
    let mut x = 1;
    let p = &raw mut x;
    unsafe {
        my_write(p, 100);
    }
    println!("x is {} (or {})", x, unsafe { p.read() });
}

pub unsafe fn my_write<T>(p: *mut T, new_value: T) {
    p.write(new_value)
}

Note:

Modern Rust generally tries to have only a small number of unsafe operations per unsafe block. And any unsafe function should still use unsafe blocks for the unsafe code within, even though the function itself is unsafe to call.

Try running clippy on this example and play with clippy::multiple_unsafe_ops_per_block and clippy::undocumented_unsafe_blocks. Then try "Edition 2024".

Traps of unsafe

  • Not all examples are that simple. unsafe must guarantee the invariants that Rust expects.
  • This especially applies to ownership and mutable borrowing
  • unsafe can lead to a value having 2 owners -> double free
  • unsafe can make immutable data temporarily mutable, which will lead to broken promises and tears.

Rust allows you to shoot yourself in the foot, it just requires you to take your gun out of the holster and remove the safety first.

Practical example

As Rust forbids aliasing, it is impossible in safe Rust to split a slice into 2 non-overlapping parts.

#![allow(unused)]
fn main() {
#[inline]
fn split_at_mut<T>(value: &mut [T], mid: usize) -> (&mut [T], &mut [T]) {
    let len = value.len();
    let ptr = value.as_mut_ptr();
    assert!(mid <= len);
    unsafe {
        (std::slice::from_raw_parts_mut(ptr, mid),
         std::slice::from_raw_parts_mut(ptr.add(mid), len - mid))
    }
}
}

Highlight unsafe code in VSCode

  • Will highlight which function calls are unsafe inside an unsafe block
  • Helpful for longer unsafe blocks
{
    "editor.semanticTokenColorCustomizations": {
        "rules": {
            "*.unsafe:rust": "#ff00ff"
        }
    }
}