unsafe Rust
Reviewed & published by Brayan K
unsafe is a keyword that unlocks five extra abilities the compiler cannot verify — like dereferencing raw pointers — handing you responsibility for upholding the guarantees Rust normally checks for you.
Part of the free Rust course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.
In this lesson you'll learn the five unsafe superpowers, work with raw pointers (*const/*mut), write and call an unsafe fn, and see how to keep unsafe code small and sound.
What You'll Learn in This Lesson
- The five superpowers unsafe unlocks
- Creating and dereferencing raw pointers
- Why making a pointer is safe but using it isn't
- Writing and calling an unsafe fn
- Wrapping unsafe in a sound safe function
- Why and when unsafe is justified (FFI, performance)
1️⃣ Raw Pointers
Unsafe grants exactly five superpowers; the most common is working with raw pointers, written *const T and *mut T. Unlike references, raw pointers are not tracked by the borrow checker. Creating a raw pointer is perfectly safe — only dereferencing one requires an unsafe block, because the compiler can't guarantee it points to valid data.
fn main() {
let mut num = 5;
// Creating raw pointers is SAFE; only DEREFERENCING them needs unsafe.
let r1 = &num as *const i32; // immutable raw pointer
let r2 = &mut num as *mut i32; // mutable raw pointer
unsafe {
// Inside unsafe, we may dereference raw pointers.
println!("r1 reads: {}", *r1); // -> 5
*r2 = 10; // write through the mutable pointer
println!("r2 now reads: {}", *r2); // -> 10
}
// Back in safe code, the change is visible.
println!("num is now: {}", num); // -> 10
}Inside unsafe, the type system and ordinary borrow rules are still enforced — what changes is that raw pointers, which the checker simply ignores, become dereferenceable. Unsafe is not an "anything goes" mode.
2️⃣ Unsafe Functions
Marking a function unsafe fn declares that calling it carries obligations the compiler can't check — the caller must satisfy a documented contract. Every call site must therefore appear inside an unsafe block, which is the caller saying "I've read the rules and I'm upholding them."
// An unsafe fn declares "calling me has rules you must uphold."
// Callers must wrap the call in an unsafe block.
unsafe fn read_first(ptr: *const i32, len: usize) -> i32 {
// Safety: caller guarantees ptr points to at least len valid i32s,
// and len >= 1. We only read index 0, which is in bounds here.
if len == 0 {
return -1;
}
*ptr
}
fn main() {
let data = [42, 7, 99];
let ptr = data.as_ptr(); // *const i32 to the first element
// SAFE wrapper: we know data has 3 elements, so the contract holds.
let first = unsafe { read_first(ptr, data.len()) };
println!("first element = {}", first); // -> 42
}Always pair an unsafe fn with a // Safety: comment stating exactly what the caller must guarantee. This is the same pattern the standard library uses for functions like get_unchecked.
3️⃣ Wrapping Unsafe in a Safe API
The hallmark of good unsafe code is that it is encapsulated: a small, audited unsafe core sits behind a safe function that upholds the necessary invariants, so callers never touch unsafe at all. The standard library's split_at_mut is the textbook case — it returns two non-overlapping mutable slices, something safe Rust can't prove on its own.
// A classic: split_at_mut hands out TWO mutable slices into one array.
// Safe Rust can't prove the halves don't overlap, so the std version
// uses unsafe internally. Here's a sound, simplified version.
fn split_at_mut(slice: &mut [i32], mid: usize) -> (&mut [i32], &mut [i32]) {
let len = slice.len();
let ptr = slice.as_mut_ptr();
assert!(mid <= len); // upholds the safety requirement up front
unsafe {
// Safety: the two ranges [0, mid) and [mid, len) never overlap,
// and both stay within the original allocation.
(
std::slice::from_raw_parts_mut(ptr, mid),
std::slice::from_raw_parts_mut(ptr.add(mid), len - mid),
)
}
}
fn main() {
let mut v = [1, 2, 3, 4, 5, 6];
let (left, right) = split_at_mut(&mut v, 3);
// We can mutate both halves at once — impossible in pure safe code.
left[0] = 100;
right[0] = 200;
println!("{:?}", v); // -> [100, 2, 3, 200, 5, 6]
}The assert!(mid <= len) guarantees the precondition before any pointer math, and the two ranges provably never overlap — so this safe wrapper can never be misused into undefined behavior. That is the standard you should hold all unsafe code to.
Your turn. Fill in the blank marked ___, then run it.
// 🎯 YOUR TURN — read a value through a raw pointer, then run it.
fn main() {
let x = 99;
let p = &x as *const i32; // making the pointer is safe
// Dereferencing a raw pointer requires an unsafe block.
___ { // 👉 the keyword that opens the block
println!("value via pointer: {}", *p);
}
}
// ✅ Expected output:
// value via pointer: 99Mini-Challenge: Write Through a *mut Pointer
Use a single, sound unsafe block to increment a value through a mutable raw pointer. Run it with cargo run and check the output.
// 🎯 MINI-CHALLENGE: increment through a mutable raw pointer.
//
// 1. Start with let mut total = 0;
// 2. Make a *mut i32 raw pointer to it:
// let counter = &mut total as *mut i32;
// 3. In a single unsafe block, loop 5 times and add 10 each pass
// by writing through the pointer: *counter += 10;
// 4. Back in safe code, print total.
//
// Keep it SOUND: the pointer stays valid the whole time and there are
// no other live references to total while you use it.
//
// ✅ Expected output:
// total = 50
fn main() {
// your code here
}Common Errors (and the fix)
- ❌ "dereference of raw pointer is unsafe". ✅ Wrap the *ptr in an unsafe { } block.
- ❌ "call to unsafe function is unsafe". ✅ Calling an unsafe fn must happen inside an unsafe block.
- ❌ Thinking unsafe turns off the borrow checker. ✅ It doesn't — normal references are still checked; only raw pointers are unchecked.
- ❌ Creating a pointer, then using it after its target is gone. ✅ A dangling raw pointer is undefined behavior — keep the data alive for the pointer's whole life.
Pro Tips
- 💡 Make it small. Shrink every unsafe block to the fewest operations possible, then audit it hard.
- 💡 Wrap it safe. Expose a checked, safe function so callers never write unsafe themselves.
- 💡 Run Miri. The cargo +nightly miri interpreter detects much undefined behavior your tests would miss.
📋 Quick Reference — unsafe
| Item | Meaning |
|---|---|
| *const T | Immutable raw pointer |
| *mut T | Mutable raw pointer |
| unsafe { *p } | Dereference a raw pointer |
| unsafe fn | Has a caller-upheld contract |
| // Safety: | Document the invariants |
🎉 Lesson Complete!
- ✅ unsafe unlocks exactly five superpowers — raw pointers are the common one
- ✅ Making a raw pointer is safe; dereferencing it needs an unsafe block
- ✅ The borrow checker stays on; only raw pointers escape its tracking
- ✅ Keep unsafe small, document a // Safety contract, and wrap it in a safe API
Practice quiz
How many extra abilities does unsafe Rust unlock?
- One
- Three
- Five superpowers
- Unlimited
Answer: Five superpowers. Unsafe unlocks exactly five operations the compiler cannot verify.
Which operation requires an unsafe block?
- Dereferencing a raw pointer
- Creating a raw pointer
- Declaring a struct
- Calling a safe function
Answer: Dereferencing a raw pointer. Making a raw pointer is safe; only dereferencing it needs unsafe.
Is CREATING a raw pointer like &x as *const i32 unsafe?
- Yes, always unsafe
- Only for *mut
- Only inside a function
- No, creating it is safe
Answer: No, creating it is safe. Creating raw pointers is safe; dereferencing them is what requires unsafe.
What does an unsafe fn declare about its caller?
- The function never returns
- The caller must uphold a documented contract
- The function is faster
- The function is private
Answer: The caller must uphold a documented contract. Calling an unsafe fn must happen in an unsafe block where the caller upholds the rules.
Does unsafe turn off the borrow checker for normal references?
- No, references are still checked
- Yes, completely
- Only for &mut
- Only in main
Answer: No, references are still checked. Ordinary references are still checked; only raw pointers escape the borrow checker.
Which is NOT one of the five unsafe superpowers?
- Dereferencing a raw pointer
- Calling an unsafe function
- Ignoring the type system
- Accessing a mutable static
Answer: Ignoring the type system. Unsafe never disables the type system; it only enables those specific operations.
How is an immutable raw pointer written?
- *mut T
- *const T
- &T
- ref T
Answer: *const T. *const T is an immutable raw pointer; *mut T is the mutable form.
What is the recommended way to keep unsafe code sound?
- Make unsafe blocks as large as possible
- Avoid all comments
- Skip bounds checks everywhere
- Wrap a small unsafe core behind a safe API
Answer: Wrap a small unsafe core behind a safe API. Keep unsafe blocks tiny and expose a checked, safe wrapper like split_at_mut does.
Why does the safe split_at_mut use unsafe internally?
- It is faster to write
- Safe Rust can't prove two mutable slices don't overlap
- It avoids the type system
- Slices require unsafe
Answer: Safe Rust can't prove two mutable slices don't overlap. The borrow checker can't verify the halves are non-overlapping, so unsafe is used soundly.
What should accompany each unsafe block as good practice?
- A panic
- A println
- A // Safety: comment stating the invariants
- A lifetime annotation
Answer: A // Safety: comment stating the invariants. Document exactly which invariants make the unsafe block sound.
Continue this course
- Previous: Error Handling with Box<dyn Error>
- Next: Checkpoint: Systems Rust — Combine a newtype, a macro and Box<dyn Error> in a build — then a quiz
- Quick reference: Rust cheat sheet
Frequently asked questions
What can you do in unsafe Rust that you can't in safe Rust?
Unsafe Rust unlocks exactly five extra abilities, often called the five superpowers: dereference a raw pointer, call an unsafe function or method, access or modify a mutable static variable, implement an unsafe trait, and access the fields of a union. That is the complete list. Notably, unsafe does NOT turn off the borrow checker for ordinary references, and it does not let you ignore the type system. It only enables those five operations that the compiler cannot verify are sound, putting the responsibility for correctness on you.
Does unsafe disable the borrow checker?
Not in the way people often assume. Inside an unsafe block, normal references are still checked, ownership still applies, and the type system is still enforced. What changes is that you gain the ability to use raw pointers (*const T and *mut T), which are not tracked by the borrow checker at all — you can have many of them, including aliasing mutable ones. So it is more accurate to say unsafe lets you opt out of borrow checking for raw pointers specifically, by working with a kind of pointer the checker ignores, rather than switching the checker off globally.
When should I actually reach for unsafe?
Rarely, and only with a clear reason. The legitimate uses are: calling into C or other languages through FFI, where the foreign code is inherently unchecked; building data structures the borrow checker cannot express, like certain doubly linked lists or arenas; squeezing out performance by skipping bounds checks in a hot loop after you have proven they are unnecessary; and writing low-level abstractions such as allocators or the internals of standard collections. For everyday application code you should almost never need unsafe — safe abstractions usually already exist.
How do I keep unsafe code sound?
Follow a few disciplines. Keep unsafe blocks as small as possible, ideally a single operation, so the surface area to audit is tiny. Wrap unsafe code in a safe function that upholds the necessary invariants — like split_at_mut, which asserts mid <= len before doing anything unsafe — so callers never have to think about it. Write a Safety comment above each unsafe block explaining exactly which invariants make it sound. And test heavily, ideally with tools like Miri, which can detect undefined behavior. The goal is that all the unsafety is contained behind safe, well-documented boundaries.