You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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.
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
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:
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:
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
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/dyntiers) returns garbage in theupper 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 ofthe source v128.
The single-pass compiler (
lazy/jit/spc) and the portable V3 interpreter arecorrect, so this shows up as a tier disagreement in differential testing.
Minimal reproducer
Lane 0 of the vector is
0x1100, soi16x8.extract_lane_u 0must yield0x0000_1100. The interpreter leaves the upper 16 bits as the vector's halfword (0x3322), giving0x3322_1100.Per-tier return values (the reproducer above)
--mode0x33221100✗0x33221100✗0x00001100✓0x00001100✓0x00001100✓i8x16.extract_lane_uhas the same defect, e.g.i8x16.extract_lane_u 0on thesame vector returns
0x33221100instead of0x00000000. The signedvariants (
extract_lane_s) and the full-widthi32x4/i64x2/float extracts areall correct.
The observed wrong value follows
result = (v128_bits[16:31] << 16) | lanefori16x8, andresult = (v128_bits[8:31] << 8) | lanefori8x16— i.e. thecorrect lane is in the low bits but the high bits are never cleared.
Environment
26.2985, commit719aab44("Stack switch event monitor (Stack switch event monitor #645)")III-11.1939, commit1cdec1861x86-64-linuxExplanation
In
src/engine/x86-64/X86_64Interpreter.v3,genExtract_lanereads the lane intoa 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:
Both the load (
movb_r_m/movw_r_m) and the store (movb_m_r/movw_m_r) arepartial-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
i32result are never overwritten and retain theoriginal 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-widthextracts 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:
movbzx_r_m/movwzx_r_mzero-extend the lane into the full 32-bit register, andmovd_m_rwrites all four bytes of the result slot.With this change all five tiers agree on the reproducer (
0x00001100), and asweep over every
extract_lanevariant (i8x16/i16x8signed+unsigned,i32x4,i64x2,f32x4,f64x2) matches betweenintandspc.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