Friend, have you had a night like this?

On the screen, the Rust compiler’s scarlet errors grip your project by the throat. Lifetimes, ownership, borrow rules — the guardians you’re usually proud of suddenly feel like a never-ending lecture you just want to shut up.

Right when you’re about to give up and smash the keyboard, a word glows in your mind with devilish temptation — unsafe.

It feels like a cheat code, a God-mode switch that makes all red squiggles vanish. You wrap those “problematic” lines inside a sacred block, type cargo build… Success! Silence.

You breathe out, feeling like a hero who tamed a dragon. But behind the compiler’s silence, there’s a disdainful glance that says: “Fine. Play your way — but what happens next is on you.”

unsafe: not a get-out-of-jail-free card, but your signed blood oath

Let’s rip off the free-spirited mask of unsafe and face its brutal truth.

In Rust, unsafe does not mean “turn off all safety checks and YOLO.” It truly means:

“Me, the mighty programmer, solemnly swear to personally take responsibility for memory safety in this code region. What the compiler can’t see, I understand; what it can’t check, I guarantee. If the program crashes, leaks, corrupts data — even explodes servers or sinks the company — I alone will bear it.”

You’re not turning off the rules; you’re signing a waiver. You’re telling the compiler, “I’ve got this region. If it goes south, that’s on me.”

It’s like disabling the world’s best ADAS in your car because it’s “too naggy,” then flooring the pedal. Sure, it’s thrilling — but whether a cliff or an open road lies ahead depends entirely on your skill and luck.

Most of us think we’re drift legends. Usually, we’re rookies. To keep you from flying off the mountain road, here are the most common “YOLO” misuses of unsafe.


Sin 1: Using unsafe as duct tape to silence the compiler

This is the most common — and the dumbest — mistake. You hit a lifetime or ownership issue, and instead of understanding it, you just slap an unsafe block around it to shut the compiler up.

Example:

let r: &i32;
unsafe {
    r = std::mem::transmute(0x123456usize);
}
println!("{r}");

You coerced an arbitrary memory address 0x123456 into an &i32. The compiler, gagged by your unsafe, can only watch.

Consequence: It compiles, then crashes. Or worse — it doesn’t crash, but data silently rots. Three months later, a client calls at 3 a.m. and you have no idea where to start.

Mantra: Every compiler error is like your mom telling you it’s cold outside. She may not be trendy, but she’s not trying to hurt you. Before you silence the compiler with unsafe, ask: am I really smarter than a system forged by decades of design and collective wisdom?


Sin 2: Holding a master key and opening mystery boxes

Raw pointers are regulars in the unsafe world. They are keys that can point anywhere in memory. Some newcomers get the key and go door-hopping with random addresses.

Example:

let ptr = 0x123456usize as *const i32;

unsafe {
    println!("{}", *ptr); // Guess what’s in here?
}

You assigned a fixed address to ptr and confidently dereferenced it.

Consequence: This is Russian roulette. That address might be nothing, OS internals, or someone else’s secrets. Best case: instant crash. Worse: weird, heisenbuggy corruption.

Mantra: unsafe grants you the right to open a lock — but only when you’re 100% sure the lock is your safe, not a munitions depot. Only dereference pointers you created from known-good objects.

Correct posture:

let x = 42;
// Create a pointer from a known, valid value
let ptr = &x as *const i32;

unsafe {
    // SAFETY: We know ptr points to x, which is alive and valid.
    println!("safe ptr: {}", *ptr); // 42
}

Sin 3: Thinking unsafe is a lawless zone

Many believe that inside an unsafe block, Rust’s rules vanish and you can do anything.

No. unsafe is diplomatic immunity for five specific powers. If you violate borrow rules, Rust will still tackle you to the ground.

Example:

unsafe {
    let mut v = vec![1, 2, 3];

    let x = &v[0];
    v.push(4); // Borrow rules still apply

    println!("{x}");
}

Even inside unsafe, this won’t compile. You tried to hold an immutable borrow (x) and then mutate (v.push(4)) — a red line in Rust.

Mantra: unsafe only allows five things:

  1. Dereference raw pointers
  2. Call unsafe or external (FFI) functions
  3. Access or mutate mutable statics
  4. Access union fields
  5. Implement unsafe traits

For everything else, you’re still a good Rust citizen.


Sin 4: Reinventing wheels — square ones

When dealing with C libraries or C-strings, you might think, “I can write a strlen with unsafe. Cool!”

Example:

// A manual C-style strlen
unsafe fn strlen(ptr: *const u8) -> usize {
    let mut len = 0;
    while *ptr.add(len) != 0 {
        len += 1;
    }
    len
}

Looks hackerish, but you likely produced a buggy, slower square wheel.

Mantra: Before writing unsafe, repeat: “Am I reinventing a wheel?” Rust’s std and ecosystem already provide safe, battle-tested abstractions for almost everything low-level.

Correct posture:

use std::ffi::CStr;

fn main() {
    let c_string_bytes = b"hello\0"; // Note the trailing \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!("Use the wheel that exists: {:?}", cstr);
}

Sin 5: “I’m the inspector” — abusing unsafe impl

This is one of the most dangerous easy-to-abuse features: unsafe impl.

When you write unsafe impl Send for MyType {}, you’re swearing to the concurrency gods that MyType can be safely moved across threads — no data races, no UB.

The compiler trusts you completely.

Example:

struct MyType {
    ptr: *mut i32, // raw pointer, yikes
}

unsafe impl Send for MyType {} // boom

Consequence: You planted a time bomb in your concurrent code. Everything looks fine in single-threaded tests; in production, two threads touch the same raw pointer and now you have races, corruption, crashes — ghosts you can’t catch.

Mantra: unsafe impl Send and unsafe impl Sync are nuclear buttons. Unless you’re writing very low-level concurrency primitives and truly understand the memory model, atomics, and data-race freedom, don’t touch them.

For 99.99% of cases, use safe primitives like Arc<Mutex<T>>:

use std::sync::{Arc, Mutex};

#[derive(Clone)]
struct MyType {
    data: Arc<Mutex<Vec<u8>>>,
}

Sin 6: Writing “sacred scripts” — no comments

unsafe code is hard to reason about because it breaks common mental models. If you write clever (or devious) unsafe without comments, you’re leaving a booby trap for future you.

Bad:

unsafe {
    // pointer hackery
    // ...
}

Better:

let mut data = [1, 2, 3];
let ptr1 = data.as_mut_ptr();
let ptr2 = unsafe { ptr1.add(2) };

unsafe {
    // SAFETY: We obtained ptr1 via as_mut_ptr() from a 3-element array.
    // ptr1.add(2) stays within bounds, so dereferencing ptr2 is valid.
    *ptr2 = 4;
}

assert_eq!(data, [1, 2, 4]);

Every unsafe block must have a Safety comment that explains:

  1. Why unsafe is required
  2. What makes it safe in this context
  3. What invariants the caller must uphold

Conclusion: Wield the dragonslayer with reverence

unsafe is Rust’s ultimate power. It lets you dance with hardware, interop with C/C++, and squeeze the last drop of performance. It’s a dragonslayer.

But an irresponsible slayer wreaks more havoc than the dragon.

Use unsafe with humility:

  • Prefer safe code first
  • Minimize the scope of unsafe
  • Wrap it in safe abstractions
  • Write a crystal-clear Safety comment for every unsafe block

Do this, and you’ll master the power — not be mastered by it.

Follow Rexai Programming on WeChat for more deep dives.