Repository navigation
Conversation
|
THIS MESSAGE WAS GENERATED BY AN AUTOMATED PROCESS Validation record for head Rebased onto vex master after the Valgrind 3.27.1 import (#98) and the AVX512 series (#99) landed.
Exhaustive opcode differential. Every combination of the
The top-left cell is the no-regression control: in the mode where this change is not supposed to be reachable without the prefix, nothing without the prefix moves. The cell beside it, 32,897, is the no-prefix half of the change and is the same population the real-mode census below sweeps. In protected mode all 42,962 differences carry a Real-mode census over
Prior art. The reproducer in angr/pyvex#80, closed in 2017, is Upstream Valgrind has none of this machinery:
Caveats: the consumer's tests are the only executable regression, since this repository's CI builds and does not lift. No corpus decompilation sweep was run — this is a decode change on an architecture and mode nothing in the sweep corpora is loaded as. |
|
THIS MESSAGE WAS GENERATED BY AN AUTOMATED PROCESS Three instructions from the Before — the first two decode and read the wrong base register; the third does not decode: angr/vex master `e3062871`After — with this change |
fb71d75 to
91c88c3
Compare
The 16-bit r/m field selects a different register set from the 32-bit one -- r/m 4 is (%si), not (%esp)+SIB -- and disAMode16 never applied that table. It passed the raw r/m value to getIReg, so (%si) read %sp, (%di) read %bp, (%bp) read %si and (%bx) read %di, on every mod value. The four base+index forms (%bx,%si), (%bx,%di), (%bp,%si) and (%bp,%di) had no implementation at all: mod 0 and mod 1 hit a "TODO" vpanic and mod 2 had no case. A negative 8-bit displacement tripped mkU16's assertion, because getSDisp8 sign-extends to 32 bits, and any segment override reached handleSegOverride as an Ity_I16 where the address argument has to be 32 bits wide, so with one in front no form decoded at all. Without a segment override, only mod 0 r/m 6 -- the bare 16-bit literal address -- came out right. The other eleven decodable forms produced plausible IR reading the wrong register, with no vpanic and no complaint. lengthAMode dispatched on protected_mode while disAMode dispatches on current_sz_addr, so in protected mode a 0x67-prefixed amode was decoded 16-bit and measured 32-bit. Everything that finds its immediate with lengthAMode -- the Grp1/2/3/5/8 extensions, SHLD/SHRD, the FPU escapes -- then read that immediate from the wrong byte. lengthAMode16 itself gave the wrong length for 17 of the 32 mod/rm combinations, and that was not latent: in real mode protected_mode is false, so lengthAMode16 ran for every instruction with no prefix at all, and `83 3e 20 df 00` -- cmpw $0,(0xdf20) -- took 0xffdf for its immediate instead of 0. Rewrite disAMode16 and lengthAMode16 around mod and r/m, widen the 16-bit effective address before the segment base is added, and dispatch lengthAMode on the address size. disAMode_copy2tmp goes back to a plain copy, since its only 16-bit caller now widens its own result. What a 16-bit address size means for the instructions that take no ModRM byte is untouched: MOVS and friends address off %esi/%edi rather than %si/%di, LOOP and JCXZ count in %ecx, XLAT indexes off %ebx, the A0-A3 moffs forms take a 32-bit offset, and in real mode PUSH and POP address the stack through 32-bit %esp. guest_amd64_toIR.c handles the prefixed cases through haveASO and this front end has no equivalent, before or after. Separate change.
91c88c3 to
5e25551
Compare
THIS MESSAGE WAS GENERATED BY AN AUTOMATED PROCESS
Problem
cmp dx, word ptr [di]reads%bp. This is3b 15at0xf9c98intests/i386/bios.bin.elf, the SeaBIOS imageangr/binariestracks, lifted in the real mode the BIOS runs in:It does not fault, does not decline, and returns
Ijk_Boringwith the correct instruction length. Of the 24 memory forms of a 16-bit ModRM byte, one is right; eleven decode and read the wrong base register like this, and twelve refuse outright with avpanic. Put a segment override in front and all 24 refuse.The bytes reported in 2017 at angr/pyvex#80 still show it:
67 20 74 6fisand %dh, 0x6f(%si), and on master it lifts cleanly reading offset 24, which is%sp. Only the sanity-check failure that issue was closed on is gone.Both routes into this code are live. One is the
0x67address-size override. The other has no prefix at all:disInstr_X86_WRKsetscurrent_sz_addr = 2for every instruction whenarchinfo->x86_cr0 & 1is clear, so in real mode every memory ModRM takes this path. Over the 4,323 instructions of that BIOS image's.textthat do, 3,521 refuse, 687 compute the address from the wrong registers, 66 come out the wrong length, 2 emit no load or store to check, and 47 show no defect under those checks -- one of which takes the wrong immediate anyway.Root cause
disAMode16squeezes mod and r/m into a five-bit key and hands the r/m half straight togetIReg:getIRegnumbers registersax cx dx bx sp bp si di, but a 16-bit r/m field means(%bx,%si) (%bx,%di) (%bp,%si) (%bp,%di) (%si) (%di) (%bp) (%bx). So(%si)reads%sp,(%di)reads%bp,(%bp)reads%siand(%bx)reads%di.The four base+index forms have no implementation: r/m 0-3 hit
vpanic("TODO disAMode16 1")at mod 0 andvpanic("TODO disAMode16 2")at mod 1, and mod 2 falls into thedefault. A negative 8-bit displacement failsmkU16'svassert(i < 65536), sincegetSDisp8sign-extends to 32 bits, and a segment override reacheshandleSegOverrideas anIty_I16where a 32-bit address is wanted.lengthAModedispatches onprotected_modewhiledisAModedispatches oncurrent_sz_addr, and those differ whenever a0x67prefix is in play, so such an amode is decoded 16-bit and measured 32-bit. Everything that finds its immediate throughlengthAMode-- Grp1/2/3/5/8,SHLD/SHRD, the FPU escapes -- then reads it from the wrong byte:67 83 06 34 12 05isaddl $5, 0x1234and lifts witht1 = 0x00000034, the low half of the displacement.lengthAMode16is separately wrong for 17 of the 32 mod/rm combinations, and that is not latent either: in real modeprotected_modeis false, so it runs with no prefix at all, and83 3e 20 df 00--cmpw $0, 0xdf20, at0xfe05din the same BIOS image -- takes0xffdffor its immediate instead of0.Fix
Rewrite
disAMode16andlengthAMode16aroundmodandr/m, with the 16-bit r/m table in one place, and dispatchlengthAModeoncurrent_sz_addrso it agrees withdisAMode. The displacement is masked back to 16 bits and the address is widened to 32 before the segment base is added, which is where the wrap belongs;disAMode_copy2tmpbecomes a plain copy again. The same instruction at the same address:What a 16-bit address size means for the instructions that take no ModRM byte is deliberately not touched.
MOVS,CMPS,STOS,LODSandSCASstill address off%esi/%edirather than%si/%di,LOOPandJCXZstill count in%ecx,XLATstill indexes off%ebx, theA0-A3moffs forms still take a 32-bit offset, and in real modePUSHandPOPstill address the stack through 32-bit%esp.guest_amd64_toIR.chandles the prefixed cases throughhaveASO; this front end has no equivalent, before or after.Testing
The regression lands with the consumer in
angr/pyvex, which pins itsvexsubmodule to this head: it walks all 24 memory forms of the 16-bit ModRM byte and asserts each reads the registers the r/m table names, then covers the negative displacement, the segment override and the immediate. Five of its seven tests fail on the merge base.An exhaustive differential over every
(0x67?, 0x0F?, opcode, ModRM)combination, 262,144 lifts per side, run in both processor modes: in protected mode, where only the prefix reaches this code, all 130,050 cases carrying no0x67byte anywhere are byte-identical on both sides, and 42,962 of the 132,094 that do carry one differ. In real mode, where every memory ModRM reaches it, 32,897 of those same 130,050 differ. On the BIOS image above, refusals fall from 3,521 to 470 and instructions with no detectable defect rise from 47 to 3,719 out of 4,323, with nothing that decoded before refusing now. Validation: #97 (comment)🤖 Generated with Claude Code
session: sharpen