On the ARM/thumb-2 backend, a call whose parameter list has an i64 before any further
parameter marshals the arguments into the wrong registers. synth compile exits 0 with
no warning, and the callee receives garbage — the i64's high half is never placed, and
every later argument is shifted one register down.
(i32, i64) — the shape #837 fixed — is correct. The defect is specifically an i64 that is
not last.
Verified on v0.55.0 (clone HEAD 97bd6db), executed under qemu-system-arm, oracle wasmtime.
Repro
(module
(func $g (param i64 i32) (result i32) (local.get 1))
(func (export "f") (param i32 i32) (result i32)
(call $g (i64.const 1234605616436508552) (local.get 0))))
$ synth compile i64arg.wat -b arm -t cortex-m3 --all-exports --relocatable -o i64arg.o
exit=0 stderr warnings: 0
|
f(7) |
f(42) |
| wasmtime |
7 |
42 |
| thumb-2 (cortex-m3) |
287454020 |
287454020 |
287454020 = 0x11223344 = the high half of the i64 constant, returned regardless of
the actual argument.
Not a constant-folding artifact — with a runtime-computed i64 the callee still gets the
wrong operand:
(func (export "f") (param i32 i32) (result i32)
(call $g (i64.extend_i32_s (local.get 1)) (local.get 0)))
f(9,5) → wasmtime 9, thumb-2 5.
Which signatures are affected
Callee returns one of its parameters; caller invoked as f(9,5); rc is synth's exit code.
| callee params |
rc |
wasmtime |
thumb-2 |
|
(i64, i32) |
0 |
9 |
5 |
✗ silent miscompile |
(i64, i64) |
0 |
5 |
9 |
✗ silent miscompile |
(i64, i32, i32) |
0 |
5 |
9 |
✗ silent miscompile |
(i32, i64) |
0 |
9 |
9 |
✓ correct (#837's shape) |
(i32, i32) |
0 |
5 |
5 |
✓ correct |
aarch64 and rv32 are unaffected — both produce the wasmtime result for all five.
Root cause
crates/synth-synthesis/src/instruction_selector.rs:9730 pop_call_args pops one register
per WASM parameter:
let mut srcs: Vec<Reg> = Vec::with_capacity(n);
for _ in 0..n {
let r = pop_operand(stack, next_temp, instructions, spill, &[], idx)?;
srcs.push(r);
}
srcs.reverse();
and the caller then marshals srcs[k] into ARG_REGS[k]. Under AAPCS an i64 parameter
occupies an even-aligned pair of core registers, so as soon as one appears, every
subsequent parameter is off by one register and the i64's high half is never written. The
emitted caller shows it plainly — r2 (the high half) is never assigned, and the following
i32 is placed into r1 where that half belongs.
What makes this a gap rather than an oversight is that the same hazard is already guarded
on both adjacent paths:
- the >4-arg stack path refuses it loudly (
#503: call arg {k} is passed on the stack and is i64/f64; only i32 stack args are supported, :9748);
pop_call_args_mixed (:9780) declines i64-mixed-with-float for exactly this reason —
its own comment says the "even-aligned core PAIR is not marshalled on this path".
Only the plain integer register path stays silent.
Suggested fix
Assign ARG_REGS by core-register position, not WASM parameter index: advance the
allocator by 2 (with even alignment) for an i64 param and emit both halves. If that is more
than this path should carry right now, the minimum safe change is to make it match its
neighbours and decline loudly when a non-final i64 parameter is present — an honest
refusal is enormously better than a wrong value here, because the shape
fn(u64, u32)/fn(i64, i32) is completely ordinary in C and Rust code compiled to wasm,
and nothing about the failure is observable on target.
Found while executing spec assertions across all three backends (context: #928).
On the ARM/thumb-2 backend, a call whose parameter list has an i64 before any further
parameter marshals the arguments into the wrong registers.
synth compileexits 0 withno warning, and the callee receives garbage — the i64's high half is never placed, and
every later argument is shifted one register down.
(i32, i64)— the shape #837 fixed — is correct. The defect is specifically an i64 that isnot last.
Verified on v0.55.0 (clone HEAD 97bd6db), executed under
qemu-system-arm, oracle wasmtime.Repro
f(7)f(42)287454020=0x11223344= the high half of the i64 constant, returned regardless ofthe actual argument.
Not a constant-folding artifact — with a runtime-computed i64 the callee still gets the
wrong operand:
f(9,5)→ wasmtime 9, thumb-2 5.Which signatures are affected
Callee returns one of its parameters; caller invoked as
f(9,5);rcis synth's exit code.(i64, i32)(i64, i64)(i64, i32, i32)(i32, i64)(i32, i32)aarch64 and rv32 are unaffected — both produce the wasmtime result for all five.
Root cause
crates/synth-synthesis/src/instruction_selector.rs:9730pop_call_argspops one registerper WASM parameter:
and the caller then marshals
srcs[k]intoARG_REGS[k]. Under AAPCS an i64 parameteroccupies an even-aligned pair of core registers, so as soon as one appears, every
subsequent parameter is off by one register and the i64's high half is never written. The
emitted caller shows it plainly —
r2(the high half) is never assigned, and the followingi32 is placed into
r1where that half belongs.What makes this a gap rather than an oversight is that the same hazard is already guarded
on both adjacent paths:
#503: call arg {k} is passed on the stack and is i64/f64; only i32 stack args are supported,:9748);pop_call_args_mixed(:9780) declines i64-mixed-with-float for exactly this reason —its own comment says the "even-aligned core PAIR is not marshalled on this path".
Only the plain integer register path stays silent.
Suggested fix
Assign
ARG_REGSby core-register position, not WASM parameter index: advance theallocator by 2 (with even alignment) for an i64 param and emit both halves. If that is more
than this path should carry right now, the minimum safe change is to make it match its
neighbours and decline loudly when a non-final i64 parameter is present — an honest
refusal is enormously better than a wrong value here, because the shape
fn(u64, u32)/fn(i64, i32)is completely ordinary in C and Rust code compiled to wasm,and nothing about the failure is observable on target.
Found while executing spec assertions across all three backends (context: #928).