Introduction

There's plenty of things that you're obviously supposed to worry about in unsafe code; such as making sure that you don't dereference pointers to invalid data, and that you don't use something after it is freed. But oftentimes there are problems that are not so obvious, and you might forget to think about them even if they are mentioned in the docs of an unsafe fn!

This book is a (modest) collection of those unsafe "gotchas."

About

The source for this book is hosted on GitHub. If you'd like to contribute, just submit a PR!

Omnipresent concerns

These concerns may come up regardless of what kind of unsafe code you're writing.

Drop safety

Things to look for:

  • Usage of unsafe in any generic function that doesn't have T: Copy bounds.
  • Usage of unsafe near code that can panic.

Summary: unsafe code often puts data in a state where it would be dangerous for a destructor to run. The possibility that code may unwind amplifies this problem immensely. Most unsafe code needs to worry about drop safety at some point.

Danger: A value read using std::ptr::read may get dropped twice

(This also applies to <*const T>::read, which is basically the same function)

Incorrect

# fn main() {}
use std::ptr;

pub struct ArrayIntoIter<T> {
    array: [T; 3],
    index: usize,
}

impl<T> Iterator for ArrayIntoIter<T> {
    type Item = T;

    fn next(&mut self) -> Option<T> {
        match self.index {
            3 => None,
            i => {
                self.index += 1;
                Some(unsafe { ptr::read(&self.array[i]) })
            }
        }
    }
}

When the ArrayIntoIter<T> is dropped, all of the elements will be dropped, even though ownership of some of the elements may have already been given away.

For this reason, usage of std::ptr::read must almost always be paired together with usage of std::mem::forget, or, better yet, std::mem::ManuallyDrop (available since 1.20.0) which is capable of solving a broader variety of problems. (In fact, it is impossible to fix the above example using only mem::forget)

Correct

# fn main() {}
use std::mem::ManuallyDrop;
use std::ptr;

pub struct ArrayIntoIter<T> {
    array: [ManuallyDrop<T>; 3],
    index: usize,
}

impl<T> ArrayIntoIter<T> {
    pub fn new(array: [T; 3]) -> Self {
        let [a, b, c] = array;
        let wrap = ManuallyDrop::new;
        ArrayIntoIter {
            array: [wrap(a), wrap(b), wrap(c)],
            index: 0,
        }
    }
}

impl<T> Iterator for ArrayIntoIter<T> {
    type Item = T;

    fn next(&mut self) -> Option<T> {
        match self.index {
            3 => None,
            i => {
                self.index += 1;
                Some(ManuallyDrop::into_inner(unsafe { ptr::read(&self.array[i]) }))
            }
        }
    }
}

impl<T> Drop for ArrayIntoIter<T> {
    fn drop(&mut self) {
        // Run to completion
        self.for_each(drop);
    }
}

Danger: Closures can panic

Incorrect

# fn main() {}
use std::ptr;

pub fn filter_inplace<T>(
    vec: &mut Vec<T>,
    mut pred: impl FnMut(&mut T) -> bool,
) {
    let mut write_idx = 0;

    for read_idx in 0..vec.len() {
        if pred(&mut vec[read_idx]) {
            if read_idx != write_idx {
                unsafe {
                    ptr::copy_nonoverlapping(&vec[read_idx], &mut vec[write_idx], 1);
                }
            }
            write_idx += 1;
        } else {
            drop(unsafe { ptr::read(&vec[read_idx]) });
        }
    }
    unsafe { vec.set_len(write_idx); }
}

When pred() panics, we never reach the final .set_len(), and some elements may get dropped twice.

Danger: Any method on any safe trait can panic

A generalization of the previous point. You can't even trust clone to not panic!

Incorrect

# fn main() {}
pub fn remove_all<T: Eq>(
    vec: &mut Vec<T>,
    target: &T,
) {
    // same as filter_inplace
    // but replace   if pred(&mut vec[read_idx])
    //        with   if &vec[read_idx] == target
# let _ = (vec, target);
}

Danger: Drop can panic!

This particularly nefarious special case of the prior point will leave you tearing your hair out.

Still Incorrect:

# fn main() {}
/// Marker trait for Eq impls that do not panic.
///
/// # Safety
/// Behavior is undefined if any of the methods of `Eq` panic.
pub unsafe trait NoPanicEq: Eq {}

pub fn remove_all<T: NoPanicEq>(
    vec: &mut Vec<T>,
    target: &T,
) {
    // same as before
# let _ = (vec, target);
}

In this case, the line

# use std::ptr;
# fn main() {
# let read_idx = 0;
# let vec = vec![1];
drop(unsafe { ptr::read(&vec[read_idx]) });
# }

in the else block may still panic. And in this case we should consider ourselves fortunate that the drop is even visible! Most drops will be invisible, hidden at the end of a scope.

Many of these problems can be solved through extremely liberal use of std::mem::ManuallyDrop; basically, whenever you own a T or a container of Ts, put it in a std::mem::ManuallyDrop so that it won't drop on unwind. Then you only need to worry about the ones you don't own (anything your function receives by &mut reference).

Pointer alignment

Things to look for: Code that parses &[u8] into references of other types.

Summary: Any attempt to convert a *const T into a &T (or to call std::ptr::read) requires an aligned pointer, in addition to all the other, more obvious requirements.

Generic usage of std::mem::uninitialized or std::mem::zeroed

Things to look for: Usage of either std::mem::uninitialized or std::mem::zeroed in a function with a generic type parameter T.

Summary: Sometimes people try to use std::mem::uninitialized as a substitute for T::default() in cases where they cannot add a T: Default bound. This usage is almost always incorrect due to multiple edge cases.

Danger: T may have a destructor

Yep, these functions are yet another instance of our mortal enemy, Drop unsafety.

Incorrect

# #![allow(unused_assignments)]
# fn main() {}
pub fn call_function<T>(
    func: impl FnOnce() -> T,
) -> T {
    let mut out: T;
    out = unsafe { std::mem::uninitialized() };
    out = func(); // <----
    out
}

This function exhibits UB because, at the marked line, the original, uninitialized value assigned to out is dropped.

Still Incorrect

# fn main() {}
pub fn call_function<T>(
    func: impl FnOnce() -> T,
) -> T {
    let mut out: T;
    out = unsafe { std::mem::uninitialized() };
    unsafe { std::ptr::write(&mut out, func()) };
    out
}

This function still exhibits UB because func() can panic, causing the uninitialized value assigned to out to be dropped during unwind.

Danger: T may be uninhabited

Still incorrect!!

# #![allow(unused_assignments)]
# fn main() {}
pub fn call_function<T: Copy>(
    func: impl FnOnce() -> T,
) -> T {
    let mut out: T;
    out = unsafe { std::mem::uninitialized() };
    out = func(); 
    out
}

Here, the Copy bound forbids T from having a destructor, so we no longer have to worry about drops. However, this function still exhibits undefined behavior in the case where T is uninhabited:

# #![allow(unused_assignments)]
# fn call_function<T: Copy>(
#     func: impl FnOnce() -> T,
# ) -> T {
#     let mut out: T;
#     out = unsafe { std::mem::uninitialized() };
#     out = func(); 
#     out
# }
#
/// A type that is impossible to construct.
#[derive(Copy, Clone)]
enum Never {}

fn main() {
    let _: Never = call_function(|| panic!("Hello, world!"));
}

The problem here is that std::mem::uninitialized::<Never> successfully returns a value of a type that cannot possibly exist.

Or at least, it used to. Recent versions of the standard library (early rust 1.3x) include an explicit check for uninitialized types inside std::mem::{uninitialized, zeroed}, and these functions will now panic with a nice error message.

How about std::mem::MaybeUninit?

This new type (on the road to stabilization in 1.36.0) has none of the issues listed above.

  • Dropping a MaybeUninit does not run destructors.
  • The type MaybeUninit<T> is always inhabited even if T is not.

This makes it significantly safer.

Concerns for FFI

enums are not FFI-safe

What to look for: enums appearing in signatures of extern fns.

Summary: It is undefined behavior for an enum in rust to carry an invalid value. Therefore, do not make it possible for C code to supply the value of an enum type.

Incorrect:

# fn main() {}
#[repr(u16)]
pub enum Mode {
    Read = 0,
    Write = 1,
}

#[allow(unused)]
extern "C" fn rust_from_c(mode: Mode) {
    // ...
}

Also incorrect:

# #[repr(u16)]
# pub enum Mode {
#     Read = 0,
#     Write = 1,
# }
#
extern "C" {
    fn c_from_rust(mode: *mut Mode);
}

fn main() {
    let mut mode = Mode::Read;
    unsafe { c_from_rust(&mut mode); }
}

CString::from_raw

Things to look for: Any usage of CString::{into_raw, from_raw}.

Summary: As documented, CString::from_raw recomputes the length by scanning for a null byte. What it doesn't (currently) mention is that this length must match the original length.

I think you'll be hard pressed to find any C API function that mutates a char * without changing its length!

Incorrect

extern crate libc;

use std::ffi::{CString, CStr};

fn main() {
    let ptr = CString::new("Hello, world!").unwrap().into_raw();
    let delim = CString::new(" ").unwrap();
    
    let first_word_ptr = unsafe { libc::strtok(ptr, delim.as_ptr()) };
    
    assert_eq!(
        unsafe { CStr::from_ptr(first_word_ptr) },
        &CString::new("Hello,").unwrap()[..],
    );
    
    drop(unsafe { CString::from_raw(ptr) });
}

This is incorrect because strtok inserts a NUL byte after the comma in "Hello, world!", causing the CString to have a different length once it is reconstructed. As a result, when the CString is freed, it will pass the wrong size to the allocator.

The fix is to never use these methods. If a C API needs to modify a string, use a Vec<u8> buffer instead.

Correct

extern crate libc;

use std::ffi::{CString, CStr};
use libc::c_char;

fn main() {
    let mut buf = CString::new("Hello, world!").unwrap().into_bytes_with_nul();
    let delim = CString::new(" ").unwrap();

    let first_word_ptr = unsafe {
        libc::strtok(buf.as_mut_ptr() as *mut c_char, delim.as_ptr())
    };

    assert_eq!(
        unsafe { CStr::from_ptr(first_word_ptr) },
        &CString::new("Hello,").unwrap()[..],
    );
}

Also: Store a CString to a local before calling as_ptr()

Just as an aside, there's another footgun here. If I had written:

Incorrect:

# use std::ffi::CString;
# fn main() {
let delim = CString::new(" ").unwrap().as_ptr();
# let _ = delim;
# }

the buffer would have been freed immediately.

Concerns for thread synchronization

Shared mutability without UnsafeCell

What to look for: Mutable data that is shared by multiple threads, but isn't atomic or wrapped in an UnsafeCell. Casts from *const _ to *mut _.

Summary: Threads usually exchange data by reading and writing to shared memory locations. But by default, Rust assumes that non-atomic data accessed via a shared & reference cannot change. This assumption must be suppressed using an UnsafeCell in objects meant for thread synchronization.

Incorrect:

# fn main() {}
use std::sync::atomic::{AtomicBool, Ordering};

pub struct SpinLock<T> {
    data: T,
    locked: AtomicBool,
}

impl<T> SpinLock<T> {
    pub fn new(data: T) -> Self {
        Self {
            data,
            locked: AtomicBool::new(false),
        }
    }

    pub fn try_lock(&self) -> Option<LockGuard<T>> {
        let was_locked = self.locked.swap(true, Ordering::Acquire);
        if was_locked {
            None
        } else {
            Some(LockGuard(&self))
        }
    }
}

pub struct LockGuard<'a, T>(&'a SpinLock<T>);

impl<'a, T> LockGuard<'a, T> {
    pub fn get_mut(&mut self) -> &mut T {
        let data_ptr = &self.0.data as *const _ as *mut _;
        unsafe { &mut *data_ptr }
    }
}

impl<'a, T> Drop for LockGuard<'a, T> {
    fn drop(&mut self) {
        self.0.locked.store(false, Ordering::Release);
    }
}

Correct:

# fn main() {}
use std::cell::UnsafeCell;
use std::sync::atomic::{AtomicBool, Ordering};

pub struct SpinLock<T> {
    cell: UnsafeCell<T>,
    locked: AtomicBool,
}

impl<T> SpinLock<T> {
    pub fn new(data: T) -> Self {
        Self {
            cell: UnsafeCell::new(data),
            locked: AtomicBool::new(false),
        }
    }

    pub fn try_lock(&self) -> Option<LockGuard<T>> {
        let was_locked = self.locked.swap(true, Ordering::Acquire);
        if was_locked {
            None
        } else {
            Some(LockGuard(&self))
        }
    }
}

pub struct LockGuard<'a, T>(&'a SpinLock<T>);

impl<'a, T> LockGuard<'a, T> {
    pub fn get_mut(&mut self) -> &mut T {
        unsafe { &mut *self.0.cell.get() }
    }
}

impl<'a, T> Drop for LockGuard<'a, T> {
    fn drop(&mut self) {
        self.0.locked.store(false, Ordering::Release);
    }
}

Multiple &mut to the same data

What to look for: Multiple &muts to a single piece of data, or APIs that allow creating them.

Summary: As seen above, to synchronize threads through shared memory, we need to cheat Rust's "no shared mutability" rule using UnsafeCell. This makes it easy to accidentally expose an API that allows creating multiple &muts to a single piece of data, which is Undefined Behavior.

Incorrect:

# fn main() {}
use std::cell::UnsafeCell;
use std::sync::atomic::{AtomicU32, Ordering};

pub struct RecursiveSpinLock<T> {
    cell: UnsafeCell<T>,
    owner_id: AtomicU32,
}

const NO_THREAD_ID: u32 = 0;
static THREAD_ID_CTR: AtomicU32 = AtomicU32::new(1);
thread_local!(static THREAD_ID: u32 = THREAD_ID_CTR.fetch_add(1, Ordering::Relaxed));

impl<T> RecursiveSpinLock<T> {
    pub fn new(data: T) -> Self {
        Self {
            cell: UnsafeCell::new(data),
            owner_id: AtomicU32::new(NO_THREAD_ID),
        }
    }

    pub fn try_lock(&self) -> Option<&mut T> {
        THREAD_ID.with(|&my_id| {
            let old_id = self.owner_id.compare_and_swap(NO_THREAD_ID, my_id, Ordering::Acquire);
            if old_id == NO_THREAD_ID || old_id == my_id {
                Some(unsafe { &mut *self.cell.get() })
            } else {
                None
            }
        })
    }

    pub fn unlock(&self) {
        THREAD_ID.with(|&my_id| {
            let old_id = self.owner_id.compare_and_swap(my_id, NO_THREAD_ID, Ordering::Release);
            assert_eq!(old_id, my_id, "Incorrect lock usage detected!");
        })
    }
}

Here, a single thread calling try_lock() multiple times on a RecursiveSpinLock object (or, for that matter, slyly keeping the &mut T around after calling unlock()) can get multiple mutable references to its inner data, which is illegal in Rust.

If you really need a recursive lock, you will need to make its API return a shared & reference, or to turn it into an unsafe API that returns a raw *mut pointer (possibly wrapped in NonNull).

Data races

What to look for: One thread writing to a piece of data in a fashion that is observable by another thread writing to or reading from it.

Summary: Even in the presence of an UnsafeCell, data races are undefined behavior. Intuitions of memory accesses based on reading the code may not match the actual memory access patterns of optimized binaries running on modern out-of-order CPUs. Please ensure that other threads wait for writes to be finished before accessing the shared data.

Incorrect:

# fn main() {}
use std::cell::UnsafeCell;
use std::sync::atomic::{AtomicBool, Ordering};

pub struct Racey<T> {
    cell: UnsafeCell<T>,
    writing: AtomicBool,
}

impl<T> Racey<T> {
    pub fn new(data: T) -> Self {
        Self {
            cell: UnsafeCell::new(data),
            writing: AtomicBool::new(false),
        }
    }

    pub fn read(&self) -> *const T {
        self.cell.get()
    }

    pub fn try_write(&self) -> Option<WriteGuard<T>> {
        let was_writing = self.writing.swap(true, Ordering::Acquire);
        if was_writing {
            None
        } else {
            Some(WriteGuard(&self))
        }
    }
}

pub struct WriteGuard<'a, T>(&'a Racey<T>);

impl<'a, T> WriteGuard<'a, T> {
    // Notice the use of &mut self, which prevents multiple &mut T to be created
    pub fn get_mut(&mut self) -> &mut T {
        unsafe { &mut *self.0.cell.get() }
    }
}

impl<'a, T> Drop for WriteGuard<'a, T> {
    fn drop(&mut self) {
        self.0.writing.store(false, Ordering::Release);
    }
}

Although this design correctly prevents multiple writers from acquiring an &mut to the data at the same time (which, as we've seen, is UB even if they don't use those references), it does not prevents readers from observing the writes of the writers.

For that matter, simply modifying read to return a &T instead of a *const T would be Undefined Behavior per se, because &mut and & references are not allowed to coexist.

Insufficient synchronization

What to look for: Insufficient atomic memory orderings and unforeseen interleavings of thread operations on shared memory.

Summary: Modern optimizing compilers and CPUs will add, remove, and reorder memory accesses in a fashion that is observable by other threads. It is your responsability to tell the compiler which of these alterations should be prevented so that your code remains correct.

Incorrect:

# fn main() {}
use std::cell::UnsafeCell;
use std::sync::atomic::{AtomicBool, Ordering};

pub struct SpinLock<T> {
    cell: UnsafeCell<T>,
    locked: AtomicBool,
}

impl<T> SpinLock<T> {
    pub fn new(data: T) -> Self {
        Self {
            cell: UnsafeCell::new(data),
            locked: AtomicBool::new(false),
        }
    }

    pub fn try_lock(&self) -> Option<LockGuard<T>> {
        let was_locked = self.locked.swap(true, Ordering::Relaxed);
        if was_locked {
            None
        } else {
            Some(LockGuard(&self))
        }
    }
}

pub struct LockGuard<'a, T>(&'a SpinLock<T>);

impl<'a, T> LockGuard<'a, T> {
    pub fn get_mut(&mut self) -> &mut T {
        unsafe { &mut *self.0.cell.get() }
    }
}

impl<'a, T> Drop for LockGuard<'a, T> {
    fn drop(&mut self) {
        self.0.locked.store(false, Ordering::Relaxed);
    }
}

Use of Relaxed memory ordering means that the compiler and CPU are allowed to move reads and writes to the lock-protected data before the atomic swap that acquires the lock or after the atomic CAS that releases the lock. This may result in data races.

Correct:

# fn main() {}
use std::cell::UnsafeCell;
use std::sync::atomic::{AtomicBool, Ordering};

pub struct SpinLock<T> {
    cell: UnsafeCell<T>,
    locked: AtomicBool,
}

impl<T> SpinLock<T> {
    pub fn new(data: T) -> Self {
        Self {
            cell: UnsafeCell::new(data),
            locked: AtomicBool::new(false),
        }
    }

    pub fn try_lock(&self) -> Option<LockGuard<T>> {
        let was_locked = self.locked.swap(true, Ordering::Acquire);
        if was_locked {
            None
        } else {
            Some(LockGuard(&self))
        }
    }
}

pub struct LockGuard<'a, T>(&'a SpinLock<T>);

impl<'a, T> LockGuard<'a, T> {
    pub fn get_mut(&mut self) -> &mut T {
        unsafe { &mut *self.0.cell.get() }
    }
}

impl<'a, T> Drop for LockGuard<'a, T> {
    fn drop(&mut self) {
        self.0.locked.store(false, Ordering::Release);
    }
}

Acquire ordering ensures that no reads and writes can be speculatively carried out on the locked data before the lock has been acquired. Release ordering ensures that all reads and writes to locked data have been flushed to shared memory before the lock is released.

Together, these memory orderings guarantee that a thread acquiring the lock will see the inner data as the thread that previously released the lock saw it.

Contributing

The source for this book is hosted on github.

Please feel free to contribute your own gotchas!