6 Rust Unsafe Patterns That Compile Cleanly but Cause Undefined Behavior
6 Rust Unsafe Patterns That Compile Cleanly but Cause Undefined Behavior
Learn 6 Rust unsafe patterns that compile but trigger undefined behavior, plus practical fixes and audit tips with Miri to catch bugs early.
As a best-selling author, I invite you to explore my books on Amazon. Don’t forget to follow me on Medium and show your support. Thank you! Your support means the world!
I have been working with Rust for years, and one thing that keeps me up at night is the silent corruption that unsafe blocks can introduce. You write code that compiles without a single warning, you run your tests, everything passes — and then your application starts behaving weirdly in production. Random crashes, data races that reproduce only once every thousand runs, or memory corruption that takes days to track down. The problem is not that unsafe is evil; the problem is that unsafe shifts the burden of memory safety from the compiler to you, and most of us are not as careful as we think we are.
I want to walk you through six specific patterns where unsafe code compiles cleanly but triggers undefined behavior (UB) at runtime. I learned each of these the hard way — by debugging production incidents. After each example I will show you how to fix the pattern and how to audit your own codebase so you catch these issues before your users do.
What happens when you tell the Rust compiler “trust me, I know what I’m doing”? Let me show you.
1. Transmuting types of different sizes
The function std::mem::transmute takes any type and reinterprets its bits as another type. The compiler does not check that the source and destination have the same size. You can transmute a u32 (4 bytes) into a u64 (8 bytes) — the code compiles, but at runtime Rust will read 4 bytes of garbage memory to fill the missing 32 bits.
let x: u32 = 42;
let y: u64 = unsafe { std::mem::transmute(x) };
println!("{}", y); // may print 42, may print 294341234, may crash
Why would you ever write code like this? I once saw it in a hot‑loop optimization where someone tried to reinterpret a float bit pattern. The fix is trivial — use as casts or, if you really need bit‑level reinterpretation, use the bytemuck crate, which enforces size and alignment at compile time.
let x: u32 = 42;
let y: u64 = x as u64; // safe, zero overhead
2. Returning a raw pointer to a stack variable from FFI
This one is a classic. You are writing a C FFI function that returns a pointer to some configuration. You define a local variable on the stack, take its address, and return that address. The function returns, the stack frame is popped, and the pointer now points to deallocated memory. Any caller that dereferences it will read stale or corrupted data.
extern "C" fn get_config() -> *const u8 {
let config: u8 = 42;
&config as *const u8 // UB: dangling pointer
}
I debugged this exact pattern in a Rust‑based embedded web server that talked to a C library. The configuration happened to stay valid for a while because the stack memory was not immediately reused — until a particularly heavy request caused a stack overflow and the corrupted pointer crashed the server.
The fix is to allocate on the heap and return a Box::into_raw pointer. The caller must later call Box::from_raw to free it.
extern "C" fn get_config() -> *const u8 {
let config = Box::new(42u8);
Box::into_raw(config) // pointer to heap memory
}
3. Unsound manual Send and Sync implementations
Rust’s Send and Sync traits are automatically derived for types that are thread‑safe. But when you use raw pointers inside a struct, the compiler cannot infer those traits. The temptation is to blindly implement them.
struct MyCache {
inner: *mut HashMap<String, String>,
}
unsafe impl Send for MyCache {}
unsafe impl Sync for MyCache {}
This compiles. It also allows you to share a mutable raw pointer across threads without any locking. The result is a data race. In one of my projects, I spent three hours tracking down an intermittent crash that only happened under high concurrency — it turned out I had implemented Send on a struct that contained a *mut to a HashMap. The compiler trusted me, and I was wrong.
Do not implement Send or Sync manually unless you have formally proven thread safety. Use standard library primitives like Arc<Mutex<T>> instead.
use std::sync::{Arc, Mutex};
use std::collections::HashMap;
struct MyCache {
inner: Arc<Mutex<HashMap<String, String>>>,
}
// Send and Sync are automatically derived (safe)
4. Simultaneous mutable aliasing of &mut T and *mut T
Rust’s borrow checker ensures there is only one mutable reference to a value at any time. But when you use raw pointers inside an unsafe block, you can bypass that rule — and the compiler will let you keep a &mut T while you write through a *mut T. This violates Rust's stacked borrows model and is UB.
let mut data = vec![1u8, 2, 3];
let raw: *mut u8 = data.as_mut_ptr();
let reference: &mut Vec<u8> = &mut data; // aliased mutable borrow
unsafe { *raw = 99; } // UB: both raw and reference are live
reference.push(4);
I have seen this pattern in performance‑critical code where someone wanted to avoid bounds checks. The fix is to scope the raw pointer usage so it does not overlap with the reference.
let mut data = vec![1u8, 2, 3];
unsafe {
let raw: *mut u8 = data.as_mut_ptr();
*raw = 99; // raw used before reference is taken
}
data.push(4); // safe mutable access restored
5. slice::from_raw_parts with a wrong length
This function creates a slice from a raw pointer and a length parameter. The compiler does not verify that the length matches the actual allocated buffer. Pass a length of 10 when the array only has 3 elements, and you get an out‑of‑bounds read.
let array = [1u32, 2, 3];
let ptr = array.as_ptr();
let slice = unsafe { std::slice::from_raw_parts(ptr, 10) }; // UB: reads beyond array
println!("{:?}", slice);
Would you ever do this intentionally? No. But when you write generic code that computes the length dynamically, it is easy to introduce off‑by‑one errors. I once wrote a zero‑copy parser that calculated the length from a packet header — an attacker could send a malicious header that made the calculation overflow and create a slice that read into arbitrary memory.
Always derive the length from the source you know is valid.
let array = [1u32, 2, 3];
let ptr = array.as_ptr();
let slice = unsafe {
std::slice::from_raw_parts(ptr, array.len()) // exact length
};
println!("{:?}", slice);
6. Storing a reference inside a struct without proper lifetime annotation via unsafe
Sometimes you want to store a reference inside a struct, but you cannot express the lifetime in the type signature. So you use a raw pointer and write unsafe code to treat it as a reference later. If the referent is dropped before the struct, you get a dangling reference.
struct Holder {
ptr: *const u32,
}
impl Holder {
fn get(&self) -> &u32 {
unsafe { &*self.ptr } // UB if referent was dropped
}
}
let r;
{
let x = 42u32;
r = Holder { ptr: &x as *const u32 }; // x is dropped here
}
println!("{}", r.get()); // dangling read
Why would someone do this? To avoid generic lifetime parameters, or to write self‑referential structures. The safe solution is to use Box or Arc with a shared ownership model, or to use Pin when you need self‑referential async futures.
struct Holder(Box<u32>); // safe, owned, no lifetime issues
How to audit your unsafe code
You cannot rely on manual code review alone. I use three tools in CI to catch these patterns before they reach production.
First, Miri — the gold standard for detecting UB in unsafe blocks. Run it on nightly:
cargo +nightly miri test
Second, cargo-careful — it recompiles the standard library with extra checks:
cargo install cargo-careful
cargo +nightly careful test
Third, cargo-geiger — it counts every unsafe function in your dependency tree and flags any that are unverified:
cargo install cargo-geiger
cargo geiger
Also enable the unsafe_op_in_unsafe_fn lint in your Cargo.toml:
[deny]
unsafe_op_in_unsafe_fn = true
This forces you to wrap each unsafe operation inside an unsafe block, making your unsafe surface area explicit and auditable.
What you should take away
Treat every unsafe block as a promise that you are willing to prove. Use Miri in CI. Prefer bytemuck over transmute. Never implement Send or Sync manually. And when you write unsafe code, comment why the invariants are upheld.
I have learned these lessons through painful debugging sessions. I hope this article saves you that pain. If you found it helpful, please share it with your team — and let me know in the comments what your worst unsafe bug was. I read every comment.
📘 Checkout my latest ebook for free on my channel! Be sure to like, share, comment, and subscribe to the channel!
101 Books
101 Books is an AI-driven publishing company co-founded by author Aarav Joshi. By leveraging advanced AI technology, we keep our publishing costs incredibly low — some books are priced as low as $4 — making quality knowledge accessible to everyone.
Check out our book **Golang Clean Code** available on Amazon.
Stay tuned for updates and exciting news. When shopping for books, search for Aarav Joshi to find more of our titles. Use the provided link to enjoy special discounts!
Our Creations
Be sure to check out our creations:
**Investor Central | [Investor Central Spanish](https://spanish.investorcentral.co.uk/) | [Investor Central German](https://german.investorcentral.co.uk/) | [Smart Living](https://smartliving.investorcentral.co.uk/) | [Epochs & Echoes](https://epochsandechoes.com/) | [Puzzling Mysteries](https://www.puzzlingmysteries.com/) | [Hindutva](http://hindutva.epochsandechoes.com/) | [Elite Dev](https://elitedev.in/) | [Java Elite Dev](https://java.elitedev.in/) | [Golang Elite Dev](https://golang.elitedev.in/) | [Python Elite Dev](https://python.elitedev.in/) | [JS Elite Dev](https://js.elitedev.in/) | [JS Schools](https://jsschools.com/)**
We are on Medium
**Tech Koala Insights | [Epochs & Echoes World](https://world.epochsandechoes.com/) | [Investor Central Medium](https://medium.investorcentral.co.uk/) | [Puzzling Mysteries Medium](https://medium.com/puzzling-mysteries) | [Science & Epochs Medium](https://science.epochsandechoes.com/) | [Modern Hindutva](https://modernhindutva.substack.com/)**
메타데이터
- post_id
- 573906bbf0f0
- slug
- 6-rust-unsafe-patterns-that-compile-cleanly-but-cause-undefined-behavior-573906bbf0f0
- url
- https://medium.techkoalainsights.com/6-rust-unsafe-patterns-that-compile-cleanly-but-cause-undefined-behavior-573906bbf0f0
- canonical_url
- https://medium.techkoalainsights.com/6-rust-unsafe-patterns-that-compile-cleanly-but-cause-undefined-behavior-573906bbf0f0
- author_url
- https://medium.com/@nithin-bharadwaj
- status
- ok
- fetched_at
- 2026-07-20 17:17:34