Skip to content

Issue in M31 Field Arithmetic in xor_constraint Constraint Verification #11

@BubeAudit

Description

@BubeAudit

Summary

The xor_constraint constraint verification function in rv32im.rs uses M31 (Mersenne prime field mod 2³¹ - 1) arithmetic to validate 32-bit RISC-V XOR operations. The problem stems from a fundamental mathematical incompatibility: attempting to represent all 32-bit integers (0 to 2³² - 1) within a field that can only uniquely represent values 0 to 2³¹ - 2. This creates exploitable collisions that allow attackers to forge valid-looking proofs for incorrect XOR computations. Also, this issue persists in and_constraint and or_constraint, but here is considered only the xor_constraint function.

Vulnerability Details

The code attempts to reconstruct 32-bit integers from their bit decomposition within M31:

#[inline]
pub fn xor_constraint(row: &CpuTraceRow) -> M31 {
    if row.is_xor == M31::ZERO {
        return M31::ZERO;
    }

    let two_16 = M31::new(1 << 16);
    let rs1_full = row.rs1_val_lo + row.rs1_val_hi * two_16;
    let rs2_full = row.rs2_val_lo + row.rs2_val_hi * two_16;
    let rd_full = row.rd_val_lo + row.rd_val_hi * two_16;

    let mut rs1_reconstructed = M31::ZERO;
    let mut rs2_reconstructed = M31::ZERO;
    let mut rd_reconstructed = M31::ZERO;
    let mut xor_check = M31::ZERO;

@>  for i in 0..31 {
@>      let pow2 = M31::new(1 << i);
@>      rs1_reconstructed += row.rs1_bits[i] * pow2;
        rs2_reconstructed += row.rs2_bits[i] * pow2;
        rd_reconstructed += row.xor_bits[i] * pow2;
        // XOR logic: xor_bit = a + b - 2ab
        let expected_xor = row.rs1_bits[i] + row.rs2_bits[i] - M31::new(2) * row.rs1_bits[i] * row.rs2_bits[i];
        xor_check += row.xor_bits[i] - expected_xor;
    }
    // Bit 31
@>  let pow2_30 = M31::new(1 << 30);
@>  rs1_reconstructed += row.rs1_bits[31] * pow2_30 * M31::new(2);
    rs2_reconstructed += row.rs2_bits[31] * pow2_30 * M31::new(2);
    rd_reconstructed += row.xor_bits[31] * pow2_30 * M31::new(2);
    let expected_xor = row.rs1_bits[31] + row.rs2_bits[31] - M31::new(2) * row.rs1_bits[31] * row.rs2_bits[31];
    xor_check += row.xor_bits[31] - expected_xor;

    row.is_xor * (
        (rs1_full - rs1_reconstructed) +
        (rs2_full - rs2_reconstructed) +
        xor_check +
        (rd_full - rd_reconstructed)
    )
}

For bit 31, the code computes:
2³⁰ × 2 = 2³¹ in field arithmetic, but 2³¹ mod (2³¹ - 1) = 1. This means bit 31 contributes a weight of 1, not 2³¹ as intended in integer arithmetic.

Mathematical Analysis

Integer reconstruction (intended):
value = Σ(bit[i] × 2^i) for i=0 to 31 = bit[0]×1 + bit[1]×2 + ... + bit[31]×2,147,483,648
Actual M31 reconstruction:
value_m31 = Σ(bit[i] × 2^i) for i=0 to 30, plus bit[31]×1 = bit[0]×1 + bit[1]×2 + ... + bit[30]×1,073,741,824 + bit[31]×1

Multiple distinct 32-bit values collapse to the same M31 representation:

32-bit Value Binary Representation M31 Value
0x00000001 00000000...00000001 1
0x80000000 10000000...00000000 1
0x80000001 10000000...00000001 2
0x00000002 00000000...00000010 2

General pattern:
value (32-bit) ≡ (value mod (2³¹ - 1)) (in M31). Values that differ by 2³¹ - 1 collide: 0 and 2,147,483,647, 1 and 2,147,483,648, etc.

The user can claim rs1 = 0x80000000, but providing bits for rs1 = [0,0,...,0,1] (appears to be bit 31 set). M31 reconstruction passes: 0 + 0 + ... + 1 = 1
Also, it can be claimed rs1 = 0x00000001, but providing bits for rs1 = [1,0,...,0,0] (appears to be bit 0 set). M31 reconstruction passes: 1 + 0 + ... + 0 = 1. Both pass the constraint rs1_full - rs1_reconstructed = 0 in M31, despite representing different integers.

Recommendation

Only use bits 0-30, treating RISC-V operations as signed 31-bit.

Metadata

Metadata

Labels

No labels
No labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions