Unsafe Rust: Raw Pointers (*const T, *mut T), Undefined Behavior, Miri
- Last updated
- Prerequisites:
- Rust ownership, references, and lifetimes
- Understanding of memory safety
- rust
- unsafe
- raw-pointers
- undefined-behavior
- miri
- soundness
Read at your depth
The practical view
unsafe blocks let you do things the compiler cannot verify: dereference raw pointers, call unsafe functions (FFI), access/modify static mut, implement unsafe traits, call unsafe methods. Raw pointers: *const T and *mut T. They can be null, dangling, and ignore borrow rules. Creating a raw pointer is safe; DEREFERENCING it is unsafe (must be in an unsafe block). Unsafe does NOT disable the borrow checker — it only unlocks these specific operations. Rules: ensure the pointer is valid and aligned, the data is initialized, and no aliasing rules are violated (unless using interior mutability properly). Use unsafe sparingly and document the invariants.
The same idea in other frameworks
N/A — Rust language feature.N/A — Rust feature.N/A — Rust feature.N/A — Rust feature.Legacy vs modern
Safe reference API vs raw-pointer unsafe API
Safe code uses references with compiler guarantees; raw pointers require manual validity/aliasing checks inside unsafe blocks.
let p: *const i32 = ptr; // ❌ OLD: no check that it's valid// ✅ NEW: check null/alignment, document the lifetime contract
if !p.is_null() && (p as usize) % align_of::<i32>() == 0 {
let v = unsafe { *p }; // ✅ NEW: invariants upheld
}Interview gotchas
Context
This tests the UB contract — interviewers want the optimizer's assumption model, not just 'it crashes', because UB can silently corrupt behavior under optimization.
The mechanical answer
Undefined behavior is a set of actions the Rust abstract machine declares as having NO defined semantics: dereferencing null/dangling/misaligned pointers, reading uninitialized memory, creating invalid values (e.g. a bool that is neither 0 nor 1), violating aliasing rules (two &mut to the same location), data races, and calling functions with wrong ABIs. The danger: the compiler is allowed to assume UB never happens. Given that assumption, the optimizer can transform code in ways that are correct only if the assumption holds — so a subtle UB (like a dangling pointer in a cold path) can cause bizarre miscompilation in unrelated code, not just a crash. This is why UB is a soundness issue, not a reliability issue: it invalidates the entire program's guarantees. Miri and sanitizers exist to find it in testing before it ships.
Trap
The naive answer is 'UB means it might crash'. UB means the compiler may assume it never happens and optimize accordingly — behavior is entirely unpredictable. Another trap: thinking a small unsafe block 'contains' the danger — UB from unsafe code can make even safe code behave wrongly because the optimizer's assumptions were violated globally. Candidates who explain the optimizer-assumption model show real soundness understanding.
Context
This probes the tooling — interviewers want the abstract-machine interpretation and the Rust-specific checks (aliasing, provenance, uninitialized reads) that native tools miss.
The mechanical answer
Miri interprets the Rust MIR (mid-level intermediate representation) rather than executing native code. It implements the Rust abstract machine, so it can detect UB that native tools cannot see: reads of uninitialized memory (tracked per byte), pointer provenance violations (using a pointer to access memory it wasn't derived from), aliasing violations (via the Stacked Borrows or Tree Borrows model — e.g. using a shared reference to mutate), invalid enum/bool values, and data races (it includes a data-race detector for the concurrent interpreter). Native sanitizers (ASan, Valgrind) catch memory errors at the hardware/allocator level but miss the language-level UB (uninitialized reads, provenance, invalid values) that Miri models. That's why the Rust workflow is: run tests under Miri (cargo +miri test) for unsafe code, and use sanitizers in addition for native-level issues.
Trap
The naive answer is 'sanitizers catch everything Rust can misbehave on'. Miri models language-level UB that native tools can't detect. Another trap: thinking Miri runs the optimized program — it interprets MIR (with its own semantics), so it may report issues that optimization would hide or miss performance-only behavior. Candidates who distinguish the abstract machine from native execution show real understanding.