On RV32, br out of a value-producing block computes the branch value into one register but
the merge point reads a different register — the one holding the fallthrough value. The
branch's value is discarded, so the block yields whatever the not-taken path computed.
synth compile exits 0 with no warning.
This is the most basic form of structured control flow with a value, so the blast radius is
wide: it breaks block, switch-style br_table dispatch, and label shadowing alike
(8 labels.wast assertions).
Verified on v0.55.0 (clone HEAD 97bd6db). Found by executing the official WebAssembly
testsuite across backends (context: #928).
Repro
(module
(func (export "t") (result i32)
(block $exit (result i32)
(br $exit (i32.const 1))
(i32.const 0))))
wasmtime → 1. RV32 → 0.
The generated code says it outright
$ synth compile blk.wat -b riscv -t rv32imac --all-exports --relocatable -o blk.o # exit 0
$ llvm-objdump -d --triple=riscv32 blk.o
00000000 <t>:
0: 00100293 li t0, 0x1 ; br $exit's value -> t0
4: 0080006f j 0xc ; br $exit -> merge
8: 00000313 li t1, 0x0 ; fallthrough value -> t1
c: 00030513 mv a0, t1 ; merge: reads t1, NOT t0
10: 00008067 ret
The taken edge writes t0; the merge unconditionally reads t1. Nothing moves the branch
value into the block's result register (or, equivalently, the two edges were never assigned a
common register), so the br value is dead and the block returns the fallthrough value.
Affected assertions (official labels.wast)
All eight RV32 failures in that file are this one mechanism:
| assertion |
want |
got |
block |
1 |
0 |
switch ×5 |
50 / 20 / 20 / 3 |
0 |
switch ×2 |
50 |
15 |
shadowing |
1 |
0 |
The switch cases return 15 or 0 — in each case a value produced by a different edge —
which is the same defect reached through br_table.
Cross-backend
|
block repro |
| wasmtime |
1 |
| aarch64 |
1 |
| thumb-2 |
1 |
| rv32imac |
0 |
So this is RV32-specific; the ARM and AArch64 selectors get the branch-edge value right.
(Filed alongside #930, which is a different defect: on thumb-2 the simple case above is
correct, and only the combination of a branch out of an if whose value operand is itself a
branching block fails. Same symptom class, different backends and different shapes — but both
point at how branch-edge values are placed.)
Impact
Silent wrong value on a safety-targeted backend, in the most ordinary control flow a producer
emits. Exit 0, no diagnostic. Any block (result …) that is exited early by br is affected,
which in practice is every early-return and every switch lowering.
On RV32,
brout of a value-producingblockcomputes the branch value into one register butthe merge point reads a different register — the one holding the fallthrough value. The
branch's value is discarded, so the block yields whatever the not-taken path computed.
synth compileexits 0 with no warning.This is the most basic form of structured control flow with a value, so the blast radius is
wide: it breaks
block,switch-stylebr_tabledispatch, and label shadowing alike(8
labels.wastassertions).Verified on v0.55.0 (clone HEAD 97bd6db). Found by executing the official WebAssembly
testsuite across backends (context: #928).
Repro
wasmtime → 1. RV32 → 0.
The generated code says it outright
The taken edge writes
t0; the merge unconditionally readst1. Nothing moves the branchvalue into the block's result register (or, equivalently, the two edges were never assigned a
common register), so the
brvalue is dead and the block returns the fallthrough value.Affected assertions (official
labels.wast)All eight RV32 failures in that file are this one mechanism:
blockswitch×5switch×2shadowingThe
switchcases return15or0— in each case a value produced by a different edge —which is the same defect reached through
br_table.Cross-backend
blockreproSo this is RV32-specific; the ARM and AArch64 selectors get the branch-edge value right.
(Filed alongside #930, which is a different defect: on thumb-2 the simple case above is
correct, and only the combination of a branch out of an
ifwhose value operand is itself abranching block fails. Same symptom class, different backends and different shapes — but both
point at how branch-edge values are placed.)
Impact
Silent wrong value on a safety-targeted backend, in the most ordinary control flow a producer
emits. Exit 0, no diagnostic. Any
block (result …)that is exited early bybris affected,which in practice is every early-return and every
switchlowering.