Rust's borrow checker has 11-year-old bugs: what changes
A new issue in the Rust repository describes borrow checker bugs that have been in the compiler for about 11 years. That's pretty much since Rust 1.0. In practice, it means you can write safe code, with no unsafe at all, that the compiler accepts and that still reads memory that was already freed. Rust's borrow checker is exactly the piece that should block this. The source is issue #164250 on the rust-lang GitHub.
Before someone posts "Rust is over" on X, relax. The story is more interesting than that. It says a lot about how we should think about language guarantees. Including when the topic is Bun.
What is an unsoundness bug in Rust's borrow checker?
Rust makes you a simple promise. If the code compiles and has no unsafe, it has no use-after-free, no data races and no dangling pointers. The borrow checker is what keeps that promise. It analyzes lifetimes and borrows and rejects anything dangerous.
"Unsound" is the technical name for when that promise breaks. The compiler accepts a program that violates its own rules. It's not a performance bug or a confusing error message. It's the language's core guarantee with a hole in it.
The Rust team tags these cases with the I-unsound label, and the list isn't short. Some stay open for years. The most famous example is issue #25860, opened in 2015 and still alive. Look at this beauty:
static UNIT: &'static &'static () = &&();
fn foo<'a, 'b, T>(_: &'a &'b (), v: &'b T) -> &'a T { v }
fn bad<'a, T>(x: &'a T) -> &'static T {
let f: fn(_, &'a T) -> &'static T = foo;
f(UNIT, x)
}
No unsafe there. Even so, bad takes a short-lived reference and returns it as if it lived forever. Use this with a value on the stack and you get a pointer to garbage, approved by the compiler.
Why did these bugs survive 11 years?
Because nobody writes this code by accident. You need a very specific mix of implicit lifetimes, function coercion and variance to fall into the hole. In normal production code, with a struct, an impl and a Vec here and there, the odds of hitting this by accident are very low.
There's also the engineering side. Fixing some of these cases means changing how the compiler reasons about types. That can break crates that compile today. Rust takes compatibility seriously. The work on the new trait solver and on Polonius (the next generation of the borrow checker) exists partly to close these gaps without blowing up the ecosystem.
So the bugs didn't stay open out of laziness. They stayed open because the right fix is expensive and the practical risk is low. You can disagree with that priority, but it makes sense.
A language guarantee is a contract, not a miracle. Contracts have fine print.
Does this mean Rust isn't safe?
No. And here's my blunt opinion: anyone using this issue to say Rust is as unsafe as C is stretching it.
Compare the scales. In C or C++, use-after-free shows up through carelessness on a Friday afternoon. In safe Rust, to get there you practically have to build the bug on purpose, with a construction that looks like a puzzle challenge. Studies from Google and Microsoft have shown for years that around 70% of serious vulnerabilities in native code come from memory errors. Even with rare holes, Rust cuts out almost that whole category.
What the issue changes is the narrative. "If it compiles, it's safe" was always an "almost always". Now we have one more concrete example of where the "almost" lives.
Where this becomes a real problem:
- Intentionally malicious code. A supply chain crate can use one of these holes to hide dangerous behavior without any
unsafeshowing up in a grep. - Sandboxes that trust the compiler. Platforms that run third-party Rust thinking "no
unsafe= isolated" are betting wrong. - Lazy audits. If your review process is "look for
unsafeand approve the rest", that was never enough.
And where does Bun come into this?
Bun is written in Zig, not Rust. And every time Bun ships a release with a crash or a segfault, someone shows up in the comments saying "it should have been written in Rust".
I get the argument. Zig has no borrow checker. It gives you better tools than C (explicit allocators, a debug mode with checks, defer), but memory is your responsibility. Bun's issue history has its share of memory crashes, like any large runtime written in a language without automatic memory management.
This Rust issue helps put the discussion in the right place. The question isn't "which language is perfect", because none is. The question is where the holes are and how easy it is to fall into them.
- In Zig, the hole is everywhere you manage memory. You fall into it with normal code.
- In safe Rust, the hole exists, but it lives in pathological type cases. You almost have to go looking for it.
As a Bun user, you run TypeScript on top of JavaScriptCore. None of this changes your day. Your Bun.serve won't break because of a Rust lifetime or a Zig allocator. But if you're choosing a language to write a runtime, a parser or a native extension, this difference matters.
What changes in your workflow
If you write Rust, almost nothing changes tomorrow in practice. But it's worth adjusting a few things:
- Keep an eye on the
I-unsoundlabel. No need to read it every day. Take a look when you update your toolchain. - Keep the compiler up to date. Soundness fixes land in regular releases, sometimes with a compatibility warning first.
- Don't treat "no
unsafe" as proof of safety in third-party dependencies. Usecargo-audit,cargo-vet, or at least check who maintains the crate. - Run Miri on tests for critical code. It interprets the program and catches undefined behavior at runtime, including some cases the compiler let through.
rustup +nightly component add miri
cargo +nightly miri test
If you write Zig or work on a native addon for Bun, the lesson is similar, just with the sign flipped. Run your tests in Debug or ReleaseSafe mode, use the GeneralPurposeAllocator to catch leaks and double frees, and treat every [*]u8 with the suspicion it deserves.
And if you only write TypeScript, the message is about supply chain. npm packages with native binaries (and there are plenty today, from bundlers to database drivers) run code you haven't audited, in a language you might not even read. The safety of the language underneath helps, but it doesn't replace knowing what you install.
Is it worth switching languages because of this?
No. If you already use Rust, keep going. If you already use Bun and like it, keep going too. This issue is no reason to panic or migrate.
For me, what matters here is the transparency. The Rust team documents the holes in its own guarantee in public, with a label, a minimal example and an open discussion. That's how you treat a safety promise. You show where it fails instead of pretending it doesn't. I'll admit a lifetime bug turning 11 has its charm. It's almost old enough to review PRs on its own.
My takeaway is that a compiler guarantee is a layer, not a shield. It shrinks the error surface a lot, but review, tests and care with dependencies are still your job. If you want to see how I apply this in real projects, with TypeScript, Bun and a bit of healthy paranoia, take a look at my projects.
LinkedIn summary
There is safe Rust code, with no unsafe at all, that reads memory that was already freed. And the compiler has been approving it for 11 years. A new issue on rust-lang exposes borrow checker bugs that have been there pretty much since Rust 1.0. Before anyone says "Rust is over": to fall into this hole you almost have to build the bug on purpose. In C, it shows up on a Friday afternoon. The real risk is in supply chain and in reviews that only look for unsafe and approve the rest. What caught my attention most was the transparency. The team documents in public where its own guarantee fails. A compiler guarantee is a layer, not a shield. Review, tests and care with dependencies are still your job. I wrote about this, and about where Bun and Zig fit into the conversation, on my blog. Link in the comments. #Rust #Bun #SoftwareSecurity #SupplyChain #SoftwareDevelopment