5 Rust Unsafe Mistakes That Cause Undefined Behavior and How to Contain Them
5 Rust Unsafe Mistakes That Cause Undefined Behavior and How to Contain Them
Learn 5 common Rust unsafe mistakes, how they trigger undefined behavior, and practical ways to audit, isolate, and contain unsafe code.
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 remember the first time I saw a production Rust codebase with unsafe blocks scattered like confetti. It felt like watching someone juggle knives over a trampoline. The developers were smart, the tests passed, and nothing had crashed — yet. But the moment a single pointer assumption turned stale, the whole system could have turned into undefined behavior. That's when I asked myself: how do you contain the blast radius of unsafe without banning it outright?
unsafe is not a cheat code. It's a contract with the compiler that says: "I have verified the invariants, the borrow checker can relax." Most real misuse happens not because people are careless, but because they trust the unsafe block too much, or they put it in the wrong place. Let me walk you through five patterns I've seen — and fixed — in real libraries and applications.
1. The transmute Trap Across ABI Boundaries
You’ve probably written something like this:
let val: u64 = unsafe { std::mem::transmute(my_struct) };
It looks convenient. You have a struct and you need its raw bytes as a u64. But transmute is a weapon of mass destruction. It forces the compiler to treat the bit pattern of my_struct as a u64, regardless of alignment, padding, or layout guarantees. The standard library does not guarantee that my_struct has the same representation as a u64 — in fact, for most non‑trivial structs, it won't.
A safer choice is a crate like bytemuck, which requires explicit Pod (plain old data) and Zeroable implementations and will reject the cast at compile time if the layout is incompatible:
use bytemuck::cast;
let val: u64 = cast(my_struct); // compile error if layout mismatches
Have you ever asked yourself: “Do I really need that transmute, or can I use a checked alternative?” Most of the time, the answer is the latter.
2. from_raw_parts Without Safety Net
Creating a slice from a raw pointer is one of the most common unsafe operations in systems-level Rust. The misuse pattern is almost always the same: the length argument is not validated, or the pointer is invalid.
let slice = unsafe { std::slice::from_raw_parts(ptr, len) };
This looks harmless. But if ptr is dangling, misaligned, or len exceeds the actual allocation, you get immediate undefined behavior. I once debugged a video decoder where the memory arena was reused and the pointer became stale — the crash was subtle, happening only on certain frames.
The fix is not to avoid unsafe but to wrap it in a function that validates every precondition:
/// # Safety
/// `ptr` must be non‑null, aligned, and point to a valid allocation of at least `len` elements.
unsafe fn checked_slice<T>(ptr: *const T, len: usize) -> Option<&'static [T]> {
if ptr.is_null() || !ptr.is_aligned() { return None; }
// (In practice you'd also need to check allocation bounds, often through a private counter.)
Some(std::slice::from_raw_parts(ptr, len))
}
Notice I still use unsafe inside at the final call — but the responsibility for correctness is now confined to a tiny, auditable scope.
3. Blindly Implementing Send and Sync
One of the most dangerous things you can do is mark a type that wraps a raw pointer as Send or Sync without auditing every field. Consider a cache that holds a raw pointer to a shared memory region:
struct MyCache {
ptr: *mut u8,
}
unsafe impl Send for MyCache {}
unsafe impl Sync for MyCache {}
This tells the compiler: “It is safe to send and share this cache between threads.” But if ptr points to a region that is not synchronized, you now have a data race that the Rust type system cannot detect. The offending code is completely safe Rust from the caller’s perspective.
A better approach is to opt out of automatic trait implementations using PhantomData<*mut T> and then only manually implement Send/Sync after reviewing every path that touches the pointer. I've started writing a safety comment that enumerates why each field is thread‑safe. For example:
struct MyCache {
ptr: *mut u8,
_not_send: PhantomData<*mut ()>, // opt‑out
}
Then, if I truly need the trait, I write:
// Safety: `ptr` is behind a mutex stored in the outer struct (not shown).
unsafe impl Send for MyCache {}
Do you know exactly why your Send implementation is safe? If you can’t explain it in one sentence, you probably shouldn’t have written it.
4. Mixing Safe and Unsafe Code in the Same Scope
Large functions with unsafe blocks buried inside are ticking time bombs. A colleague once refactored a hot loop that had an unsafe block in the middle — he moved a variable declaration, and suddenly the pointer used inside unsafe no longer lived long enough. The code still compiled, but it started crashing intermittently.
The principle is simple: encapsulate each unsafe operation into its own small function with a clear # Safety section. That way, the surrounding safe code cannot accidentally invalidate the invariants.
/// # Safety
/// `ptr` must point to a valid, initialized `u32`.
unsafe fn read_u32(ptr: *const u32) -> u32 {
*ptr
}
Even if read_u32 is only one line, by isolating it you force any future programmer to think about the safety contract when they call it. The Rust community calls this “unsafe abstraction boundary.”
5. Ignoring unsafe_op_in_unsafe_fn
When you write an unsafe fn, every line inside that function is implicitly trusted — the compiler assumes you already know it's all safe. This hides which specific operations are actually unsafe. For example:
unsafe fn do_something(ptr: *const u32) -> u32 {
*ptr // this dereference is unsafe, but it's hidden inside an unsafe fn
}
To make the dangerous operations visible, enable the lint unsafe_op_in_unsafe_fn in your crate:
#![deny(unsafe_op_in_unsafe_fn)]
Now every line that uses an unsafe operation inside an unsafe fn must be wrapped in its own unsafe block, forcing you to document why that line needs it.
unsafe fn do_something(ptr: *const u32) -> u32 {
unsafe { *ptr } // now it's explicit, and you can add a safety comment
}
This small change has caught many subtle mistakes in my own code. It turns a vague trust into a concrete checklist.
Putting It All Together
unsafe is not evil. It's a tool — but it's a chainsaw, not a pocketknife. The blast radius of a misuse can corrupt memory, crash servers, or create security holes that are invisible to unit tests. That's why I always take these steps:
- Run
cargo geigeron every dependency to count theirunsafeusage. - Set
#![forbid(unsafe_code)]at the crate root and only lift it inside a tightly scoped sub‑module. - Test with Miri (
cargo +nightly miri test) to catch undefined behavior before it ships.
If you’ve ever had a bug that “only happens in release mode,” there’s a good chance unsafe was involved. The goal isn't to eliminate it — it's to make every unsafe block auditable, documented, and small enough that a single person can verify it in five minutes.
What’s your experience? Have you seen an unsafe misuse that slipped through review? I'd love to hear your story in the comments. If this article helped you think about unsafe differently, please share it with a teammate and hit the like button. Let's write safer Rust together.
📘 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
- ffbefc2bf2eb
- slug
- 5-rust-unsafe-mistakes-that-cause-undefined-behavior-and-how-to-contain-them-ffbefc2bf2eb
- url
- https://medium.techkoalainsights.com/5-rust-unsafe-mistakes-that-cause-undefined-behavior-and-how-to-contain-them-ffbefc2bf2eb
- canonical_url
- https://medium.techkoalainsights.com/5-rust-unsafe-mistakes-that-cause-undefined-behavior-and-how-to-contain-them-ffbefc2bf2eb
- author_url
- https://medium.com/@nithin-bharadwaj
- status
- ok
- fetched_at
- 2026-07-20 17:17:34