Potential Bug: `i8x16.extract_lane_u` / `i16x8.extract_lane_u` not zero-extended in the x86-64 interpreter

Open Beginner friendly
#648 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Assessment

Difficulty
1/5
Estimated time
1-3 hours
Newbie friendliness
88/100
Issue type
Bug
Clarity
Clearly specified
Activity status
Quiet
Tech stack
wasm
Domain
compilers

Research direction

Start in src/engine/x86-64/X86_64Interpreter.v3 at genExtract_lane and compare the unsigned narrow extract cases with the signed variants. Verify the reproducer across int, dyn, lazy, jit, and spc modes, then confirm that the extract_lane variant sweep produces matching results.

Written by the indexing model from the issue text.

Description

At long last, I have written CRC checksum for my favorite Wasm engine. (Feel free to assign me some of them if you are busy)

The x86-64 assembly interpreter (the int/dyn tiers) returns garbage in the
upper bits of an unsigned narrow lane extract. The result should be the lane
value zero-extended to i32, but the upper bits instead contain stale bytes of
the source v128.

The single-pass compiler (lazy/jit/spc) and the portable V3 interpreter are
correct, so this shows up as a tier disagreement in differential testing.

Minimal reproducer

(module (func (export "main") (result i32)
  v128.const i32x4 0x33221100 0x77665544 0xbbaa9988 0xffeeddcc
  i16x8.extract_lane_u 0))
$ wizeng --mode=int --print-result --invoke=main repro.wasm
857870592            # 0x33221100   ← wrong
$ wizeng --mode=spc --print-result --invoke=main repro.wasm
4352                 # 0x00001100   ← correct

Lane 0 of the vector is 0x1100, so i16x8.extract_lane_u 0 must yield
0x0000_1100. The interpreter leaves the upper 16 bits as the vector's halfword (0x3322), giving 0x3322_1100.

Per-tier return values (the reproducer above)
tier --mode result
int int 0x33221100
dyn dyn 0x33221100
lazy lazy 0x00001100
jit jit 0x00001100
spc spc 0x00001100

i8x16.extract_lane_u has the same defect, e.g. i8x16.extract_lane_u 0 on the
same vector returns 0x33221100 instead of 0x00000000. The signed
variants (extract_lane_s) and the full-width i32x4/i64x2/float extracts are
all correct.

The observed wrong value follows result = (v128_bits[16:31] << 16) | lane for
i16x8, and result = (v128_bits[8:31] << 8) | lane for i8x16 — i.e. the
correct lane is in the low bits but the high bits are never cleared.

Environment

  • OS: Ubuntu 22.04.5 LTS, Linux 5.15.0-177-generic, x86_64
  • Wizard: 26.2985, commit 719aab44 ("Stack switch event monitor (#645)")
  • Virgil: Aeneas III-11.1939, commit 1cdec1861
  • Target: x86-64-linux

Explanation

In src/engine/x86-64/X86_64Interpreter.v3, genExtract_lane reads the lane into
a temporary register and stores it back into the value-stack slot (which still
holds the full 16-byte vector). The unsigned narrow cases were generated as:

genExtract_lane(Opcode.I8X16_EXTRACT_LANE_U, BpTypeCode.I32.code, 1, asm.movb_r_m, asm.movb_m_r, false);
genExtract_lane(Opcode.I16X8_EXTRACT_LANE_U, BpTypeCode.I32.code, 2, asm.movw_r_m, asm.movw_m_r, false);

Both the load (movb_r_m/movw_r_m) and the store (movb_m_r/movw_m_r) are
partial-width: an 8/16-bit move on x86-64 does not clear the rest of the
register, and the partial store writes only the low 1–2 bytes of the result slot.
So the upper bytes of the i32 result are never overwritten and retain the
original vector bytes.

The signed variants avoid this because they sign-extend the register
(movbsx/movwsx) and store the full 32 bits (movd_m_r); the full-width
extracts move the whole value, so neither path leaves stale bits.

Fix

Use a zero-extending load and a full-width store for the unsigned narrow
extracts, mirroring the signed path:

-		genExtract_lane(Opcode.I8X16_EXTRACT_LANE_U, BpTypeCode.I32.code, 1, asm.movb_r_m, asm.movb_m_r, false);
-		genExtract_lane(Opcode.I16X8_EXTRACT_LANE_U, BpTypeCode.I32.code, 2, asm.movw_r_m, asm.movw_m_r, false);
+		genExtract_lane(Opcode.I8X16_EXTRACT_LANE_U, BpTypeCode.I32.code, 1, asm.movbzx_r_m, asm.movd_m_r, false);
+		genExtract_lane(Opcode.I16X8_EXTRACT_LANE_U, BpTypeCode.I32.code, 2, asm.movwzx_r_m, asm.movd_m_r, false);

movbzx_r_m/movwzx_r_m zero-extend the lane into the full 32-bit register, and
movd_m_r writes all four bytes of the result slot.

With this change all five tiers agree on the reproducer (0x00001100), and a
sweep over every extract_lane variant (i8x16/i16x8 signed+unsigned,
i32x4, i64x2, f32x4, f64x2) matches between int and spc.

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

Dominant language
WebAssembly
Stars
512
Forks
51
Avg merge
7h 34m
Merged PRs (30d)
21

Contributor guide

No contributing guide indexed for this repository

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

More from titzer/wizard-engine

All issues in titzer/wizard-engine

Similar issues

More Compilers issues

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.