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:

  1. Dereferencing a raw pointer
  2. Calling an unsafe function or external function (FFI)
  3. Accessing or modifying a mutable static
  4. Accessing a field of a union
  5. Implementing an unsafe trait

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.