One Compilation, Three Seconds to Crash: How I Exposed Memory Safety with Rust

💥 It is said that Rust bids farewell to segmentation faults forever, yet I encountered a <span>cargo build</span> crash with SIGSEGV just 3 seconds later.Memory safety? Yes, but it only guarantees “safety” without ensuring “correctness”.Today, this post will dissect the three UB monsters I created with Rust, along with a troubleshooting toolbox, which I recommend saving and sharing to help you avoid detours.

Commit Phenomenon Cause
commit 3ea74 <span>unsafe</span> dereferencing <span>*mut T</span> immediately followed by <span>free</span> Dangling pointer
commit 8c112 <span>tokio::spawn</span> closure capturing <span>&mut self</span> Data race
commit 9f5ab <span>Vec::set_len</span> increased by 1, new slot uninitialized Memory uninitialized

Commonality: All compilations passed, but miri failed all tests.

Key Point 1: Dangling Pointers – A Landmine Beyond the “Safe” Boundary

One Compilation, Three Seconds to Crash: How I Exposed Memory Safety with Rustrust

let<span><span>mut</span></span><span> x </span><span><span>=</span></span><span><span>Box</span></span><span><span>::</span></span><span><span>new</span></span><span><span>(</span></span><span><span>42</span></span><span><span>)</span></span><span><span>;let</span></span><span> p </span><span><span>=</span></span><span><span>&</span></span><span><span>mut</span></span><span><span>*</span></span><span>x </span><span><span>as</span></span><span><span>*</span></span><span><span>mut</span></span><span><span>i32</span></span><span><span>;</span></span><span><span>drop</span></span><span><span>(</span></span><span>x</span><span><span>);</span></span><span><span>// Memory has been freed</span></span><span><span>unsafe</span></span><span><span>{</span></span><span><span>*</span></span><span>p </span><span><span>=</span></span><span><span>0</span></span><span><span>;</span></span><span><span>}</span></span><span><span>// BOOM</span></span>

✅ Defusal Plan

  1. Use <span>NonNull<T></span> to mark “potentially invalid” pointers

  2. Add lifetimes <span>'a</span> and <span>PhantomData</span> to the type to ensure <span>&self</span> lifespan ≥ pointer usage period

  3. If raw pointers are necessary, implement a <span>Drop</span> guard: set the pointer to <span>null</span> in <span>Drop</span>, and check for null in subsequent <span>unsafe</span> blocks

Key Point 2: “Invisible &mut” in Asynchronous Closures

rust

struct<span>Conn</span><span>{</span><span> buf</span><span>:</span><span>[</span><span>u8</span><span>;</span><span>4096</span><span>]</span><span>}</span>

<span>impl</span><span>Conn</span><span>{</span><span>async</span><span>fn</span><span>read</span><span>(</span><span>&</span><span>mut</span><span>self</span><span>)</span><span>{</span><span>tokio</span><span>::</span><span>spawn</span><span>(</span><span>async</span><span>move</span><span>{</span><span>self</span><span>.</span><span>buf</span><span>[</span><span>0</span><span>]</span><span>=</span><span>1</span><span>;</span><span>// Compiled, but crashes on run</span><span>}</span><span>)</span><span>;</span><span>}</span><span>}</span>

Issue:<span>&mut self</span><code><span> is moved to a new thread by spawn,</span><strong><span> leading to two &mut</span></strong><span> existing simultaneously = data race.</span>

✅ Defusal Plan

  1. Extract shared fields into <span>Arc<Mutex<T>></span><span> or </span><code><span>RwLock</span>

  2. Use message channels (<span>tokio::mpsc</span>) to return “memory modification rights” to a single thread

  3. If zero-copy is desired, use <span>tokio::io::AsyncReadExt::read_buf</span> + <span>Vec<u8></span> pooling to avoid mutable references across threads

Key Point 3: The “Uninitialized Trap” of Vec::set_len

rust

let mut v = Vec::with_capacity(10);
unsafe { v.set_len(5); }// 0..5 are garbage
v[0] // UB: Reading uninitialized memory

✅ Defusal Plan

  1. Use <span>resize(n, default)</span><span> or </span><code><span>vec![val; n]</span> for safe initialization

  2. If you must call <span>set_len</span> directly, write before expanding: fill new slots using <span>ptr::write</span>, then <span>set_len(old + n)</span>

  3. Run <span>cargo +nightly miri test</span><span> to set CI gates,</span><strong><span> catching any uninitialized reads instantly</span></strong>

Troubleshooting Toolbox (Tested End-to-End)

Tool Scenario Command Example
miri Detect UB cargo +nightly miri test
san Runtime overflow/out-of-bounds RUSTFLAGS=”-Z sanitizer=address” cargo test
cargo-asm View generated assembly cargo asm –rust
tokio-console Async scheduling anomalies <span>console-subscriber</span> + <span>tokio-console</span>
valgrind Raw C interaction segmentation faults valgrind –tool=memhog ./target/debug/app

A Summary Chart: Rust UB Quick Reference

(Save to your phone, review for 10 seconds before writing unsafe code)

Dangling pointer → NonNull + PhantomData<span>Data race → Arc<Mutex> or channels</span><span>Uninitialized → ptr::write then set_len</span>

Comments Section Open

👇 Share your “safe but crashed” experiences with Rust, and five lucky winners will receive miri stickers + a physical copy of “The Rust Programming Language”, let’s write memory safety into CI and leave crashes in the past!

Memory safety ≠ logical correctness; Rust is not magic, it is a sharper scalpel—don’t cut yourself.

Leave a Comment