Rust Unsafe
Every guarantee this course has talked about — no dangling references, no data races, no out-of-bounds access — is enforced by the compiler. unsafe is how you tell the compiler “let me do something you can’t verify is safe; I’m taking responsibility for it instead.”
What unsafe does — and doesn’t do
Section titled “What unsafe does — and doesn’t do”A common misconception is that unsafe turns off Rust’s safety checks entirely. It doesn’t. Inside an unsafe block, you can do exactly five extra things that are otherwise forbidden:
- Dereference a raw pointer
- Call an
unsafefunction - Access or modify a mutable
staticvariable - Implement an
unsafetrait - Access fields of a
union
Everything else — the borrowing rules, move semantics, type checking — still fully applies inside unsafe code. It’s a scalpel for five specific operations, not an off switch.
Raw pointers
Section titled “Raw pointers”Rust’s normal references (& and &mut) always point to valid, properly aligned data — that’s part of what the compiler guarantees. A raw pointer (*const T or *mut T) drops that guarantee, which is why reading one requires unsafe:
fn main() { let value = 5; let pointer = &value as *const i32;
unsafe { println!("{}", *pointer); }}Output:
5You can create a raw pointer in safe code — it’s only dereferencing it (reading the value it points to) that needs unsafe, since the compiler can no longer verify the pointer is valid.
Calling an unsafe function
Section titled “Calling an unsafe function”Some functions are themselves marked unsafe because they have requirements the compiler can’t check for you — calling them is your promise that you’ve verified those requirements yourself:
unsafe fn dangerous() { println!("this function trusts you to have checked its requirements");}
fn main() { unsafe { dangerous(); }}Output:
this function trusts you to have checked its requirementsWrapping unsafe in a safe function
Section titled “Wrapping unsafe in a safe function”The standard library has real unsafe functions you can call. One is get_unchecked on slices. It works like numbers[i], but skips the bounds check. If i is out of range, it doesn’t panic. It reads whatever memory happens to be there, which is called undefined behavior: your program might crash, print garbage, or seem fine until it doesn’t.
So the rule for calling it is “only with an index you know is valid”. The usual pattern is to check that rule yourself, inside a normal, safe function:
fn first_or_zero(numbers: &[i32]) -> i32 { if numbers.is_empty() { return 0; } // SAFETY: the slice is not empty, so index 0 is in bounds. unsafe { *numbers.get_unchecked(0) }}
fn main() { println!("{}", first_or_zero(&[7, 8, 9])); println!("{}", first_or_zero(&[]));}Output:
70Look at what the caller sees: fn first_or_zero(numbers: &[i32]) -> i32. There’s no unsafe in the signature, and no way to call it wrongly, because the function checks the rule before it ever reaches the unsafe block. The unsafe stays small and hidden inside.
Two habits that experienced Rust developers follow:
- Write a
// SAFETY:comment above everyunsafeblock, saying why the rules are met. It’s for the next person reading the code, and for future you. - Keep the
unsafeblock as small as you can, so there’s less code to double-check.
To be honest, you wouldn’t write this exact function in real code: numbers.first() does the same job safely, and the compiler is very good at removing bounds checks it can prove are not needed. But the pattern (check the rules, then call unsafe inside a safe function) is exactly how Vec, String and friends are built. The FFI page uses the same trick to wrap C functions.
Why unsafe exists at all
Section titled “Why unsafe exists at all”If Rust is all about safety, why allow this at all? Because some things are genuinely impossible to prove safe at compile time, even though they’re perfectly safe in practice — talking directly to hardware, calling into a C library (see FFI), or building certain high-performance data structures. unsafe lets the standard library (and, rarely, your own code) implement these low-level pieces, while everything built on top of them stays in ordinary, fully-checked safe Rust.
The honest tradeoff
Section titled “The honest tradeoff”Writing unsafe doesn’t mean your code is now buggy — it means the compiler can no longer catch certain classes of mistakes for you in that specific block, and it’s on you to uphold the same guarantees by hand. Most experienced Rust developers write very little unsafe code directly; it tends to live in small, carefully-reviewed pockets at the bottom of a library, wrapped in a safe API for everyone else to use.
Practice
Section titled “Practice”1. Change a value through a raw pointer
Section titled “1. Change a value through a raw pointer”Make a let mut score = 10;, create a *mut i32 raw pointer to it, and use the pointer to add 5. Then print score. Hint: create the pointer with &mut score as *mut i32, and write *pointer += 5 inside an unsafe block.
Show solution
fn main() { let mut score = 10; let pointer = &mut score as *mut i32; // creating it is safe
unsafe { *pointer += 5; // writing through it is not }
println!("score = {score}");}Output:
score = 15This is safe in practice because score is still alive and nothing else is using it while we write. Keeping those promises is your job once you use unsafe.
2. A safe get_or
Section titled “2. A safe get_or”Write fn get_or(numbers: &[i32], index: usize, default: i32) -> i32. It returns the item at index, or default if the index is out of range. Use get_unchecked for the read, and make sure the function can never be called in a way that breaks its rule. Hint: compare index < numbers.len() first, and only call get_unchecked when that’s true.
Show solution
fn get_or(numbers: &[i32], index: usize, default: i32) -> i32 { if index < numbers.len() { // SAFETY: we just checked that index is in bounds. unsafe { *numbers.get_unchecked(index) } } else { default }}
fn main() { let numbers = [10, 20, 30]; println!("{}", get_or(&numbers, 1, -1)); println!("{}", get_or(&numbers, 5, -1));}Output:
20-1Index 5 never reaches the unsafe block, so it’s handled safely. For comparison, the fully safe version is numbers.get(index).copied().unwrap_or(default), and that’s what you’d normally write.
unsafeunlocks exactly five operations (mainly raw pointers and calling unsafe functions) — everything else is still fully checked.- A raw pointer (
*const T/*mut T) can be created in safe code; dereferencing it requiresunsafe. - Wrap
unsafein a small, safe function that checks the rules first, and explain why it’s sound in a// SAFETY:comment. unsafeexists so low-level code (hardware access, C interop) can be built, while code using it stays safe.- As a beginner, you’ll rarely need to write it yourself — reach for a safe abstraction first.
Quick check
Section titled “Quick check”Quick check
1. What does an unsafe block actually unlock?
2. Can you create a raw pointer in safe code?
3. As a beginner, how often should you expect to write unsafe yourself?
4. Does writing unsafe code mean your program now has bugs?
Score: 0 / 4
Read more
Section titled “Read more”- Unsafe Rust — The Rust Book: the five “unsafe superpowers” in detail, including building a safe
split_at_muton top of raw pointers. - The Rustonomicon: the official guide to writing unsafe code correctly. Advanced, but the first chapter is a great read.
- The
unsafekeyword — standard library docs: unsafe blocks, functions and traits, plus the// SAFETY:comment convention. - Unsafety — The Rust Reference: the exact list of operations that need
unsafe. slice::get_unchecked— standard library docs: a real unsafe function, with its “Safety” section spelling out the rule you must follow.