Skip to content

[BUG] RV32E illegal CSRRW modifies mscratch before taking the illegal-instruction exception #2554

Closed as duplicate

Description

@huntersubmit

Summary

In an Ibex configuration with RV32E=1 and WritebackStage=0, a CSRRW instruction whose rd field specifies the unavailable register x16 is correctly reported as an illegal instruction, but its CSR write is not suppressed.

Consequently, after entering the illegal-instruction handler, the processor observes that mscratch has already changed from its sentinel value to zero.

Although Ibex raises an Illegal Instruction exception, the same faulting instruction still leaves an architecturally visible CSR side effect.

The RISC-V RV32E specification defines encodings that specify x16 through x31 as reserved, and the base specification defines the decoding behavior of reserved instructions as UNSPECIFIED.

Therefore, the base ISA alone does not establish that this encoding must trap without side effects. This report asks to clarify whether Ibex intends the illegal_reg_16 check to produce a precise, side-effect-free trap. The RTL comment, Trap on illegal register addresses, together with the observed mcause=2, indicates that Ibex has at least chosen to handle this encoding through the illegal-instruction exception path.

This issue is similar to Issue 2553: illegal_reg_16 is incorporated into the final illegal-instruction signal only after the normal decoder has generated the instruction's side-effect control signals.

Tested version and configuration

  • Ibex commit: f4515520c0045bb8ef1e72d46ed82499a6ac5f18
  • Configuration: RV32E=1, WritebackStage=0, RegFile=ibex_pkg::RegFileFF
  • Simulation platform: ibex_simple_system with Verilator
  • Toolchain: riscv64-unknown-elf-gcc 14.2.0
  • FuseSoC:
$ fusesoc --version
2.4.3

Reproduction

test.s

  .option norvc

  .section .text.init
  .globl _start
_start:
  la t0, trap_handler
  csrw mtvec, t0

  # The sentinel must survive the illegal instruction.
  li t0, 0x11223344
  csrw mscratch, t0

faulting_instruction:
  # csrrw x16, mscratch, x0
  # x16 is unavailable in RV32E, so the encoding is reserved. This test checks Ibex's chosen illegal-trap behavior.
  .word 0x34001873

  # Reaching here means that no illegal-instruction trap was taken.
  li a0, 2
  j finish

  .section .trap, "ax"
  .balign 256
trap_handler:
  csrr t1, mcause
  li a0, 3
  li t2, 2
  bne t1, t2, finish

  # The candidate's expected precise-trap behavior preserves the sentinel.
  csrr t1, mscratch
  li t2, 0x11223344
  li a0, 1
  bne t1, t2, finish

  li a0, 0

finish:
  # 1: pass
  # 3: CSR side effect
  # 5: no trap
  # 7: wrong trap cause
  slli a0, a0, 1
  ori a0, a0, 1
  la t0, tohost
  sw a0, 0(t0)

  # Terminate the Ibex simple_system simulation.
  li t0, 0x20008
  sw a0, 0(t0)
1:
  j 1b

link.ld

OUTPUT_ARCH(riscv)
ENTRY(_start)

MEMORY {
  ram (rwx) : ORIGIN = 0x100000, LENGTH = 0x30000
}

SECTIONS {
  . = ORIGIN(ram);
  .vectors : { . = . + 0x80; } > ram
  .text : { *(.text.init) *(.text*) } > ram
  .trap : { *(.trap) } > ram
  .data : { *(.data*) } > ram
  .bss : { *(.bss*) *(COMMON) } > ram

  tohost = 0x12fff0;
  fromhost = 0x12fff8;
}

Compile the test:

riscv64-unknown-elf-gcc -march=rv32e_zicsr -mabi=ilp32e -nostdlib -nostartfiles -static -Wl,-T,link.ld -Wl,--build-id=none \
  -o ./build/test.elf ./test.s

riscv64-unknown-elf-objdump -d -M no-aliases \
  ./build/test.elf > ./build/test.dump

test.dump

./build/test.elf:     file format elf32-littleriscv

Disassembly of section .text:

00100080 <_start>:
  100080:	00000297          	auipc	t0,0x0
  100084:	08028293          	addi	t0,t0,128 # 100100 <trap_handler>
  100088:	30529073          	csrrw	zero,mtvec,t0
  10008c:	112232b7          	lui	t0,0x11223
  100090:	34428293          	addi	t0,t0,836 # 11223344 <fromhost+0x110f334c>
  100094:	34029073          	csrrw	zero,mscratch,t0

00100098 <faulting_instruction>:
  100098:	34001873          	.word	0x34001873
  10009c:	00200513          	addi	a0,zero,2
  1000a0:	0880006f          	jal	zero,100128 <finish>

Disassembly of section .trap:

00100100 <trap_handler>:
  100100:	34202373          	csrrs	t1,mcause,zero
  100104:	00300513          	addi	a0,zero,3
  100108:	00200393          	addi	t2,zero,2
  10010c:	00731e63          	bne	t1,t2,100128 <finish>
  100110:	34002373          	csrrs	t1,mscratch,zero
  100114:	112233b7          	lui	t2,0x11223
  100118:	34438393          	addi	t2,t2,836 # 11223344 <fromhost+0x110f334c>
  10011c:	00100513          	addi	a0,zero,1
  100120:	00731463          	bne	t1,t2,100128 <finish>
  100124:	00000513          	addi	a0,zero,0

00100128 <finish>:
  100128:	00151513          	slli	a0,a0,0x1
  10012c:	00156513          	ori	a0,a0,1
  100130:	00030297          	auipc	t0,0x30
  100134:	ec028293          	addi	t0,t0,-320 # 12fff0 <tohost>
  100138:	00a2a023          	sw	a0,0(t0)
  10013c:	000202b7          	lui	t0,0x20
  100140:	00828293          	addi	t0,t0,8 # 20008 <_start-0xe0078>
  100144:	00a2a023          	sw	a0,0(t0)
  100148:	0000006f          	jal	zero,100148 <finish+0x20>
  10014c:	00000013          	addi	zero,zero,0
  100150:	00000013          	addi	zero,zero,0
  100154:	00000013          	addi	zero,zero,0
  100158:	00000013          	addi	zero,zero,0
  10015c:	00000013          	addi	zero,zero,0
  100160:	00000013          	addi	zero,zero,0
  100164:	00000013          	addi	zero,zero,0
  100168:	00000013          	addi	zero,zero,0
  10016c:	00000013          	addi	zero,zero,0
  100170:	00000013          	addi	zero,zero,0
  100174:	00000013          	addi	zero,zero,0
  100178:	00000013          	addi	zero,zero,0
  10017c:	00000013          	addi	zero,zero,0
  100180:	00000013          	addi	zero,zero,0
  100184:	00000013          	addi	zero,zero,0
  100188:	00000013          	addi	zero,zero,0
  10018c:	00000013          	addi	zero,zero,0
  100190:	00000013          	addi	zero,zero,0
  100194:	00000013          	addi	zero,zero,0
  100198:	00000013          	addi	zero,zero,0
  10019c:	00000013          	addi	zero,zero,0
  1001a0:	00000013          	addi	zero,zero,0
  1001a4:	00000013          	addi	zero,zero,0
  1001a8:	00000013          	addi	zero,zero,0
  1001ac:	00000013          	addi	zero,zero,0
  1001b0:	00000013          	addi	zero,zero,0
  1001b4:	00000013          	addi	zero,zero,0
  1001b8:	00000013          	addi	zero,zero,0
  1001bc:	00000013          	addi	zero,zero,0
  1001c0:	00000013          	addi	zero,zero,0
  1001c4:	00000013          	addi	zero,zero,0
  1001c8:	00000013          	addi	zero,zero,0
  1001cc:	00000013          	addi	zero,zero,0
  1001d0:	00000013          	addi	zero,zero,0
  1001d4:	00000013          	addi	zero,zero,0
  1001d8:	00000013          	addi	zero,zero,0
  1001dc:	00000013          	addi	zero,zero,0
  1001e0:	00000013          	addi	zero,zero,0
  1001e4:	00000013          	addi	zero,zero,0
  1001e8:	00000013          	addi	zero,zero,0
  1001ec:	00000013          	addi	zero,zero,0
  1001f0:	00000013          	addi	zero,zero,0
  1001f4:	00000013          	addi	zero,zero,0
  1001f8:	00000013          	addi	zero,zero,0
  1001fc:	00000013          	addi	zero,zero,0
  100200:	00000013          	addi	zero,zero,0

Build the simulation model:

# repo
cd ibex

fusesoc --cores-root=. run --target=sim --setup --build \
  --build-root=build/configs/illegal_csr_side_effect \
  lowrisc:ibex:ibex_simple_system \
  --RV32E=1 --WritebackStage=0 --RegFile=ibex_pkg::RegFileFF

Run the simulation:

model=/path/to/ibex/build/configs/illegal_csr_side_effect/lowrisc_ibex_ibex_simple_system_0/sim-verilator/Vibex_simple_system

"${model}" --term-after-cycles=1000 --meminit=ram,"/path/to/build/test.elf" |& tee ibex.log

Expected result

The rd field of 0x34001873 is 16. Because RV32E does not provide x16, this encoding is reserved.

Ibex has chosen to raise an Illegal Instruction exception for this encoding. If this exception is intended to be a precise trap, the project presumably should not commit the faulting instruction's CSR side effect. The handler should therefore observe:

mcause = 2
mscratch = 0x11223344
tohost = 1

Actual Ibex result

ibex.log reports the illegal instruction:

Simulation running, end by pressing CTRL-c.
TOP.ibex_simple_system.u_top.u_ibex_tracer.unnamedblk2.unnamedblk3: Writing execution trace to trace_core_00000000.log
                  31: Illegal instruction (hart 0) at PC 0x00100098: 0x34001873
Terminating simulation by software request.

trace_core_00000000.log shows that the exception handler reads mscratch=0 and ultimately writes tohost=3:

             34	        13	00100098	34001873	-->csrrw	x0,mscratch,x0	  x0:0x00000000  x0=0x00000000
             48	        20	00100110	34002373	csrrs	x6,mscratch,x0	  x0:0x00000000  x6=0x00000000
             72	        32	00100138	00a2a023	sw	x10,0(x5)	  x5:0x0012fff0 x10:0x00000003 PA:0x0012fff0 store:0x00000003

In other words, Ibex raises an Illegal Instruction exception while also performing the faulting instruction's CSR write.

Root cause analysis

rtl/ibex_decoder.sv computes the unavailable-RV32E-register condition separately:

assign illegal_reg_16 = (RV32E || (cheriot_enable_i == IbexMuBiOn)) &&
                        ((raddr_a[4]  && rf_ren_a_o) ||
                         (raddr_b[4]  && rf_ren_b_o) ||
                         (instr_rd[4] && rf_we_or_load));

The normal decode path has already asserted csr_access_o for the CSR instruction, while its internal illegal_insn signal remains low.

The ordinary illegal-instruction side-effect cleanup checks only illegal_insn. The source comment explicitly notes that unavailable RV32E registers are not handled at this point:

if (illegal_insn) begin
  rf_we           = 1'b0;
  data_req_o      = 1'b0;
  data_we_o       = 1'b0;
  jump_in_dec_o   = 1'b0;
  jump_set_o      = 1'b0;
  branch_in_dec_o = 1'b0;
  csr_access_o    = 1'b0;
end

The final outputs incorporate illegal_reg_16 into the illegal-instruction indication and suppress the register-file writeback only:

assign illegal_insn_o = illegal_insn | illegal_reg_16;
assign rf_we_o        = rf_we & ~illegal_reg_16;

csr_access_o is not suppressed here.

On the WritebackStage=0 path, instr_executing in rtl/ibex_id_stage.sv does not include ~illegal_insn_o:

assign instr_executing_spec = instr_valid_i & ~instr_fetch_err_i & controller_run;
assign instr_executing      = instr_executing_spec;

The CSR enable can therefore still be asserted:

assign csr_op_en_o = csr_access_o & instr_executing & instr_id_done_o;

csr_we_int in rtl/ibex_cs_registers.sv excludes illegal CSR addresses, privilege violations, and writes to read-only CSRs, but it is unaware that the instruction is illegal because rd=x16:

assign csr_we_int = csr_wr & csr_op_en_i & ~illegal_csr_insn_o;

As a result, the mscratch write enable and the illegal-instruction exception can be generated for the same faulting instruction, and exception entry does not restore the already-cleared mscratch value.

Possible fix direction

Should every instruction that is ultimately classified as illegal by illegal_reg_16 be prevented from producing architectural side effects?

If so, one possible fix would be to make illegal_reg_16 trigger the same side-effect cleanup as illegal_insn in the decoder.

Activity

  1. h-filali commented on Nov 6, 2026

    @h-filali
    Contributor

    Thanks @huntersubmit for flagging this issue.
    I will close this issue as it seems to be a duplicate of #2547.

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

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions