Safe Rust is supposed to make use-after-free impossible. A soundness report filed on October 13 shows one way that promise currently fails, and the twist is that the compiler has been accepting the bad code for over a decade. The bug lives in a GitHub issue against the Rust compiler, rust-lang/rust#164250, and it lets a short-lived reference escape its lifetime through a struct field write, ending in a genuine use-after-scope that AddressSanitizer catches. Labeled I-unsound and P-high by the types team, the issue blew up fast: 696 points on Hacker News within a day.
The headline detail came out of the discussion. Commenters traced variants of the reproducer back through old releases, and the working estimate is that this hole has existed since roughly Rust 1.18, possibly earlier. Eleven years of “safe by construction,” and the compiler was quietly letting a dangling reference through one specific door the whole time.
What the code does
Stripped to its bones, the pattern looks like this:
struct Q<T: 'static + Send>(Option<T>);
impl<T: 'static + Send> Drop for Q<T> { /* relies on the bound */ }
let mut q = Q(None);
q.0 = Some(&local_string); // short-lived reference
// scope ends, compiler-inserted drop runs with the dangling ref
The struct declares that its type parameter must outlive ‘static. Its Drop implementation is entitled to rely on that promise. The assignment q.0 = Some(&l) should be rejected, because l is a short-lived local. Instead, the compiler checks only the field’s type, and the type system’s variance lets a Q<&’static String> drift into a Q holding a shorter reference. The implicit destructor call the compiler inserts at the closing brace never re-proves the ‘static bound, so the Drop impl runs with a reference to memory that is about to go out of scope. In the original reproducer, the value is stashed in a global and printed afterwards: the program happily prints freed memory, and under AddressSanitizer it crashes with stack-use-after-scope.
The compiler checks the bound everywhere else. Go through a method, a function call, Box::new, or even an explicit drop(q) call and you get error E0597, the borrow checker doing its job. The one path that skips the check is the one you never write yourself: the implicit drop at scope end. Independent testing of 26 program variants confirmed the hole needs all four ingredients at once: a struct with a lifetime bound, a Drop impl that depends on the bound, a direct write into a public field, and a value dying at scope end. Remove any one of them and the compiler rejects the code.
Why the team is not panicking
A types team member’s pinned note on the issue is worth reading in full. The bug is P-high rather than P-critical for a reason that sounds backwards until you think about it: a soundness hole that survived eleven years of production use without anyone finding it cannot be easy to hit. Exploiting it requires an intentionally malicious Drop implementation, which in safe Rust means the attacker has to be inside your dependency tree already. The team’s judgment, which I think is right, is that this has almost certainly never caused accidental memory unsafety.
That said, the response is not complacency. The note ties the fix effort to the team’s ongoing work: the next-generation trait solver, the region constraint rework, and Project Zero, a 2026 roadmap commitment to fixing every known type system soundness bug so that unsafe is the only route to undefined behavior. For scale: the compiler currently tracks more than 100 open issues labeled I-unsound, some of them long-standing. The next solver, which already powers coherence checking and is slated to stabilize across Rust this year, unblocks fixes for a good chunk of that backlog.
What Rust developers should do now
There is no compiler fix yet; as of the issue being filed, even experimental nightly flags still accepted the reproducers. The practical defenses are library-level and cheap:
- Make fields private on any struct that combines a generic lifetime bound with a Drop impl that relies on the bound. Testing showed this single change turns the silent use-after-free into an E0597 compile error on every toolchain checked.
- Run the types with Drop impls and lifetime parameters under Miri (cargo +nightly miri test) in CI. Miri catches the use-after-scope in tests even while rustc accepts the code.
- Follow issue 164250. When the fix lands, code that compiles today may stop compiling, which is standard Rust practice for soundness fixes.
The uncomfortable footnote
The Hacker News discussion drifted, as these threads do, into whether any soundness guarantee can be absolute. Someone invoked Rice’s theorem, someone else pointed out that RustBelt formally verified only a subset of the language, and the whole debate landed where these debates always land: you cannot prove a checker both sound and complete, so absolute certainty is off the table.
That argument is technically correct and mostly beside the point. Rust’s memory safety has always meant “safe assuming the compiler is correct,” a qualifier that quietly applies to every language nobody prints in the marketing. What matters is the shape of the failure: the bug was found by a researcher poking at the type system, disclosed on a public tracker, triaged within hours, and connected to a funded roadmap for eliminating the entire bug class. Try getting that response for a use-after-free in a C codebase. The hole is real, eleven years of silence around it is humbling, and the system that found it, labeled it, and scheduled its death is the actual story.