Repository navigation
Description
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_systemwith 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 1blink.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.dumptest.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::RegFileFFRun 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.logExpected 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;
endThe 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
h-filali commentedon Nov 6, 2026
Thanks @huntersubmit for flagging this issue.
I will close this issue as it seems to be a duplicate of #2547.