Unsafe Rust and FFI · about 45 min

What unsafe Means, and Raw Pointers

Why this matters

Everything so far has been safe Rust: the compiler proves that your program has no dangling pointers, no data races, no out-of-bounds accesses. Some things cannot be proven by the compiler: talking to hardware, calling C libraries, implementing the very collections (Vec, String) that safe code relies on. For those, Rust has unsafe: a way to say "I have checked this myself". Understanding what unsafe does (and does not) do lets you read the standard library, call C code, and recognize when a crate's use of unsafe deserves scrutiny.

Undefined behavior

The reason unsafe exists, and the reason to be careful with it, is undefined behavior (UB). Some operations, such as reading through a dangling pointer, creating two &mut to the same value, reading uninitialized memory, or a data race, are declared by the language to have no defined meaning. The compiler optimizes on the assumption that they never happen. If they do, the program may crash, silently compute nonsense, or appear to work until a compiler upgrade changes the optimization. UB is not "it might crash": it means anything can happen, including the program behaving differently from what the source code says.

Safe Rust guarantees no UB. unsafe code can cause it, and the programmer must prevent it.

The five unsafe superpowers

An unsafe block lets you do five things the compiler otherwise forbids:

  1. Dereference a raw pointer.
  2. Call an unsafe function (including functions from other languages via FFI).
  3. Access or modify a mutable static variable.
  4. Implement an unsafe trait (like Send or Sync by hand).
  5. Access fields of a union.

That is the complete list. Type checking, ownership, and borrow checking of ordinary references all still apply inside unsafe. The keyword does not turn off the compiler; it marks the places where you are responsible for upholding rules the compiler cannot verify. If a Rust program has UB, the cause is in (or reached from) an unsafe block, which tells you where to look.

Raw pointers

Raw pointers, *const T (read) and *mut T (write), are Rust's equivalent of C pointers. Unlike references, they:

Creating a raw pointer is safe; only dereferencing one is unsafe:

fn main() {
    let mut x = 10;
    let p_const = &x as *const i32;
    let p_mut = &mut x as *mut i32;
    println!("addresses: {:p} {:p}", p_const, p_mut);
    unsafe {
        *p_mut += 5;
        println!("value through p_const: {}", *p_const);
    }
    let dangling = 0x1234usize as *const i32;
    println!("a pointer to nowhere is fine as long as nobody reads it: {:p}", dangling);
}
addresses: 0x7ffdccc1654c 0x7ffdccc1654c
value through p_const: 15
a pointer to nowhere is fine as long as nobody reads it: 0x1234

(Addresses vary.) The first lines are safe; the reads and writes need unsafe. Forgetting the block gives E0133:

fn main() {
    let x = 5;
    let p = &x as *const i32;
    println!("{}", *p);
}
error[E0133]: dereference of raw pointer is unsafe and requires unsafe function or block
 --> main.rs:4:20
  |
4 |     println!("{}", *p);
  |                    ^^ dereference of raw pointer
  |
  = note: raw pointers may be null, dangling or unaligned; they can violate aliasing rules and cause data races: all of these are undefined behavior

Pointer arithmetic

ptr.add(n) moves a pointer forward by n elements (not bytes), just like p + n in C. It is unsafe because the result must stay inside the same allocation:

fn main() {
    let arr = [10u16, 20, 30, 40];
    let p = arr.as_ptr();
    unsafe {
        for i in 0..arr.len() {
            let elem = p.add(i);
            println!("element {i} at {:p} = {}", elem, *elem);
        }
    }
}
element 0 at 0x7ffc40fae560 = 10
element 1 at 0x7ffc40fae562 = 20
element 2 at 0x7ffc40fae564 = 30
element 3 at 0x7ffc40fae566 = 40

The addresses go up by 2 bytes each, the size of a u16. Going past the end (p.add(4) and reading it) would be UB: no bounds check protects you here.

unsafe functions and SAFETY comments

A function whose correct use depends on conditions the compiler cannot check is declared unsafe fn; calling it requires an unsafe block, which forces the caller to acknowledge the contract. Convention (enforced by lints in many projects):

/// Returns the element at `index` without bounds checking.
///
/// # Safety
/// `index` must be less than `slice.len()`.
unsafe fn get_unchecked_demo(slice: &[i32], index: usize) -> i32 {
    unsafe { *slice.as_ptr().add(index) }
}

fn main() {
    let v = [5, 6, 7];
    for i in 0..v.len() {
        // SAFETY: i ranges over 0..v.len(), so it is in bounds.
        let x = unsafe { get_unchecked_demo(&v, i) };
        print!("{x} ");
    }
    println!();
}
5 6 7 

The standard library has exactly such functions (get_unchecked, from_utf8_unchecked): faster, because the caller promises what the checked version would verify.

Mutable statics

A static mut is a global variable that can be changed. Any thread could access it at any time, so every access is unsafe, and in practice you should use an atomic or a Mutex in a normal static instead:

use std::sync::atomic::{AtomicU32, Ordering};

static CALLS: AtomicU32 = AtomicU32::new(0);

fn tracked() {
    CALLS.fetch_add(1, Ordering::Relaxed);
}

fn main() {
    tracked();
    tracked();
    println!("called {} times", CALLS.load(Ordering::Relaxed));
}
called 2 times

Safe code, no unsafe needed: prefer it.

Common mistakes

Recap

Going further: read the Rust Book chapter 20.1 ("Unsafe Rust") and skim the Rustonomicon (doc.rust-lang.org/nomicon), the official guide to writing unsafe code.

Exercises

Exercise 1 · write code

Swap through raw pointers

Write fn swap_raw(a: *mut i32, b: *mut i32) that swaps the two values the pointers point to, using an unsafe block with a // SAFETY: comment explaining the assumption. (Mark the function unsafe fn if you prefer, and then call it in an unsafe block.) The given main reads two integers from stdin and prints them before and after:

before: 3 8
after: 8 3

File: main.rs

Starting point

use std::io;

fn main() {
    let mut s = String::new();
    io::stdin().read_line(&mut s).unwrap();
    let v: Vec<i32> = s.split_whitespace().map(|x| x.parse().unwrap()).collect();
    let (mut x, mut y) = (v[0], v[1]);
    println!("before: {x} {y}");
    let px = &mut x as *mut i32;
    let py = &mut y as *mut i32;
    swap_raw(px, py);
    println!("after: {x} {y}");
}

Example

input

3 8

expected output

before: 3 8
after: 8 3
Hint 1

Dereferencing a raw pointer (*a) is only allowed inside unsafe { ... }.

Hint 2

let tmp = *a; *a = *b; *b = tmp; inside the unsafe block.

Reference solution
use std::io;

fn swap_raw(a: *mut i32, b: *mut i32) {
    // SAFETY: callers pass pointers to two distinct, live, initialized i32 values that nothing
    // else accesses during this call.
    unsafe {
        let tmp = *a;
        *a = *b;
        *b = tmp;
    }
}

fn main() {
    let mut s = String::new();
    io::stdin().read_line(&mut s).unwrap();
    let v: Vec<i32> = s.split_whitespace().map(|x| x.parse().unwrap()).collect();
    let (mut x, mut y) = (v[0], v[1]);
    println!("before: {x} {y}");
    let px = &mut x as *mut i32;
    let py = &mut y as *mut i32;
    swap_raw(px, py);
    println!("after: {x} {y}");
}

Exercise 2 · write code

Walk an array with pointer arithmetic

Write fn sum_ptr(start: *const u32, len: usize) -> u64 that sums len consecutive u32 values beginning at start, using start.add(i) and dereferencing in an unsafe block (no slices inside the function). The given main reads numbers from stdin into a Vec and prints the sum of all of them and of the middle part (skipping the first and last):

total 15
middle 9

for 1 2 3 4 5 (one per line). Assume at least two numbers.

File: main.rs

Starting point

use std::io;

fn main() {
    let v: Vec<u32> = io::stdin().lines().map(|l| l.unwrap().trim().parse().unwrap()).collect();
    println!("total {}", sum_ptr(v.as_ptr(), v.len()));
    let middle = unsafe { v.as_ptr().add(1) };
    println!("middle {}", sum_ptr(middle, v.len() - 2));
}

Example

input

1
2
3
4
5

expected output

total 15
middle 9

Example

input

4000000000
4000000000

expected output

total 8000000000
middle 0
Hint 1

start.add(i) is the pointer to element i; *start.add(i) reads it (inside unsafe).

Hint 2

Convert each value with as u64 before adding.

Reference solution
use std::io;

fn sum_ptr(start: *const u32, len: usize) -> u64 {
    let mut total = 0u64;
    for i in 0..len {
        // SAFETY: the caller guarantees that start..start+len lies inside one live allocation of u32.
        total += unsafe { *start.add(i) } as u64;
    }
    total
}

fn main() {
    let v: Vec<u32> = io::stdin().lines().map(|l| l.unwrap().trim().parse().unwrap()).collect();
    println!("total {}", sum_ptr(v.as_ptr(), v.len()));
    let middle = unsafe { v.as_ptr().add(1) };
    println!("middle {}", sum_ptr(middle, v.len() - 2));
}

Exercise 3 · quiz

What does unsafe turn off?

Inside an unsafe { } block, which of these become possible? Select all that apply.

Answer
  • Dereferencing a raw pointer

  • Calling an unsafe function, including foreign (C) functions

  • Reading or writing a mutable static variable

unsafe unlocks five extra operations: dereference raw pointers, call unsafe functions, access or modify mutable statics, implement unsafe traits, and access union fields. Everything else is still checked: types, ownership, and the borrow rules for references. unsafe means "I, the programmer, guarantee the invariants the compiler cannot check here", not "turn off the compiler".