Unsafe Rust and FFI · about 40 min

Safe Abstractions over Unsafe Code

Why this matters

Vec, String, HashMap, Rc, Mutex, threads and channels are all built on unsafe code inside the standard library. You use them from safe code every day without being able to cause undefined behavior. That is the central design idea of unsafe Rust: wrap a small amount of carefully reasoned unsafe code in a safe API, so that no possible use of the API from safe code can break its invariants. This lesson shows how that works, using the function you met in module 5, split_at_mut, and explains what makes a safe API "sound".

The problem split_at_mut solves

You cannot take two mutable borrows of one slice, even of disjoint parts, because the borrow checker does not reason about index ranges:

fn split(s: &mut [i32], mid: usize) -> (&mut [i32], &mut [i32]) {
    (&mut s[..mid], &mut s[mid..])
}

fn main() {
    let mut a = [1, 2, 3, 4];
    let (l, r) = split(&mut a, 2);
    l[0] = 9;
    r[0] = 9;
}
error[E0499]: cannot borrow `*s` as mutable more than once at a time
 --> main.rs:2:26
  |
1 | fn split(s: &mut [i32], mid: usize) -> (&mut [i32], &mut [i32]) {
  |             - let's call the lifetime of this reference `'1`
2 |     (&mut s[..mid], &mut s[mid..])
  |     ---------------------^--------
  |     |     |              |
  |     |     |              second mutable borrow occurs here
  |     |     first mutable borrow occurs here
  |     returning this value requires that `*s` is borrowed for `'1`
  |
  = help: use `.split_at_mut(position)` to obtain two mutable non-overlapping sub-slices

We know the two ranges do not overlap, so handing out both is perfectly safe. The compiler cannot prove it. This is exactly the situation unsafe is for: a fact the programmer can verify that the type system cannot express.

Implementing it with raw pointers

use std::slice;

fn split_at_mut(s: &mut [i32], mid: usize) -> (&mut [i32], &mut [i32]) {
    let len = s.len();
    let ptr = s.as_mut_ptr();
    assert!(mid <= len);
    // SAFETY: 0..mid and mid..len are within the slice (checked above) and do not overlap,
    // so the two &mut slices never alias. Both borrow from `s` and cannot outlive it.
    unsafe { (slice::from_raw_parts_mut(ptr, mid), slice::from_raw_parts_mut(ptr.add(mid), len - mid)) }
}

fn main() {
    let mut a = [1, 2, 3, 4, 5];
    let (l, r) = split_at_mut(&mut a, 2);
    l[0] = 100;
    r[0] = 300;
    println!("{:?} {:?}", l, r);
    println!("{:?}", a);
}
[100, 2] [300, 4, 5]
[100, 2, 300, 4, 5]

This is, in essence, how the standard library's split_at_mut is written.

Soundness

A safe function is sound if no call from safe code, with any arguments, can cause undefined behavior. An API is unsound if some safe call can. Soundness is a property of the whole API, not of the unsafe block alone:

pub fn first(v: &[u8]) -> u8 {
    unsafe { *v.as_ptr() }       // unsound: first(&[]) reads out of bounds
}

pub fn first(v: &[u8]) -> Option<u8> {
    if v.is_empty() { return None; }
    unsafe { Some(*v.as_ptr()) } // sound: the condition is checked first
}

Ways to make the unsafe condition hold:

  1. Check it at run time (assert!, return None/Err) before the unsafe operation, as above.
  2. Make it impossible by construction, for example with types (a non-empty type) or with private fields that only your code can change.
  3. Push the obligation to the caller by declaring the function unsafe fn with a # Safety doc section.

Privacy protects invariants

Option 2 is how collections work. A Vec keeps len <= capacity and "the first len elements are initialized" as invariants. Its fields are private, so the only code that can change them is Vec's own methods, which maintain the invariants. Its unsafe reads can then rely on them. The SmallVec exercise builds a tiny version: get checks i < len, and len is only changed by push, which never exceeds the array's size. If the fields were pub, any safe code could set len = 1000, and get would become unsound. Module privacy is part of the safety argument.

The boundary to audit is therefore the module, not just the unsafe block: every function that can modify the fields an unsafe block relies on must preserve the invariants.

When (not) to write unsafe

Common mistakes

Recap

Going further: read the Rustonomicon chapters "Meet Safe and Unsafe" and "How Safe and Unsafe Interact", and look at the source of slice::split_at_mut (the docs have a "source" link).

Exercises

Exercise 1 · write code

Write split_at_mut yourself

Implement fn my_split_at_mut(s: &mut [i32], mid: usize) -> (&mut [i32], &mut [i32]) without calling the standard split_at_mut or split_at: assert that mid <= s.len() (so bad input panics instead of causing undefined behavior), then build the two halves with std::slice::from_raw_parts_mut from s.as_mut_ptr(). The given main reads numbers, splits at the middle, negates every element of the left half and doubles every element of the right half:

[-1, -2, 6, 8, 10]

for 1 2 3 4 5 (one per line).

File: main.rs

Starting point

use std::io;

fn main() {
    let mut v: Vec<i32> = io::stdin().lines().map(|l| l.unwrap().trim().parse().unwrap()).collect();
    let mid = v.len() / 2;
    let (left, right) = my_split_at_mut(&mut v, mid);
    for x in left.iter_mut() {
        *x = -*x;
    }
    for x in right.iter_mut() {
        *x *= 2;
    }
    println!("{:?}", v);
}

Example

input

1
2
3
4
5

expected output

[-1, -2, 6, 8, 10]
Hint 1

let ptr = s.as_mut_ptr(); then slice::from_raw_parts_mut(ptr, mid) and slice::from_raw_parts_mut(ptr.add(mid), len - mid).

Hint 2

Put the assert! before the unsafe block: the unsafe code is only sound if mid <= len.

Reference solution
use std::io;
use std::slice;

fn my_split_at_mut(s: &mut [i32], mid: usize) -> (&mut [i32], &mut [i32]) {
    let len = s.len();
    assert!(mid <= len, "mid {mid} out of bounds for length {len}");
    let ptr = s.as_mut_ptr();
    // SAFETY: 0..mid and mid..len are in bounds (checked above) and do not overlap, so the two
    // mutable slices never alias; both borrow from `s` and cannot outlive it.
    unsafe { (slice::from_raw_parts_mut(ptr, mid), slice::from_raw_parts_mut(ptr.add(mid), len - mid)) }
}

fn main() {
    let mut v: Vec<i32> = io::stdin().lines().map(|l| l.unwrap().trim().parse().unwrap()).collect();
    let mid = v.len() / 2;
    let (left, right) = my_split_at_mut(&mut v, mid);
    for x in left.iter_mut() {
        *x = -*x;
    }
    for x in right.iter_mut() {
        *x *= 2;
    }
    println!("{:?}", v);
}

Exercise 2 · quiz

Is this API sound?

A library exposes this safe function:

pub fn get(v: &[u8], i: usize) -> u8 {
    unsafe { *v.as_ptr().add(i) }
}

Which statement is correct?

Answer
  • It is unsound: safe code can call get(&v, 1000) and read out of bounds, causing undefined behavior; it must check i < v.len() or be marked unsafe fn

A safe function must be impossible to misuse into undefined behavior, whatever arguments safe code passes. This one relies on i < v.len() but never checks it, so the unsafe block's condition can be violated by safe callers. Fix: check (and panic or return None) before the unsafe read, or make it unsafe fn with a documented # Safety requirement, moving the responsibility to the caller.

Exercise 3 · write code

A tiny fixed-capacity vector

Complete struct SmallVec { data: [i32; 4], len: usize } with private fields and these safe methods:

The given main pushes numbers from stdin and prints get results for indexes 0 to 4:

pushed 4 of 5
Some(10) Some(20) Some(30) Some(40) None

File: main.rs

Starting point

use std::io;

mod small {
    pub struct SmallVec {
        data: [i32; 4],
        len: usize,
    }
}

use small::SmallVec;

fn main() {
    let mut v = SmallVec::new();
    let mut pushed = 0;
    let mut total = 0;
    for line in io::stdin().lines() {
        total += 1;
        if v.push(line.unwrap().trim().parse().unwrap()) {
            pushed += 1;
        }
    }
    println!("pushed {pushed} of {total}");
    let parts: Vec<String> = (0..5).map(|i| format!("{:?}", v.get(i))).collect();
    println!("{}", parts.join(" "));
    let _ = v.len();
}

Example

input

10
20
30
40
50

expected output

pushed 4 of 5
Some(10) Some(20) Some(30) Some(40) None

Example

input

7

expected output

pushed 1 of 1
Some(7) None None None None
Hint 1

Put a pub impl block inside mod small so the methods can see the private fields.

Hint 2

get must return None for i >= self.len before the unsafe read; that check is what makes the method sound.

Reference solution
use std::io;

mod small {
    pub struct SmallVec {
        data: [i32; 4],
        len: usize,
    }

    impl SmallVec {
        pub fn new() -> Self {
            SmallVec { data: [0; 4], len: 0 }
        }

        pub fn push(&mut self, x: i32) -> bool {
            if self.len == self.data.len() {
                return false;
            }
            self.data[self.len] = x;
            self.len += 1;
            true
        }

        pub fn get(&self, i: usize) -> Option<i32> {
            if i >= self.len {
                return None;
            }
            // SAFETY: i < self.len <= data.len(), an invariant maintained by push (the only
            // code that changes len); the fields are private so nothing else can break it.
            Some(unsafe { *self.data.get_unchecked(i) })
        }

        pub fn len(&self) -> usize {
            self.len
        }
    }
}

use small::SmallVec;

fn main() {
    let mut v = SmallVec::new();
    let mut pushed = 0;
    let mut total = 0;
    for line in io::stdin().lines() {
        total += 1;
        if v.push(line.unwrap().trim().parse().unwrap()) {
            pushed += 1;
        }
    }
    println!("pushed {pushed} of {total}");
    let parts: Vec<String> = (0..5).map(|i| format!("{:?}", v.get(i))).collect();
    println!("{}", parts.join(" "));
    let _ = v.len();
}