Skip to content

riscv64: widen feq/flt results before assigning them to an Ity_I32 temp - #95

Open
zardus wants to merge 1 commit into
masterfrom
fix/riscv-fcmp-i1-to-i32
Open

riscv64: widen feq/flt results before assigning them to an Ity_I32 temp#95
zardus wants to merge 1 commit into
masterfrom
fix/riscv-fcmp-i1-to-i32

Conversation

@zardus

@zardus zardus commented Aug 29, 2026

Copy link
Copy Markdown
Member

THIS MESSAGE WAS GENERATED BY AN AUTOMATED PROCESS

Problem

feq.s, flt.s, feq.d and flt.d emit IR that libVEX's own sanity checker
rejects, so every block containing one panics instead of lifting. Lifting the
four bytes d3a5d8a1feq.s a1, fa7, ft9, at 0x402390 in
binaries/tests/riscv/autotalent-autotalent.so — gives:

ERROR = IRStmt.Put.Tmp: tmp and expr do not match
IN STATEMENT:  t3 = CmpEQ32(t2,0x40:I32)
vex: the `impossible' happened:  sanityCheckFail: exiting due to bad IR

Consumers never see that message. pyvex converts the panic into a fabricated
zero-length Ijk_NoDecode block, so the bytes are reported as undecodable and
the enclosing function is cut short at the comparison. Over the six RISC-V
objects committed to angr/binaries this silently loses 50 instructions,
25 of them in autotalent-autotalent.so alone.

Root cause

dis_RV64F and dis_RV64D both hold the comparison result in
IRTemp res = newTemp(irsb, Ity_I32) and then assign the Ity_I1 output of
Iop_CmpEQ32 straight into it:

case 0b010:
   assign(irsb, res,
          binop(Iop_CmpEQ32, mkexpr(cmp), mkU32(Ircr_EQ)));
   break;

The fle arm two cases below is already correct — it wraps both comparisons in
unop(Iop_1Uto32, ...) before combining them with Iop_Or32 — which is why
fle.s and fle.d lift while feq and flt do not.

This is a regression from 7576d04 ("riscv: fix instructions that use the
unsupported Ity_1 data type"), which widened res to Ity_I32 and converted
only the fle arm.

Fix

Wrap the feq and flt arms in unop(Iop_1Uto32, ...), matching the form the
fle arm already uses. Four hunks, two in dis_RV64F and two in dis_RV64D,
in priv/guest_riscv64_toIR.c. feq.s a1, fa7, ft9 then lifts to a normal
four-byte Ijk_Boring block of 13 statements.

Nothing else changes: the fle arms are untouched, and the rd == 0 path never
reached the bad assignment.

Testing

angr/vex has no test suite of its own, so the regression lives in the paired
pyvex pull request, which pins this commit and asserts that every feq/flt
encoding in a committed gcc-built RISC-V object lifts to a real block. It fails
against vex master with pyvex failed to decode 25 float comparisons and passes
here. A capstone linear disassembly of .text re-lifted instruction by
instruction puts the six RISC-V fixtures in angr/binaries at 50 unliftable
instructions before this change and 0 after.

Validation: #95 (comment)

sync: angr/pyvex#578

session: sharpen

dis_RV64F and dis_RV64D allocate the result of feq.s/flt.s and feq.d/flt.d
in an Ity_I32 temporary but assign the Ity_I1 output of Iop_CmpEQ32 to it
directly, so the IR sanity checker rejects the block with

  IRStmt.Put.Tmp: tmp and expr do not match

and libVEX panics. The fle arm of the same switch already wraps its
comparisons in Iop_1Uto32; do the same for feq and flt.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@zardus

zardus commented Aug 29, 2026

Copy link
Copy Markdown
Member Author

THIS MESSAGE WAS GENERATED BY AN AUTOMATED PROCESS

Full lift of the feq.s a1, fa7, ft9 at 0x402390 in
binaries/tests/riscv/autotalent-autotalent.so, before and after this change.
Both sides were built from the same pyvex tree with the same toolchain, and
differ only in the vex submodule commit; the libpyvex.so each process
actually mapped is hashed in the capture.

data = open("binaries/tests/riscv/autotalent-autotalent.so", "rb").read()[0x2390:0x2394]
irsb = pyvex.lift(data, 0x402390, pyvex.ARCH_RISCV64_LE)
print("size=%d jumpkind=%s statements=%d" % (irsb.size, irsb.jumpkind, len(irsb.statements)))
irsb.pp()

Before — libVEX panics on its own sanity check and pyvex hands back a
fabricated zero-length block, so the instruction reads as undecodable:

vex 875f7c9
libpyvex.so sha256: 3373c845b0470f2f698e900aae5bf34fb62a39d45f8d2d19a5c32fbf4de8608c
bytes at 0x402390: d3a5d8a1
size=0 jumpkind=Ijk_NoDecode statements=0
IRSB {
   

   NEXT: PUT(pc) = 0x0000000000402390; Ijk_NoDecode
}

After — the comparison is widened at t4 = 1Uto32(t5), the block type
checks, and execution falls through to the next instruction:

vex c2409bb
libpyvex.so sha256: 5c7aedb2f9c2644727c11da6e6269624ace1e9ea2da022e0aef0abcec18d8d6b
bytes at 0x402390: d3a5d8a1
size=4 jumpkind=Ijk_Boring statements=13
IRSB {
   t0:Ity_F32 t1:Ity_F32 t2:Ity_I32 t3:Ity_I32 t4:Ity_I32 t5:Ity_I1 t6:Ity_I64 t7:Ity_I32 t8:Ity_I32 t9:Ity_I32 t10:Ity_I32 t11:Ity_I64

   00 | ------ IMark(0x402390, 4, 0) ------
   01 | t0 = GET:F32(f17)
   02 | t1 = GET:F32(f29)
   03 | t2 = CmpF32(t0,t1)
   04 | t5 = CmpEQ32(t2,0x00000040)
   05 | t4 = 1Uto32(t5)
   06 | t6 = 32Uto64(t4)
   07 | PUT(x11) = t6
   08 | t9 = riscv64g_calculate_fflags_feq_s(t0,t1):Ity_I32
   09 | t8 = And32(t9,0x0000001f)
   10 | t10 = GET:I32(536)
   11 | t7 = Or32(t10,t8)
   12 | PUT(536) = t7
   NEXT: PUT(pc) = 0x0000000000402394; Ijk_Boring
}

@zardus

zardus commented Aug 29, 2026

Copy link
Copy Markdown
Member Author

THIS MESSAGE WAS GENERATED BY AN AUTOMATED PROCESS

Validation record for head c2409bb4561cd4f5219419a42c46e5f6818f15ed against baseline 875f7c9a5f6be621b4f000c29c016e15ddf32207.

angr/vex has no test suite, so the evidence runs through the paired pyvex pull request, whose git ls-tree HEAD vex equals this head.

  • Regression: pytest tests/test_riscv_fcmp.py in pyvex — fails against vex 875f7c9 with AssertionError: pyvex failed to decode 25 float comparisons, passes against this head
  • Full pyvex suite: pytest tests — 1 failed, 64 passed against 875f7c9; 65 passed against this head
  • Build: cmake -G Ninja && ninja with gcc 15.3.0 — the same four warnings on both sides, none of them from guest_riscv64_toIR.c
  • Both sides built from the same pyvex tree with the same compiler, differing only in the vex submodule commit; libpyvex.so sha256 3373c845b0470f2f698e900aae5bf34fb62a39d45f8d2d19a5c32fbf4de8608c (baseline) and 5c7aedb2f9c2644727c11da6e6269624ace1e9ea2da022e0aef0abcec18d8d6b (head)

Instruction census over every RISC-V object in angr/binaries at tests/riscv: linear capstone disassembly of .text, each instruction re-lifted on its own through pyvex, counting those that come back size == 0 or Ijk_NoDecode.

Object Instructions Unliftable, baseline Unliftable, head
abgate-libabGateQt.so 2,893 1 0
asterisk-libasteriskpj.so.2 286,165 23 0
autotalent-autotalent.so 5,010 25 0
borgbackup2-chunker.cpython-312-riscv64-linux-gnu.so 31,450 1 0
libdevel-leak-perl-Leak.so 828 0 0
server_eapp.eapp_riscv 23,364 0 0

Every one of the 50 is a feq or flt: 33 flt.s, 7 feq.s, 4 flt.d, 6 feq.d. No fle appears, and no instruction liftable at the baseline stops lifting at the head.

Caveats: the complete workspace gate did not run — this machine's shared toolchain is pinned by other work, and entering the devshell would reinstall its editables. What ran is pyvex's own suite against both vex revisions, the build, and the census. valgrind's own regression suite was not run; this repository carries no test harness for it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant