梦兽编程
AI_SUITE

Rust 1.97.1 Emergency Fix: A Compiler Miscompilation Has Been Silently Crashing Programs Since 1.87

Rust 1.97.1 ships an emergency fix for a long-dormant LLVM miscompilation: 1.97.0 changed Option::None's stored tag from 2 to -1, which triggered a trunc nuw mis-optimization on x86/x64, crashing optimized builds with segfaults. Reproduced on Windows, upgrade guide inside

Rust 1.97.1 Emergency Fix: A Compiler Miscompilation Has Been Silently Crashing Programs Since 1.87

Your program suddenly segfaults? Don’t blame your code just yet

Quick question: after your last Rust upgrade, have you ever hit a mystery segfault in -O optimized builds, while the debug build runs perfectly fine?

If so, there’s a good chance it was not your code — it was the compiler itself.

On July 16, 2026, the Rust team shipped an emergency release, 1.97.1, fixing a long-dormant LLVM miscompilation: the optimizer was generating wrong machine code, causing perfectly correct Rust programs to crash on specific code paths. Officially, the underlying problem has existed since Rust 1.87.

Upgrade first, as usual:

rustup update stable

quote-card

Timeline: from report to emergency fix in 7 days

  • July 9: Rust 1.97.0 released
  • July 9, same day: issue #159035 filed — “segfault on Rust 1.97.0”. A tiny program with a nested enum match, passing None, should print None and exit cleanly. Instead: segfault
  • July 9–10: compiler team isolates the root cause overnight: a 1.97.0 change to how Option discriminants are stored collided with an LLVM x86 backend optimization bug
  • July 10: rustc reverts the triggering PR and files an upstream LLVM fix
  • July 16: Rust 1.97.1 emergency release — both an LLVM fix backport and a rustc-side revert

One week from first report to fixed release.

Miscompilation timeline: lurking since 1.87, triggered in 1.97.0, fixed in 1.97.1

Root cause: how an “optimization” became a ticking bomb

Two layers, both required.

Layer 1: 1.97.0 changed how Option::None is stored

Option<T> doesn’t literally store “is there a value” — it has a tag field distinguishing Some from None. To save memory, the compiler uses clever tricks: Option<bool> can be a single byte with 0/1.

PR #155850 in 1.97.0 adjusted the stored encoding for these discriminants: None’s tag changed from 2 to -1 (bool-like enums such as Option<bool> went from 0/1 to 0/-1).

That change alone is fine — the compiler remaps the stored tag back to the logical discriminant (0 or 1) in IR. Like switching internal encodings: observable behavior unchanged.

Layer 2: LLVM x86 backend’s trunc nuw mis-optimization

The problem was in an LLVM optimization pass. The generated IR contained:

%1 = trunc nuw i64 %0 to i1

trunc nuw means “truncate with no unsigned wrap”. If any of the truncated bits of %0 are non-zero, the nuw guarantee is violated and the result should be poison — undefined behavior downstream.

But %0 here is the tag: -1 (i.e. 0xFFFFFFFFFFFFFFFF). Truncating to i1 drops almost everything, breaking the nuw contract — and LLVM’s x86 backend mis-optimized exactly this case.

In the LLVM 19 era, this compiled to and ecx, 1 (safe, correct). After LLVM 20 it became mov ecx, ecx — an apparently harmless instruction that is actually fatal. When the tag is -1, subsequent address computation goes completely wrong, the program reads garbage memory, and segfault follows.

LLVM miscompilation root cause: trunc nuw produces poison when tag=-1

Scope: x86/x64 only

Key facts:

  • x86/x64 only. Maintainers verified aarch64 and riscv64gc are unaffected — this is specific to the LLVM x86 backend
  • The underlying bug has existed since Rust 1.87 (LLVM 20); 1.97.0’s IR change just made it much more likely to trigger
  • Trigger shape is picky: enum with multiple payload fields + match + optimized build (-O). Not every program hits it — but when you do, it’s a hard crash

Reproduction: 1.97.0 crashes, 1.97.1 works

Rather than trust the discussion, I ran the minimal reproducer from the issue on Windows x64 / MSVC:

use std::hint::black_box;

#[repr(u16)]
enum Checksum {
    X(bool, u64),
    Y(u64, u64),
}

#[inline(never)]
fn run(c: Option<Checksum>) -> Option<u64> {
    match c {
        Some(x) => Some(match x {
            Checksum::X(false, s) => s,
            Checksum::X(true, s) => s,
            Checksum::Y(_, s) => s,
        }),
        None => None,
    }
}

fn main() {
    println!("{:?}", run(black_box(None)));
    println!("{:?}", run(black_box(Some(Checksum::Y(0, 42)))));
    println!("exit-ok");
}

Results:

ToolchainResult
rustc 1.97.0 -OSegmentation fault (exit 139)
rustc 1.97.1 -ONone / Some(42) / exit-ok — all good
rustc 1.96.1 -OFine (not reproduced on Windows; the issue reporter also saw it crash on Linux with 1.96.1)

Note the 1.96.1 row: the underlying issue has lurked since 1.87, but 1.97.0 was the main trigger. Different platforms and code shapes reproduce differently — which is exactly why miscompilations are terrifying: they don’t reproduce reliably.

How to check whether you’re affected

Three steps:

1. Check your version

rustc --version

Still on 1.97.0? Don’t hesitate — upgrade.

2. Upgrade

rustup update stable

3. Run your tests

If your project ever showed the “crashes only in optimized builds” mystery, run a release regression pass after upgrading:

cargo test --release

Especially if your code has the “multi-field enum + match, nested in Option/Result” shape — run it a few times.

Final thoughts

This incident is a counterintuitive reminder: Rust’s memory safety guarantees don’t cover compiler bugs. Rust ensures your code doesn’t go out of bounds or dangle — provided the compiler generates correct machine code. When an LLVM pass misbehaves, memory safety becomes “safe operations on wrong memory”.

Credit where due: the Rust team’s response was fast — root cause isolated and trigger reverted within 48 hours, fix released in a week. This is also why I recommend a cautious upgrade rhythm for production: watch the first point release after a major version before rolling it out broadly.

If your program ever crashed in release but worked in debug, tell us in the comments — it might have been this exact bug.


Found this useful? Give it a like so more people see it, and bookmark it for your next upgrade. If you know someone still on 1.97.0, share this with them — it might save them an all-nighter. Questions? The comments are open.

Frequently Asked Questions

Why does an optimized build randomly segfault in my program?

It may be a compiler bug: Rust 1.97.0 changed the storage tag for Option::None from 2 to -1, which collided with a misoptimization of trunc nuw in the LLVM x86 backend and generated incorrect machine code causing a segfault. The underlying issue had been latent since 1.87, but 1.97.0 greatly increased the trigger probability, and 1.97.1 was released as an emergency fix.

Which platforms and code shapes trigger the bug?

Only x86/x64 is affected; maintainers tested aarch64 and riscv64gc and found both normal. The trigger conditions are tricky: an enum with multiple payload fields plus a match plus an optimized build (-O). Not every program hits it, but when it does it is a hard crash, and the miscompilation is not guaranteed to reproduce reliably.

How can I confirm whether I have been affected?

First run rustc --version to check the version; if you are still on 1.97.0, run rustup update stable to upgrade directly to 1.97.1. If your project previously had a strange problem where it crashed only in optimized builds but not in debug builds, run cargo test --release as a regression test after upgrading.

Can Rust's memory safety guarantees prevent this kind of problem?

No. Rust can ensure that code does not go out of bounds or use dangling references, but only if the machine code generated by the compiler is correct; when an LLVM optimization pass goes wrong, memory safety becomes 'safe operations on incorrect memory.' The lesson is to wait about a week after a major release in production, and upgrading after the first point release lands is more stable.