February 27, 2020·5 min read·engineeringrustcompilersafetylifetimes

Rust 1.41.1 restores lifetime checks across compiler representations

The point release repairs a static-item regression and projected Copy checks. A separate LLVM revert restores bounds checks lost during optimization.

A lifetime rule provides protection only if every compiler representation preserves and checks it. Rust 1.41.1 repairs a case where an internal change to static values caused the borrow checker to stop enforcing their declared types. The compiler could then accept a reference to a local temporary where a static lifetime was required. The source still contained a lifetime annotation, but the relevant check had disappeared.

The release announcement identifies two critical regressions from Rust 1.41.0 and recommends updating promptly. It also distinguishes a lifetime-checking defect involving Copy that dates back to Rust 1.0. The release therefore combines regression repair with a correction to older behavior. These problems do not all share the same introduction date.

I am drawn to Rust's explicit account of ownership and lifetime constraints. Compiler repairs like these deserve a technical explanation because they restore the connection between those source-level rules and the program the tools accept. They do not add a new obligation for a programmer to guess which lifetime checks happen to run.

Static storage does not extend a local borrow

The announcement demonstrates the static-item regression with a mutable static holding a reference. Its declared reference requires the referent to remain valid for a static lifetime. A local variable in main cannot meet that requirement merely because code stores its address into static storage.

This intentionally invalid example should fail to compile on the corrected release.

static mut SAVED: &'static u8 = &0;

fn main() {
    let temporary = 42;
    unsafe {
        SAVED = &temporary;
    }
}

The unsafe block permits access to the mutable static. It does not make the reference to temporary live longer. Rust 1.41.0 erroneously accepted the assignment. Rust 1.41.1 reports that the local does not live long enough and ties the error to the static lifetime required by the assignment.

That diagnostic expresses the actual conflict. Static storage survives independently of the stack local, while the reference promises that its target remains valid. The correct response is to reject the inconsistent promise. Moving the assignment into an unsafe block cannot supply the missing lifetime.

The repair in pull request 69145 adds a check for static pointers in the MIR borrow-checking type verifier. It obtains the static item's declared type, normalizes it, and equates it with the type represented by the constant's dereferenced literal. This reinstates the relationship between the declaration and the internal use.

Regression tests cover both explicitly written static reference lifetimes and elided ones. Another test uses a static function object whose argument must meet a static lifetime. The coverage addresses the type-checking boundary, beyond only the particular mutable assignment used to introduce the bug.

Copy has lifetime conditions too

The same pull request fixes a second omission in checking copies from projections. A projection accesses part of a value, such as a tuple field. The compiler had checked the relevant Copy obligation for an unprojected place but skipped that proof for projected places.

The release illustrates this with a wrapper that implements Clone generally and Copy only when its contained reference has a static lifetime. Creating the wrapper around a local reference does not make that particular wrapper copyable. Accessing it through a tuple field must not erase the lifetime condition attached to the implementation.

The old accepted example drops the same tuple field twice. If the field were genuinely Copy, each access could supply a copy. But the local referent cannot satisfy the static bound required for that implementation. The corrected compiler rejects the program with a lifetime error explaining that copying the value requires the local borrow to last for a static lifetime.

The patch moves the proof until after projection processing. It builds the Copy trait obligation for the type actually selected by the projection. Its comments distinguish proving the full trait requirement from a helper that ignores region obligations. A preliminary decision that a type might be copyable cannot replace the proof that its lifetime bounds hold here.

This is why simply checking the field's apparent shape is insufficient. A reference wrapper may have representation suitable for copying while its trait implementation remains conditional. The operation needs the implementation's full conditions, including lifetimes, at the projected use.

I find this case particularly useful for reading compiler diagnostics. A Copy error can reflect a lifetime restriction in an implementation, rather than a general inability to duplicate the underlying bits. The condition belongs to the trait contract the operation relies on.

The patch includes a compile-fail test for the tuple-field case and records the expected diagnostic. That is an appropriate test because the intended result is rejection, not a particular runtime output. A program that should never have been accepted cannot be validated by running it and observing one apparently successful execution. The test establishes that the compiler carries the lifetime obligation through the projected access before code generation begins. It also gives a concrete example of the guarantee the repair restores.

Optimization must preserve the required check

The other Rust 1.41.0 regression happened later in compilation. The release announcement describes bounds checks omitted from allocation-related code in some cases. Out-of-bounds inputs could then produce segmentation faults. The source-level presence of safe operations does not protect execution if an optimizer removes a required check incorrectly.

The reported root cause was an LLVM optimization change introduced in LLVM 9. Rust 1.41.0 used a snapshot of that version. The fix brings in a revert of the offending optimization. This is distinct from borrow checking, because the problem concerns transformation of already accepted code.

The two repair strategies match their respective failures. Static and projected-copy checks need the compiler to prove omitted obligations. The miscompilation needs optimization to retain program semantics. Treating all of them as stricter syntax checking would miss the generated-code defect.

Rust 1.41.1 restores those implementation boundaries within the existing release line. It rejects the short borrow stored behind a static promise and checks the lifetime conditions on a projected copy. Its LLVM correction separately prevents the reported allocation paths from losing required bounds checks during optimization.