The tiny safe-code pattern that broke a big promise
Rust’s fundamental promise — that its borrow checker prevents use-after-free — recently encountered a significant challenge. Issue #164250, opened last week, details a memory safety bug reproducible in entirely safe Rust code, a scenario previously considered impossible. This unsoundness has persisted undetected for 11 years, since Rust 1.4.0 in 2015.
The exploit hinges on a specific pattern: a generic struct, such as struct Q<T: 'static>(Option<T>);, declares a 'static lifetime requirement for its type parameter T. Crucially, this struct also implements a custom Drop function that implicitly trusts this 'static bound, assuming the contained data will persist for the program's entire duration.
The surprising step occurs during assignment. Developers instantiate the struct empty, for example, let mut q = Q(None);. Then, they assign a reference to a short-lived local variable, like &l, into its covariant field: q.0 = Some(&l);. Here, the compiler fails to enforce the outer type's 'static lifetime obligation on the assigned reference.
This oversight allows the Drop implementation to receive a reference it believes is 'static, but which actually points to deallocated memory once the local variable goes out of scope. This scenario, verified by AddressSanitizer and Miri, results in stack-use-after-scope or dangling reference use-after-free, directly violating Rust's core memory safety guarantees.
How a lifetime promise turned into a dangling reference
The core of the failure lies in how Rust's compiler handles lifetime promises during specific assignments. A struct can declare that any reference it holds must live for the entire program's duration, using the 'static lifetime bound. A custom Drop implementation for this struct then implicitly trusts this promise, assuming its stored reference will remain valid.
Problems arise when a reference to a local variable—one with a limited scope—is assigned to a field within such a struct. The compiler performs a type check on the assignment but crucially omits re-validating the struct's overall lifetime requirement against the newly assigned reference. This creates a disconnect: the struct's destructor believes it holds a 'static reference, but the underlying data is a local variable about to expire.
When the struct is dropped, its Drop implementation executes, operating on a reference that now points to deallocated memory. A deliberately crafted Drop can then stash this dangling reference or attempt to use it, resulting in use-after-free or stack-use-after-scope memory errors. For example, the Drop could hand the invalid reference to a separate thread for later access.
While the example pattern does not involve unsafe blocks directly, the resulting memory corruption demonstrates a compiler-checking hole. Tools like Miri and AddressSanitizer (ASan) can detect these invalid memory accesses, flagging issues such as stack-use-after-scope or dangling reference use-after-free, confirming the exploitability of Issue #164250.
Eleven years old—and difficult to hit by accident
A Rust compiler contributor traced the memory safety bug in Issue #164250 back to Rust 1.4, released in October 2015. This makes the flaw approximately 11 years old, an astonishing longevity for a vulnerability in a language whose core promise is memory safety. The discovery led the reporter to exclaim, "How on earth has this not been found before?"
Despite its age and impact on Rust's fundamental guarantees, the issue is triaged as I-unsound and P-high, not P-critical. This classification reflects the contrived nature of the exploit pattern, not a diminished view of soundness. A P-critical rating is typically reserved for bugs easily triggered accidentally in common code.
Accidental exposure to this flaw is considered highly unlikely in ordinary application code. Exploiting the bug requires a deliberate combination of factors:
- A generic type with a
'staticlifetime bound - A permissive field assignment that bypasses lifetime validation
- A custom
Dropimplementation designed to exploit the mismatch, often by moving a dangling reference to a global or another thread.
Rust Types Team member lcnr noted that the bug’s 11-year dormancy suggests it is not easily hit. This specific pattern, while breaking a core invariant, demands a malicious or highly unusual Drop implementation to trigger a use-after-free. For more technical details, refer to the GitHub issue: struct's lifetime requirements can be bypassed via covariant field assignment · Issue #164250 · rust-lang/rust.
Enjoying this? Get one like it in your inbox each morning.
one email a day · unsubscribe in two clicks · no third-party tracking
The fix—and the lesson behind the headline
Proposed remedy for Issue #164250 arrived swiftly in PR #164277. The fix involves enhancing the compiler's well-formedness validation for MIR local types, specifically ensuring that assignments and implicit subtyping do not bypass crucial lifetime-bound checks. This directly addresses the missing validation that allowed the 'static lifetime promise to be broken.
This incident, while alarming, underscores a broader truth: memory safety, even in a language like Rust, fundamentally relies on compiler-enforced guarantees. Compilers are intricate software systems, and like all complex code, they can harbor subtle edge cases and unforeseen interactions. Rigorous testing, continuous code review, and ongoing soundness work are indispensable.
Rust’s core promise of memory safety in safe code remains robust, but this 11-year-old bug serves as a stark reminder. It highlights why compiler soundness is as critical as language design in delivering on that promise. While a real breach of Rust's safe-code guarantee, it does not suggest Rust is broadly unsafe; rather, it reinforces the necessity of vigilant compiler development.
Frequently Asked Questions
What was the Rust memory safety bug?
Issue #164250 describes how a field assignment could bypass a lifetime requirement on a struct, allowing safe code to create a dangling reference under a specially crafted destructor.
Could ordinary Rust code trigger the bug accidentally?
The demonstrated pattern requires an unusual combination, especially a Drop implementation that deliberately trusts and uses a reference with a falsely long lifetime.
Does the bug mean Rust is no longer memory safe?
No. It is a serious compiler soundness flaw, but it does not invalidate Rust's broader memory-safety benefits or mean typical Rust programs are exposed.
How was the Rust bug proposed to be fixed?
The proposed fix checks that MIR local types satisfy their well-formedness requirements, preventing assignments from bypassing lifetime constraints.

