RC RANDOM CHAOS

Safe Rust prints freed stack memory

Rust issue #164250 exposes an 11-year borrow-checker unsoundness: covariant field assignment lets a malicious Drop read freed stack memory in safe Rust.

· 3 min read
Safe Rust prints freed stack memory

On October 13, 2026, someone filed rust-lang issue #164250 with a program the compiler accepts that assigns a reference to a local String into a struct field, lets the String go out of scope, and then prints its contents back out of freed stack memory. Under AddressSanitizer the same program crashes with stack-use-after-scope. There is no unsafe block anywhere in it, and it has compiled cleanly on stable Rust for years.

The setup is a generic struct with a 'static bound and a hand-written Drop:

struct Q<T: 'static + Send>(Option<T>);
impl<T: 'static + Send> Drop for Q<T> {
    fn drop(&mut self) {
        if let Some(t) = self.0.take() {
            KEPT.lock().unwrap().push(Box::new(t));
        }
    }
}

The bound says T: 'static, so the Drop code is entitled to treat the stored value as living for the whole program. The hole is in how the field gets populated. Writing q.0 = Some(&l) assigns a reference to a local, which does not live for 'static. Because the field is covariant and unification does not check the type’s lifetime bounds on assignment, the compiler accepts the shorter-lived reference where a 'static one is required. When q is dropped at the end of its block, drop reads what it believes is a &'static String but is actually a dangling pointer to a local that already went out of scope. The reported example stashes that reference in a global Vec and reads it after the stack slot has been reused, which is why it prints a different string than the one that was stored.

The filer found several variants: one that reaches the same end state through unifying coercion with no field assignment at all, one phrased with a 'a: 'static bound that is probably the clearest to read, and plain versions that segfault or print freed memory. Their minimised reproductions compile on different versions going back a long way. One has built since 1.18.0 in 2017; another segfaults since 1.4. The issue reproduces on current nightly and on stables at least down to 1.85.0.

The Types Team marked it P-high and was explicit that it would have been P-critical regardless of whether anyone hit it in the wild. The reason it is only P-high, per @lcnr, is its age: a bug that has gone unfound for 11 years cannot be that easy to trigger. The same comment notes that exploiting it requires an intentionally malicious Drop implementation, and that it is very likely this has never caused memory unsafety by accident. The labels on the issue (A-variance, A-lifetimes, A-destructors, A-borrow-checker) point at the interaction between variance, region constraints, and destructor timing rather than at any one component.

For code you wrote and trust, this changes very little. You will not stumble into it writing ordinary Rust, and a decade of production code is the evidence. The part worth acting on is the trust boundary. The guarantee people lean on, that safe Rust cannot produce a use-after-free, now has a known exception, and the ingredient it needs is a hand-written Drop that quietly assumes a 'static lifetime it was never actually given. That is exactly the kind of thing that can sit in a dependency or a proc-macro you did not audit. If you run untrusted or security-sensitive Rust, a malicious destructor is a concrete vector against the memory-safety assumption, not a theoretical one.

The practical move is to run those builds under AddressSanitizer in CI. The reproductions fault as stack-use-after-scope, which ASan catches cleanly, so it will flag an exploit attempt that passes type-checking. There is no stable release to fall back to, since the bug reproduces straight through current stables; the fix is being tracked under the Types Team’s region constraint rework and the broader Project Zero effort to close type-system unsoundnesses. Until that lands, the compiler’s acceptance of a 'static-bounded Drop is not by itself proof that the stored reference actually lives that long.

Share

Keep Reading

Latest on the Wire

Full wire →

New signal daily · RSS

Stay in the loop

New writing delivered when it's ready. No schedule, no spam.