unsafe is the most misunderstood keyword in Rust. It does not “turn off safety checks” — it hands you the responsibility for specific operations. This article walks through six common misuses with runnable examples.
What unsafe actually is
An unsafe block allows exactly 5 kinds of operations. Every other rule — borrow checking, ownership — still applies inside the block:
- Dereferencing a raw pointer
- Calling an
unsafefunction or external function (FFI) - Accessing or modifying a mutable static
- Accessing a field of a
union - Implementing an
unsafetrait
The compiler stops checking those 5 operations, so you must guarantee their correctness yourself and document your reasoning in a // SAFETY: comment.
Misuse 1: using unsafe to silence the compiler
The most common mistake: when you cannot figure out a lifetime or ownership error, wrap the code in unsafe to make the compiler “shut up.”
let r: &i32;
unsafe {
r = std::mem::transmute(0x123456usize);
}
println!("{r}");
transmute turns an arbitrary address into an &i32. It compiles and probably crashes at runtime; worse, it may “accidentally” not crash while the data is already corrupt, and the bug surfaces somewhere completely unrelated.
Do this instead: understand the borrow/lifetime error. Compiler errors are information, not obstacles. unsafe cannot fix ownership problems — it can only hide them.
Misuse 2: dereferencing arbitrary raw pointers
let ptr = 0x123456usize as *const i32;
unsafe {
println!("{}", *ptr);
}
That address may hold nothing, kernel data, or another process’s memory. Dereferencing it is not treasure hunting — it is undefined behavior.
Do this instead: only dereference pointers you created yourself with a known lifetime, and document why:
let x = 42;
let ptr = &x as *const i32;
unsafe {
// SAFETY: ptr points to x, which is still alive here.
println!("safe ptr: {}", *ptr); // prints: 42
}
Misuse 3: assuming borrow rules are suspended inside unsafe
unsafe only exempts those 5 operations. The borrow checker still applies, and the following code does not compile, even inside unsafe:
unsafe {
let mut v = vec![1, 2, 3];
let x = &v[0]; // immutable borrow
v.push(4); // mutable borrow — conflict
println!("{x}");
}
Holding &v[0] while calling v.push(4) violates borrow rules, and unsafe will not let you bypass that.
Misuse 4: reinventing the wheel
Hand-rolling a C-style strlen when you need to interact with C strings is the classic “square wheel”:
unsafe fn strlen(ptr: *const u8) -> usize {
let mut len = 0;
while *ptr.add(len) != 0 {
len += 1;
}
len
}
Do this instead: the standard library already wraps this. Keep unsafe to the minimum:
use std::ffi::CStr;
fn main() {
let c_string_bytes = b"hello\0";
let c_str_ptr = c_string_bytes.as_ptr() as *const i8;
let cstr = unsafe {
// SAFETY: the pointer is valid and points to a NUL-terminated C string.
CStr::from_ptr(c_str_ptr)
};
println!("CStr: {:?}", cstr);
}
Before writing any unsafe code, ask: does the standard library or a mature crate already provide a safe wrapper?
Misuse 5: abusing unsafe impl Send / Sync
unsafe impl Send for MyType {} is a promise to the compiler: “this type can be moved across threads without data races.” The compiler trusts you completely and performs no checks.
If MyType contains Rc, raw pointers, or non-thread-safe interior mutability, the promise is wrong — you get data races at runtime that are extremely hard to debug.
Do this instead: implement unsafe impl Send/Sync only when you can argue why every piece of internal state is safe under cross-thread access, and document each field in a comment.
Misuse 6: missing SAFETY comments
Every unsafe block should carry a // SAFETY: comment explaining why the operation is sound: pointer provenance, lifetimes, invariants, external constraints. This is not formalism — it is the evidence chain for the next maintainer (including you, three months from now).
Tooling to enforce this: clippy::undocumented_unsafe_blocks warns on any unsafe block without a SAFETY comment.
When you actually need unsafe
- FFI: calling C/C++ libraries — the most common legitimate case.
- Performance-critical paths: when benchmarks prove the safe abstraction is the bottleneck, and the unsafe version passes review.
- Low-level data structures: self-referential structures, lock-free queues — things safe abstractions cannot express.
The rule of thumb: write safe code first, prove with benchmarks that it is the bottleneck, then consider unsafe — and every unsafe block needs a thorough SAFETY comment.