flawopen.com/Incidents/Rust unsafe & smallvec

When Rust is not memory safe: unsafe blocks and the smallvec overflow

Critical — CVSS 9.8 CWE-787: Out-of-bounds Write RUSTSEC-2021-0003 · CVE-2021-25900
ELI5

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.

Key terms on this page
unsafe
A Rust keyword marking code where the compiler's memory-safety checks are suspended and the programmer takes responsibility for upholding them manually.
soundness
The property that no combination of safe code can trigger undefined behaviour. An API is unsound if any safe usage of it can corrupt memory — that makes it a bug in the library, not in the caller.
size_hint
An Iterator method returning an estimate of how many items remain. Its lower bound is documented as correct — but it is a safe method, so an incorrect implementation is allowed to exist.

What happened

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 technical root cause

Unsafe code trusted a promise that safe code is permitted to break

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.

THE UNSOUND PATTERN
// 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);
    }
}
SOUND
// 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.

What this says about Rust's guarantee

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.

The rules for writing unsafe correctly

FAQ

Does this mean Rust's safety claims are overstated?

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".

Is there a way to make a trait trustworthy for unsafe code?

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.

How do I find unsafe code in my dependencies?

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.

Related reading

Sources