By the end of this post, you’ll have Miri installed on your machine and you’ll be able to run it against any Rust crate to check whether its unsafe blocks actually uphold their invariants. That’s the practical skill. The Bun drama is the hook.
Quick context: the Bun Rust rewrite merged in May 2026, and within hours someone filed issue #30719 – titled, yes, literally – “This codebase fails even the most basic miri checks, allows for UB in safe rust.” The repro is eleven lines long. If you’ve never heard of Miri before today, that’s exactly why this post exists.
The trap: a passing test suite tells you almost nothing about UB
cargo test passes. Must be correct, right? Not even close. Normal compilation catches logic bugs and panics – it does not catch undefined behavior hiding in unsafe blocks. Use-after-free, misaligned pointer access, aliasing violations: those bugs can run fine for years, then corrupt data silently the moment the compiler decides to inline something differently.
That’s exactly the split happening with Bun right now. Jarred Sumner posted (via The Register, May 2026) that 99.8% of Bun’s pre-existing test suite passes on Linux x64 glibc in the Rust rewrite. And the Miri failures are also real. Both are true simultaneously – they measure completely different things.
Why standard sanitizers miss it
ASan, TSan, UBSan: useful tools, but they weren’t built for Rust’s semantics. Pointer provenance – the idea that every pointer has exactly one allocation it’s allowed to access, even if a neighboring allocation sits at the same address – is invisible to them. A pointer that drifts into adjacent memory looks fine in compiled output; Miri tracks the full provenance chain and catches it.
Think of it like a flight data recorder versus a crash-test dummy. The dummy tells you the car survived impact. The recorder tells you every decision the pilot made on the way down. cargo test is the dummy. Miri is the recorder.
Turns out Miri is more than a clever hack – it’s peer-reviewed infrastructure. The paper (Jung et al., POPL 2026) tested it against 100,000+ Rust libraries and achieved over 70% test execution success. It ships with the official Rust nightly toolchain, so no exotic install required.
Install Miri: three commands
Per the rust-lang/miri README (as of May 2026):
rustup toolchain install nightly --component miri
rustup override set nightly
cargo miri setup
cargo miri setup builds a custom standard library for Miri to interpret against – takes a few minutes the first time. After that, cargo miri test works like cargo test, except slower, and it actually checks your unsafe code. One hard constraint: cargo miri is nightly-only. Stable Rust won’t do it.
Reproducing the Bun bug locally
New crate:
cargo new --bin ub-demo
cd ub-demo
Drop this into src/main.rs. It mirrors the pattern that broke in Bun’s PathString::slice (from issue #30719, May 2026):
struct PathString {
ptr: *const u8,
len: usize,
}
impl PathString {
fn init(slice: &[u8]) -> Self {
PathString { ptr: slice.as_ptr(), len: slice.len() }
}
fn slice(&self) -> &[u8] {
unsafe { core::slice::from_raw_parts(self.ptr, self.len) }
}
}
fn main() {
let test = Box::new(*b"Hello World");
let init = PathString::init(&*test);
drop(test);
println!("{:?}", init.slice());
}
Run it with cargo run – probably prints “Hello World”. That’s the danger. Now:
cargo miri run
Miri immediately yells: something close to “Undefined Behavior: constructing invalid value of type &[u8]: encountered a dangling reference” – pointing at the from_raw_parts line. The pointer outlived its allocation. Textbook use-after-free, caught in under a second. According to the GitHub issue, nobody on Bun’s team ran this before merging.
What Miri catches
| UB category | Example |
|---|---|
| Use-after-free / dangling pointer | The Bun repro above |
| Out-of-bounds memory access | Reading past an allocation’s end |
| Uninitialized memory reads | MaybeUninit misuse |
| Misaligned access | *const u32 at an odd address |
| Invalid type values | A bool that isn’t 0 or 1 |
| Data races | Two threads, unsynchronized access |
| Aliasing violations | Stacked/Tree Borrows breakage |
| Memory leaks | Allocations alive at exit |
The provenance tracking is the technically interesting piece. Every pointer carries a tag – its “home” allocation. Use that pointer to touch memory outside that allocation’s bounds and Miri flags it, even if the address happens to land inside a neighboring allocation. Normal compiled code strips this information entirely; Miri keeps it alive through the whole execution. That’s how it catches bugs that look fine at runtime and only surface during a production incident three months later.
Three gotchas nobody mentions in the Bun threads
Single-threaded and slow. A 5-second test suite can become 5 minutes under Miri. The nextest workaround (cargo-nextest Miri docs, as of May 2026) runs Miri tests in parallel – 3-4× faster – by giving each test its own process. The catch: that per-process model means it stops detecting cross-test data races. If two tests share a global resource and race on it, only cargo miri test (not nextest) catches it. Pick which risk you’d rather miss.
FFI is a wall. Bun embeds JavaScriptCore – Apple’s C++ JavaScript engine (byteiota analysis, May 2026). Every call across that FFI boundary is inherently unsafe and Miri can’t interpret inside the foreign code. Even if Bun’s team wanted full Miri coverage, large chunks of the codebase hide behind FFI that Miri simply can’t reach. That’s a legitimate structural constraint, not an excuse – but it does mean the 13,000 unsafe block count overstates what Miri could realistically check even under ideal conditions.
Nightly only. Can’t run Miri on stable Rust. Projects pinned to a specific stable release need a separate nightly toolchain just for Miri runs, which adds CI complexity.
Practical tip: Don’t start by running Miri on your entire test suite. Find the modules with the most
unsafeblocks, write small targeted tests for their edge cases, run those. A focused 30-second Miri run beats a 40-minute one you’ll skip every time.
What the numbers actually mean
13,000 unsafe blocks in 681,000 lines. uv – a Rust project of roughly comparable scope at 350,000 lines – has 73. That’s about 181× more unsafe per line than a well-regarded Rust project doing similar systems work (byteiota analysis, May 2026).
Some of those 13,000 are unavoidable: FFI into JavaScriptCore, low-level event loop work. Some aren’t. The Zig-to-Rust translation preserved patterns that didn’t need preserving, and a use-after-free that Miri catches in seconds shipped to main. The lesson: if an Anthropic-backed company’s codebase ships this kind of UB, your 200-line crate with two unsafe blocks should absolutely be running through Miri. The bar isn’t high – it’s just not being cleared.
Is this damning or fair? Probably neither, cleanly. Bun is doing something at a scale nobody else has tried in public. The merge happened; the cleanup will happen in public too. Whether that’s responsible or reckless probably depends on what breaks first in production.
Run it now
Open the most recent Rust project you’ve touched. Run:
cargo miri test
Passes? Good – you now have actual reason to believe your unsafe code is correct. Fails? Even better: you found a bug before production did.
FAQ
Does this mean I shouldn’t use Bun?
Use it for what you were using it for. Most Bun users are bundling JavaScript, not handling cryptographic secrets. If you’re cautious, wait a few weeks before upgrading to the first Rust-based release – let the early reports come in.
Can Miri prove my code has zero UB?
Only for the execution paths you actually exercise. Miri is a dynamic interpreter – it checks what your tests run, nothing more. A bug that only triggers on a specific malformed input won’t appear if you never test with that input. That said, on a targeted suite covering your unsafe boundaries, it’s the closest thing to a proof you’ll get short of formal verification. The Jung et al. POPL 2026 paper ran it successfully against more than 70% of test suites across 100,000+ Rust libraries – coverage in practice is wide, but not total. Think of it as a strong filter, not a certificate.
Why didn’t Bun’s team just run Miri before merging?
Nobody outside the team knows. But the structural case for why it’s genuinely hard: the codebase is 681K lines, Miri is single-threaded, and a large portion of the codebase calls into JavaScriptCore through FFI that Miri can’t interpret at all. Running Miri on even a subset of that, in CI, would require significant setup. That’s a real constraint. It’s also exactly when the constraint matters most – which is the uncomfortable part. Less charitable reads exist; the evidence supports several explanations simultaneously.