egraph stack-overflow on simple generic slice function
Filed against: bytecodealliance/wasmtime (cranelift-codegen
0.132)
Mighty version: v0.9.0 (commit 06b6efe).
Summary
cranelift-codegen 0.132's egraph optimization pass infinite-
recurses on a single 88-byte Mighty input that lowers to perfectly
ordinary CLIF — a generic-over-T, &[T] → Option<&T> helper.
The recursion blows the C stack inside libFuzzer's instrumented
binary in well under a second. The recursion lives entirely inside
cranelift_codegen::opts::generated_code::constructor_simplify
(the ISLE-generated egraph entry point); nothing in the calling
backend (mty-codegen-cranelift → cranelift_module →
Module::define_function) is on the cycle.
Workaround: set opt_level = "none" in
cranelift_codegen::settings::Flags — disables the egraph pass at
the cost of optimization quality.
Reproducer
Mighty source
The full input is 88 bytes:
fn first[T](xs: &[T]) -> Option[&T] {
if xs.len == 0 { None } else { Some(&xs[0]) }
}
The bug is triggered by the front-end accepting this program and
the IR lowerer emitting CLIF for the generic. We have not been
able to reproduce by hand-written CLIF — the issue depends on the
specific operand sequence the IR lowerer emits for a generic-over-
T slice indexing + Option-wrapping pattern.
Cranelift configuration
let mut b = cranelift_codegen::settings::builder();
b.set("opt_level", "speed").unwrap();
b.set("is_pic", "false").unwrap();
let flags = cranelift_codegen::settings::Flags::new(b);
(default mty-codegen-cranelift flags; see
crates/mty-codegen-cranelift/src/lower.rs::default_flags.)
Driver invocation
We trigger the crash via:
mty_codegen_cranelift::object::compile_object(&prog)
.expect("compile-object"); // → SIGSEGV stack-overflow inside Cranelift
prog is the result of parse → lower → typeck → ir-lower of the
Mighty source above. We have not yet been able to extract a
self-contained CLIF reproducer because the IR lowerer is the only
producer of this CLIF shape — we'll attach a CLIF dump if you'd
like one (it requires us to add a debug-print pass).
Stack trace (libFuzzer / ASAN, Windows MSVC)
Truncated to the recursive frames:
cranelift_codegen::opts::generated_code::constructor_simplify
cranelift_codegen::opts::generated_code::optimize_pure_enode
cranelift_codegen::opts::generated_code::make_inst_ctor
cranelift_codegen::opts::generated_code::constructor_icmp
cranelift_codegen::opts::generated_code::constructor_simplify ← cycle
cranelift_codegen::opts::generated_code::optimize_pure_enode
cranelift_codegen::opts::generated_code::make_inst_ctor
cranelift_codegen::opts::generated_code::constructor_icmp
cranelift_codegen::opts::generated_code::constructor_simplify ← cycle
...
[~5000 frames before the OS kills the thread]
ASAN tags the crash as stack-overflow (not a heap OOB).
Reproduces identically on:
- Windows 11 + nightly Rust
1.98.0-nightly (23a3312d9 2026-05-23)
- cargo-fuzz 0.13.1 + libFuzzer.
- (Not yet verified on Linux — the libFuzzer binary build path on
Windows MSVC is the one that's instrumented; happy to repro on
Linux if requested.)
Suspected root cause
The recursion bounces between constructor_simplify and
constructor_icmp via optimize_pure_enode →
make_inst_ctor. That suggests an ISLE rewrite rule pair that
rewrites icmp ↔ something-that-resimplifies-to-icmp without
adding any new e-node, so the optimizer's saturation check doesn't
fire. The CLIF that triggers it is plausibly an icmp chain emitted
for the if xs.len == 0 test plus the bounds check inside xs[0]
— specifically, a redundant comparison the optimizer keeps trying
to rewrite.
(Speculation — we have not bisected the ISLE rules. Happy to bisect
on request if you give us the right knob to enable rule-by-rule
emission.)
Environment
| Field |
Value |
cranelift-codegen |
0.132.0 (workspace pin in |
Cargo.toml) |
|
| Rust toolchain |
1.98.0-nightly (23a3312d9 2026-05-23) |
| Host triple |
x86_64-pc-windows-msvc |
| Optimizer level |
speed (the default; "none" is a workaround) |
| Reproducer |
crates/mty-codegen-cranelift/fuzz/artifacts/codegen_fuzz/crash-eb52420944e0ab2856e40ae22f6d6587e218a5da |
Workaround we shipped
crates/mty-codegen-cranelift/src/lower.rs::default_flags documents
the opt_level = "none" escape hatch. The Mighty CLI exposes it
via a future --no-opt flag (v0.10 follow-up); meanwhile, programs
matching the generic-slice shape can be compiled by editing
default_flags to set opt_level = "none".
We have NOT applied the workaround by default — the perf cost is
real, and the bug only affects one synthetic shape we have
identified so far. The escape hatch lives in code comments for
operators who hit a repro in the wild.
What we'd like upstream to look at
- The cycle —
constructor_simplify ↔ constructor_icmp via
optimize_pure_enode / make_inst_ctor. Adding a depth limit
(or a more thorough saturation check) would close the crash
surface even before the underlying rule pair is identified.
- The triggering CLIF shape — we can produce a CLIF dump if
the original Mighty input isn't useful. The pattern is generic-
over-T + &[T] indexing + Option wrapping; we suspect any
front-end that emits a similar icmp chain would hit it.
- Whether 0.133+ has fixed it — we'd like to pin a newer
cranelift once a fix lands.
Cross-references
- Bug-bash notes:
FUZZ_V0_9_NOTES.md (Bug 3) in the Mighty repo
(hassard0/Mighty, commit 06b6efe).
- Cleanup follow-up:
CLEANUP_V0_10_NOTES.md.
Thanks for cranelift! Happy to provide additional repros, a CLIF
dump, or test against a candidate fix on request.
egraph stack-overflow on simple generic slice function
Filed against:
bytecodealliance/wasmtime(cranelift-codegen0.132)
Mighty version: v0.9.0 (commit
06b6efe).Summary
cranelift-codegen 0.132's egraph optimization pass infinite-recurses on a single 88-byte Mighty input that lowers to perfectly
ordinary CLIF — a generic-over-
T,&[T]→Option<&T>helper.The recursion blows the C stack inside libFuzzer's instrumented
binary in well under a second. The recursion lives entirely inside
cranelift_codegen::opts::generated_code::constructor_simplify(the ISLE-generated egraph entry point); nothing in the calling
backend (
mty-codegen-cranelift→cranelift_module→Module::define_function) is on the cycle.Workaround: set
opt_level = "none"incranelift_codegen::settings::Flags— disables the egraph pass atthe cost of optimization quality.
Reproducer
Mighty source
The full input is 88 bytes:
The bug is triggered by the front-end accepting this program and
the IR lowerer emitting CLIF for the generic. We have not been
able to reproduce by hand-written CLIF — the issue depends on the
specific operand sequence the IR lowerer emits for a generic-over-
Tslice indexing + Option-wrapping pattern.Cranelift configuration
(default
mty-codegen-craneliftflags; seecrates/mty-codegen-cranelift/src/lower.rs::default_flags.)Driver invocation
We trigger the crash via:
progis the result ofparse → lower → typeck → ir-lowerof theMighty source above. We have not yet been able to extract a
self-contained CLIF reproducer because the IR lowerer is the only
producer of this CLIF shape — we'll attach a CLIF dump if you'd
like one (it requires us to add a debug-print pass).
Stack trace (libFuzzer / ASAN, Windows MSVC)
Truncated to the recursive frames:
ASAN tags the crash as
stack-overflow(not a heap OOB).Reproduces identically on:
1.98.0-nightly (23a3312d9 2026-05-23)Windows MSVC is the one that's instrumented; happy to repro on
Linux if requested.)
Suspected root cause
The recursion bounces between
constructor_simplifyandconstructor_icmpviaoptimize_pure_enode→make_inst_ctor. That suggests an ISLE rewrite rule pair thatrewrites
icmp↔ something-that-resimplifies-to-icmpwithoutadding any new e-node, so the optimizer's saturation check doesn't
fire. The CLIF that triggers it is plausibly an icmp chain emitted
for the
if xs.len == 0test plus the bounds check insidexs[0]— specifically, a redundant comparison the optimizer keeps trying
to rewrite.
(Speculation — we have not bisected the ISLE rules. Happy to bisect
on request if you give us the right knob to enable rule-by-rule
emission.)
Environment
cranelift-codegen0.132.0(workspace pin inCargo.toml)1.98.0-nightly (23a3312d9 2026-05-23)x86_64-pc-windows-msvcspeed(the default;"none"is a workaround)crates/mty-codegen-cranelift/fuzz/artifacts/codegen_fuzz/crash-eb52420944e0ab2856e40ae22f6d6587e218a5daWorkaround we shipped
crates/mty-codegen-cranelift/src/lower.rs::default_flagsdocumentsthe
opt_level = "none"escape hatch. The Mighty CLI exposes itvia a future
--no-optflag (v0.10 follow-up); meanwhile, programsmatching the generic-slice shape can be compiled by editing
default_flagsto setopt_level = "none".We have NOT applied the workaround by default — the perf cost is
real, and the bug only affects one synthetic shape we have
identified so far. The escape hatch lives in code comments for
operators who hit a repro in the wild.
What we'd like upstream to look at
constructor_simplify↔constructor_icmpviaoptimize_pure_enode/make_inst_ctor. Adding a depth limit(or a more thorough saturation check) would close the crash
surface even before the underlying rule pair is identified.
the original Mighty input isn't useful. The pattern is generic-
over-T +
&[T]indexing + Option wrapping; we suspect anyfront-end that emits a similar icmp chain would hit it.
cranelift once a fix lands.
Cross-references
FUZZ_V0_9_NOTES.md(Bug 3) in the Mighty repo(
hassard0/Mighty, commit06b6efe).CLEANUP_V0_10_NOTES.md.Thanks for cranelift! Happy to provide additional repros, a CLIF
dump, or test against a candidate fix on request.