DEV Community

pickuma
pickuma

Posted on Originally published at pickuma.com

What the 11-year-old Rust borrow checker bug means

On October 13, 2026, someone filed a bug in the Rust project's issue tracker. It shows that the Rust compiler accepts programs it should reject, and that those programs can read memory after it has been freed. The report is struct's lifetime requirements can be bypassed via covariant field assignment, issue #164250 on GitHub. It carries the project's label for soundness holes. A note the project added to the issue says the flaw is 11 years old, and that exploiting it takes deliberately hostile code.

The background you need

Memory safety. Programs in languages like C and C++ can use a piece of memory after it has been given back to the system. This is called a use-after-free. The program then reads whatever happens to sit there now, or it crashes. Bugs like this are a classic source of security holes.

Rust's promise. Rust's main selling point is that code written without the unsafe keyword cannot have these bugs. The compiler checks this before your program ever runs. The issue's note puts it directly: Rust's type system is intended to prevent memory unsafety.

The borrow checker. In Rust, you can borrow a value by taking a reference to it, written &value. The borrow checker is the part of the compiler that proves every reference stops being used before the thing it points to is destroyed.

Lifetimes. To do this, the compiler tracks a lifetime for each reference: the stretch of code during which it is valid. The special lifetime 'static means valid for the whole run of the program. A string literal baked into the binary is 'static. A String you create inside a block is not, because it is freed when the block ends.

Bounds on structs. A type can declare requirements on what goes inside it. Writing struct Q<T: 'static> means Q may only hold things that live forever, or that contain no borrowed data at all. Code that works with Q is allowed to rely on that promise.

Drop. A Drop implementation is code that runs automatically when a value is destroyed. It is Rust's version of a destructor. It can do anything ordinary code can do, including copying data into a global variable that outlives everything else.

Variance. Variance describes when the compiler lets you substitute one lifetime for another. A type is covariant in a lifetime if you can pass a longer-lived reference where a shorter-lived one is expected. That direction is safe: something that lives longer than required is fine. The reverse, shorter where longer is required, should always be rejected.

Soundness. A type system is sound if every program it accepts really keeps its promises. An unsoundness bug means the compiler said yes to a program that breaks the rules. The Rust tracker calls these the worst kind of bug.

A rough analogy: think of lifetimes as library loan periods. 'static is a permanent loan. A struct with a 'static bound is a shelf marked permanent loans only. Its Drop code is a clerk who trusts that label and files everything on the shelf into the permanent archive. The analogy breaks in one key way. A real library notices when a borrowed book is missing. A Rust program does not. The dangling reference just points at whatever now occupies that memory.

What actually changed

The compiler did not change. What is new is that someone found and published a way around a check that has been missing for years.

The smallest version, posted by contributor theemathas, looks like this:

struct Q<T: 'static>(Option<T>);
impl<T: 'static> Drop for Q<T> {
    fn drop(&mut self) {}
}
fn main() {
    let l = String::new();
    let mut q = Q(None);
    q.0 = Some(&l);
}
Enter fullscreen mode Exit fullscreen mode

Q is declared to accept only 'static contents. It is created empty. Then a reference to a local, short-lived String is assigned into its field. The compiler should refuse that last line. Instead, it accepts it. Inside drop, the code holds a reference that is not 'static but is allowed to treat it as one.

The original report by jsnell takes the next step. Its Drop implementation stores the value in a global list. After the local String is freed, the program prints from that list. According to the issue, the program compiles and prints a different string, XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX, which was created afterward and most likely reused the same memory. Built with AddressSanitizer, a tool that detects memory errors at runtime, it crashes with a stack-use-after-scope error.

The reporter confirmed the bug on current nightly builds and on stable releases at least as far back as 1.85.0. Other contributors went further back. theemathas posted a variant that has compiled since 1.18.0 (2017). maxdexh posted one that segfaults or prints freed memory since 1.4. maxdexh also suggested the problem is broader than field assignment: "unification just generally doesn't check lifetime bounds of the type". Unification is the step where the compiler works out that two types must match. maxdexh showed a version that triggers the bug by putting two values in an array, with no field assignment at all.

The issue is labeled P-high. The project's note explains why it is not P-critical: it is 11 years old, and a bug nobody found in that time "cannot actually be that easy to hit". A moderator later locked the issue to keep out spam.

What it means for you

If you write or ship ordinary Rust, this almost certainly has not corrupted your program. According to the note, exploiting the bug requires "an intentionally malicious Drop implementation". The hole only lets bad things happen if someone writes Drop code that deliberately smuggles a reference out. Normal destructors close files or free buffers. They do not do that.

The guarantee matters more in places that rely on the compiler as a security boundary. Examples include systems that compile and run Rust from people they do not trust, or that treat the absence of unsafe as proof that code cannot touch memory it should not. For those setups, a known hole like this is a real gap until it is fixed.

The note also points to longer-term work the Types Team is doing to shrink this class of bug: a next-generation trait solver, a rework of how region (lifetime) constraints are handled, and an effort it calls Project Zero.

What to watch next:

  • A fix landing in nightly, then in a stable release.
  • Whether the fix covers maxdexh's array variant as well as the field-assignment case.
  • Whether closing the hole makes the compiler reject some existing code that it used to accept. Soundness fixes in any compiler can do this. The issue does not say whether this one will.

What we don't know yet

  • When it will be fixed. The issue page does not give a timeline or a target release.
  • How many code paths are affected. The reproductions use different mechanisms. If maxdexh is right that unification generally skips these checks, the fix may need to be broader than one special case.
  • How far back it goes. Each reproduction was tested to a different floor: 1.85.0, 1.18.0, and 1.4. The project's note says 11 years. Nobody on the issue has established a single exact starting point.
  • Whether it has been exploited. The note calls accidental memory unsafety very unlikely. The issue does not address deliberate use.

You do not need to change your code today. Pay attention to the release notes for the next few Rust versions. If you run untrusted Rust code and rely on the compiler's checks for isolation, treat this as an open hole until a fix ships.


Originally published at pickuma.com. Subscribe to the RSS or follow @pickuma.bsky.social for new reviews.

Top comments (0)