Safe Rust cannot cause undefined behaviour. Some operations the compiler cannot verify
are allowed only in unsafe contexts, which puts the duty of correctness on
the programmer. The unsafe keyword does not change what is correct: Rust
programs must never cause undefined behaviour (Reference: Unsafety, Behavior considered
undefined).
What needs unsafe
The Reference lists these as the operations that cannot be used in safe code:
- Dereferencing a raw pointer
- Reading or writing a
static mutor an unsafe extern static - Accessing a union field, other than assigning to it
- Calling an unsafe function
- Calling a safe function marked with
#[target_feature]from a function that does not enable the same features - Implementing an unsafe trait
- Declaring an
externblock (in edition 2024; before that theunsafeis optional) - Applying an unsafe attribute to an item
Forms of unsafe
| Form | Who promises what |
|---|---|
unsafe { … } | The block’s author promises the operations inside are correct |
unsafe fn f() | The caller must uphold the function’s documented preconditions, and must call it in an unsafe context |
unsafe trait T | Implementers promise the trait’s invariants hold |
unsafe impl T for U | The implementer makes that promise |
unsafe extern "C" { … } | The author promises the declarations are correct |
#[unsafe(no_mangle)] | The author promises the attribute is used correctly |
In edition 2024, the lint unsafe_op_in_unsafe_fn warns by default when an
unsafe operation is not wrapped in an unsafe block inside an
unsafe fn, so that each unsafe operation stays explicitly marked.
Unsafe code that no safe caller can misuse is sound. If safe code can cause undefined behaviour through it, it is unsound.
/// # Safety
/// `p` must be valid to read and properly aligned.
unsafe fn read_it(p: *const u8) -> u8 {
unsafe { *p }
}
fn main() {
let x = 7u8;
let v = unsafe { read_it(&x) }; // the safety condition holds here
println!("{v}");
}
Raw pointers
*const T and *mut T are raw pointers. Creating one, comparing
it and casting it are safe. Dereferencing one requires unsafe.
| Way to create | Example |
|---|---|
| Coercion from a reference | let p: *const i32 = &x; |
| Raw borrow | let p = &raw const s.field; (no intermediate reference) |
| Standard library | std::ptr::null(), ptr::null_mut(), slice.as_ptr(), Box::into_raw |
| Casts | p as *mut u8, addr as *const T |
Raw pointers can be null, dangling or aliased, carry no lifetime and do not implement
Send or Sync. Useful standard library items are in
std::ptr (read, write,
read_unaligned, drop_in_place), std::mem
(transmute, MaybeUninit, ManuallyDrop) and
std::slice::from_raw_parts. Inline assembly uses
core::arch::asm! and global_asm!.
An integer cast to a pointer and back may not carry the original pointer’s permission
to access memory. See the provenance rules in std::ptr.
FFI
Rust calls foreign code through
extern blocks, and can export
functions with a stable name and ABI. A #[repr(C)] layout is needed for
types that cross the boundary.
use std::ffi::{c_char, CStr, CString};
unsafe extern "C" {
fn strlen(s: *const c_char) -> usize;
}
#[unsafe(no_mangle)]
pub extern "C" fn rust_add(a: i32, b: i32) -> i32 { a + b }
fn call() {
let s = CString::new("hello").unwrap(); // owned, null-terminated
let n = unsafe { strlen(s.as_ptr()) };
let text: &CStr = c"literal"; // a C string literal
println!("{n} {text:?}");
}
- Use
CStrandCString, and the C type aliases instd::ffi, such asc_intandc_char. - A Rust panic must not unwind out of an
extern "C"function. Use"C-unwind"if unwinding must cross the boundary. - Non-null C pointers map to references, and nullable ones to
Option<&T>,Option<Box<T>>or raw pointers.
Behaviour considered undefined
Rust code is incorrect if it does any of the following, inside or outside
unsafe. The Reference warns that this list is not exhaustive and that
there is no formal model of Rust’s semantics yet.
- Data races.
- Accessing (loading from or storing to) a place that is dangling or based on a misaligned pointer.
- An offsetting place projection (a field, tuple index or array or slice index expression) that violates the requirements of in-bounds pointer arithmetic.
- Breaking the pointer aliasing rules. The exact rules are not determined yet. In
outline,
&Tmust point to memory that is not mutated while the reference is live, except data inside anUnsafeCell<U>, and&mut Tmust point to memory that is not read or written by any pointer not derived from the reference, and that no other reference points to, while it is live.Box<T>is treated similarly to&'static mut T. - Mutating immutable bytes. Bytes reachable through a const-promoted expression are
immutable, as are those owned by an immutable binding or immutable static (unless
inside an
UnsafeCell<U>), and those pointed to by a shared reference. A mutation is any write of more than 0 bytes that overlaps them, even if it does not change their contents. - Invoking undefined behaviour through compiler intrinsics.
- Executing code compiled with platform features that the current platform does not support, unless the platform documents this as safe.
- Calling a function with the wrong call ABI, or unwinding past a stack frame that does not allow unwinding.
- Producing an invalid value. A value is produced when it is assigned to or read from a place, passed to a function or primitive operation, or returned from one.
- Incorrect use of inline assembly.
- Violating assumptions of the Rust runtime, including deallocating a stack frame
without running the destructors of its locals, which C’s
longjmpcan do.
Undefined behaviour affects the entire program. Undefined behaviour in C code called from Rust affects the Rust code too, and the reverse.
Dangling and misaligned pointers
A reference or pointer is dangling if not all of the bytes it points to
are part of the same live allocation. A zero-sized access is never dangling. The
size of a value, as given by size_of_val, must never exceed
isize::MAX.
A place is based on a misaligned pointer if the last *
projection in computing it used a pointer that was not aligned for its type. Merely
taking &raw const of such a place is allowed; loading from or
storing to it is undefined.
Invalid values
| Type | A value must be |
|---|---|
bool | false (0) or true (1) |
fn pointer | Non-null |
char | Not a surrogate (0xD800..=0xDFFF) and no greater than char::MAX |
! | Never exist |
| Integer, float, raw pointer | Initialised: not obtained from uninitialised memory |
str | Treated like [u8]: initialised |
enum | A valid discriminant, with the indicated variant’s fields valid |
struct, tuple, array | All fields or elements valid |
union | Not decided yet in general |
Reference or Box<T> | Aligned, non-null, not dangling, and pointing to a valid value |
| Wide pointer metadata | dyn Trait metadata must point to a matching vtable; slice and str metadata must be a valid usize, and the total size must not exceed isize::MAX |
| Types with a restricted range | In range, as for NonNull<T> and NonZero<T> |
Uninitialised memory is therefore implicitly invalid for any type with a restricted set of values. Reading it is permitted only inside unions and in padding bytes.