Skip to content

riscv64: fix remu (signed->unsigned) and remw/remuw dividend width - #83

Open
DORA-B wants to merge 1 commit into
angr:masterfrom
DORA-B:fix/riscv64_rem
Open

riscv64: fix remu (signed->unsigned) and remw/remuw dividend width#83
DORA-B wants to merge 1 commit into
angr:masterfrom
DORA-B:fix/riscv64_rem

Conversation

@DORA-B

@DORA-B DORA-B commented Jul 26, 2026

Copy link
Copy Markdown

The RV64M remainder lifters were wrong for three of the four cases:

  • remu (OP, funct3=0b111) used Iop_DivModS64to64 -- the signed divmod, identical to rem -- so it returned the SIGNED remainder. Wrong whenever the signed and unsigned remainders differ (~60% of random operand pairs). VEX has no Iop_DivModU64to64, so this zero-extends rs1 to 128 bits and uses the unsigned 128/64 divmod (Iop_DivModU128to64); its high 64 bits are the unsigned remainder.

  • remw / remuw (OP-32, funct3=0b110 / 0b111) passed getIReg64(rs1), the full 64-bit register, as the dividend of a 64->32 divmod. These operate on rs1[31:0], so the dividend must be the sign/zero-extension of the low 32 bits, exactly as divw/divuw already do via getIReg32(rs1). Fixed to feed 32Sto64 / 32Uto64 of getIReg32(rs1).

div/divu/divw/divuw and 64-bit rem were already correct. Verified by rebuilding pyvex against the patched VEX: all 8 div/rem ops match the RISC-V Unprivileged ISA (M extension) and the Sail formal model over 1500 random + edge inputs (0 mismatch); previously remu ~60% wrong and remw/remuw ~100% wrong.

Reproduction (no build needed to observe the wrong IR)

import angr, claripy, archinfo, logging
[logging.getLogger(n).setLevel(logging.CRITICAL) for n in ('angr','pyvex','cle')]
A = archinfo.ArchRISCV64()

def run(word, rs1, rs2):
    p = angr.load_shellcode(word.to_bytes(4,'little'), arch=A)
    st = p.factory.blank_state(addr=0)
    st.regs.x6 = claripy.BVV(rs1,64); st.regs.x7 = claripy.BVV(rs2,64)
    s = st.step(num_inst=1).successors[0]
    return st.solver.eval(s.regs.x5) & ((1<<64)-1)

# remu x5,x6,x7   (a < b unsigned -> result must be a)
a, b = 0x4fa31c34c405b54b, 0xbeb833f853ce5645
print(hex(run(0x027372b3, a, b)))   # angr: 0x0e5b502d17d40b90 (= a+b, SIGNED rem)
print(hex(a % b))                   # spec: 0x4fa31c34c405b54b

# remw x5,x6,x7   (operates on low 32 bits)
a, b = 0xdca4f47306fdb803, 0xafdb609e4803b2a6
print(hex(run(0x027362bb, a, b)))   # angr: 0xffffffffb9283f0b (uses full 64-bit a)
# spec: sext32( int32(0x06fdb803) rem int32(0x4803b2a6) ) = 0x06fdb803

The lifted IR shows the defect directly:

remu:  t1 = DivModS64to64(rs1, rs2)   ; <- signed
remw:  t2 = DivModS64to32(rs1_FULL64, 64to32(rs2))   ; <- 64-bit dividend
remuw: t2 = DivModU64to32(rs1_FULL64, 64to32(rs2))   ; <- 64-bit dividend

The RV64M remainder lifters were wrong for three of the four cases:

* remu (OP, funct3=0b111) used Iop_DivModS64to64 -- the signed divmod,
  identical to rem -- so it returned the SIGNED remainder. Wrong whenever the
  signed and unsigned remainders differ (~60% of random operand pairs). VEX has
  no Iop_DivModU64to64, so this zero-extends rs1 to 128 bits and uses the
  unsigned 128/64 divmod (Iop_DivModU128to64); its high 64 bits are the
  unsigned remainder.

* remw / remuw (OP-32, funct3=0b110 / 0b111) passed getIReg64(rs1), the full
  64-bit register, as the dividend of a 64->32 divmod. These operate on
  rs1[31:0], so the dividend must be the sign/zero-extension of the low 32
  bits, exactly as divw/divuw already do via getIReg32(rs1). Fixed to feed
  32Sto64 / 32Uto64 of getIReg32(rs1).

div/divu/divw/divuw and 64-bit rem were already correct. Verified by rebuilding
pyvex against the patched VEX: all 8 div/rem ops match the RISC-V Unprivileged
ISA (M extension) and the Sail formal model over 1500 random + edge inputs
(0 mismatch); previously remu ~60% wrong and remw/remuw ~100% wrong.
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