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.