unsafe, honestly
Going further · Lesson 52
unsafe does not turn the compiler off. It unlocks a short list of operations the compiler cannot check, such as reading through a raw pointer, and moves the proof to you: you write the conditions down and promise they hold. You can write whole programs without it. It is there for talking to C, for building the pieces the standard library is made of, and for data structures the borrow checker cannot express. The craft is to keep the promise small and wrap it in a function that is safe to call.
Coming from GoGo has the unsafe package: unsafe.Pointer and unsafe.Add give you pointer arithmetic, as the program above shows. A file that imports unsafe can use it anywhere, with no block marking each use. Rust marks every use and expects a // SAFETY: comment saying why it is sound.
Coming from KotlinKotlin has no unsafe keyword. On the JVM the low-level tools are Java's: sun.misc.Unsafe and the java.lang.foreign API that the program above uses. They are library calls, not a language boundary: no keyword or block marks the code that uses them, though the JDK now prints warnings.
Coming from PythonPython has no raw pointers in the language. The ctypes module is the escape hatch, and its documentation says that indexing a ctypes pointer can "access or change arbitrary memory locations". Rust draws that line in the language: the unsafe keyword, checked by the compiler.
Coming from TypeScriptTypeScript has no pointers and no way to read memory by address. Its escape hatches (as, any) switch off the type checker but not memory safety. The nearest thing to the program above is a DataView over an ArrayBuffer, which is bounds-checked.
Coming from JavaJava hides raw memory behind APIs, not a keyword: sun.misc.Unsafe (now warning that it will be removed) and the java.lang.foreign API that the program above uses, which is bounds-checked. The JDK now warns when you use either, but they are library calls. In Rust the keyword marks each block, and calling an unsafe fn is itself unsafe.
Coming from C#C# has an unsafe keyword for pointers, and the compiler refuses it unless the project sets AllowUnsafeBlocks. By default unsafe there places no obligation on callers; in Rust an unsafe fn does. The program above avoids the keyword with the Unsafe class.
Coming from CC has no safe subset marked in the language: pointer arithmetic, indexing past the end and use after free all compile. Rust's unsafe adds a boundary, not a new power: inside it you can do what C does (with stricter rules for references), outside it the compiler checks.
Coming from C++C++ has the same unchecked pointers as C, plus checked alternatives such as at() that you have to choose. Rust inverts the default: the checked forms are what you get, and the unchecked ones are behind unsafe, with a name (get_unchecked) that says so.
The quick comparison
fn main() { let prices = [10, 20, 30]; let p = prices.as_ptr(); // making a raw pointer is safe // SAFETY: `p` points at the first of three elements of `prices`, which is alive, and 1 < 3 let second = unsafe { *p.add(1) }; println!("second, read by offset: {second}"); println!("get(1): {:?}", prices.get(1)); println!("get(9): {:?}", prices.get(9));}second, read by offset: 20 get(1): Some(20) get(9): None
package main
import ( "fmt" "unsafe")
func get(a []int, i int) string { if i < len(a) { return fmt.Sprintf("Some(%d)", a[i]) } return "None"}
func main() { prices := []int{10, 20, 30} p := unsafe.Pointer(&prices[0]) second := *(*int)(unsafe.Add(p, unsafe.Sizeof(prices[0]))) fmt.Printf("second, read by offset: %d\n", second) fmt.Printf("get(1): %s\n", get(prices, 1)) fmt.Printf("get(9): %s\n", get(prices, 9))}second, read by offset: 20 get(1): Some(20) get(9): None
import java.lang.foreign.MemorySegmentimport java.lang.foreign.ValueLayout
fun get(a: IntArray, i: Int) = a.getOrNull(i)?.let { "Some($it)" } ?: "None"
fun main() { val prices = intArrayOf(10, 20, 30) // a MemorySegment is a bounds-checked view of memory, read by byte offset val second = MemorySegment.ofArray(prices).get(ValueLayout.JAVA_INT, 4) println("second, read by offset: $second") println("get(1): ${get(prices, 1)}") println("get(9): ${get(prices, 9)}")}second, read by offset: 20 get(1): Some(20) get(9): None
import ctypes
def get(a, i): return f"Some({a[i]})" if i < len(a) else "None"
prices = (ctypes.c_int * 3)(10, 20, 30)p = ctypes.cast(prices, ctypes.POINTER(ctypes.c_int))second = p[1] # a ctypes pointer is not bounds-checkedprint(f"second, read by offset: {second}")print(f"get(1): {get(prices, 1)}")print(f"get(9): {get(prices, 9)}")second, read by offset: 20 get(1): Some(20) get(9): None
function get(a: Int32Array, i: number): string { return i < a.length ? `Some(${a[i]})` : "None";}
const prices = new Int32Array([10, 20, 30]);// there are no pointers: the nearest thing is a byte offset into an ArrayBufferconst second = new DataView(prices.buffer).getInt32(4, true);console.log(`second, read by offset: ${second}`);console.log(`get(1): ${get(prices, 1)}`);console.log(`get(9): ${get(prices, 9)}`);second, read by offset: 20 get(1): Some(20) get(9): None
import java.lang.foreign.MemorySegment;import java.lang.foreign.ValueLayout;
public class Main { static String get(int[] a, int i) { return i < a.length ? "Some(" + a[i] + ")" : "None"; }
public static void main(String[] args) { int[] prices = {10, 20, 30}; // a MemorySegment is a bounds-checked view of memory, read by byte offset MemorySegment seg = MemorySegment.ofArray(prices); int second = seg.get(ValueLayout.JAVA_INT, 4); System.out.println("second, read by offset: " + second); System.out.println("get(1): " + get(prices, 1)); System.out.println("get(9): " + get(prices, 9)); }}second, read by offset: 20 get(1): Some(20) get(9): None
using System.Runtime.CompilerServices;using System.Runtime.InteropServices;
static string Get(int[] a, int i) => i < a.Length ? $"Some({a[i]})" : "None";
int[] prices = [10, 20, 30];// no `unsafe` keyword needed: the Unsafe class does pointer-style arithmetic on a refint second = Unsafe.Add(ref MemoryMarshal.GetArrayDataReference(prices), 1);Console.WriteLine($"second, read by offset: {second}");Console.WriteLine($"get(1): {Get(prices, 1)}");Console.WriteLine($"get(9): {Get(prices, 9)}");second, read by offset: 20 get(1): Some(20) get(9): None
#include <stdio.h>
static void get(const int *a, int len, int i) { if (i < len) printf("get(%d): Some(%d)\n", i, a[i]); else printf("get(%d): None\n", i);}
int main(void) { int prices[] = {10, 20, 30}; int *p = prices; int second = *(p + 1); printf("second, read by offset: %d\n", second); get(prices, 3, 1); get(prices, 3, 9); return 0;}second, read by offset: 20 get(1): Some(20) get(9): None
#include <array>#include <cstdio>#include <string>
static std::string get(const std::array<int, 3> &a, std::size_t i) { return i < a.size() ? "Some(" + std::to_string(a[i]) + ")" : "None";}
int main() { std::array<int, 3> prices = {10, 20, 30}; const int *p = prices.data(); int second = *(p + 1); std::printf("second, read by offset: %d\n", second); std::printf("get(1): %s\n", get(prices, 1).c_str()); std::printf("get(9): %s\n", get(prices, 9).c_str());}second, read by offset: 20 get(1): Some(20) get(9): None
What's different
unsafedoes not turn off the borrow checker. A reference inside anunsafeblock is still checked. The keyword unlocks five main operations (the Reference lists a few more): dereference a raw pointer, call anunsafe fn, use astatic mut, implement anunsafe trait, and read aunionfield.- Making a raw pointer is safe. Reading or writing through it is not, because only you know whether it is null, dangling, misaligned or aliased.
- An
unsafe fnlists conditions its caller must meet. Anunsafeblock says "I checked them, here is why". Write the why in a// SAFETY:comment, and the conditions in a# Safetysection. - The point is a safe abstraction: a function whose checks make every call sound, so callers never write
unsafe. The standard library'sVecandsplit_at_mutwork this way.
Reading through a raw pointer without unsafe
Looks fine: in Go, Python, C and C++ it runs fine. Rust won't compile it.
Fine in Go. Rust can't prove it's safe.No direct equivalent in Kotlin. Just a Rust rule.Fine in Python. Rust can't prove it's safe.No direct equivalent in TypeScript. Just a Rust rule.No direct equivalent in Java. Just a Rust rule.C# refuses this too. Same idea, different words.Fine in C. Rust can't prove it's safe.Fine in C++. Rust can't prove it's safe.fn main() { let prices = [10, 20, 30]; let p = prices.as_ptr(); let second = *p.add(1); println!("second: {second}");}package main
import ( "fmt" "unsafe")
func main() { prices := []int{10, 20, 30} p := unsafe.Pointer(&prices[0]) second := *(*int)(unsafe.Add(p, unsafe.Sizeof(prices[0]))) fmt.Printf("second: %d\n", second)}second: 20
Kotlin on the JVM has no pointers to dereference. The JVM's raw-memory APIs are Java's, and they are calls, not syntax.
import ctypes
prices = (ctypes.c_int * 3)(10, 20, 30)p = ctypes.cast(prices, ctypes.POINTER(ctypes.c_int))second = p[1]print(f"second: {second}")second: 20
TypeScript has no pointers. The nearest thing is a DataView, which reads by byte offset inside an ArrayBuffer.
Java has no pointers in the language. Reading memory by address goes through sun.misc.Unsafe or java.lang.foreign, which are library calls and not syntax.
C# refuses this too: CS0214, pointers may only be used in an unsafe context. It has the same idea, and it adds a project switch (AllowUnsafeBlocks) before an unsafe block compiles at all.
int[] prices = [10, 20, 30];fixed (int* p = prices){ int second = *(p + 1); Console.WriteLine($"second: {second}");}Program.cs(2,1): error CS0214: Pointers and fixed size buffers may only be used in an unsafe context Program.cs(2,8): error CS0214: Pointers and fixed size buffers may only be used in an unsafe context Program.cs(4,20): error CS0214: Pointers and fixed size buffers may only be used in an unsafe context Program.cs(4,19): error CS0214: Pointers and fixed size buffers may only be used in an unsafe context
#include <stdio.h>
int main(void) { int prices[] = {10, 20, 30}; int *p = prices; int second = *(p + 1); printf("second: %d\n", second); return 0;}second: 20
#include <array>#include <cstdio>
int main() { std::array<int, 3> prices = {10, 20, 30}; const int *p = prices.data(); int second = *(p + 1); std::printf("second: %d\n", second);}second: 20
What does the compiler say? Guess first, then open.
error[E0133]: dereference of raw pointer is unsafe and requires unsafe block --> src/main.rs:4:18 | 4 | let second = *p.add(1); | ^^^^^^^^^ 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 error[E0133]: call to unsafe function `std::ptr::const_ptr::<impl *const T>::add` is unsafe and requires unsafe block --> src/main.rs:4:19 | 4 | let second = *p.add(1); | ^^^^^^^^ call to unsafe function | = note: consult the function's documentation for information on how to avoid undefined behavior
Go compiles this and prints second: 20. The import "unsafe" line is the marker; the compiler asks for no block and no reason at the place of use.
Kotlin on the JVM has no pointers to dereference. The JVM's raw-memory APIs are Java's, and they are calls, not syntax.
Python prints second: 20. ctypes pointers can be indexed with no marker, and its documentation says that you can use them to "access or change arbitrary memory locations".
TypeScript has no pointers. The nearest thing is a DataView, which reads by byte offset inside an ArrayBuffer.
Java has no pointers in the language. Reading memory by address goes through sun.misc.Unsafe or java.lang.foreign, which are library calls and not syntax.
C# refuses this too: CS0214, pointers may only be used in an unsafe context. It has the same idea, and it adds a project switch (AllowUnsafeBlocks) before an unsafe block compiles at all.
C prints second: 20. Pointer arithmetic is ordinary C: it needs no marker.
C++ prints second: 20. Pointer arithmetic on data() needs no marker, and at() is the checked alternative you have to choose.
p.add(1) is a call to an unsafe function (it needs the result to stay inside the same allocation) and * on a raw pointer is a dereference. Both are among the operations that only an unsafe block allows, so the compiler reports each.
The compiler cannot see what p points at. It could be null, freed, misaligned or already borrowed, and a read through it would then be undefined behavior. An unsafe block says you checked, and the // SAFETY: comment is where you write what you checked.
The fix
Compiles and runs.
fn main() { let prices = [10, 20, 30]; let p = prices.as_ptr(); // SAFETY: `p` points at the first of three elements of `prices`, which is alive, and 1 < 3 let second = unsafe { *p.add(1) }; println!("second: {second}");}second: 20
The toolkit
Outputs come from Linux on 64-bit ARM with Rust 1.99.1, edition 2024, debug build. No program here prints the result of undefined behavior, because that result is not predictable (defined below).
What unsafe unlocks
The Rust Book says it plainly: “unsafe doesn’t turn off the borrow checker or disable any of Rust’s other safety checks”. It lets you do five more things. The compiler cannot check any of them, so each comes with a promise, and breaking one is undefined behavior (UB). That is not an exception, a crash you can reproduce or a wrong answer. A program with UB has no defined meaning: the compiler may assume it never happens, so the program can do anything, including look fine until the optimizer changes. The Nomicon says UB “gives the compiler full rights to do arbitrarily bad things to your program”. The Reference’s list of what counts, which is not exhaustive, includes data races, accessing a dangling or misaligned place, and producing an invalid value such as a bool that is not 0 or 1. To dereference a pointer is to read or write the value at its address.
unsafe lets you |
You promise |
|---|---|
| dereference a raw pointer (see Raw pointers) | it is not null, is aligned for its type, points at a live value of that type, and no other access conflicts with it |
call an unsafe fn |
the conditions in its # Safety documentation hold |
read or write a static mut |
no other access happens at the same time |
implement an unsafe trait |
you meet the contract the trait states (Send and Sync are the ones you meet) |
read a field of a union (all its fields share the same bytes) |
the bytes are a valid value of that field’s type |
The Reference lists a few more, such as declaring an extern block and applying an unsafe attribute; these five are the ones you will meet first. Each appears in this program, and the ZeroIsValid trait shows the pattern for a trait: its author writes the contract, and each unsafe impl signs it.
union Bits { float: f32, int: u32,}
static mut HITS: u32 = 0;
/// # Safety/// `p` must point to a valid, initialized `i32`.unsafe fn read(p: *const i32) -> i32 { // SAFETY: the caller promised `p` is valid for reads unsafe { *p }}
/// # Safety/// Implement this only for types where every all-zero bit pattern is a valid value.unsafe trait ZeroIsValid {}
// SAFETY: the integer 0 is a valid u32unsafe impl ZeroIsValid for u32 {}
fn zeroed<T: ZeroIsValid>() -> T { // SAFETY: the trait's contract says all-zero bits are a valid T unsafe { std::mem::zeroed() }}
fn main() { let x = 7; let p = &raw const x; // making a raw pointer is safe println!("p is null: {}", p.is_null()); // SAFETY: `p` points at `x`, which is alive, initialized and not mutated let first = unsafe { *p }; println!("1 dereference: {first}"); // SAFETY: the same `p` meets `read`'s condition let second = unsafe { read(p) }; println!("2 unsafe fn: {second}"); // SAFETY: this program has one thread, so no other access to `HITS` happens let hits = unsafe { HITS += 1; HITS // a copy, not a reference }; println!("3 static mut: {hits}"); println!("4 unsafe trait: {}", zeroed::<u32>()); let bits = Bits { float: 1.0 }; // SAFETY: every 4-byte pattern is a valid u32 let int = unsafe { bits.int }; println!("5 union field: {int}");}p is null: false 1 dereference: 7 2 unsafe fn: 7 3 static mut: 1 4 unsafe trait: 0 5 union field: 1065353216
Without the unsafe blocks, the dereference, the call, the static mut and the union read are each an E0133 error, as in the gotcha above. And the block does not switch the borrow checker off: two &mut into one vector are still refused inside it.
fn main() { let mut scores = vec![1, 2, 3]; unsafe { let p = scores.as_ptr(); let last = *p.add(2); // needs unsafe let first = &mut scores[0]; // an ordinary reference let second = &mut scores[1]; // a second one: still checked *first += last; *second += last; } println!("{scores:?}");}error[E0499]: cannot borrow `scores` as mutable more than once at a time --> src/main.rs:7:27 | 6 | let first = &mut scores[0]; // an ordinary reference | ------ first mutable borrow occurs here 7 | let second = &mut scores[1]; // a second one: still checked | ^^^^^^ second mutable borrow occurs here 8 | *first += last; | -------------- first borrow later used here | = help: use `.split_at_mut(position)` to obtain two mutable non-overlapping sub-slices
Should you write it?
Usually not. It is needed to call C or an operating system, to build a primitive such as Vec that safe Rust cannot express, and for a structure the borrow checker rejects (the borrow checker lesson lists the safe alternatives first). It is not a speed switch. If you know sun.misc.Unsafe as the Java way to go faster, do not carry that over: get_unchecked skips one check and hands you a proof to write, and this page has no timing. Try safe code, then std (split_at_mut, swap, mem::take), then a well-used crate, and only then your own block, small and commented, behind a safe function.
Raw pointers
A raw pointer, *const T or *mut T, is an address with a type and nothing else: no lifetime, no promise that it is valid, and it can be null or dangling (pointing at memory that is gone). You can make one from a reference (&raw const x, &raw mut x, or as_ptr()), and arithmetic that only produces a pointer (wrapping_add) is safe too. Reading or writing through one is the unsafe part.
use std::mem::align_of;use std::ptr;
fn main() { let mut scores = [10, 20, 30];
// creating raw pointers is safe, from an array or from nothing let first: *const i32 = scores.as_ptr(); let nothing: *const i32 = ptr::null(); println!( "first is null: {}, nothing is null: {}", first.is_null(), nothing.is_null() ); println!( "first is aligned: {}", (first as usize).is_multiple_of(align_of::<i32>()) );
// wrapping_add is safe: it makes a pointer and promises nothing about it let end = first.wrapping_add(3); // SAFETY: both pointers come from `scores`; `end` is one past its last element let gap = unsafe { end.offset_from(first) }; println!("end - first, in elements: {gap}");
// reading and writing through a pointer are not safe let last: *mut i32 = &raw mut scores[2]; // SAFETY: `last` points at scores[2], which is alive, initialised and not borrowed unsafe { *last += 1; println!("last through the pointer: {}", *last); } println!("scores: {scores:?}");}first is null: false, nothing is null: true first is aligned: true end - first, in elements: 3 last through the pointer: 31 scores: [10, 20, 31]
Safe code cannot get a dangling reference, but it can make a dangling pointer. Reading this one would be undefined behavior, so it only checks for null:
fn main() { let p: *const i32; { let x = 5; p = &raw const x; } // `x` is gone. Making the pointer and checking it for null are allowed; reading it is not. println!("null: {}", p.is_null());}null: false
The compiler does not follow raw pointers, so it cannot tell you two of them overlap. That is the second half of “you promise”: here a and b point at the same n and nothing complains (with two &mut n it would be E0499).
fn main() { let mut n = 1; let a = &raw mut n; let b = a; // a second pointer to the same place: the compiler tracks neither // SAFETY: `a` and `b` are copies of one pointer to `n`, which is alive, and the // two writes happen one after the other unsafe { *a += 10; *b += 100; } println!("n = {n}");}n = 111
Raw pointers may alias, as in C. References may not: a &mut is a promise that nothing else reaches that place, like a C restrict pointer, and merely creating two &mut to one place is UB even if neither is used. Turning raw pointers back into references is a common way to get UB inside unsafe.
unsafe fn, blocks and // SAFETY: comments
The unsafe keyword has two jobs, and the Reference names them: it is “used to create or discharge the obligation to prove something safe”. An unsafe fn creates an obligation: its caller must meet the conditions it lists. An unsafe block discharges one: the author says “I checked”. The habit that makes this readable is two comments. On the function, a # Safety section in the docs lists the conditions, as get_unchecked does (“Calling this method with an out-of-bounds index is undefined behavior”). On each block, a // SAFETY: comment says why the conditions hold here. The Book calls the SAFETY comment idiomatic. Clippy can ask for both: missing_safety_doc is on by default for a pub unsafe fn, undocumented_unsafe_blocks is opt-in (the page build does not run Clippy).
Since the 2024 edition, the body of an unsafe fn is no longer an unsafe block. The edition guide says the unsafe_op_in_unsafe_fn lint “now warns by default” (warning-level output is not shown on this page, so the snippet denies it to show the message):
#![deny(unsafe_op_in_unsafe_fn)]
/// # Safety/// `p` must point to a valid, initialised `i32`.unsafe fn read(p: *const i32) -> i32 { *p}
fn main() { let x = 7; // SAFETY: `&raw const x` points at `x`, which is alive and initialised println!("{}", unsafe { read(&raw const x) });}error[E0133]: dereference of raw pointer is unsafe and requires unsafe block --> src/main.rs:6:5 | 6 | *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 note: an unsafe function restricts its caller, but its body is safe by default --> src/main.rs:5:1 | 5 | unsafe fn read(p: *const i32) -> i32 { | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ = note: for more information, see <https://doc.rust-lang.org/edition-guide/rust-2024/unsafe-op-in-unsafe-fn.html> note: the lint level is defined here --> src/main.rs:1:9 | 1 | #![deny(unsafe_op_in_unsafe_fn)] | ^^^^^^^^^^^^^^^^^^^^^^
Keep blocks small, as the Book advises.
A safe function over unsafe code
The aim is not to scatter unsafe through a program. It is to hide it behind a function that is safe to call, whose checks make the promise true for every input. The classic case is split_at_mut: two non-overlapping &mut halves of one slice. Safe Rust cannot write it, because the borrow checker sees two borrows of v, even though they cover different elements:
fn split_at_mut(v: &mut [i32], mid: usize) -> (&mut [i32], &mut [i32]) { assert!(mid <= v.len()); (&mut v[..mid], &mut v[mid..])}
fn main() { let mut v = [1, 2, 3, 4]; let (a, b) = split_at_mut(&mut v, 2); a[0] = 10; b[0] = 30; println!("{v:?}");}error[E0499]: cannot borrow `*v` as mutable more than once at a time --> src/main.rs:3:26 | 1 | fn split_at_mut(v: &mut [i32], mid: usize) -> (&mut [i32], &mut [i32]) { | - let's call the lifetime of this reference `'1` 2 | assert!(mid <= v.len()); 3 | (&mut v[..mid], &mut v[mid..]) | ---------------------^-------- | | | | | | | second mutable borrow occurs here | | first mutable borrow occurs here | returning this value requires that `*v` is borrowed for `'1` | = help: use `.split_at_mut(position)` to obtain two mutable non-overlapping sub-slices
The standard library has split_at_mut already. Writing it again with raw parts shows the shape:
use std::slice;
/// Splits `v` into two non-overlapping mutable halves at `mid`.fn split_at_mut(v: &mut [i32], mid: usize) -> (&mut [i32], &mut [i32]) { let len = v.len(); let ptr = v.as_mut_ptr(); assert!(mid <= len, "mid {mid} is past the end ({len})"); // SAFETY: `ptr` is valid for `len` elements and we hold the only `&mut` to them. // mid <= len, so [0, mid) and [mid, len) are in bounds and do not overlap. unsafe { ( slice::from_raw_parts_mut(ptr, mid), slice::from_raw_parts_mut(ptr.add(mid), len - mid), ) }}
fn main() { let mut v = [1, 2, 3, 4, 5]; let (a, b) = split_at_mut(&mut v, 2); a[0] = 10; b[0] = 30; println!("halves: {a:?} and {b:?}"); println!("whole: {v:?}");}halves: [10, 2] and [30, 4, 5] whole: [10, 2, 30, 4, 5]
Who is responsible for what?
| Piece | Who makes the promise | Written as |
|---|---|---|
from_raw_parts_mut (an unsafe fn) |
the code that calls it | its # Safety docs, your // SAFETY: |
split_at_mut (a safe fn) |
its author, for every caller and input | the assert!, and the comment above the block |
| a private field that must stay true (an invariant) | the module that owns it (Modules) | no code outside can break it |
If the assert! were missing, a safe call with a big mid could create slices past the end. Such a function is called unsound: safe code can cause undefined behavior through it. With the check, a bad input is a panic, which is defined:
use std::slice;
fn split_at_mut(v: &mut [i32], mid: usize) -> (&mut [i32], &mut [i32]) { let len = v.len(); let ptr = v.as_mut_ptr(); assert!(mid <= len, "mid {mid} is past the end ({len})"); // SAFETY: mid <= len, as asserted above, so both halves are in bounds and do not overlap unsafe { ( slice::from_raw_parts_mut(ptr, mid), slice::from_raw_parts_mut(ptr.add(mid), len - mid), ) }}
fn main() { let mut v = [1, 2, 3]; let _halves = split_at_mut(&mut v, 9);}thread 'main' (1) panicked at src/main.rs:6:5: mid 9 is past the end (3) note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
Vec is built the same way. By the documentation it is “a (pointer, capacity, length) triplet”, with len initialized elements followed by capacity - len uninitialized ones. Safe methods are the only code that can change len, so they can keep it true, and safe code never sees the uninitialized slots.
This is also why Rust’s standard library needs unsafe when Java’s ArrayList and Python’s list do not. They sit on a layer that is already unchecked: ArrayList on an array the JVM allocates and bounds-checks (HotSpot is C++), a list on C code inside CPython. Rust has no such runtime underneath, so that layer is Rust: a small amount of unsafe inside Vec and friends, behind safe methods.
use std::mem::size_of;
fn main() { let mut v: Vec<i32> = Vec::with_capacity(4); v.extend([10, 20, 30]); println!("size_of::<Vec<i32>>(): {} bytes", size_of::<Vec<i32>>()); println!( "three words: {}", size_of::<Vec<i32>>() == 3 * size_of::<usize>() ); println!("len: {}", v.len()); println!("capacity >= 4: {}", v.capacity() >= 4); // slot 3 is inside the buffer but holds no value yet: only `len` slots are initialised println!("get(3): {:?}", v.get(3));}size_of::<Vec<i32>>(): 24 bytes three words: true len: 3 capacity >= 4: true get(3): None
Globals and static mut
A static mut is a global you can change, and every read or write is unsafe. What Rust doesn’t have shows the edition 2024 error for creating a reference to one (static_mut_refs, deny by default). The fix is usually a type that makes the sharing safe: an atomic for a counter, a Mutex for a collection, a OnceLock for a value set once (Shared state). Mutex::new is a const fn, so it can start a static:
use std::sync::Mutex;
// `Mutex::new` is a `const fn`, so it can start a `static`static LOG: Mutex<Vec<String>> = Mutex::new(Vec::new());
fn main() { LOG.lock().unwrap().push(String::from("started")); println!("log: {:?}", LOG.lock().unwrap());}log: ["started"]
The same goes for unsafe impl Send and Sync: Send and Sync shows when that promise can be made.
Calling C
Rust cannot see inside a C function, so calling one is a main reason to need unsafe. In edition 2024 the block that declares them must itself be unsafe:
use std::ffi::{CString, c_char};extern "C" { fn strlen(s: *const c_char) -> usize;}fn main() { let s = CString::new("hello").unwrap(); println!("{}", unsafe { strlen(s.as_ptr()) });}error: extern blocks must be unsafe --> src/main.rs:2:1 | 2 | extern "C" { | ^ | | | _help: needs `unsafe` before the extern keyword: `unsafe` | | 3 | | fn strlen(s: *const c_char) -> usize; 4 | | } | |_^
Each item inside is unsafe to call unless you mark it safe, which is you promising on the C function’s behalf (edition guide). sqrt can carry that mark because it is defined for every f64; C’s abs could not, because abs(INT_MIN) is undefined behavior in C.
use std::ffi::{CString, c_char};
unsafe extern "C" { fn strlen(s: *const c_char) -> usize; safe fn sqrt(x: f64) -> f64;}
fn main() { let text = CString::new("héllo").unwrap(); // SAFETY: `text` is a valid NUL-terminated string that lives until after the call let n = unsafe { strlen(text.as_ptr()) }; println!("strlen(\"héllo\") = {n} bytes"); println!("sqrt(81) = {}", sqrt(81.0)); println!("str::len = {}", "héllo".len());}strlen("héllo") = 6 bytes
sqrt(81) = 9
str::len = 6strlen counts bytes, not characters: héllo is 6. Rust and C/C++ goes through the whole boundary: who frees what, repr(C), and panics.
Counting unsafe in your own code
The unsafe_code lint is allowed by default and reports unsafe code. #![forbid(unsafe_code)] turns any unsafe in the crate into an error, so the crate itself provably has none:
#![forbid(unsafe_code)]
fn main() { let x = 7; let p = &raw const x; // SAFETY: `p` points at `x`, which is alive println!("{}", unsafe { *p });}error: usage of an `unsafe` block --> src/main.rs:7:20 | 7 | println!("{}", unsafe { *p }); | ^^^^^^^^^^^^^ | note: the lint level is defined here --> src/main.rs:1:11 | 1 | #![forbid(unsafe_code)] | ^^^^^^^^^^^
Going deeper
What can be detected?
Some UB can be shown. The compiler runs const code in an interpreter that checks some of these rules, so a violation there is a compile error (E0080). transmute reinterprets a value’s bits as another type of the same size:
// SAFETY: none. Only 0 and 1 are valid bools, so this breaks the rule for `transmute`.const FLAG: bool = unsafe { std::mem::transmute::<u8, bool>(3) };
fn main() { println!("{FLAG}");}error[E0080]: constructing invalid value of type bool: encountered 0x03, but expected a boolean --> src/main.rs:2:1 | 2 | const FLAG: bool = unsafe { std::mem::transmute::<u8, bool>(3) }; | ^^^^^^^^^^^^^^^^ it is undefined behavior to use this value | = note: the rules on what exactly is undefined behavior aren't clear, so this check might be overzealous. Please open an issue on the rustc repository if you believe it should not be considered undefined behavior. = note: the raw bytes of the constant (size: 1, align: 1) { 03 │ . } note: erroneous constant encountered --> src/main.rs:5:16 | 5 | println!("{FLAG}"); | ^^^^
At run time the net has holes. In a debug build, the standard library checks some preconditions of unsafe functions, and the message admits it is optional:
fn main() { let v = vec![1, 2, 3]; let i = v.len() + 6; // SAFETY: none. This breaks the rule that `i` must be less than `v.len()`. let x = unsafe { *v.get_unchecked(i) }; println!("{x}");}thread 'main' (1) panicked at src/main.rs:5:25: unsafe precondition(s) violated: slice::get_unchecked requires that the index is within the slice This indicates a bug in the program. This Undefined Behavior check is optional, and cannot be relied on for safety. note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace thread caused non-unwinding panic. aborting.
The runner builds only in debug, so a release build was not run. The proper tool is Miri, “an Undefined Behavior detection tool for Rust”. It needs a nightly toolchain and is not installed in this site’s image, so not run here. For C and C++ the equivalent is a sanitizer (below).
Reviewing a block: five questions
When you read someone’s unsafe block, or write your own, ask the questions that the Reference’s list of undefined behavior turns into. They are the choices in the first exercise.
| Ask | What goes wrong if the answer is no |
|---|---|
| Is the index or offset in bounds? | an access past the end of the allocation (get_unchecked, ptr.add) |
| Is the pointer aligned for the type it is read as? | a misaligned access, which the Reference lists as undefined behavior |
| Is what it points at alive? | a dangling access, as with the pointer to x above |
| Is the memory initialized, and a valid value of the type? | an invalid value, such as the bool that held 3 |
| Is anything else using the same memory (is it aliased)? | two &mut to one place, or a data race (Send and Sync) |
A good // SAFETY: comment answers the ones that apply, with the reason: “mid is at most len, asserted above” answers the first and the last in split_at_mut. A comment that says “this is fine” answers none.
How much unsafe is out there
A 2020 study (Evans, Campbell and Soffa) found unsafe “in less than 30% of Rust libraries”, while more than half of the libraries “cannot be entirely statically checked by the Rust compiler because of Unsafe Rust hidden somewhere in a library’s call chain”. Both facts matter: your own code may have none, and your program still runs on code that does.
Translating from what you know
Practice
Try it
Each exercise has checks in main: replace the todo!() calls and run until it prints all checks passed. The starters compile and fail when run.
Audit six unsafe blocks
Six functions have a // SAFETY: comment. For each, name the condition its block relies on and the code does not make true, using the choices in the five questions (NothingMissing for the one that is sound). slice::from_raw_parts(p, n) builds a &[T] from a pointer and a length, the shared twin of from_raw_parts_mut. Do not call the functions: five are undefined behavior on some input, and Miri is not run here, so reading is the point. A wrong answer names the first wrong function and asks a question to reason from, not the answer.
// `blank` makes rustc warn (invalid_value); the lint is allowed so the file stays quiet#![allow(dead_code, invalid_value)]
use std::slice;
/// What a `// SAFETY:` comment on the unsafe block below would have to promise,/// and the code does not make true.#[derive(Debug, Clone, Copy, PartialEq)]enum Missing { /// Nothing is missing: the code establishes everything its unsafe block needs. NothingMissing, /// The index is within the slice. InBounds, /// The pointer has the alignment of the type it is read as. Aligned, /// The pointer points at something that is still alive. Alive, /// The memory holds a valid value of the type. Initialized, /// No other reference to the same memory exists while this one is used. NotAliased,}
fn first(v: &[u32]) -> u32 { // SAFETY: `v` is a valid slice, so its pointer is valid for reads unsafe { *v.as_ptr() }}
fn as_words(bytes: &[u8]) -> &[u32] { // SAFETY: the pointer is valid for `bytes.len() / 4` words of 4 bytes each unsafe { slice::from_raw_parts(bytes.as_ptr() as *const u32, bytes.len() / 4) }}
fn leftover() -> i32 { let p = { let x = 7; &raw const x }; // SAFETY: `p` was made from a valid variable unsafe { *p }}
fn blank() -> String { // SAFETY: a `String` is just bytes, so all zero bytes are a valid `String` unsafe { std::mem::zeroed() }}
fn last(v: &[u32]) -> Option<u32> { if v.is_empty() { return None; } // SAFETY: `v` is not empty, so `len - 1` is in bounds, and the pointer comes from a live slice Some(unsafe { *v.as_ptr().add(v.len() - 1) })}
fn twice(v: &mut [i32]) -> (&mut i32, &mut i32) { assert!(!v.is_empty()); let p = v.as_mut_ptr(); // SAFETY: `p` is valid for reads and writes of one `i32`, since `v` is not empty unsafe { (&mut *p, &mut *p) }}
/// For each function above, in order (`first`, `as_words`, `leftover`, `blank`, `last`,/// `twice`), say which condition its unsafe block relies on without making it true./// Do not call them: five of the six are undefined behavior on some input.fn audit() -> [Missing; 6] { todo!("one Missing per function: read each SAFETY comment, then the code around it")}
fn main() { let expected = [ Missing::InBounds, Missing::Aligned, Missing::Alive, Missing::Initialized, Missing::NothingMissing, Missing::NotAliased, ]; let names = ["first", "as_words", "leftover", "blank", "last", "twice"]; let hints = [ "what does `v.as_ptr()` point at when `v` has no elements?", "a `&[u8]` can start at any address; which addresses may a `u32` live at?", "where does `x` live, and when does that end?", "can all-zero bytes be a `String`? Think about its pointer field.", "read the code above the block: is each condition already made true there?", "how many `&mut` to the same `i32` exist at once? (the `assert!` already handles the empty slice)", ]; let guess = audit(); // the first wrong answer is named, with a question to reason from, not the answer for (i, (g, e)) in guess.iter().zip(&expected).enumerate() { assert!( g == e, "{}: you answered {g:?}. Ask: {}", names[i], hints[i] ); } // the one sound function really is sound: it handles both of the cases that matter assert_eq!(last(&[]), None); assert_eq!(last(&[1, 2, 3]), Some(3)); println!("all checks passed");}Open in the Rust Playground ↗Run it first: it stops at todo!() with not yet implemented and a hint, and the compiler warns that the parameter is unused. Both are expected. Replace the todo!() with your code and run it again until it prints all checks passed.
Show a solution
// `blank` makes rustc warn (invalid_value); the lint is allowed so the file stays quiet#![allow(dead_code, invalid_value)]
use std::slice;
/// What a `// SAFETY:` comment on the unsafe block below would have to promise,/// and the code does not make true.#[derive(Debug, Clone, Copy, PartialEq)]enum Missing { /// Nothing is missing: the code establishes everything its unsafe block needs. NothingMissing, /// The index is within the slice. InBounds, /// The pointer has the alignment of the type it is read as. Aligned, /// The pointer points at something that is still alive. Alive, /// The memory holds a valid value of the type. Initialized, /// No other reference to the same memory exists while this one is used. NotAliased,}
fn first(v: &[u32]) -> u32 { // SAFETY: `v` is a valid slice, so its pointer is valid for reads unsafe { *v.as_ptr() }}
fn as_words(bytes: &[u8]) -> &[u32] { // SAFETY: the pointer is valid for `bytes.len() / 4` words of 4 bytes each unsafe { slice::from_raw_parts(bytes.as_ptr() as *const u32, bytes.len() / 4) }}
fn leftover() -> i32 { let p = { let x = 7; &raw const x }; // SAFETY: `p` was made from a valid variable unsafe { *p }}
fn blank() -> String { // SAFETY: a `String` is just bytes, so all zero bytes are a valid `String` unsafe { std::mem::zeroed() }}
fn last(v: &[u32]) -> Option<u32> { if v.is_empty() { return None; } // SAFETY: `v` is not empty, so `len - 1` is in bounds, and the pointer comes from a live slice Some(unsafe { *v.as_ptr().add(v.len() - 1) })}
fn twice(v: &mut [i32]) -> (&mut i32, &mut i32) { assert!(!v.is_empty()); let p = v.as_mut_ptr(); // SAFETY: `p` is valid for reads and writes of one `i32`, since `v` is not empty unsafe { (&mut *p, &mut *p) }}
/// For each function above, in order (`first`, `as_words`, `leftover`, `blank`, `last`,/// `twice`), say which condition its unsafe block relies on without making it true./// Do not call them: five of the six are undefined behavior on some input.fn audit() -> [Missing; 6] { [ Missing::InBounds, Missing::Aligned, Missing::Alive, Missing::Initialized, Missing::NothingMissing, Missing::NotAliased, ]}
fn main() { let expected = [ Missing::InBounds, Missing::Aligned, Missing::Alive, Missing::Initialized, Missing::NothingMissing, Missing::NotAliased, ]; let names = ["first", "as_words", "leftover", "blank", "last", "twice"]; let hints = [ "what does `v.as_ptr()` point at when `v` has no elements?", "a `&[u8]` can start at any address; which addresses may a `u32` live at?", "where does `x` live, and when does that end?", "can all-zero bytes be a `String`? Think about its pointer field.", "read the code above the block: is each condition already made true there?", "how many `&mut` to the same `i32` exist at once? (the `assert!` already handles the empty slice)", ]; let guess = audit(); // the first wrong answer is named, with a question to reason from, not the answer for (i, (g, e)) in guess.iter().zip(&expected).enumerate() { assert!( g == e, "{}: you answered {g:?}. Ask: {}", names[i], hints[i] ); } // the one sound function really is sound: it handles both of the cases that matter assert_eq!(last(&[]), None); assert_eq!(last(&[1, 2, 3]), Some(3)); println!("all checks passed");}all checks passed
Wrap an unsafe function
swap_unchecked is an unsafe fn. Write swap_checked, a safe function that returns an error for a bad index and calls it otherwise. The unsafe function only counts a bad call, so a mistake cannot corrupt memory here. The checks count calls to it: using the safe slice::swap instead (real code would), or calling it without checking, fails.
use std::sync::atomic::{AtomicUsize, Ordering};
static CALLS: AtomicUsize = AtomicUsize::new(0);static BAD_CALLS: AtomicUsize = AtomicUsize::new(0);
/// Swaps `v[i]` and `v[j]` without checking the indices.////// # Safety/// `i` and `j` must both be less than `v.len()`.unsafe fn swap_unchecked(v: &mut [i32], i: usize, j: usize) { CALLS.fetch_add(1, Ordering::SeqCst); if i >= v.len() || j >= v.len() { // The real thing would be undefined behavior. For this exercise the mistake is // counted and nothing else happens, so a wrong answer cannot corrupt memory. BAD_CALLS.fetch_add(1, Ordering::SeqCst); return; } let p = v.as_mut_ptr(); // SAFETY: both indices are in bounds, and `ptr::swap` allows `i == j` unsafe { std::ptr::swap(p.add(i), p.add(j)) }}
#[derive(Debug, PartialEq)]enum SwapError { OutOfRange { index: usize, len: usize },}
/// A safe wrapper around `swap_unchecked`: check `i` first, then `j`, and call the unsafe/// function only when both are in range. Say in a `// SAFETY:` comment why the call is sound.fn swap_checked(v: &mut [i32], i: usize, j: usize) -> Result<(), SwapError> { todo!("return Err(OutOfRange) for the first index that is too big, else call swap_unchecked")}
fn main() { let mut v = [1, 2, 3]; assert_eq!(swap_checked(&mut v, 0, 2), Ok(())); assert_eq!(v, [3, 2, 1]); assert_eq!(swap_checked(&mut v, 1, 1), Ok(())); assert_eq!(v, [3, 2, 1]);
assert_eq!( swap_checked(&mut v, 7, 0), Err(SwapError::OutOfRange { index: 7, len: 3 }) ); assert_eq!( swap_checked(&mut v, 0, 9), Err(SwapError::OutOfRange { index: 9, len: 3 }) ); assert_eq!( swap_checked(&mut v, 7, 9), Err(SwapError::OutOfRange { index: 7, len: 3 }) ); assert_eq!(v, [3, 2, 1], "a rejected call must not change the slice"); assert_eq!( swap_checked(&mut [], 0, 0), Err(SwapError::OutOfRange { index: 0, len: 0 }) );
// two calls were allowed to reach the unsafe function: no more, no fewer, and none wrong assert_eq!( CALLS.load(Ordering::SeqCst), 2, "call the unsafe function once per valid swap, and only then" ); assert_eq!( BAD_CALLS.load(Ordering::SeqCst), 0, "the unsafe function was called with a bad index" ); println!("all checks passed");}Open in the Rust Playground ↗Run it first: it stops at todo!() with not yet implemented and a hint, and the compiler warns that the parameter is unused. Both are expected. Replace the todo!() with your code and run it again until it prints all checks passed.
Show a solution
use std::sync::atomic::{AtomicUsize, Ordering};
static CALLS: AtomicUsize = AtomicUsize::new(0);static BAD_CALLS: AtomicUsize = AtomicUsize::new(0);
/// Swaps `v[i]` and `v[j]` without checking the indices.////// # Safety/// `i` and `j` must both be less than `v.len()`.unsafe fn swap_unchecked(v: &mut [i32], i: usize, j: usize) { CALLS.fetch_add(1, Ordering::SeqCst); if i >= v.len() || j >= v.len() { // The real thing would be undefined behavior. For this exercise the mistake is // counted and nothing else happens, so a wrong answer cannot corrupt memory. BAD_CALLS.fetch_add(1, Ordering::SeqCst); return; } let p = v.as_mut_ptr(); // SAFETY: both indices are in bounds, and `ptr::swap` allows `i == j` unsafe { std::ptr::swap(p.add(i), p.add(j)) }}
#[derive(Debug, PartialEq)]enum SwapError { OutOfRange { index: usize, len: usize },}
/// A safe wrapper around `swap_unchecked`: check `i` first, then `j`, and call the unsafe/// function only when both are in range. Say in a `// SAFETY:` comment why the call is sound.fn swap_checked(v: &mut [i32], i: usize, j: usize) -> Result<(), SwapError> { for index in [i, j] { if index >= v.len() { return Err(SwapError::OutOfRange { index, len: v.len(), }); } } // SAFETY: both indices were just checked to be less than `v.len()` unsafe { swap_unchecked(v, i, j) }; Ok(())}
fn main() { let mut v = [1, 2, 3]; assert_eq!(swap_checked(&mut v, 0, 2), Ok(())); assert_eq!(v, [3, 2, 1]); assert_eq!(swap_checked(&mut v, 1, 1), Ok(())); assert_eq!(v, [3, 2, 1]);
assert_eq!( swap_checked(&mut v, 7, 0), Err(SwapError::OutOfRange { index: 7, len: 3 }) ); assert_eq!( swap_checked(&mut v, 0, 9), Err(SwapError::OutOfRange { index: 9, len: 3 }) ); assert_eq!( swap_checked(&mut v, 7, 9), Err(SwapError::OutOfRange { index: 7, len: 3 }) ); assert_eq!(v, [3, 2, 1], "a rejected call must not change the slice"); assert_eq!( swap_checked(&mut [], 0, 0), Err(SwapError::OutOfRange { index: 0, len: 0 }) );
// two calls were allowed to reach the unsafe function: no more, no fewer, and none wrong assert_eq!( CALLS.load(Ordering::SeqCst), 2, "call the unsafe function once per valid swap, and only then" ); assert_eq!( BAD_CALLS.load(Ordering::SeqCst), 0, "the unsafe function was called with a bad index" ); println!("all checks passed");}all checks passed
Share state without `unsafe`
This is the lesson’s “do not reach for unsafe” exercise. A registry of names has to be shared by four threads. Declare the global yourself and write register and names; count and push under one lock. The file has #![forbid(unsafe_code)], so static mut will not compile. The checks run 1,000 registrations on four threads and want every returned count to be different, then register one more name and expect names to contain it.
#![forbid(unsafe_code)]
use std::collections::HashSet;use std::thread;
// Declare the global state here. It must be a `static` that several threads can use.// The file forbids `unsafe`, so `static mut` is not an option.
/// Records `name` and returns how many names are recorded, counting this one.fn register(name: &str) -> usize { todo!("add the name to a global and return the new count: {name}")}
/// Every recorded name, sorted.fn names() -> Vec<String> { todo!("read the global")}
fn main() { let counts: Vec<usize> = thread::scope(|s| { let handles: Vec<_> = (0..4) .map(|t| { s.spawn(move || { (0..250) .map(|i| register(&format!("t{t}-{i:03}"))) .collect::<Vec<_>>() }) }) .collect(); handles .into_iter() .flat_map(|h| h.join().unwrap()) .collect() });
// every call saw a different count, 1 through 1000: no update was lost let unique: HashSet<usize> = counts.iter().copied().collect(); assert_eq!(unique.len(), 1000, "two calls returned the same count"); assert_eq!(counts.iter().max(), Some(&1000));
let all = names(); assert_eq!(all.len(), 1000); assert!( all.windows(2).all(|w| w[0] < w[1]), "names must be sorted and unique" ); assert_eq!(all[0], "t0-000"); assert_eq!(all[999], "t3-249");
// a late registration must show up too: the global holds the names, not just a count assert_eq!(register("zzz"), 1001); assert!( names().contains(&String::from("zzz")), "names() lost a name" ); println!("all checks passed");}Open in the Rust Playground ↗Run it first: it stops at todo!() with not yet implemented and a hint, and the compiler warns that the parameter is unused. Both are expected. Replace the todo!() with your code and run it again until it prints all checks passed.
Show a solution
#![forbid(unsafe_code)]
use std::collections::HashSet;use std::sync::Mutex;use std::thread;
// Declare the global state here. It must be a `static` that several threads can use.// The file forbids `unsafe`, so `static mut` is not an option.static NAMES: Mutex<Vec<String>> = Mutex::new(Vec::new());
/// Records `name` and returns how many names are recorded, counting this one.fn register(name: &str) -> usize { let mut names = NAMES.lock().unwrap(); names.push(name.to_string()); names.len()}
/// Every recorded name, sorted.fn names() -> Vec<String> { let mut all = NAMES.lock().unwrap().clone(); all.sort(); all}
fn main() { let counts: Vec<usize> = thread::scope(|s| { let handles: Vec<_> = (0..4) .map(|t| { s.spawn(move || { (0..250) .map(|i| register(&format!("t{t}-{i:03}"))) .collect::<Vec<_>>() }) }) .collect(); handles .into_iter() .flat_map(|h| h.join().unwrap()) .collect() });
// every call saw a different count, 1 through 1000: no update was lost let unique: HashSet<usize> = counts.iter().copied().collect(); assert_eq!(unique.len(), 1000, "two calls returned the same count"); assert_eq!(counts.iter().max(), Some(&1000));
let all = names(); assert_eq!(all.len(), 1000); assert!( all.windows(2).all(|w| w[0] < w[1]), "names must be sorted and unique" ); assert_eq!(all[0], "t0-000"); assert_eq!(all[999], "t3-249");
// a late registration must show up too: the global holds the names, not just a count assert_eq!(register("zzz"), 1001); assert!( names().contains(&String::from("zzz")), "names() lost a name" ); println!("all checks passed");}all checks passed
Read more
- The Rust Book, Unsafe Rust, and Rust by Example, Unsafe Operations.
- The Rustonomicon, Meet Safe and Unsafe and What Unsafe Can Do.
- The Reference: the
unsafekeyword, unsafety and behavior considered undefined. - The 2024 edition guide:
unsafe extern,unsafe_op_in_unsafe_fnandstatic mutreferences. - Miri, for checking unsafe code.
- Errors: E0133, E0080, or this site’s index.
Learn Rust is an independent project, not affiliated with the Rust Project or the Rust Foundation. Code and text are under the MIT licence.Source on GitHub.