Potential Bug: `i8x16.extract_lane_u` / `i16x8.extract_lane_u` not zero-extended in the x86-64 interpreter
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, commit719aab44("Stack switch event monitor (#645)") - Virgil: Aeneas
III-11.1939, commit1cdec1861 - 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
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
More from titzer/wizard-engine
-
Difficulty 2/5 1-3 hours Newbie friendliness 88/100
titzer/wizard-engine#649 ·
-
Difficulty 3/5 1-2 days Newbie friendliness 68/100
titzer/wizard-engine#655 · 1 comment ·
-
Difficulty 3/5 1-2 days Newbie friendliness 68/100
titzer/wizard-engine#654 ·
-
Difficulty 3/5 1-2 days Newbie friendliness 76/100
titzer/wizard-engine#650 ·
-
Difficulty 4/5 3-5 days Newbie friendliness 35/100
titzer/wizard-engine#638 · 3 comments ·
All issues in titzer/wizard-engine
Similar issues
-
mlir
Difficulty 2/5 1-3 hours Newbie friendliness 84/100
llvm/llvm-project#224908 · 1 comment ·
-
Difficulty 2/5 1-3 hours Newbie friendliness 75/100
-
area-CodeGen-coreclr untriaged
Difficulty 1/5 Under an hour Newbie friendliness 92/100
-
Difficulty 2/5 1-3 hours Newbie friendliness 88/100
secondlife/sl-vscode-plugin#147 ·
-
Difficulty 2/5 1-3 hours Newbie friendliness 84/100
objectionary/phie#149 ·