Can you write this from memory?
Write an unsafe block that dereferences a raw pointer p.
Rust's unsafe keyword unlocks operations the compiler cannot verify: dereferencing raw pointers, calling unsafe functions, accessing mutable statics, and implementing unsafe traits.
Most Rust code is safe. But sometimes you need unsafe for performance, FFI, or low-level operations. The key is minimizing unsafe surface area and documenting invariants.
This page focuses on unsafe syntax and patterns.
Raw pointer operations require an unsafe block. The compiler cannot verify the pointer is valid:
let x = 42;
let ptr: *const i32 = &x;
// WRONG: raw pointer deref outside unsafe
// println!("{}", *ptr); // error[E0133]: dereference of raw pointer is unsafe
// RIGHT: wrap in unsafe block
// SAFETY: ptr was created from a valid reference and x is still alive
unsafe {
println!("{}", *ptr);
}
Calling an unsafe fn also requires an unsafe block at the call site:
unsafe fn dangerous(ptr: *const i32) -> i32 {
// SAFETY: caller guarantees ptr is valid, aligned, and points to initialized i32
unsafe { *ptr }
}
// WRONG: calling unsafe fn without unsafe block
// dangerous(ptr); // error[E0133]: call to unsafe function is unsafe
// RIGHT: wrap in unsafe block
// SAFETY: we know ptr points to a live i32 (created from &x above)
let val = unsafe { dangerous(ptr) };
Rust 2024 note: unsafe_op_in_unsafe_fn is now a warning by default. Even inside an unsafe fn, use explicit unsafe { ... } blocks around each unsafe operation. This makes it clear exactly which operations are unsafe and lets you attach a // SAFETY: comment to each one.
Mutable statics are inherently racy — any thread can access them at any time:
static mut COUNTER: u32 = 0;
// WRONG: accessing static mut outside unsafe
// COUNTER += 1; // error[E0133]: use of mutable static is unsafe
// RIGHT: wrap in unsafe block
// SAFETY: single-threaded context, no concurrent access
unsafe {
COUNTER += 1;
println!("{}", COUNTER);
}
Inside an unsafe block you can do exactly five things that safe Rust forbids:
- Dereference raw pointers —
*const Tand*mut Tcan point anywhere. The compiler cannot verify the memory is valid. - Call unsafe functions — Functions marked
unsafe fnor foreign functions fromexternblocks. - Access mutable static variables — Global mutable state is inherently racy. You must guarantee no data races.
- Implement unsafe traits — Traits like
SendandSyncwhere the compiler cannot verify the contract. - Access union fields — Unions overlap memory; reading the wrong field is undefined behavior.
Everything else — ownership, borrowing, lifetimes — still applies inside unsafe. The borrow checker does not turn off.
Two changes in Rust 2024 make safety obligations more explicit:
| Old syntax | Rust 2024 syntax | Why |
|---|---|---|
extern "C" { fn abs(x: i32) -> i32; } | unsafe extern "C" { fn abs(x: i32) -> i32; } | Extern blocks declare unsafe functions — the block should say so |
#[no_mangle] | #[unsafe(no_mangle)] | Attributes with safety implications must be marked unsafe |
// Rust 2024: extern blocks must be marked unsafe
unsafe extern "C" {
fn abs(input: i32) -> i32;
}
let result = unsafe { abs(-5) };
// Rust 2024: #[no_mangle] is an unsafe attribute
#[unsafe(no_mangle)]
// SAFETY: symbol name "foo" does not conflict with other exported symbols
pub extern "C" fn foo() -> i32 {
42
}
The Rust standard library requires a // SAFETY: comment before every unsafe block. Follow this pattern in your own code:
// SAFETY: <state the invariant>.
// - ptr is non-null, aligned, and points to initialized T
// - the pointed-to memory lives at least as long as this use
// - aliasing rules are upheld (no mutable aliasing)
unsafe { *ptr }
Each // SAFETY: comment should explain why the preconditions hold at this call site, not just restate the function's docs.
The idiomatic pattern is to wrap unsafe internals in a safe public API. Callers never touch unsafe directly — they rely on your module to uphold invariants:
pub struct SafeBuffer {
ptr: *mut u8,
len: usize,
}
impl SafeBuffer {
pub fn new(size: usize) -> Self {
let layout = std::alloc::Layout::array::<u8>(size).unwrap();
// SAFETY: layout is valid (non-zero size), returns zeroed memory
let ptr = unsafe { std::alloc::alloc_zeroed(layout) };
if ptr.is_null() {
std::alloc::handle_alloc_error(layout);
}
SafeBuffer { ptr, len: size }
}
pub fn get(&self, index: usize) -> Option<u8> {
if index < self.len {
// SAFETY: bounds check above guarantees index < len
Some(unsafe { *self.ptr.add(index) })
} else {
None
}
}
}
impl Drop for SafeBuffer {
fn drop(&mut self) {
let layout = std::alloc::Layout::array::<u8>(self.len).unwrap();
// SAFETY: ptr was allocated with this layout in new()
unsafe { std::alloc::dealloc(self.ptr, layout) };
}
}
The key rule: unsafe is an implementation detail, not a public interface. Keep unsafe blocks minimal, document the invariant each one relies on, and expose safe functions to the rest of your codebase. The smart pointers guide shows how standard library types like Box, Rc, and Arc use this exact pattern -- safe APIs wrapping unsafe internals.
Three things that burn people in practice:
#[repr(C)] for structs crossing FFI — Rust's default struct layout is unspecified. C expects a specific layout. Always add #[repr(C)] to types shared across the boundary:
#[repr(C)]
struct Point {
x: f64,
y: f64,
}
Strings are not NUL-terminated — Rust String and &str are UTF-8 without a trailing NUL byte. Use CString for strings going to C, and CStr for strings coming from C:
use std::ffi::CString;
let c_str = CString::new("hello").unwrap();
unsafe { c_function(c_str.as_ptr()) };
Panics across FFI are undefined behavior — A Rust panic unwinding into C code is UB. Use catch_unwind at FFI boundaries, or mark FFI-exported functions as extern "C-unwind" if both sides handle unwinding. Understanding references and lifetimes is critical for FFI, since raw pointers bypass all lifetime guarantees.
Miri is an interpreter that detects undefined behavior at runtime. Run it on your test suite:
cargo +nightly miri test
Miri catches: use-after-free, out-of-bounds access, invalid alignment, data races, stacked borrows violations. It cannot catch all UB (e.g., logic errors), but it catches many common raw pointer mistakes.
- The Rust Book: Unsafe Rust — the five superpowers, raw pointers, FFI
- The Rustonomicon — the dark arts of unsafe Rust
- Rust Edition Guide: Unsafe extern — Rust 2024 extern block changes
- Rust Edition Guide: Unsafe attributes — #[unsafe(no_mangle)]
- Miri — UB detection tool for unsafe code
When to Use Rust Unsafe: Raw Pointers, FFI & Unsafe Blocks
- Use unsafe for FFI (calling C libraries).
- Use unsafe for performance-critical code that needs raw pointers.
- Use unsafe for implementing safe abstractions over unsafe operations.
- Minimize unsafe blocks. Wrap unsafe code in safe APIs.
- Document safety invariants that callers must uphold.
Check Your Understanding: Rust Unsafe: Raw Pointers, FFI & Unsafe Blocks
What can you do in unsafe Rust that you cannot do in safe Rust?
Five things: dereference raw pointers, call unsafe functions, access mutable static variables, implement unsafe traits, and access fields of unions. The borrow checker still works—unsafe just enables these specific operations.
What You'll Practice: Rust Unsafe: Raw Pointers, FFI & Unsafe Blocks
Common Rust Unsafe: Raw Pointers, FFI & Unsafe Blocks Pitfalls
- You reach for unsafe to skip the borrow checker -- unsafe does NOT disable borrowing or lifetime rules. It only unlocks five specific operations. Rewrite your logic with safe abstractions first; most of the time you never need unsafe.
- Your unsafe block spans 50 lines -- the larger the block, the harder it is to audit. Shrink each unsafe block to the single operation that requires it, then return to safe code immediately. Add a `// SAFETY:` comment to each.
- Code compiles but crashes at runtime with a segfault -- you dereferenced a null or dangling raw pointer. Unlike references, raw pointers have no validity guarantees. Run `cargo +nightly miri test` to catch UB.
- A safe wrapper silently introduces undefined behavior -- you exposed an unsafe operation behind a safe API without upholding the required invariant. Add a `// SAFETY:` comment explaining why the preconditions hold at this call site.
- You get 'extern blocks must be unsafe' after upgrading to Rust 2024 -- extern blocks now require the `unsafe` keyword: `unsafe extern "C" { ... }`. This makes the safety obligation visible at the declaration site.
- You get 'unsafe attribute used without unsafe' for `#[no_mangle]` -- Rust 2024 requires `#[unsafe(no_mangle)]` because controlling symbol names has safety implications.
- A Rust panic reaches C code via FFI -- unwinding across FFI boundaries is undefined behavior. Use `catch_unwind` at FFI boundaries or `extern "C-unwind"` if both sides handle unwinding.
Rust Unsafe: Raw Pointers, FFI & Unsafe Blocks FAQ
Does unsafe disable the borrow checker?
No. The borrow checker still applies. Unsafe only enables five specific operations (raw pointer dereference, unsafe fn calls, mutable statics, unsafe traits, union fields). You can still get borrow errors in unsafe code.
When should I use unsafe?
When you need FFI, hardware access, or performance that safe Rust cannot provide. Always wrap unsafe in safe abstractions. Most Rust programmers rarely write unsafe.
What is the difference between *const T and *mut T?
*const T is a raw pointer to immutable data. *mut T is a raw pointer to mutable data. Both can be created from references and both can be null. Dereferencing either requires an unsafe block.
What does #[unsafe(no_mangle)] do?
Prevents Rust from mangling the function name, so C code can call it by its declared name. In Rust 2024, the `unsafe()` wrapper is required because controlling symbol names has safety implications.
How do I safely wrap unsafe code?
Expose a safe public API that upholds invariants internally. The unsafe block handles the dangerous operation; callers never see it. Add a `// SAFETY:` comment explaining why the preconditions hold.
Why did Rust 2024 add unsafe to extern blocks?
Extern blocks declare foreign functions that are inherently unsafe to call. Making the block `unsafe extern` makes this obligation visible at the declaration site, not just at the call site.
What is Miri and when should I use it?
Miri is an interpreter that detects undefined behavior (use-after-free, data races, alignment issues). Run `cargo +nightly miri test` on any code with unsafe blocks. It catches many bugs that the compiler cannot.
Rust Unsafe: Raw Pointers, FFI & Unsafe Blocks Syntax Quick Reference
unsafe {
// SAFETY: <invariant>
*ptr
}let ptr: *const i32 = &x;let ptr: *mut i32 = &mut x;unsafe { *ptr }unsafe fn danger(ptr: *const i32) -> i32 {
unsafe { *ptr }
}unsafe { danger(ptr); }static mut X: i32 = 0;unsafe extern "C" {
fn abs(x: i32) -> i32;
}#[unsafe(no_mangle)]
pub extern "C" fn foo() -> i32 {
42
}#[repr(C)]
struct Point { x: f64, y: f64 }let s = CString::new("hello").unwrap();
unsafe { c_fn(s.as_ptr()) };Rust Unsafe: Raw Pointers, FFI & Unsafe Blocks Sample Exercises
Fill in the type to cast a reference to a const raw pointer.
*const i32Fill in the type to cast a mutable reference to a mutable raw pointer.
*mut i32Fill in how to create a null raw pointer.
std::ptr::null()+ 21 more exercises