Skip to content

rvfi_mem_rmask is non-zero on instructions that make no memory access #2476

Description

@martin-robert-fink

Hello — first, thanks for Ibex. We've been using it as the real-core reference for some RVFI tooling precisely because it's clean, well documented, and easy to build, and it executed every test program we threw at it exactly right. This is a reporting issue rather than an execution one.

Summary

On any retirement whose instruction performs no memory access, Ibex reports rvfi_mem_rmask = 4'b1111, alongside an rvfi_mem_addr that is not a memory address and an rvfi_mem_rdata held over from an earlier load. The trace therefore makes a positive claim that four bytes were read from a given address, for instructions that issued no memory request.

Where it comes from

All line numbers against master as of 2026-08-20.

  1. rtl/ibex_core.sv:1653 drives the read mask from the store flag alone:

    rvfi_stage_mem_rmask[i] <= data_we_o ? 4'b0000 : rvfi_mem_mask_int;

    Nothing here is conditioned on whether the instruction actually issued a memory request, so every non-store takes the rvfi_mem_mask_int branch.

  2. rtl/ibex_core.sv:1792-1799 derives that mask from lsu_type alone:

    unique case (lsu_type)
      2'b00:   rvfi_mem_mask_int = 4'b1111;
      2'b01:   rvfi_mem_mask_int = 4'b0011;
      2'b10:   rvfi_mem_mask_int = 4'b0001;
      default: rvfi_mem_mask_int = 4'b0000;
    endcase
  3. rtl/ibex_decoder.sv:227 sets data_type_o = 2'b00 as the unconditional default in the decoder's default-assignment block, and only the load/store cases override it. So for every non-memory instruction, lsu_type is the word encoding and the mask is 4'b1111.

The two companion fields are stale rather than zeroed, which is what turns this from a cosmetic issue into a checkable one:

  • rtl/ibex_core.sv:1761-1769 sets rvfi_mem_addr_d = alu_adder_result_ex on the first ID cycle of every instruction, so on an addi the reported address is the ALU result.
  • rtl/ibex_core.sv:1771-1778 holds rvfi_mem_rdata_d at its previous value unless lsu_resp_valid, so the reported data is whatever the last load returned.

Stores are correct — data_we_o gates rmask to zero and the write mask carries the real size.

Why this is a spec deviation

docs/source/rvfi.rst in riscv-formal:

For memory operations (rvfi_mem_rmask and/or rvfi_mem_wmask are non-zero), rvfi_mem_addr holds the accessed memory location.

rvfi_mem_rmask is a bitmask that specifies which bytes in rvfi_mem_rdata contain valid read data from rvfi_mem_addr.

A non-zero mask is what identifies a retirement as a memory operation, and it asserts that the named bytes of rvfi_mem_rdata are valid data from rvfi_mem_addr. On a non-memory instruction none of those three fields carries meaning, so the claim can't hold.

I want to be fair about the counter-argument, because there is one. checks/rvfi_insn_check.sv only asserts one direction — if (spec_mem_rmask[i]) assert(mem_rmask[i]); — and never the converse, and the CSR section of the spec explicitly permits activating more rmask bits than an instruction requires. So a superset mask is tolerated in general. The condition attached to that allowance is "as long as the reported bits correctly reflect the machine state," and that's the part that fails here: the address isn't a memory address and the data was never read from it.

It's reachable by riscv-formal's own check

checks/rvfi_dmem_check.sv picks an address and asserts, for any retirement reporting that address:

if (enable && rvfi_mem_rmask[channel_idx*`RISCV_FORMAL_XLEN/8 + i] && dmem_written[i])
    assert(dmem_shadow[i*8 +: 8] == rvfi_mem_rdata[i*8 +: 8]);

Since dmem_addr is rand_const, the solver is free to choose an address that a non-memory instruction's stale rvfi_mem_addr happens to alias. With rmask asserted and rvfi_mem_rdata holding an unrelated earlier value, that assertion has a counterexample. I haven't run the proof myself, so I'd treat that as a strong expectation rather than a result — but it does mean this is inside the scope of the existing checks rather than a matter of interpretation.

I'd note in the same breath that doc/03_reference/rvfi.rst already says Ibex "is not yet formally verified," and as far as I can tell dv/uvm only wires the RVFI signals into core_ibex_rvfi_if while the Spike co-simulation compares architectural state rather than RVFI reporting. So this looks like a gap that nothing currently exercises, not a regression.

Observed

Verilator 5.050, ibex_top at 3250d99482f1963891ef1cf19356eeaeeaa71d30, upstream small config plus +define+RVFI, running a 26-instruction RV32I program (arithmetic, word/byte/halfword load-store pairs, a taken forward beq, a backward bne loop, a trapping ecall). 20 of the 26 retirements report a non-zero rvfi_mem_rmask with no memory access; the 6 genuine accesses all report correctly. The RTL is unmodified and the testbench tie-offs are copied from examples/simple_system/.

Happy to share the program listing, the build recipe or the captured waveform if any of that is useful — just say which.

Possible fix

Deliberately vague on the sampling point, since you'll know better than I do where it belongs — but the shape seems to be qualifying the masks with the LSU request rather than with data_we_o alone. lsu_req is already in scope in ibex_core.sv:

rvfi_stage_mem_rmask[i] <= (lsu_req && !data_we_o) ? rvfi_mem_mask_int : 4'b0000;
rvfi_stage_mem_wmask[i] <= (lsu_req &&  data_we_o) ? rvfi_mem_mask_int : 4'b0000;

though it may be cleaner to capture the qualifier at instr_first_cycle_id alongside rvfi_mem_addr_d, so all three fields are sampled consistently. Zeroing rvfi_mem_addr and rvfi_mem_rdata when there's no access would also make traces easier to read, but that's taste rather than spec.


Found with the RVFI Commit Inspector in WaveCrux, which flags RVFI traces against the specification. Everything above is checkable from the RTL and the riscv-formal sources without it.

Activity

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

Metadata

Metadata

Assignees

No one assigned

    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