You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
thumb-2: call_indirect emits no bounds-check and no type-check — OOB/wrong-type index does an uncontrolled indirect branch instead of trapping (WASM 4.4.8) #642
On the Thumb-2 backend (-t cortex-m3), call_indirect emits no bounds check and no type/signature check — it computes table[index] from a runtime index and branches to it directly. WASM Core §4.4.8 requires call_indirect to trap when index >= table.size and when the table entry's runtime type does not match the static type. synth does neither, so an out-of-bounds or wrong-type index performs an uncontrolled indirect branch (arbitrary/garbage call) instead of a deterministic trap — a safety/security hole on the safety-critical embedded target synth exists for.
synth 0.32.1
oracle: wasmtime 42.0.1 (traps correctly)
The emitted code (Thumb-2)
For (call_indirect (type $bin) <args> (local.get 1)) with a runtime index in local 1:
Three instructions, zerocmp/bounds-branch/udf, zero type-tag comparison. Confirmed by disassembly (llvm-objdump --triple=thumbv7m; note synth disasm mis-decodes thumb as A32, see #637) and pinned in source:
Executed under QEMU (Cortex-M semihosting) with the table linked at r11, vs wasmtime:
index
table size
WASM spec
wasmtime
synth (qemu)
0
3
add(a,10)
15
15 ✓
1
3
sub(a,10)
−5
−5 ✓
2
3
mul(a,10)
50
50 ✓
5
3
trap (OOB)
wasm trap
no trap — reads [r11 + 5*4] past the table and blx to that word (in this layout an invalid address → HardFault; with a different memory layout, a valid code/data word there is called = arbitrary indirect call)
99
3
trap (OOB)
wasm trap
no trap — same uncontrolled branch
In-bounds calls are correct; the OOB index produces behavior that depends entirely on whatever lies past the table in memory — undefined and uncontrolled, where the spec mandates a deterministic trap.
Type check — after the load: compare the callee's runtime type id against the static type_idx and trap on mismatch. Currently type_idx is discarded, so a table holding a differently-typed funcref is called with a mismatched signature → register/stack corruption.
Suggested fix
Emit, per call site: a bounds compare against the table length (trap via udf on hs), then load the entry, then (for the type check) load the entry's type id and compare against type_idx (trap on mismatch). This mirrors what wasmtime and every conformant engine do, and matches the trap sequences synth already emits for div-by-zero / INT_MIN overflow.
Distinct from #594 (A32 NOP, closed) and #597 (index shift in the type field, closed) — both were about reaching/indexing the table; this is about the safety checks around the access, which have never been present.
Found via the QEMU full-image execution-differential harness (all functions linked, table at r11) vs wasmtime; source-confirmed independently.
Summary
On the Thumb-2 backend (
-t cortex-m3),call_indirectemits no bounds check and no type/signature check — it computestable[index]from a runtime index and branches to it directly. WASM Core §4.4.8 requirescall_indirectto trap whenindex >= table.sizeand when the table entry's runtime type does not match the static type. synth does neither, so an out-of-bounds or wrong-type index performs an uncontrolled indirect branch (arbitrary/garbage call) instead of a deterministic trap — a safety/security hole on the safety-critical embedded target synth exists for.0.32.1The emitted code (Thumb-2)
For
(call_indirect (type $bin) <args> (local.get 1))with a runtime index inlocal 1:Three instructions, zero
cmp/bounds-branch/udf, zero type-tag comparison. Confirmed by disassembly (llvm-objdump --triple=thumbv7m; notesynth disasmmis-decodes thumb as A32, see #637) and pinned in source:arm_encoder.rs(the CallIndirect expansion test):mov.w ip, rm, LSL #2; ldr.w ip, [r11, ip]; blx ip— the call_indirect on the A32 path (--target cortex-r5) compiles to a NOP — silently returns wrong result instead of calling the target #594/Thumb-2 CallIndirect shift lands in the type field (mov.w ip, rm, ASR #32) — always dispatches table entry 0 (shipped miscompile, masked by index-0 probes) #597 history only ever corrected the index computation (LSL #2vs theASR #32type-field bug); a bounds/type check was never added.type_idxand a source comment admits full support still needs "Type signature checking / Bounds checking."Reproducer
Executed under QEMU (Cortex-M semihosting) with the table linked at
r11, vs wasmtime:wasm trap[r11 + 5*4]past the table andblxto that word (in this layout an invalid address → HardFault; with a different memory layout, a valid code/data word there is called = arbitrary indirect call)wasm trapIn-bounds calls are correct; the OOB index produces behavior that depends entirely on whatever lies past the table in memory — undefined and uncontrolled, where the spec mandates a deterministic trap.
Two missing checks (WASM Core §4.4.8)
if (index >= table.size) trap;. Currently absent → OOB indirect branch. (Same defect class as the closed encoder: load/store imm12 offset truncated (& 0xFFF) with no bounds check — silent wrong-address class (cf #253/#255) #259 "load/store no bounds check — silent wrong-address".)type_idxand trap on mismatch. Currentlytype_idxis discarded, so a table holding a differently-typed funcref is called with a mismatched signature → register/stack corruption.Suggested fix
Emit, per call site: a bounds compare against the table length (trap via
udfonhs), then load the entry, then (for the type check) load the entry's type id and compare againsttype_idx(trap on mismatch). This mirrors what wasmtime and every conformant engine do, and matches the trap sequences synth already emits for div-by-zero / INT_MIN overflow.Distinct from #594 (A32 NOP, closed) and #597 (index shift in the type field, closed) — both were about reaching/indexing the table; this is about the safety checks around the access, which have never been present.
Found via the QEMU full-image execution-differential harness (all functions linked, table at r11) vs wasmtime; source-confirmed independently.