Unsafe Rust
Why Unsafe Rust Exists
Unsafe Rust is a superset of safe Rust that unlocks low-level operations whose memory safety cannot be verified automatically by the compiler's static type and borrow checker. Rather than disabling Rust's safety rules, unsafe transfers the responsibility for upholding memory and aliasing invariants from the compiler to the programmer within explicitly marked boundaries.
Three Reasons Unsafe Rust Is Necessary
- Static analysis is conservatively incomplete: To guarantee that no rejected program ever commits a memory safety violation, the borrow checker rejects some programs that are actually memory-safe at runtime. For example, splitting a slice
[T]into two disjoint mutable halves (split_at_mut) requires creating two&mut [T]slices from the same backing array—an invariant the compiler cannot prove from index arithmetic alone. - Underlying computer hardware and operating systems are inherently unsafe: Interacting with memory-mapped hardware registers (
0x4000_0000 as *mut u32), executing CPU-specific SIMD intrinsics or inline assembly (asm!), and invoking OS system calls (libc::read,mmap) require raw addresses and foreign calling conventions outside the Rust type system. - Building foundational safe abstractions: Core standard library types—including
Vec<T>,String,Box<T>,Rc<T>,Arc<T>,RefCell<T>,Mutex<T>, andHashMap<K, V>—are implemented in Rust itself using raw pointers and manual heap allocation inside small, auditedunsafeblocks, exposing a 100% safe public API to callers.
+-------------------------------------------------------------------+
| Safe Rust Application Code |
| (100% compiler-verified; cannot cause Undefined Behavior) |
+---------------------------------+---------------------------------+
| Calls safe public methods
v
+-------------------------------------------------------------------+
| Safe Standard Library / Crate Abstractions (Vec, Arc, ...) |
| +-------------------------------------------------------------+ |
| | Small, Audited `unsafe` Blocks | |
| | (Raw pointers, allocator calls, OS syscalls, SIMD, FFI) | |
| +-------------------------------------------------------------+ |
+-------------------------------------------------------------------+
|
v
Hardware / OS Kernel / C ABI Libraries
Example: Why split_at_mut Needs unsafe Internally
Borrowing the same slice mutably twice in safe Rust is rejected with error E0499 even when the index ranges ..mid and mid.. do not overlap:
// ❌ does not compile
fn split_naive(slice: &mut [i32], mid: usize) -> (&mut [i32], &mut [i32]) {
assert!(mid <= slice.len());
(&mut slice[..mid], &mut slice[mid..])
}
fn main() {}
error[E0499]: cannot borrow `*slice` as mutable more than once at a time
--> src/main.rs:4:25
|
2 | fn split_naive(slice: &mut [i32], mid: usize) -> (&mut [i32], &mut [i32]) {
| ----- let's call the lifetime of this reference `'1`
3 | assert!(mid <= slice.len());
4 | (&mut slice[..mid], &mut slice[mid..])
| --------------------^^^^^^^^^^^^^^^^^-
| | |
| | second mutable borrow occurs here
| first mutable borrow occurs here
| returning this value requires that `*slice` is borrowed for `'1`
By checking assert!(mid <= len) first and then using raw pointer arithmetic inside a small unsafe block, split_at_mut provides a completely safe function signature to callers:
use std::slice;
fn split_disjoint_mut(slice: &mut [i32], mid: usize) -> (&mut [i32], &mut [i32]) {
let len = slice.len();
let ptr = slice.as_mut_ptr();
assert!(mid <= len, "mid out of bounds");
// SAFETY: `mid <= len` is verified above, so `[ptr .. ptr + mid]` and
// `[ptr + mid .. ptr + len]` are valid, non-overlapping sub-slices of `slice`.
unsafe {
(
slice::from_raw_parts_mut(ptr, mid),
slice::from_raw_parts_mut(ptr.add(mid), len - mid),
)
}
}
fn main() {
let mut data = [10, 20, 30, 40, 50];
let (left, right) = split_disjoint_mut(&mut data, 2);
left[0] += 1;
right[0] += 100;
println!("left: {:?}, right: {:?}", left, right);
}
left: [11, 20], right: [130, 40, 50]