An 11-year-old soundness hole surfaces in Rust's borrow checker
Safe Rust can bypass a struct's lifetime bounds via covariant fields plus Drop, causing use-after-scope — an 11-year-old hole, triaged P-high, unfixed.

On October 13, 2026, a contributor filing as jsnell opened issue #164250 against rust-lang/rust: a few lines of perfectly safe Rust — no unsafe anywhere — pass every type and borrow check, then crash under AddressSanitizer with a stack-use-after-scope. The cause is a blind spot in the borrow checker: when a struct declares lifetime bounds and carries a hand-written Drop impl, assigning to a covariant field skips those bounds entirely. The issue briefly carried a P-critical label on the day it was filed; lcnr of the Types team reviewed it and set P-high, for a reason stated plainly in a pinned comment: the bug is 11 years old.
Key points
- Safe code alone can bypass a struct’s declared lifetime bounds: with
struct Q<T: 'static>(Option<T>)and a manualDrop, assigning to the covariant field never re-checksT: 'static, sodropreceives a non-‘static reference the code may use as&'static— ASan reports stack-use-after-scope. - The hole is old: the issue body reproduces it on stable 1.85.0 through current nightly; theemathas’s minimal version compiles since 1.18.0 (2017), and maxdexh’s closure variant segfaults or prints freed memory since Rust 1.4 (2015). lcnr puts its age at 11 years.
- No fix has landed: PR #164277 is open, the issue was locked on the day it was filed, and the HN thread stood at roughly 700 points with 386 comments (read October 14, 2026).
Background
Rust’s core promise is enforced at compile time: memory safety without unsafe. Compiler defects that break it carry a dedicated I-unsound label in rust-lang/rust and land with the T-types team. On Hacker News, weinzierl counted more than 100 issues wearing that label, some standing for years — this one, in his reading, is remarkable but not unusual. He also offered the right frame of comparison: the same code is undefined behavior by design in C++, and Rust’s difference is turning the mistake into a compiler bug rather than the programmer’s fault. Language infrastructure has had a restless month — Zig moved off GitHub over CI problems and banned AI contributions earlier in October. The difference here is what got shaken: Rust’s safety promise itself.
The facts
The shape of the bug is simple. struct Q<T: 'static>(Option<T>) declares that the field’s type must meet 'static, and carries a hand-written Drop — the reporter’s version pushes the value into a static store and reads it back out. The trouble starts at assignment: &'a String is covariant in 'a, so q.0 = Some(&l) (with l a local String) quietly shrinks the field’s lifetime to the enclosing scope, and the compiler never re-checks the struct’s T: 'static. When q leaves scope, drop holds a non-‘static reference that the code is free to use as a &'static String — reading stack memory that is already dead. theemathas’s distilled repro states the gist: inside drop you hold a non-‘static &String, and the type system lets you treat it as 'static.
maxdexh then showed the hole is wider than field assignment: his variant that mem::swaps two elements of a Q array through function-pointer coercions shows unification failing to check a type’s lifetime bounds on paths unrelated to assignment — a type he argues is ill-formed (that variant carries no version claim) — while his separate FnMut-closure variant, in his words, “segfaults/prints freed memory” since Rust 1.4. Version archaeology so far — the Godbolt links in the issue body let you reproduce all of this in a browser:
| Repro variant | Earliest affected | Evidence |
|---|---|---|
| jsnell’s original report | stable 1.85.0 through current nightly | issue body, with Godbolt links |
| theemathas’s minimal version | compiles since 1.18.0 (2017) | contributor’s own testing in the issue |
| maxdexh’s closure variant | segfaults or prints freed memory since 1.4 (2015) | contributor’s own testing in the issue |
The triage discussion sits in a pinned note: lcnr confirms a case like this would normally be P-critical, and that it is “only P-high” because it is 11 years old — the team reasons that a hole this old has most likely never caused memory unsoundness by accident, since triggering it takes an intentionally malicious Drop impl. A moderator locked the issue on October 13, citing spam; the linked fix, PR #164277, has not landed, and theemathas flagged #128225 as possibly sharing the root cause. As of publication, no CVE identifier exists on any official channel — some third-party blogs describe the bug in CVE language, which does not match the issue’s status.
What others say
The HN thread — about 700 points and 386 comments as of October 14 — argues mostly over what to call this: a compiler bug, or a flaw in the type system’s design. GrantMoyer argued the compiler “should always have been rejecting that code according to lifetime rules,” which makes it an implementation defect with a straightforward fix; foldr supplied the mechanism — the compiler never type-checks the implicit call to drop it inserts at the end of a nested block, and correct code cannot depend on that missing check. The counterposition exists too: csande17 and others point out that Rust has no formal spec, so “the type system’s rules” live only in consensus — an unverified implementation all the way down. nextaccountic put it most bluntly: Rust is not a theorem prover, and compiler bugs are ordinary logic bugs. Readers of this site have met the verified alternative in AlphaProof Nexus, which checks every reasoning step with the Lean proof assistant. On the cost of fixing, bluGill’s “no valid program can rely on the bug” was sarcasm, as he himself conceded; afdbcreid countered with Rice’s theorem that a perfect compiler is impossible, and added that for some other known soundness issues the likely resolution path does include rejecting programs that compile today — though not necessarily this one. Rust’s current rules let such fixes land in any semver revision, with crater runs gauging the ecosystem impact.
Our take
Two readers should care most: anyone auditing safety-critical Rust, and anyone who hand-writes Drop. If you audit, “lifetime bounds plus a manual Drop” now deserves a second look; if you write Drop, do not treat a field’s reference as if it met the struct’s bounds — this bug proves the compiler will not check that for you. The more durable lesson is the triage logic: memory unsoundness reachable from pure safe code was downgraded because it had survived 11 years, treating “no incidents in years” as evidence that incidents are unlikely. weinzierl gave the plainest defense of that trade: the promise of safe Rust is real, the implementation is imperfect, and every known hole is fixable. The other honest half: no real-world code is known to be affected, and the fix may reject programs that compile today. Until PR #164277’s crater results are in, nobody can price what the ecosystem will pay.
What to watch
- The landing of PR #164277 and its crater run: whether the fix breaks downstream crates will show up in the PR discussion and rust-lang/rust’s crater reports.
- What happens when the issue unlocks: whether #128225 is confirmed as the same root cause, and whether the priority moves again.