September 21, 2018·6 min read·engineeringrustsecuritymemory-safety

str repeat overflow breaks an unsafe capacity assumption

An unchecked size multiplication can leave str::repeat with too little storage for its raw copies. The disclosed fix checks capacity before the unsafe implementation uses it.

Allocating storage before copying into it is safe only when the allocation's size agrees with the later writes. Rust's str::repeat has a defect in that agreement. A sufficiently large repetition count can overflow the arithmetic used to determine capacity, leaving the implementation's unsafe copying code with less storage than it assumes. Today's standard-library advisory discloses that this can cause an out-of-bounds write.

The full announcement by Alex Crichton identifies the affected stable releases and includes the proposed correction. Rust 1.29.1 is planned for September 25, while fixes have landed on the relevant branches. That is a planned stable release, not a version users can already assume they have installed today.

I take this seriously because a safe public interface depends on its implementation preserving the invariants that callers cannot inspect. Rust's type system gives safe callers substantial protection, but unsafe standard-library code must still establish that its raw operations are valid. Here a size calculation fails to supply that evidence.

Repetition needs the product to fit

The operation returns an owned string containing the original string repeated a chosen number of times. Its required byte length is the original byte length multiplied by the repetition count. That multiplication has to be representable before it can describe a valid allocation request.

The arithmetic distinction is easy to miss. A machine integer's multiplication result is not necessarily the mathematical product when the latter exceeds its range. If the implementation obtains a wrapped value and allocates that many elements, the allocator may satisfy exactly the request it received. The request itself can still be wrong for the intended operation.

This is why an allocator's ordinary size checks cannot substitute for checking the multiplication. The allocator sees the computed request. It does not know that the caller intended a larger mathematical value or that later code will write according to that intention. The capacity calculation and the copying algorithm need to agree before storage is requested.

The advisory classifies the issue as integer overflow leading to buffer overflow. Those are two steps in one failure. An arithmetic result first understates the required capacity. Unsafe writes then rely on the assumption that the original intended capacity exists. Correcting only how the program reports an eventual allocation failure would miss that sequence.

The string implementation delegates to byte repetition

The Rust 1.29.0 string source shows that str::repeat invokes repetition on the string's bytes. It then constructs the resulting string with an unchecked UTF-8 conversion. Repeating valid UTF-8 bytes preserves their encoding, but that property does not prove the byte buffer was constructed within its allocation.

The distinction matters because there are several invariants in the implementation. Valid text encoding and valid memory access are different requirements. Satisfying the first cannot repair a failure in the second. A reader following only the final string conversion would miss the capacity assumption beneath it.

The slice implementation performs the relevant work. It requests a vector with capacity computed as self.len() * n. It copies an initial instance and then repeatedly doubles the contents using raw non-overlapping copies. A remaining partial section completes the requested repetition count.

That algorithm avoids copying the original input one repetition at a time. It can reuse the already copied prefix as the source for larger copies. The efficiency depends on knowing that the destination has enough capacity for the whole result. The raw copying operations do not grow the vector automatically as a safe extending operation would.

The implementation also updates the vector's length through set_len. That operation asserts that the corresponding elements are initialized and valid within the allocated storage. It does not itself create the storage. The arithmetic establishing capacity therefore supports both the raw copy and the length update.

The source handles a zero repetition count by returning an empty vector before allocating. That case does not exercise the doubling loop. For a positive count, the loop builds a power-of-two number of repetitions and then copies any remainder from the existing prefix. Both stages depend on enough destination capacity, even though they use different copy lengths.

This structure shows why a correction at the initial size calculation is consequential. Checking there establishes a shared prerequisite before either copying stage begins. A check added only to the final remainder would leave the earlier doubling operations dependent on an unproved allocation size. The safety argument has to cover every raw write, including writes made before the algorithm reaches its last step.

The correction checks before allocating

The patch included in the full advisory changes the capacity expression to use checked multiplication. Its relevant expression is small.

self.len().checked_mul(n).expect("capacity overflow")

If the product is representable, the expression supplies it to the allocation request. If it is not, the operation panics deterministically before the unsafe copying path proceeds on an invalid capacity assumption. The correction does not attempt to produce the impossible result or silently reduce the repetition count.

This use of a panic has a specific purpose. The implementation cannot satisfy an output length that its index type cannot represent. Continuing after overflow would make later memory operations invalid. The patch turns that situation into a defined failure at the arithmetic boundary.

The distinction is relevant to application behavior too. A checked product can establish representability without establishing that a request is sensible for an application. A huge but representable request can still demand too much memory. The security correction preserves the library's memory invariant, while an application can separately limit the amount of work it is willing to perform.

Those limits answer different questions. The library must never write outside its allocation for any valid call through the safe interface. A service accepting a repetition count may also need a much smaller policy limit for its own resource budget. The latter is not a substitute for fixing the library, and the former does not choose the service's acceptable workload.

The affected interval starts after the API's introduction

The full announcement says str::repeat has existed since Rust 1.16.0, but this vulnerability entered through a later implementation change. That change merged on March 6 and first appeared in stable Rust 1.26.0 on May 10. The distinction prevents the API's entire history from being treated as affected without evidence.

The advisory lists the stable releases from 1.26.0 through 1.29.0 that contain the vulnerable implementation. It also describes affected nightly and beta builds from the introduction date. The relevant property is the included implementation, not simply whether a compiler recognizes the method name.

This is another reason to read a security announcement beyond its headline. A method can retain its public signature while an internal optimization changes its safety obligations. Source code using the method need not gain an unsafe block to become exposed to a defect in the library's existing unsafe implementation.

The announcement credits Scott McMurray with reporting the issue. Its timeline records the report on September 16, confirmation the following day, and preparation of the patch before today's disclosure. These facts establish the report's sequence without requiring any account of later releases or consequences.

A safe API still needs a sound implementation

The advisory says code not using str::repeat is unaffected by this vulnerability. For code that does use it, the relevant analysis follows the call and the repetition count into the affected implementation. A general concern about strings does not establish exposure in a program that never performs the operation.

I consider the source-level explanation more useful than treating this as an abstract argument about safe languages. The specific failed invariant is visible. Capacity is calculated with unchecked multiplication, and raw copies assume that calculation describes enough storage. The patch supplies a check where the evidence was missing.

The stable response remains scheduled as of today. The advisory also describes checking capacity at affected call sites as a manual mitigation and says the team will not issue its own patches for older stable versions. Its included patch is intended to help maintainers who need to adapt a correction to an older implementation. Its central requirement is unchanged across those arrangements, the byte-length product must be checked before unsafe copying relies on it.