Summary
On the x86-64 single-pass compiler tiers (lazy, jit, spc), a vector
*.ne instruction whose operand is a register-cached local overwrites that
local with all-ones. Later reads of the local return -1 instead of its real
value. The interpreter tiers (int, dyn) are correct, so the tiers disagree.
Reproducer (ne_clobber_repro.wat)
(module
(func (export "main") (result i32)
(local $v v128)
v128.const i8x16 0 0 0 0 0 0x9c 0 0 0 0 0 0 0 0 0 0 ;; lane 5 = 0x9c (-100)
local.set $v
local.get $v local.get $v i8x16.ne drop ;; result discarded
local.get $v i8x16.extract_lane_s 5)) ;; return local back
wizeng --print-result --mode=int ne_clobber_repro.wasm -> -100 (correct)
wizeng --print-result --mode=spc ne_clobber_repro.wasm -> -1 (wrong)
The i8x16.ne result is discarded; it only acts as a side effect. The bug is
that it mutates $v, which main then returns directly.
Root cause
emit_v128_ne in src/engine/x86-64/X86_64MacroAssembler.v3 builds the all-ones
XOR mask for the complement-of-equal in the src operand register itself:
f(dst, src) ; dst = (dst == src) pcmpeqb
f(src, src) ; src = all-ones <-- clobbers src
pxor dst, src ; dst = (dst != src)
In the SPC, visit_I8X16_NE uses do_op2_x_x, which allocates no scratch and
passes the local's cached register as src. Overwriting it corrupts the local.
The interpreter is unaffected because it loads operands into throwaway scratch
xmms. Affects i8x16/i16x8/i32x4/i64x2.ne.
Fix
Build the all-ones mask in a dedicated scratch register instead of src
(the same pattern gt_u already uses):
X86_64MacroAssembler.v3: emit_v128_ne takes a scratch param; use
f(scratch, scratch) for the mask.
X86_64SinglePassCompiler.v3: the four visit_*_NE use do_op2_x_x_xtmp.
X86_64Interpreter.v3: pass the existing scratch r_xmm2 to the four
emit_*_ne calls.
After the fix all five tiers agree (-100).
Additional information
A combination of AFL++ and Wasmlike, an Xsmith-based random program generator produced the snippet of code that found the issue. Xsmith Project
Summary
On the x86-64 single-pass compiler tiers (
lazy,jit,spc), a vector*.neinstruction whose operand is a register-cached local overwrites thatlocal with all-ones. Later reads of the local return
-1instead of its realvalue. The interpreter tiers (
int,dyn) are correct, so the tiers disagree.Reproducer (
ne_clobber_repro.wat)The
i8x16.neresult is discarded; it only acts as a side effect. The bug isthat it mutates
$v, whichmainthen returns directly.Root cause
emit_v128_neinsrc/engine/x86-64/X86_64MacroAssembler.v3builds the all-onesXOR mask for the complement-of-equal in the
srcoperand register itself:In the SPC,
visit_I8X16_NEusesdo_op2_x_x, which allocates no scratch andpasses the local's cached register as
src. Overwriting it corrupts the local.The interpreter is unaffected because it loads operands into throwaway scratch
xmms. Affects
i8x16/i16x8/i32x4/i64x2.ne.Fix
Build the all-ones mask in a dedicated scratch register instead of
src(the same pattern
gt_ualready uses):X86_64MacroAssembler.v3:emit_v128_netakes ascratchparam; usef(scratch, scratch)for the mask.X86_64SinglePassCompiler.v3: the fourvisit_*_NEusedo_op2_x_x_xtmp.X86_64Interpreter.v3: pass the existing scratchr_xmm2to the fouremit_*_necalls.After the fix all five tiers agree (
-100).Additional information
A combination of AFL++ and Wasmlike, an Xsmith-based random program generator produced the snippet of code that found the issue. Xsmith Project