Skip to content

[CRITICAL] rv32: br out of a value-producing block discards its value — merge reads the fallthrough register (8 labels.wast assertions), silent, exit 0 #931

Description

@avrabe

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions