Skip to content

Conversation

@cfallin
Copy link
Member

@cfallin cfallin commented Jan 13, 2026

We had a set of rules introduced in #11097 that attempted to optimize the case of testing the result of an icmp for a nonzero value. This allowed optimization of, for example, (((x == 0) == 0) == 0 ...) to a single level, either x == 0 or x != 0 depending on even/odd nesting depth.

Unfortunately this kind of recursion in the backend has a depth bounded only by the user input, hence creates a compiler DoS vulnerability: the wrong kind of compiler input can cause a stack overflow in Cranelift at compilation time. This case is reachable from Wasmtime's Wasm frontend via the i32.eqz operator (for example) as well.

Ideally, this kind of deep rewrite is best done in our mid-end optimizer, where we think carefully about bounds for recursive rewrites. The left-hand sides for the backend rules should really be fixed shapes that correspond to machine instructions, rather than ad-hoc peephole optimizations in their own right.

This fix thus simply removes the recursion case that causes the blowup. The patch includes two tests: one with optimizations disabled, showing correct compilation (without the fix, this case fails to compile with a stack overflow), and one with optimizations enabled, showing that the mid-end properly cleans up the nested expression and we get the expected one-level result anyway.

Note: this was reported as a security issue by @venkkatesh-sekar (thanks!); per our security policy, compilation DoS is not covered, so this is being fixed in a public PR. I will subsequently make a patch release to our currently supported versions (v36, v39, v40, 41).

We had a set of rules introduced in bytecodealliance#11097 that attempted to optimize
the case of testing the result of an `icmp` for a nonzero value. This
allowed optimization of, for example, `(((x == 0) == 0) == 0 ...)` to
a single level, either `x == 0` or `x != 0` depending on even/odd
nesting depth.

Unfortunately this kind of recursion in the backend has a depth
bounded only by the user input, hence creates a DoS vulnerability: the
wrong kind of compiler input can cause a stack overflow in Cranelift
at compilation time. This case is reachable from Wasmtime's Wasm
frontend via the `i32.eqz` operator (for example) as well.

Ideally, this kind of deep rewrite is best done in our mid-end
optimizer, where we think carefully about bounds for recursive
rewrites. The left-hand sides for the backend rules should really be
fixed shapes that correspond to machine instructions, rather than
ad-hoc peephole optimizations in their own right.

This fix thus simply removes the recursion case that causes the
blowup. The patch includes two tests: one with optimizations disabled,
showing correct compilation (without the fix, this case fails to
compile with a stack overflow), and one with optimizations enabled,
showing that the mid-end properly cleans up the nested expression and
we get the expected one-level result anyway.
@cfallin cfallin requested a review from a team as a code owner January 13, 2026 17:49
@cfallin cfallin requested review from alexcrichton and fitzgen and removed request for a team January 13, 2026 17:49
@cfallin cfallin enabled auto-merge January 13, 2026 17:51
@alexcrichton
Copy link
Member

I haven't dug deeply into the fallout here, but it looks like just removing these rules isn't enough to retain the codegen we have from before. A rough hunch is that there's a number of entrypoints for using these ISLE helpers and while the wasm test case here is one which is also cleaned up by the optimizer I think that these rules might be load-bearing for other codegen patterns unrelated to the optimizer.

It might need to be the case that some entrypoints use one rule, which includes these deleted rules, but then these rules don't recurse on themselves?

@alexcrichton
Copy link
Member

Although, from another angle, this seems fine as a thing to backport since it's clearly such low impact. I'm not aware of these codegen patterns being load bearing in terms of performance so a temporary minor regression while a "full fix" is developed for main I think would also be reasonable. In such a case though there's a number of codegen/etc tests to update to get CI passing.

@cfallin
Copy link
Member Author

cfallin commented Jan 13, 2026

This broke some existing lowerings; playing a bit more with various ways to break the cycle.

@cfallin
Copy link
Member Author

cfallin commented Jan 13, 2026

(On refresh, what Alex said -- somehow those comments didn't appear for me.)

@cfallin
Copy link
Member Author

cfallin commented Jan 13, 2026

OK, I've reworked things a bit to preserve exactly the same codegen on branches -- I effectively peeled one layer off of is_nonzero_cmp to continue to use with branches but the previous backedge from emit_cmp to is_nonzero_cmp now invokes is_nonzero which has only the base cases. Together with explicit LHS patterns to peel one level of uextend this leaves us with no test-output changes now.

@cfallin cfallin requested a review from a team as a code owner January 13, 2026 20:37
@cfallin
Copy link
Member Author

cfallin commented Jan 13, 2026

Or, well, spoke too soon -- no Cranelift filetest changes. A few disas tests that do FP compares do regress to materializing the bool.

@cfallin cfallin added this pull request to the merge queue Jan 13, 2026
@cfallin cfallin removed this pull request from the merge queue due to a manual request Jan 13, 2026
@github-actions github-actions bot added cranelift Issues related to the Cranelift code generator cranelift:area:x64 Issues related to x64 codegen labels Jan 13, 2026
@cfallin
Copy link
Member Author

cfallin commented Jan 13, 2026

OK, I've gotten full equivalence on the disas tests too -- this needed one new mid-end rule to see through a pattern (fcmp(...) != 0) that we were previously only ever simplifying with this peephole rule in the backend.

@fitzgen mind giving this another look, since it's a little heavier than before?

Re: backport, I could certainly backport the rule deletions only, but I'd be a little concerned about performance regressions in that case because this affects many branches. If we feel ok about these rewrites I think the full patch should be applicable back to v36 (LTS).

@alexcrichton
Copy link
Member

Also, logistically, @cfallin are you ok running point on backporting/point releases? And 24.0.0, while supported, is unaffected right?

@cfallin
Copy link
Member Author

cfallin commented Jan 13, 2026

Yes, I'm happy to do the backports too. (Sorry, very slow today taking scraps of time between meetings and other interrupts; can hopefully do backports by end of tomorrow at latest.)

@alexcrichton
Copy link
Member

Oh no worries! Just want to make sure there's explicitly an owner. Tomorrow is totally fine and I can be around to hit approvals and help babysit CI. We've had some CI changes recently like trusted publishing which raises the risk of broken CI on other branches, so something to watch out for.

This change works by splitting a rule so that the entry point used by
`brif` lowering can still peel off one layer of `icmp` and emit it
directly, without entering the unbounded structural recursion.

It also adds a mid-end rule to catch one case that we were previously
catching in the backend only: `fcmp(...) != 0`.
@cfallin
Copy link
Member Author

cfallin commented Jan 13, 2026

OK, this should be good to go -- if r+ here (on new changes) then I can put up backports momentarily.

@cfallin cfallin enabled auto-merge January 13, 2026 23:11
@cfallin cfallin added this pull request to the merge queue Jan 13, 2026
Merged via the queue into bytecodealliance:main with commit 55105fb Jan 13, 2026
58 checks passed
@cfallin cfallin deleted the icmp-recursion branch January 13, 2026 23:36
cfallin added a commit to cfallin/wasmtime that referenced this pull request Jan 13, 2026
…odealliance#12333)

* Cranelift: x64: fix user-controlled recursion in cmp emission.

We had a set of rules introduced in bytecodealliance#11097 that attempted to optimize
the case of testing the result of an `icmp` for a nonzero value. This
allowed optimization of, for example, `(((x == 0) == 0) == 0 ...)` to
a single level, either `x == 0` or `x != 0` depending on even/odd
nesting depth.

Unfortunately this kind of recursion in the backend has a depth
bounded only by the user input, hence creates a DoS vulnerability: the
wrong kind of compiler input can cause a stack overflow in Cranelift
at compilation time. This case is reachable from Wasmtime's Wasm
frontend via the `i32.eqz` operator (for example) as well.

Ideally, this kind of deep rewrite is best done in our mid-end
optimizer, where we think carefully about bounds for recursive
rewrites. The left-hand sides for the backend rules should really be
fixed shapes that correspond to machine instructions, rather than
ad-hoc peephole optimizations in their own right.

This fix thus simply removes the recursion case that causes the
blowup. The patch includes two tests: one with optimizations disabled,
showing correct compilation (without the fix, this case fails to
compile with a stack overflow), and one with optimizations enabled,
showing that the mid-end properly cleans up the nested expression and
we get the expected one-level result anyway.

* Preserve codegen on branches.

This change works by splitting a rule so that the entry point used by
`brif` lowering can still peel off one layer of `icmp` and emit it
directly, without entering the unbounded structural recursion.

It also adds a mid-end rule to catch one case that we were previously
catching in the backend only: `fcmp(...) != 0`.
cfallin added a commit to cfallin/wasmtime that referenced this pull request Jan 13, 2026
…odealliance#12333)

* Cranelift: x64: fix user-controlled recursion in cmp emission.

We had a set of rules introduced in bytecodealliance#11097 that attempted to optimize
the case of testing the result of an `icmp` for a nonzero value. This
allowed optimization of, for example, `(((x == 0) == 0) == 0 ...)` to
a single level, either `x == 0` or `x != 0` depending on even/odd
nesting depth.

Unfortunately this kind of recursion in the backend has a depth
bounded only by the user input, hence creates a DoS vulnerability: the
wrong kind of compiler input can cause a stack overflow in Cranelift
at compilation time. This case is reachable from Wasmtime's Wasm
frontend via the `i32.eqz` operator (for example) as well.

Ideally, this kind of deep rewrite is best done in our mid-end
optimizer, where we think carefully about bounds for recursive
rewrites. The left-hand sides for the backend rules should really be
fixed shapes that correspond to machine instructions, rather than
ad-hoc peephole optimizations in their own right.

This fix thus simply removes the recursion case that causes the
blowup. The patch includes two tests: one with optimizations disabled,
showing correct compilation (without the fix, this case fails to
compile with a stack overflow), and one with optimizations enabled,
showing that the mid-end properly cleans up the nested expression and
we get the expected one-level result anyway.

* Preserve codegen on branches.

This change works by splitting a rule so that the entry point used by
`brif` lowering can still peel off one layer of `icmp` and emit it
directly, without entering the unbounded structural recursion.

It also adds a mid-end rule to catch one case that we were previously
catching in the backend only: `fcmp(...) != 0`.
cfallin added a commit to cfallin/wasmtime that referenced this pull request Jan 13, 2026
…odealliance#12333)

* Cranelift: x64: fix user-controlled recursion in cmp emission.

We had a set of rules introduced in bytecodealliance#11097 that attempted to optimize
the case of testing the result of an `icmp` for a nonzero value. This
allowed optimization of, for example, `(((x == 0) == 0) == 0 ...)` to
a single level, either `x == 0` or `x != 0` depending on even/odd
nesting depth.

Unfortunately this kind of recursion in the backend has a depth
bounded only by the user input, hence creates a DoS vulnerability: the
wrong kind of compiler input can cause a stack overflow in Cranelift
at compilation time. This case is reachable from Wasmtime's Wasm
frontend via the `i32.eqz` operator (for example) as well.

Ideally, this kind of deep rewrite is best done in our mid-end
optimizer, where we think carefully about bounds for recursive
rewrites. The left-hand sides for the backend rules should really be
fixed shapes that correspond to machine instructions, rather than
ad-hoc peephole optimizations in their own right.

This fix thus simply removes the recursion case that causes the
blowup. The patch includes two tests: one with optimizations disabled,
showing correct compilation (without the fix, this case fails to
compile with a stack overflow), and one with optimizations enabled,
showing that the mid-end properly cleans up the nested expression and
we get the expected one-level result anyway.

* Preserve codegen on branches.

This change works by splitting a rule so that the entry point used by
`brif` lowering can still peel off one layer of `icmp` and emit it
directly, without entering the unbounded structural recursion.

It also adds a mid-end rule to catch one case that we were previously
catching in the backend only: `fcmp(...) != 0`.
cfallin added a commit to cfallin/wasmtime that referenced this pull request Jan 13, 2026
…odealliance#12333)

* Cranelift: x64: fix user-controlled recursion in cmp emission.

We had a set of rules introduced in bytecodealliance#11097 that attempted to optimize
the case of testing the result of an `icmp` for a nonzero value. This
allowed optimization of, for example, `(((x == 0) == 0) == 0 ...)` to
a single level, either `x == 0` or `x != 0` depending on even/odd
nesting depth.

Unfortunately this kind of recursion in the backend has a depth
bounded only by the user input, hence creates a DoS vulnerability: the
wrong kind of compiler input can cause a stack overflow in Cranelift
at compilation time. This case is reachable from Wasmtime's Wasm
frontend via the `i32.eqz` operator (for example) as well.

Ideally, this kind of deep rewrite is best done in our mid-end
optimizer, where we think carefully about bounds for recursive
rewrites. The left-hand sides for the backend rules should really be
fixed shapes that correspond to machine instructions, rather than
ad-hoc peephole optimizations in their own right.

This fix thus simply removes the recursion case that causes the
blowup. The patch includes two tests: one with optimizations disabled,
showing correct compilation (without the fix, this case fails to
compile with a stack overflow), and one with optimizations enabled,
showing that the mid-end properly cleans up the nested expression and
we get the expected one-level result anyway.

* Preserve codegen on branches.

This change works by splitting a rule so that the entry point used by
`brif` lowering can still peel off one layer of `icmp` and emit it
directly, without entering the unbounded structural recursion.

It also adds a mid-end rule to catch one case that we were previously
catching in the backend only: `fcmp(...) != 0`.
cfallin added a commit that referenced this pull request Jan 14, 2026
… (#12338)

* Cranelift: x64: fix user-controlled recursion in cmp emission.

We had a set of rules introduced in #11097 that attempted to optimize
the case of testing the result of an `icmp` for a nonzero value. This
allowed optimization of, for example, `(((x == 0) == 0) == 0 ...)` to
a single level, either `x == 0` or `x != 0` depending on even/odd
nesting depth.

Unfortunately this kind of recursion in the backend has a depth
bounded only by the user input, hence creates a DoS vulnerability: the
wrong kind of compiler input can cause a stack overflow in Cranelift
at compilation time. This case is reachable from Wasmtime's Wasm
frontend via the `i32.eqz` operator (for example) as well.

Ideally, this kind of deep rewrite is best done in our mid-end
optimizer, where we think carefully about bounds for recursive
rewrites. The left-hand sides for the backend rules should really be
fixed shapes that correspond to machine instructions, rather than
ad-hoc peephole optimizations in their own right.

This fix thus simply removes the recursion case that causes the
blowup. The patch includes two tests: one with optimizations disabled,
showing correct compilation (without the fix, this case fails to
compile with a stack overflow), and one with optimizations enabled,
showing that the mid-end properly cleans up the nested expression and
we get the expected one-level result anyway.

* Preserve codegen on branches.

This change works by splitting a rule so that the entry point used by
`brif` lowering can still peel off one layer of `icmp` and emit it
directly, without entering the unbounded structural recursion.

It also adds a mid-end rule to catch one case that we were previously
catching in the backend only: `fcmp(...) != 0`.
cfallin added a commit that referenced this pull request Jan 14, 2026
… (#12341)

* Cranelift: x64: fix user-controlled recursion in cmp emission.

We had a set of rules introduced in #11097 that attempted to optimize
the case of testing the result of an `icmp` for a nonzero value. This
allowed optimization of, for example, `(((x == 0) == 0) == 0 ...)` to
a single level, either `x == 0` or `x != 0` depending on even/odd
nesting depth.

Unfortunately this kind of recursion in the backend has a depth
bounded only by the user input, hence creates a DoS vulnerability: the
wrong kind of compiler input can cause a stack overflow in Cranelift
at compilation time. This case is reachable from Wasmtime's Wasm
frontend via the `i32.eqz` operator (for example) as well.

Ideally, this kind of deep rewrite is best done in our mid-end
optimizer, where we think carefully about bounds for recursive
rewrites. The left-hand sides for the backend rules should really be
fixed shapes that correspond to machine instructions, rather than
ad-hoc peephole optimizations in their own right.

This fix thus simply removes the recursion case that causes the
blowup. The patch includes two tests: one with optimizations disabled,
showing correct compilation (without the fix, this case fails to
compile with a stack overflow), and one with optimizations enabled,
showing that the mid-end properly cleans up the nested expression and
we get the expected one-level result anyway.

* Preserve codegen on branches.

This change works by splitting a rule so that the entry point used by
`brif` lowering can still peel off one layer of `icmp` and emit it
directly, without entering the unbounded structural recursion.

It also adds a mid-end rule to catch one case that we were previously
catching in the backend only: `fcmp(...) != 0`.
cfallin added a commit that referenced this pull request Jan 14, 2026
… (#12339)

* Cranelift: x64: fix user-controlled recursion in cmp emission.

We had a set of rules introduced in #11097 that attempted to optimize
the case of testing the result of an `icmp` for a nonzero value. This
allowed optimization of, for example, `(((x == 0) == 0) == 0 ...)` to
a single level, either `x == 0` or `x != 0` depending on even/odd
nesting depth.

Unfortunately this kind of recursion in the backend has a depth
bounded only by the user input, hence creates a DoS vulnerability: the
wrong kind of compiler input can cause a stack overflow in Cranelift
at compilation time. This case is reachable from Wasmtime's Wasm
frontend via the `i32.eqz` operator (for example) as well.

Ideally, this kind of deep rewrite is best done in our mid-end
optimizer, where we think carefully about bounds for recursive
rewrites. The left-hand sides for the backend rules should really be
fixed shapes that correspond to machine instructions, rather than
ad-hoc peephole optimizations in their own right.

This fix thus simply removes the recursion case that causes the
blowup. The patch includes two tests: one with optimizations disabled,
showing correct compilation (without the fix, this case fails to
compile with a stack overflow), and one with optimizations enabled,
showing that the mid-end properly cleans up the nested expression and
we get the expected one-level result anyway.

* Preserve codegen on branches.

This change works by splitting a rule so that the entry point used by
`brif` lowering can still peel off one layer of `icmp` and emit it
directly, without entering the unbounded structural recursion.

It also adds a mid-end rule to catch one case that we were previously
catching in the backend only: `fcmp(...) != 0`.
cfallin added a commit that referenced this pull request Jan 14, 2026
… (#12342)

* Cranelift: x64: fix user-controlled recursion in cmp emission.

We had a set of rules introduced in #11097 that attempted to optimize
the case of testing the result of an `icmp` for a nonzero value. This
allowed optimization of, for example, `(((x == 0) == 0) == 0 ...)` to
a single level, either `x == 0` or `x != 0` depending on even/odd
nesting depth.

Unfortunately this kind of recursion in the backend has a depth
bounded only by the user input, hence creates a DoS vulnerability: the
wrong kind of compiler input can cause a stack overflow in Cranelift
at compilation time. This case is reachable from Wasmtime's Wasm
frontend via the `i32.eqz` operator (for example) as well.

Ideally, this kind of deep rewrite is best done in our mid-end
optimizer, where we think carefully about bounds for recursive
rewrites. The left-hand sides for the backend rules should really be
fixed shapes that correspond to machine instructions, rather than
ad-hoc peephole optimizations in their own right.

This fix thus simply removes the recursion case that causes the
blowup. The patch includes two tests: one with optimizations disabled,
showing correct compilation (without the fix, this case fails to
compile with a stack overflow), and one with optimizations enabled,
showing that the mid-end properly cleans up the nested expression and
we get the expected one-level result anyway.

* Preserve codegen on branches.

This change works by splitting a rule so that the entry point used by
`brif` lowering can still peel off one layer of `icmp` and emit it
directly, without entering the unbounded structural recursion.

It also adds a mid-end rule to catch one case that we were previously
catching in the backend only: `fcmp(...) != 0`.
@adambratschikaye
Copy link
Contributor

Thanks for taking care of this @cfallin, @alexcrichton, and @fitzgen!

github-merge-queue bot pushed a commit to dfinity/ic that referenced this pull request Jan 21, 2026
This is a patch version bump of wasmtime which contains the backport fix
for the compiler bug reported in
bytecodealliance/wasmtime#12333

---------

Co-authored-by: IDX GitHub Automation <infra+github-automation@dfinity.org>
github-merge-queue bot pushed a commit to dfinity/ic that referenced this pull request Jan 21, 2026
This is a patch version bump of wasmtime which contains the backport fix
for the compiler bug reported in
bytecodealliance/wasmtime#12333

---------

Co-authored-by: IDX GitHub Automation <infra+github-automation@dfinity.org>
github-merge-queue bot pushed a commit to dfinity/ic that referenced this pull request Jan 21, 2026
This is a patch version bump of wasmtime which contains the backport fix
for the compiler bug reported in
bytecodealliance/wasmtime#12333

---------

Co-authored-by: IDX GitHub Automation <infra+github-automation@dfinity.org>
github-merge-queue bot pushed a commit to dfinity/ic that referenced this pull request Jan 21, 2026
This is a patch version bump of wasmtime which contains the backport fix
for the compiler bug reported in
bytecodealliance/wasmtime#12333

---------

Co-authored-by: IDX GitHub Automation <infra+github-automation@dfinity.org>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

cranelift:area:x64 Issues related to x64 codegen cranelift Issues related to the Cranelift code generator

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants