flawopen.com/Incidents/Rust unsafe & smallvec
A warehouse is famously safe because a robot double-checks every shelf before anything is placed on it. For a few specific tasks the robot is switched off and a human works by hand, promising to be careful. One of those humans asked a delivery driver "roughly how many boxes are you bringing?", built a shelf that size, and started stacking. The driver's estimate was a guess, not a guarantee — and when more boxes arrived than promised, the stack ran off the end of the shelf.
In January 2021, researchers at Georgia Tech's SSLab reported a heap buffer overflow in smallvec, a widely used Rust crate providing a vector optimised to store small numbers of elements inline. The flaw was assigned RUSTSEC-2021-0003 and CVE-2021-25900, rated CVSS 9.8, and fixed in versions 0.6.14 and 1.6.1.
The method SmallVec::insert_many allocated a buffer based on an iterator's reported size and then wrote the iterator's items into it. If the iterator produced more items than its size_hint lower bound had claimed, the writes continued past the end of the allocation — corrupting the heap.
This is worth studying precisely because Rust's central promise is that this cannot happen. Understanding why it happened anyway is the most useful thing a Rust developer can learn about unsafe.
The Iterator trait documents that size_hint's lower bound should be accurate. But size_hint is not an unsafe trait method — meaning anyone can write a perfectly ordinary, compiler-approved implementation that returns a wrong number. It is a hint, useful for optimisation. The overflow occurred because unsafe code treated an optimisation hint as a memory-safety guarantee.
The crucial consequence: the bug was reachable entirely from safe Rust. A caller writing no unsafe code at all, passing a custom iterator with an understated hint, could corrupt memory. That makes the API unsound and the defect the library's, not the user's.
// Allocate based on a *hint*... let (lower, _) = iter.size_hint(); self.reserve(lower); unsafe { let ptr = self.as_mut_ptr().add(index); for (i, item) in iter.enumerate() { // ...but write however many // items actually arrive. ptr::write(ptr.add(i), item); } }
// Re-check capacity for every item. // Never let a value from safe code // determine a bound used by unsafe. for (i, item) in iter.enumerate() { self.reserve(1); unsafe { let ptr = self.as_mut_ptr() .add(index + i); ptr::write(ptr, item); } } // The real fix also shrank the // unsafe surface substantially.
Illustrative reconstruction of the pattern, not the crate's literal source. The upstream fix reserved additional space per inserted item and simplified the implementation specifically so its correctness would be easier to verify.
Rust's memory-safety guarantee is real but conditional, and the condition is usually stated imprecisely. The accurate formulation is: safe Rust cannot cause undefined behaviour, provided every unsafe block it depends on is sound.
That shifts where the risk lives rather than eliminating it. In C, memory-safety bugs can appear anywhere in millions of lines. In Rust, they can only originate inside unsafe blocks — typically a very small fraction of a codebase. This is an enormous practical improvement: it makes the auditable surface small enough to actually audit. But "small" is not "zero", and a soundness bug inside that surface is exploitable exactly like its C equivalent, as the CVSS 9.8 here reflects.
There is a second, subtler point. unsafe is not locally contained. An unsound unsafe block poisons every safe API built on top of it — which is why this bug affected callers who never wrote the keyword.
// SAFETY: comment. Stating exactly what must hold and why it does is what makes later review possible. Unexplained unsafe is unreviewable unsafe.cargo audit checks against the RustSec database. Most unsafe code in a typical project lives in crates someone else wrote.No — it means they are conditional in a way worth understanding precisely. Rust confines memory-safety defects to a small, explicitly marked, auditable portion of a codebase, which is a substantial structural improvement over a language where any line can contain one. The claim that survives scrutiny is "dramatically smaller attack surface", not "no attack surface".
Yes — that is what unsafe trait is for. Implementing one is an explicit promise that carries safety obligations, which lets unsafe code rely on it. The standard library's TrustedLen exists for exactly this iterator-length problem, though it remains unstable.
cargo geiger reports unsafe usage across a dependency tree, and cargo audit flags known advisories. Neither proves soundness, but together they show you where the risk is concentrated.