Summary
On the Thumb-2 backend (-t cortex-m3), global.set / global.get of an i64 global truncate to the low 32 bits: global.set stores only the low word and discards the high word; global.get loads only one word. The high 32 bits of every i64 global are silently lost. It compiles with no error or warning — a silent miscompile.
- synth
0.32.1
- oracle: wasmtime 42.0.1
Reproducer
(module
(global $c (mut i64) (i64.const 0))
(func (export "f")(param i32 i32)(result i32)
(global.set $c (i64.const 0x123456789ABCDEF0))
(i32.wrap_i64 (i64.shr_u (global.get $c) (i64.const 32))))) ;; return the high word
- wasmtime:
f() = 305419896 (0x12345678, the correct high word)
- synth (qemu, R9 = globals base):
f() = 32 — wrong (the high word was never stored/loaded)
Root cause (Thumb-2 disassembly)
movw r0, #0xdef0 ; movt r0, #0x9abc ; r0 = low word 0x9ABCDEF0
movw r1, #0x5678 ; movt r1, #0x1234 ; r1 = high word 0x12345678 (computed…)
str.w r0, [r9] ; global.set: stores ONLY r0 (low). r1 (high) DISCARDED.
ldr.w r2, [r9] ; global.get: loads ONLY one word.
... ; the i64.shr_u then operates on a malformed pair -> 32
Exactly one str and one ldr to [r9] (the globals base) — an i64 global needs two of each ([r9] and [r9,#4]). The high-word register r1 is materialized and then dropped. Source confirms the i32-only lowering (optimizer_bridge.rs:2382): "GlobalGet pushes a fresh i32; GlobalSet pops one." There is no i64 register-pair path for globals, and the idx*4 slot addressing has no room for the second word.
Impact
Any module with a mutable or immutable i64 global (counters, 64-bit accumulators, timestamps, bitsets, packed handles) reads back a corrupted value after the first global.set, with no diagnostic. i32 globals are correct; only i64 (and by extension f64, same pair width — worth checking) globals are affected.
Suggested fix
Lower i64 global.get/global.set as a register-pair access (str/ldr of both words at [r9,#off] and [r9,#off+4], with 8-byte slot layout), or — if i64 globals are intentionally unsupported — reject them with an honest error (the #369/#503/#518 "unsupported operator" precedent) rather than silently truncating.
Found via the QEMU full-image execution-differential harness (R9 = globals base) vs wasmtime; source + disasm confirmed.
Summary
On the Thumb-2 backend (
-t cortex-m3),global.set/global.getof an i64 global truncate to the low 32 bits:global.setstores only the low word and discards the high word;global.getloads only one word. The high 32 bits of every i64 global are silently lost. It compiles with no error or warning — a silent miscompile.0.32.1Reproducer
f() = 305419896(0x12345678, the correct high word)f() = 32— wrong (the high word was never stored/loaded)Root cause (Thumb-2 disassembly)
Exactly one
strand oneldrto[r9](the globals base) — an i64 global needs two of each ([r9]and[r9,#4]). The high-word registerr1is materialized and then dropped. Source confirms the i32-only lowering (optimizer_bridge.rs:2382): "GlobalGet pushes a fresh i32; GlobalSet pops one." There is no i64 register-pair path for globals, and theidx*4slot addressing has no room for the second word.Impact
Any module with a mutable or immutable i64 global (counters, 64-bit accumulators, timestamps, bitsets, packed handles) reads back a corrupted value after the first
global.set, with no diagnostic. i32 globals are correct; only i64 (and by extension f64, same pair width — worth checking) globals are affected.Suggested fix
Lower i64
global.get/global.setas a register-pair access (str/ldrof both words at[r9,#off]and[r9,#off+4], with 8-byte slot layout), or — if i64 globals are intentionally unsupported — reject them with an honest error (the#369/#503/#518"unsupported operator" precedent) rather than silently truncating.Found via the QEMU full-image execution-differential harness (R9 = globals base) vs wasmtime; source + disasm confirmed.