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

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, passingNone, should printNoneand exit cleanly. Instead: segfault - July 9–10: compiler team isolates the root cause overnight: a 1.97.0 change to how
Optiondiscriminants 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.

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.

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:
| Toolchain | Result |
|---|---|
rustc 1.97.0 -O | Segmentation fault (exit 139) |
rustc 1.97.1 -O | None / Some(42) / exit-ok — all good |
rustc 1.96.1 -O | Fine (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.
Related Reading
- Rust 1.92.0: never-type lint upgrades, smarter Result, stabilized APIs - how to read a release properly
- My Rust 1.90 Upgrade Disaster: My Codebase Didn’t Survive - upgrading is risky, tests come first
- Rust 1.94: Sliding Window Pattern Matching Makes Code Elegant - feature deep dive
- Your 8,000-line Rust project, rust-analyzer chewed through 2.5M lines of dependencies - why your editor lags and how to fix it
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.
