Skip to content

Raise illegal-instruction (not virtual) for indirect-CSR permission failures under V=1#2290

Open
jameshippisley wants to merge 2 commits into
riscv-software-src:masterfrom
jameshippisley:fix-vu-mireg-mpriv
Open

Raise illegal-instruction (not virtual) for indirect-CSR permission failures under V=1#2290
jameshippisley wants to merge 2 commits into
riscv-software-src:masterfrom
jameshippisley:fix-vu-mireg-mpriv

Conversation

@jameshippisley

Copy link
Copy Markdown
Contributor

Summary

sscsrind_reg_csr_t::verify_permissions and the virtualized_indirect_csr_t wrapper around sireg/vsireg contain three H-extension V=1 illegal→virtual conversions that misfire on cases the spec says should be illegal-instruction:

  1. VU mireg returns virtual instead of illegal. The unconditional state->v && state->prv == PRV_U → virtual rule added in Fix: Enforce virtual_instruction trap for VU-mode indirect CSR access #2221 for the S/HS-priv siblings also fires for the M-priv mireg, which sscsrind_reg_csr_t also serves.

  2. VS mireg with hstateen0.CSRIND=0 returns virtual instead of illegal — same class, same root cause: the Smstateen H-ext gate fires for M-priv mireg.

  3. VU sireg with mstateen0.CSRIND=0 returns virtual instead of illegal. virtualized_indirect_csr_t calls the base virtualized_csr_t::verify_permissions first, which runs the H-ext priv-class conversion before the inner sscsrind_reg_csr_t ever gets to enforce Smstateen.

The H-ext csr_priv ≤ HS conversion rule does not apply to M-priv CSRs, and Smstateen requires illegal regardless of V. Both cases must short-circuit to illegal-instruction.

Fix

Two commits, one bug class each:

  • Keep M-priv access to indirect CSRs as illegal-instruction — early carve-out at the top of sscsrind_reg_csr_t::verify_permissions for csr_priv == M && prv < M. Address bits [9:8] are used to derive the CSR's priv class because csr_priv is private to csr_t. Fixes (1) and (2).

  • Honor mstateen0.CSRIND in virtualized_indirect_csr_t wrapper — Smstateen check at the top of the wrapper, before the priv-class conversion runs. Fixes (3). The inner copy in sscsrind_reg_csr_t stays in place for mireg/mireg2..6 and vsireg/vsireg2..6, which are registered without this wrapper; leaving both is idempotent.

Testing

Three-phase RV64GH assembly testcase, each phase expects mcause=2 (illegal-instruction):

  • Phase 1: VU mireg, mstateen=1, hstateen=1 — exercises (1)
  • Phase 2: VS mireg, mstateen=1, hstateen=0 — exercises (2)
  • Phase 3: VU sireg, mstateen=0 — exercises (3)

Build:

riscv64-unknown-elf-gcc -nostdlib -nostartfiles -static \
    -march=rv64gh -mabi=lp64d \
    -Wl,-N -Wl,-Ttext=0x80000000 \
    -o mireg-vu-bug spike-mireg-vu-bug.S

Run:

spike -l --log-commits --isa=rv64gh_zicsr_smstateen_smcsrind_sscsrind \
    mireg-vu-bug 2>&1 | grep -E "trap_|csrr.*(mireg|sireg)|FAILED"
Testcase source (spike-mireg-vu-bug.S)
# Three trap-class bugs in spike's CSR-indirect machinery — all
# convert what should be an illegal-instruction exception into a
# virtual-instruction one for accesses that the H-extension's V=1
# illegal->virtual conversion is NOT supposed to apply to:
#
#   sscsrind_reg_csr_t::verify_permissions, Smstateen H-ext gate:
#       state->v && !hstateen0.CSRIND -> virtual
#     Wrong for mireg (M-priv): VS mireg with hstateen=0
#     returns virtual instead of illegal-too-low.
#   sscsrind_reg_csr_t::verify_permissions, PR #2221 VU rule:
#       state->v && state->prv == U -> virtual
#     Wrong for mireg (M-priv): VU mireg returns virtual
#     instead of illegal-too-low.
#   virtualized_indirect_csr_t::verify_permissions:
#       no Smstateen.CSRIND check before priv-class
#       conversion runs in csr_t::verify_permissions
#     Wrong for sireg / vsireg (S/HS-priv) when
#     mstateen0.CSRIND=0: VU sireg returns virtual via
#     the H-ext priv conversion instead of illegal per
#     the Smstateen rule.
#
# Three phases, each must trap mcause=2 (illegal-instruction):
#
#   Phase 1: VU mireg, mstateen=1, hstateen=1   (PR #2221 VU rule)
#   Phase 2: VS mireg, mstateen=1, hstateen=0   (Smstateen H-ext gate)
#   Phase 3: VU sireg, mstateen=0               (wrapper priv-first)
#
# Build:
#   riscv64-unknown-elf-gcc -nostdlib -nostartfiles -static \
#     -march=rv64gh -mabi=lp64d \
#     -Wl,-N -Wl,-Ttext=0x80000000 \
#     -o mireg-vu-bug spike-mireg-vu-bug.S
#
# Run (-l --log-commits emits a per-instruction trace including
# trap_virtual_instruction / trap_illegal_instruction, so a failing
# run shows which phase fired and what trap class spike returned):
#   spike -l --log-commits --isa=rv64gh_zicsr_smstateen_smcsrind_sscsrind \
#     mireg-vu-bug 2>&1 | grep -E "trap_|csrr.*(mireg|sireg)|FAILED"
#
# Example unpatched output (phase 1 fails first and short-circuits):
#   core 0: 0x...80000060 (0x351022f3) csrr    t0, mireg
#   core 0: exception trap_virtual_instruction, epc 0x...80000060
#   *** FAILED *** (tohost = 1)
#
# Exit status (read from tohost):
#   1 = PASS — all three phases trapped illegal (cause = 2)
#   3 = FAIL — at least one phase returned virtual (cause = 22)
#
# Stock upstream-spike fails. With the proposed fixes applied
# (M-priv carve-out at top of sscsrind_reg_csr_t::verify_permissions
# AND Smstateen.CSRIND check at top of
# virtualized_indirect_csr_t::verify_permissions) all three phases
# pass.

.option norvc
.global _start
.text

# === M-mode common setup ===
_start:
        # Bare translation, full R/W/X PMP.
        csrw     satp, zero
        csrw     vsatp, zero
        csrw     hgatp, zero
        li       t0, -1
        csrw     pmpaddr0, t0
        li       t0, 0x1f                       # NAPOT | R | W | X
        csrw     pmpcfg0, t0

        # mstateen0.CSRIND = 1 for phases 1+2 — phase 3 will clear it.
        li       t0, 1
        slli     t0, t0, 60
        csrs     mstateen0, t0

# === Phase 1: VU mireg, mstateen=1, hstateen=1 (PR #2221 VU rule) ===
        csrs     hstateen0, t0                  # hstateen.CSRIND=1
        la       t0, phase1_handler
        csrw     mtvec, t0
        li       t0, 0x1800                     # MPP[12:11] mask
        csrc     mstatus, t0                    # MPP=0 (U)
        li       t0, 1
        slli     t0, t0, 39                     # MPV
        csrs     mstatus, t0
        la       t0, vu_mireg_probe
        csrw     mepc, t0
        mret                                    # -> vu_mireg_probe in VU

vu_mireg_probe:                                 # priv=VU
        csrrs    t0, mireg, zero                # M-priv CSR; must trap
        j        fail                           # access must trap

        .balign  4
phase1_handler:                                 # priv=M (after trap)
        csrr     t0, mcause
        li       t1, 2                          # CAUSE_ILLEGAL_INSTRUCTION
        bne      t0, t1, fail
        # fall through to phase 2 setup

# === Phase 2: VS mireg, mstateen=1, hstateen=0 (Smstateen H-ext gate) ===
        li       t0, 1
        slli     t0, t0, 60
        csrc     hstateen0, t0                  # hstateen.CSRIND=0
        la       t0, phase2_handler
        csrw     mtvec, t0
        li       t0, 0x1800
        csrc     mstatus, t0
        li       t0, 0x800                      # MPP=01 (S)
        csrs     mstatus, t0
        li       t0, 1
        slli     t0, t0, 39                     # MPV
        csrs     mstatus, t0
        la       t0, vs_mireg_probe
        csrw     mepc, t0
        mret                                    # -> vs_mireg_probe in VS

vs_mireg_probe:                                 # priv=VS
        csrrs    t0, mireg, zero                # M-priv CSR; must trap
        j        fail

        .balign  4
phase2_handler:                                 # priv=M (after trap)
        csrr     t0, mcause
        li       t1, 2
        bne      t0, t1, fail
        # fall through to phase 3 setup

# === Phase 3: VU sireg, mstateen=0 (wrapper priv-first) ===
# sireg is S-priv (csr_priv=S); the M-priv carve-out doesn't apply.
# The Smstateen.CSRIND=0 rule should fire illegal at the wrapper
# before the H-ext priv-class conversion converts it to virtual.
        li       t0, 1
        slli     t0, t0, 60
        csrc     mstateen0, t0                  # mstateen.CSRIND=0
        la       t0, phase3_handler
        csrw     mtvec, t0
        li       t0, 0x1800                     # MPP[12:11] mask
        csrc     mstatus, t0                    # MPP=0 (U)
        li       t0, 1
        slli     t0, t0, 39                     # MPV
        csrs     mstatus, t0
        la       t0, vu_sireg_probe
        csrw     mepc, t0
        mret                                    # -> vu_sireg_probe in VU

vu_sireg_probe:                                 # priv=VU
        csrrs    t0, sireg, zero                # S-priv CSR; must trap illegal
        j        fail                           # access must trap

        .balign  4
phase3_handler:                                 # priv=M (after trap)
        csrr     t0, mcause
        li       t1, 2
        bne      t0, t1, fail
        # fall through to pass

# === Pass: all three phases trapped illegal ===
        li       t0, 1
        la       t1, tohost
        sd       t0, 0(t1)
1:      j        1b

fail:
        li       t0, 3                          # FAIL
        la       t1, tohost
        sd       t0, 0(t1)
1:      j        1b

        .balign  64
        .global  tohost
tohost: .8byte   0
        .global  fromhost
fromhost: .8byte 0

James Robinson added 2 commits May 7, 2026 23:54
sscsrind_reg_csr_t serves both Sscsrind (sireg, vsireg — S/HS-priv)
and Smcsrind (mireg — M-priv). Its verify_permissions accumulated
three H-extension V=1 illegal->virtual conversions (Smstateen
hstateen0.CSRIND gate, the unconditional VU rule from PR riscv-software-src#2221, and
the base csr_t::verify_permissions S/HS conversion). All three are
correct for the S/HS-priv siblings but spec-violating for the M-priv
mireg: the H-ext conversion is restricted to csr_priv <= HS, so
M-priv access from below M must continue to raise illegal-instruction.

Add an early carve-out at the top of verify_permissions so any M-priv
access from below M short-circuits to illegal before the V=1
conversion machinery runs. The downstream rules then only execute on
S/HS-priv CSRs where their conversions are spec-correct, and don't
need to special-case csr_priv themselves.

Address bits [9:8] are used to derive the CSR's priv class because
csr_priv is private to csr_t.
sireg / vsireg are wrapped in virtualized_indirect_csr_t to alias
sireg -> vsireg under V=1. The wrapper's verify_permissions called
the base priv check first, so a VU access to sireg with
mstateen0.CSRIND=0 hit the H-ext priv-class conversion (csr_priv=S,
V=1) and trapped virtual-instruction. Per Smstateen the bit-clear
case must raise illegal-instruction regardless of V — the inner
sscsrind_reg_csr_t::verify_permissions enforces this but never runs
once the wrapper has thrown.

Add the Smstateen.CSRIND check at the top of the wrapper so it fires
before the priv-class conversion. The inner copy stays in place for
mireg / mireg2..6 and vsireg / vsireg2..6, which are registered
without this wrapper.

@aswaterman aswaterman left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@binno @chihminchao can you review this and make sure you agree?

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants