Feb 24, 2026·8 min read·6 visits
The `lru` crate versions < 0.16.3 contain a soundness bug in their iterator implementation. By creating a mutable reference (`&mut`) to a cache key inside an `unsafe` block, the code invalidates other existing pointers to that key, violating Rust's Stacked Borrows rules. While difficult to exploit for RCE directly, it constitutes Undefined Behavior that can lead to memory corruption or compiler mis-optimizations. The fix? Downgrading the pointer to a shared reference (`&`).
A deep dive into a soundness vulnerability within the popular `lru` Rust crate, where an incorrect implementation of `IterMut` violated the Stacked Borrows memory model. This report explores how creating exclusive references to keys inside an unsafe block creates Undefined Behavior (UB), effectively breaking the guarantees that make Rust safe.
Rust is sold on a promise: memory safety without garbage collection. It’s the golden child of modern systems programming, effectively killing entire classes of bugs like buffer overflows and use-after-frees—at least, that's the brochure pitch. But there's a trap door in the floor of this ivory tower labeled unsafe. Inside that block, the compiler shuts its eyes, the borrow checker goes on a smoke break, and you are essentially writing C++ with different syntax. If you mess up here, you don't just get a crash; you get Undefined Behavior (UB), the nemesis of deterministic computing.
Enter the lru crate. It's a widely used implementation of a Least Recently Used cache, a data structure that discards the oldest items when it hits capacity. It relies on a HashMap for fast lookups and a doubly-linked list to track usage order. Because Rust's ownership model hates self-referential structures (like a map pointing to nodes that point to each other), implementing an LRU cache efficiently in Rust almost always requires dropping into unsafe to manage raw pointers manually. And that is exactly where GHSA-RHFX-M35P-FF5J lives.
This isn't a simple buffer overflow where you can just overwrite a return address. This is a subtle violation of the "Stacked Borrows" model—the formal rules Rust uses to decide which pointer is allowed to touch which piece of memory and when. The developers of lru inadvertently wrote code that lied to the compiler about exclusivity. They claimed a pointer was unique (&mut) when it wasn't, effectively poisoning the memory model and rendering any subsequent access to that memory technically illegal. It’s the digital equivalent of changing the locks on your house while your family is still inside, then wondering why their keys stopped working.
To understand this bug, you have to understand the specific flavor of brain damage involved in aliasing mutable pointers. In Rust, &mut T means "I have the only active reference to this data." This isn't just a suggestion; it's a hard rule used by the compiler for optimization. If you have a &mut, the compiler assumes no other pointers to that data are being used. The "Stacked Borrows" model tracks this: when you create a reference, it's pushed onto a virtual stack. When you use a reference, it must be near the top. If you use a reference lower down, everything above it is popped off and invalidated.
The vulnerability lies in the IterMut implementation. This iterator is supposed to let you loop through the cache and modify the values. However, the internal node structure contains both a key and a value. To iterate, the code walks the linked list of nodes. Inside the next() method of the iterator, the code needs to grab references to the key and the value to return them to the user. Here is where the logic failed: the developer created a mutable reference to the key as well as the value.
Wait, why is that bad? A HashMap (which backs the LRU) relies on the keys remaining stable. If you could mutate the key, you would change its hash, and the map would lose track of the item. But even if you don't actually write to it, the mere act of creating a &mut Key tells the compiler, "I am asserting exclusive control over this key right now." Since the HashMap and the linked list structure also hold pointers to that key, creating this exclusive reference invalidates those internal pointers. The moment that unsafe block executes, the pointers holding the cache together are technically "popped" from the borrow stack. The cache is now structurally unsound in the eyes of the abstract machine.
Let's look at the diff. The issue was located in the iterator logic where it reconstructs references from raw pointers. The developer needed to turn a raw pointer *mut Node into a reference. The original code was too aggressive.
The Vulnerable Code:
// Inside IterMut::next or similar
let key = unsafe { &mut (*(*self.ptr).key.as_mut_ptr()) as &mut K };
let val = unsafe { &mut (*(*self.ptr).val.as_mut_ptr()) as &mut V };
return Some((key, val));See that &mut K? That is the lie. The iterator yields (key, val). While yielding a mutable value (val) is the whole point of IterMut, yielding a mutable key is nonsensical for an LRU cache and dangerous for the memory model.
The Fix (PR #224):
// The corrected implementation
let key = unsafe { &(*(*self.ptr).key.as_ptr()) as &K }; // Changed to &K (shared)
let val = unsafe { &mut (*(*self.ptr).val.as_mut_ptr()) as &mut V };
return Some((key, val));The fix is subtle but profound. By changing &mut K to &K, the developer downgrades the claim from "exclusive access" to "shared access." Shared references can coexist with the internal pointers the HashMap uses. This appeases the Stacked Borrows gods, ensuring that the existing pointers in the data structure remain valid on the stack. It’s a one-character change (&mut -> &) that marks the difference between correct code and Undefined Behavior.
Exploiting this is not like overflowing a buffer in C where you overwrite EIP and jump to shellcode. In Rust, "exploitation" of soundness bugs often means tricking the compiler into optimizing away checks it thinks are redundant because "Undefined Behavior cannot happen." If the compiler sees you create a unique &mut to a key, it might cache that value in a register and assume memory reads from the HashMap structure (which uses a different pointer) are unrelated. If those pointers actually alias, you get memory corruption.
To demonstrate this, we don't use Metasploit; we use Miri. Miri is an interpreter for Rust's Mid-level Intermediate Representation (MIR) that checks for undefined behavior. A simple regression test acts as our Proof of Concept:
#[test]
fn iter_mut_stacked_borrows_violation() {
let mut cache: LruCache<i32, i32> = LruCache::new(NonZeroUsize::new(3).unwrap());
cache.put(1, 10);
cache.put(2, 20);
// The act of iterating mutably triggers the UB
for (_k, v) in cache.iter_mut() {
*v *= 2;
}
// Accessing the cache afterwards is illegal if pointers were invalidated
assert_eq!(cache.get(&1), Some(&20));
}When you run this with cargo miri test, the tool screams. It reports that a pointer was used after its borrow stack entry was popped. In a real-world scenario, this could manifest as a segmentation fault, or worse, silent data corruption where the LRU cache loses track of items or returns the wrong values, potentially leading to logic bugs in authentication or session management layers relying on that cache.
You might look at this and say, "So what? It's theoretical." In the security world, theoretical flaws in memory safety are just exploits waiting for a better compiler optimizer. As Rust's compiler (rustc) gets smarter, it exploits undefined behavior more aggressively to speed up code. Code that "accidentally works" today might segfault tomorrow after a compiler update.
For lru, a crate downloaded millions of times, this is significant. It is used in web frameworks, database engines, and network services to manage sessions and resources. If the cache state becomes corrupted due to pointer invalidation, an attacker might be able to predict cache collisions, force evictions of security-critical data, or causing a Denial of Service via panic. While we haven't seen this weaponized into Remote Code Execution (RCE) yet, soundness holes in foundational libraries are cracks in the foundation of the entire application stack.
The severity is listed as "Low" essentially because it requires a specific interaction pattern (mutable iteration) and the consequences are currently mostly theoretical under standard compilation flags. However, for high-assurance software, any UB is a critical vulnerability. If you are writing code for a pacemaker or a banking ledger in Rust, you cannot afford to have your iterators lying about memory exclusivity.
Fortunately, the remediation is straightforward. The maintainers of lru were responsive, and the fix is available in version 0.16.3. The mitigation strategy is simple: update your dependencies.
cargo tree | grep lru to see what version you are effectively using.cargo update -p lru. This will pull the latest semver-compatible version (likely 0.16.3+).unsafe in your own code, start using Miri. cargo miri test should be part of your CI/CD pipeline if you touch raw pointers. It is the only reliable way to catch Stacked Borrows violations before they manifest as cryptic segfaults in production.There is no configuration workaround. The code logic itself was flawed. You cannot flag your way out of this; you must replace the binary code with the patched version that respects Rust's memory model.
Unknown| Product | Affected Versions | Fixed Version |
|---|---|---|
lru crates.io | >= 0.9.0, < 0.16.3 | 0.16.3 |
| Attribute | Detail |
|---|---|
| CWE | CWE-825 |
| Language | Rust |
| Root Cause | Stacked Borrows Violation |
| Attack Vector | Local (Library Usage) |
| Severity | Low (Soundness Issue) |
| CVSS | N/A |
| Exploit Maturity | PoC (Miri Test) |
Expired Pointer Dereference