Unsafe Rust starts where Rust no longer guarantees safety

By leen ‱ 21 Dec 2025 · 12:45

First, we need to clear up an old misunderstanding right at the start.

Does unsafe in Rust mean “turning security off”?
No. Not at all.

Rust is, by nature, a language built on contracts.
The borrow checker, lifetimes, and aliasing rules are all tools that let Rust confidently say one very important sentence:

If your code compiles, it’s memory-safe.

But that sentence only holds as long as Rust can actually verify things.
At some point, Rust honestly steps back.


Starting from a completely safe point (Safe Rust)

Let’s start with a fully safe example:

fn main() {
    let x = 42;

    let r = &x;
    println!("r = {}", r);
}

Nothing special happens here. The borrow checker provides a few simple but vital guarantees:

  • x stays alive for as long as it’s being used
  • the reference is valid
  • there’s no dangerous aliasing
  • and most importantly: no Undefined Behavior is possible

Here, unsafe doesn’t mean anything yet. Everything is inside the language’s contract.


Creating a raw pointer without unsafe

Now let’s go one step further:

fn main() {
    let x = 42;

    let raw = &x as *const i32;

    println!("raw pointer address: {:?}", raw);
}

This part usually delivers the first mental shock. We created a raw pointer, yet we wrote no unsafe.

Creating a raw pointer in Rust is not unsafe. Why? Because we still haven’t done anything that forces Rust to guarantee anything.

A raw pointer means this very important sentence:

From here on, Rust won’t check anything.

No lifetimes, no aliasing, no validation that the memory is valid. But nothing dangerous has happened yet, because we haven’t used the pointer.


The first unsafe block (this is where trouble starts)

The problem starts exactly when we try to use it!

fn main() {
    let x = 42;
    let raw = &x as *const i32;

    unsafe {
        println!("value via raw pointer = {}", *raw);
    }
}

Dereferencing a raw pointer is officially stepping into a region where Rust no longer takes responsibility.

Why does this line need unsafe? Because Rust no longer knows:

  • whether this pointer still points to valid memory
  • whether that memory has been dropped
  • whether there’s dangerous mutable aliasing happening at the same time

So Rust offers you this contract:

If you want to do this, you have to guarantee it yourself.

And that’s the real definition of unsafe:

unsafe means: “I guarantee this is safe.”

No more, no less.


Undefined Behavior, the worst thing that can happen in Rust

The biggest danger of unsafe isn’t a crash. It isn’t panic. It isn’t even a segmentation fault.

The real danger is something Rust calls Undefined Behavior.


An example that looks “harmless,” but is completely wrong

fn main() {
    let raw_ptr: *const i32;

    {
        let x = 123;
        raw_ptr = &x as *const i32;
    }

    unsafe {
        println!("value = {}", *raw_ptr);
    }
}

This code compiles. No warnings. No errors.

But from Rust’s perspective, this code is completely wrong.

Why?

  • x is created inside an inner scope
  • after that scope ends, the variable is dropped
  • raw_ptr now points to memory that no longer belongs to the program

When you run it:

cargo run

It might:

  • print the correct number
  • print the wrong number
  • crash
  • or “for now,” appear to work

And that “for now” is the most dangerous part.


UB means Rust promises you nothing

This isn’t panic. Rust makes no promise about what will happen.

UB means: “anything is allowed.”

The compiler can assume this situation never happens, and based on that assumption, it can optimize and rewrite surrounding code. The result can be completely unpredictable.


Comparing it to panic (a crucial difference)

fn main() {
    let v = vec![1, 2, 3];
    println!("{}", v[10]);
}

Let’s pay closer attention to how this snippet panics:

  • you get an error message
  • you get a stack trace
  • the program stops

It’s bad, but it’s safe.


A second UB example: mutable aliasing

fn main() {
    let mut x = 10;

    let r1 = &x as *const i32;
    let r2 = &mut x as *mut i32;

    unsafe {
        *r2 = 20;
        println!("{}", *r1);
    }
}

Here, at the same time, we have:

  • an immutable reference
  • and a mutable reference

This is exactly what the borrow checker always prevents. But raw pointers bypass the borrow checker, not the laws of memory management.

Rust stays silent. But we have UB.

The borrow checker isn’t your enemy; it’s your guard.


Engineer unsafe

If unsafe were only danger, Rust wouldn’t provide it. unsafe is a tool for building abstractions, not a shortcut.

The goal isn’t for unsafe to spread everywhere. The goal is for unsafe to be contained.


Building a safe abstraction with internal unsafe

Scenario: We want to build a simple wrapper that holds a raw pointer internally, but is completely safe on the outside.

struct MyBox<T> {
    ptr: *mut T,
}

Here, Rust provides no guarantees. The responsibility is on us.


A safe constructor

impl<T> MyBox<T> {
    fn new(value: T) -> Self {
        let boxed = Box::new(value);
        let ptr = Box::into_raw(boxed);

        Self { ptr }
    }
}

From this moment on:

  • Rust will no longer drop this allocation
  • we are responsible for freeing it

First invariant:

ptr always points to valid heap memory


Safe access to the data

impl<T> MyBox<T> {
    fn get(&self) -> &T {
        unsafe {
            &*self.ptr
        }
    }
}

Why unsafe? Because dereferencing a raw pointer is unsafe.

Why is it safe here? Because we’ve defined the invariants ourselves.

Second invariant:

As long as MyBox is alive, ptr won’t dangle


Freeing memory with Drop

impl<T> Drop for MyBox<T> {
    fn drop(&mut self) {
        unsafe {
            drop(Box::from_raw(self.ptr));
        }
    }
}

If we don’t write this, we leak memory. If we do it twice, we get UB.

Third invariant:

ptr is freed exactly once


External usage (fully safe)

fn main() {
    let b = MyBox::new(42);

    println!("value = {}", b.get());
}

There is no unsafe here. The consumer can’t even make a mistake.


Final summary

unsafe Rust is still Rust. But Rust won’t protect you anymore.

unsafe is a tool of power, not convenience. If you don’t fully understand the invariants, don’t write unsafe — even if your code “works.”

unsafe should be like radioactive material: minimal, contained, and clearly labeled

Comments

Be the first to share your thoughts.

Leave a comment