RISC-V emitter: Fix Zcb byte offsets, and FLI of a NaN

c.lbu and c.sb keep the offset's bit 0 in bit 6 and bit 1 in bit 5, so
offsets 1 and 2 were swapped, and the JIT loaded and stored the wrong byte.

FLI's only NaN is the canonical one, but CanFLI took any NaN for it: the
pattern went through a float to double conversion that quieted it, which on
RISC-V also drops the sign and payload. vfim of a NaN lost its sign.

Neither is detected on real hardware yet (we never set Zcb or Zfa), and the
CPU fuzzer found both with the extensions forced on under qemu.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Henrik RydgårdandClaude Opus 5.5 committed 2026-10-15 14:41:51 +02:00
1 parent 253029c083
commit ae32504fa1
3 files changed
+57 -5

No files matched your search

+21 -2
View File
@@ -941,13 +941,17 @@ static inline u16 EncodeCJ(Opcode16 op, s32 simm12, Funct3 funct3) {
static inline u16 EncodeCLB(Opcode16 op, RiscCReg rd, u8 uimm2, RiscCReg rs1, Funct6 funct6) {
_assert_msg_(SupportsCompressed(), "Compressed instructions unsupported");
_assert_msg_((uimm2 & 3) == uimm2, "CLB immediate must be 2 bit: %d", uimm2);
return (u16)op | ((u16)rd << 2) | ((u16)uimm2 << 5) | ((u16)rs1 << 7) | ((u16)funct6 << 10);
// uimm[0] goes in bit 6 and uimm[1] in bit 5.
u16 imm = ((uimm2 & 1) << 1) | (uimm2 >> 1);
return (u16)op | ((u16)rd << 2) | (imm << 5) | ((u16)rs1 << 7) | ((u16)funct6 << 10);
}
static inline u16 EncodeCSB(Opcode16 op, RiscCReg rs2, u8 uimm2, RiscCReg rs1, Funct6 funct6) {
_assert_msg_(SupportsCompressed(), "Compressed instructions unsupported");
_assert_msg_((uimm2 & 3) == uimm2, "CSB immediate must be 2 bit: %d", uimm2);
return (u16)op | ((u16)rs2 << 2) | ((u16)uimm2 << 5) | ((u16)rs1 << 7) | ((u16)funct6 << 10);
// uimm[0] goes in bit 6 and uimm[1] in bit 5.
u16 imm = ((uimm2 & 1) << 1) | (uimm2 >> 1);
return (u16)op | ((u16)rs2 << 2) | (imm << 5) | ((u16)rs1 << 7) | ((u16)funct6 << 10);
}
static inline u16 EncodeCLH(Opcode16 op, RiscCReg rd, u8 uimm1, bool funct1, RiscCReg rs1, Funct6 funct6) {
@@ -2414,6 +2418,12 @@ static RiscVReg EncodeFLImm(int bits, double v) {
bool RiscVEmitter::CanFLI(int bits, double v) const {
if (!SupportsFloatExtra())
return false;
// FLI's only NaN is the canonical one. Any other would match it, since converting a NaN
// (to float in EncodeFLImm) may quiet it or drop its payload.
uint64_t dbits;
memcpy(&dbits, &v, sizeof(dbits));
if (v != v && dbits != 0x7FF8000000000000ULL)
return false;
if (bits == 16 && !SupportsFloatHalf())
return false;
if (bits > FloatBitsSupported())
@@ -2427,6 +2437,15 @@ bool RiscVEmitter::CanFLI(int bits, uint32_t pattern) const {
return CanFLI(bits, f);
}
bool RiscVEmitter::CanFLI(int bits, float v) const {
// Checked here as well, since converting to double can already change a NaN.
uint32_t fbits;
memcpy(&fbits, &v, sizeof(fbits));
if (v != v && fbits != 0x7FC00000)
return false;
return CanFLI(bits, (double)v);
}
void RiscVEmitter::FLI(int bits, RiscVReg rd, double v) {
_assert_msg_(SupportsFloatExtra(), "%s cannot be used without Zfa", __func__);
_assert_msg_(bits <= FloatBitsSupported(), "FLI cannot be used for %d bits, only %d/%d supported", bits, BitsSupported(), FloatBitsSupported());
+1 -3
View File
@@ -435,9 +435,7 @@ public:
// Additional floating point (Zfa.)
bool CanFLI(int bits, double v) const;
bool CanFLI(int bits, uint32_t pattern) const;
bool CanFLI(int bits, float v) const {
return CanFLI(bits, (double)v);
}
bool CanFLI(int bits, float v) const;
void FLI(int bits, RiscVReg rd, double v);
void FLI(int bits, RiscVReg rd, uint32_t pattern);
void FLI(int bits, RiscVReg rd, float v) {
+35
View File
@@ -70,5 +70,40 @@ bool TestRiscVEmitter() {
EXPECT_EQ_HEX(code[i], expected[i]);
}
// Zcb loads and stores, which split the offset bits differently by size.
cpu_info.RiscV_Zcb = true;
u16 *code16 = (u16 *)code;
emitter.SetCodePointer((u8 *)code, (u8 *)code);
emitter.C_LBU(X10, X11, 1);
emitter.C_LBU(X10, X11, 2);
emitter.C_SB(X10, X11, 1);
emitter.C_SB(X10, X11, 2);
emitter.C_LHU(X10, X11, 2);
emitter.C_SH(X10, X11, 2);
// From GNU as.
static constexpr uint16_t expectedZcb[] = {
0x81c8,
0x81a8,
0x89c8,
0x89a8,
0x85a8,
0x8da8,
};
len = (u16 *)emitter.GetWritableCodePtr() - code16;
EXPECT_EQ_INT(len, ARRAY_SIZE(expectedZcb));
for (ptrdiff_t i = 0; i < len; ++i) {
EXPECT_EQ_HEX(code16[i], expectedZcb[i]);
}
// FLI has only the canonical NaN.
cpu_info.RiscV_Zfa = true;
EXPECT_TRUE(emitter.CanFLI(32, (uint32_t)0x7FC00000));
EXPECT_FALSE(emitter.CanFLI(32, (uint32_t)0xFFC00000));
EXPECT_FALSE(emitter.CanFLI(32, (uint32_t)0x7F800001));
EXPECT_TRUE(emitter.CanFLI(32, 1.0f));
return true;
}