17 KiB
Privileged Test Fixes Implementation Plan
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: Make rv64mi-p-instret_overflow, rv64mi-p-mcsr, rv64si-p-dirty, and rv64si-p-icache-alias behave according to the privileged tests instead of failing or timing out.
Architecture: Fix the small M-mode CSR gaps first, then make address translation privilege-aware for data accesses, then add an instruction-side translation path. Keep the implementation minimal for these tests: Sv39 only, no ASIDs, no speculative PTW sharing, and no broader cache redesign.
Tech Stack: Scala 2.13, Chisel 7.7, ScalaTest, generated SystemVerilog, Verilator RISC-V testbench.
File Structure
- Modify
src/main/scala/common/Privileged.scala: add missing machine CSR constants and mstatus bit constants used by translation logic. - Modify
src/main/scala/csr/CSRPermission.scala: classify read-only ID CSRs and writable machine counters correctly. - Modify
src/main/scala/csr/CSRFile.scala: implementmvendorid/marchid/mimpid,minstret,mcountinhibit.IR, and instret/minstret write semantics. - Modify
src/main/scala/common/Bundles.scala: carry privilege and mstatus-derived access controls into translation requests. - Modify
src/main/scala/memory/MMU.scala: enforce Sv39 permissions for S/U/MPRV, A/D, SUM, and superpage alignment. - Modify
src/main/scala/memory/LSU.scala: decide effective privilege for loads/stores usingmstatus.MPRV/MPP, and run translation only when effective privilege is S/U with non-Baresatp. - Modify
src/main/scala/OoOBackend.scala: passcurrentPrivandmstatusinto LSU. - Modify
src/main/scala/frontend/Frontend.scala: addsatp/currentPrivinputs and an instruction PTW path. - Modify
src/main/scala/Core.scala: wire frontendsatp/currentPrivfrom backend/privilege state. - Test
src/test/scala/csr/CSRPermissionSpec.scala: cover missing CSR permissions. - Test
src/test/scala/csr/CSRFileSpec.scala: cover generated CSR read/write behavior. - Test
src/test/scala/memory/Sv39Spec.scala: cover the generated MMU/LSU/Frontend ports and key permission expressions. - Verify with
sim/scripts/run_privileged_tests.sh.
Task 1: Fix Machine CSR Address Map
Files:
-
Modify:
src/main/scala/common/Privileged.scala -
Modify:
src/main/scala/csr/CSRPermission.scala -
Test:
src/test/scala/csr/CSRPermissionSpec.scala -
Step 1: Add constants for missing machine CSRs
In src/main/scala/common/Privileged.scala, add these constants near the existing machine CSR constants:
val CSR_MVENDORID = "hf11".U(12.W)
val CSR_MARCHID = "hf12".U(12.W)
val CSR_MIMPID = "hf13".U(12.W)
val CSR_MHARTID = "hf14".U(12.W)
val CSR_MCOUNTINHIBIT = "h320".U(12.W)
val CSR_MINSTRET = "hb02".U(12.W)
Keep CSR_CYCLE, CSR_TIME, and CSR_INSTRET as user-visible read-only counter aliases at 0xc00 through 0xc02.
- Step 2: Update CSR permission sets
In CSRPermission.scala, include the missing machine CSRs in machineCsrs, and treat ID CSRs as read-only:
private val machineCsrs = Set(
0x300, 0x301, 0x302, 0x303, 0x304, 0x305, 0x306, 0x320,
0x340, 0x341, 0x342, 0x343, 0x344,
0x3a0, 0x3b0, 0x744, 0x7a0, 0x7a1, 0x7a2, 0x7a5,
0xb02, 0xf11, 0xf12, 0xf13, 0xf14)
private val counterCsrs = Set(0xc00, 0xc01, 0xc02)
private val readOnlyCsrs = Set(0xf11, 0xf12, 0xf13, 0xf14) ++ counterCsrs
Update isMachine to include CSR_MCOUNTINHIBIT, CSR_MINSTRET, CSR_MVENDORID, CSR_MARCHID, and CSR_MIMPID.
- Step 3: Add permission regression tests
Append these cases to CSRPermissionSpec.scala:
it should "allow machine-mode reads of implementation ID CSRs and reject writes" in {
val ids = Seq(Privileged.CSR_MVENDORID, Privileged.CSR_MARCHID, Privileged.CSR_MIMPID, Privileged.CSR_MHARTID)
ids.foreach { csr =>
CSRPermission.readAllowedLit(csr.litValue, Privileged.PRV_M.litValue) should be(true)
CSRPermission.writeAllowedLit(csr.litValue, Privileged.PRV_M.litValue) should be(false)
CSRPermission.readAllowedLit(csr.litValue, Privileged.PRV_S.litValue) should be(false)
}
}
it should "allow machine-mode writes to minstret and mcountinhibit" in {
CSRPermission.readAllowedLit(Privileged.CSR_MINSTRET.litValue, Privileged.PRV_M.litValue) should be(true)
CSRPermission.writeAllowedLit(Privileged.CSR_MINSTRET.litValue, Privileged.PRV_M.litValue) should be(true)
CSRPermission.readAllowedLit(Privileged.CSR_MCOUNTINHIBIT.litValue, Privileged.PRV_M.litValue) should be(true)
CSRPermission.writeAllowedLit(Privileged.CSR_MCOUNTINHIBIT.litValue, Privileged.PRV_M.litValue) should be(true)
}
- Step 4: Run the targeted test
Run:
sbt "testOnly CSRPermissionSpec"
Expected before implementation: failures for missing constants or denied CSRs. Expected after implementation: CSRPermissionSpec passes.
Task 2: Implement mcsr and instret_overflow CSR Semantics
Files:
-
Modify:
src/main/scala/csr/CSRFile.scala -
Test:
src/test/scala/csr/CSRFileSpec.scala -
Step 1: Add CSR state
In CSRFile.scala, add:
val mcountinhibit = RegInit(0.U(p.xlen.W))
val suppressInstretIncrement = WireDefault(false.B)
The existing instret register should serve both minstret and instret.
- Step 2: Increment
instretunless inhibited or just written
Replace the unconditional counter update region with:
cycle := cycle + 1.U
when(!mcountinhibit(2) && !suppressInstretIncrement) {
instret := instret + 1.U
}
This matches the test expectation that a write to minstret suppresses the increment observed by the immediately following read.
- Step 3: Add CSR reads
Add to both the read switch and writeOld switch:
is(Privileged.CSR_MCOUNTINHIBIT) { r := mcountinhibit }
is(Privileged.CSR_MINSTRET) { r := instret }
is(Privileged.CSR_MVENDORID) { r := 0.U }
is(Privileged.CSR_MARCHID) { r := 0.U }
is(Privileged.CSR_MIMPID) { r := 0.U }
For the writeOld switch, use writeOld := ... instead of r := ....
- Step 4: Add CSR writes
Inside the CSR write switch:
is(Privileged.CSR_MCOUNTINHIBIT) { mcountinhibit := next & 4.U }
is(Privileged.CSR_MINSTRET) {
instret := next
suppressInstretIncrement := true.B
}
Do not make mvendorid/marchid/mimpid/mhartid writable.
- Step 5: Add generated-Verilog smoke tests
Append to CSRFileSpec.scala:
it should "contain machine counter and ID CSR decode paths" in {
val verilog = ChiselStage.emitSystemVerilog(new CSRFile())
verilog should include("mcountinhibit")
verilog should include("12'hB02")
verilog should include("12'hF11")
verilog should include("12'hF12")
verilog should include("12'hF13")
}
- Step 6: Run targeted tests and two privileged binaries
Run:
sbt "testOnly CSRPermissionSpec CSRFileSpec"
make -C sim/verilator compile
timeout 5s sim/verilator/obj_dir/VCore ../../riscv-tests/isa/rv64mi-p-mcsr
timeout 5s sim/verilator/obj_dir/VCore ../../riscv-tests/isa/rv64mi-p-instret_overflow
Expected: both Verilator commands exit 0.
Task 3: Make Data Translation Privilege-Aware
Files:
-
Modify:
src/main/scala/common/Privileged.scala -
Modify:
src/main/scala/common/Bundles.scala -
Modify:
src/main/scala/memory/MMU.scala -
Modify:
src/main/scala/memory/LSU.scala -
Modify:
src/main/scala/OoOBackend.scala -
Test:
src/test/scala/memory/Sv39Spec.scala -
Step 1: Add mstatus bit constants
In Privileged.scala, add:
val MSTATUS_MPRV_BIT = 17
val MSTATUS_SUM_BIT = 18
val MSTATUS_MXR_BIT = 19
val MSTATUS_MPP_HI = 12
val MSTATUS_MPP_LO = 11
- Step 2: Extend TLB request metadata
In Bundles.scala, extend TlbReq:
class TlbReq(p: CoreParams = CoreParams()) extends Bundle {
val valid = Bool()
val vaddr = UInt(p.xlen.W)
val isStore = Bool()
val isFetch = Bool()
val priv = UInt(2.W)
val sum = Bool()
val mxr = Bool()
}
- Step 3: Enforce Sv39 permissions in the page walker
In MMU.scala, register priv/sum/mxr alongside request state:
val privReg = Reg(UInt(2.W))
val sumReg = Reg(Bool())
val mxrReg = Reg(Bool())
Set them in sIdle when accepting a request:
privReg := io.req.priv
sumReg := io.req.sum
mxrReg := io.req.mxr
Replace the existing permFault with:
val userPage = pteU
val supervisorAccessToUser = privReg === Privileged.PRV_S && userPage && !sumReg && !isFetchReg
val supervisorFetchUser = privReg === Privileged.PRV_S && userPage && isFetchReg
val userAccessSupervisor = privReg === Privileged.PRV_U && !userPage
val readAllowed = pteR || (mxrReg && pteX)
val accessPermFault = Mux(isFetchReg, !pteX, Mux(isStoreReg, !pteW || !pteD, !readAllowed))
val adFault = !pteA || (isStoreReg && !pteD)
val permFault = accessPermFault || adFault || supervisorAccessToUser || supervisorFetchUser || userAccessSupervisor
Add superpage PPN alignment checks in the leaf case:
val misalignedSuperpage =
(level === 2.U && ptePpn(17, 0) =/= 0.U) ||
(level === 1.U && ptePpn(8, 0) =/= 0.U)
when(permFault || misalignedSuperpage) {
walkFault := true.B
}
- Step 4: Make LSU choose effective privilege
Add LSU IO inputs:
val currentPriv = Input(UInt(2.W))
val mstatus = Input(UInt(p.xlen.W))
In LSU.scala, replace bare with effective-mode logic:
val mprv = io.mstatus(Privileged.MSTATUS_MPRV_BIT)
val mpp = io.mstatus(Privileged.MSTATUS_MPP_HI, Privileged.MSTATUS_MPP_LO)
val effectivePriv = Mux(io.currentPriv === Privileged.PRV_M && mprv, mpp, io.currentPriv)
val translate = io.satp(63, 60) =/= 0.U && effectivePriv =/= Privileged.PRV_M
val bare = !translate
Populate DTLB/MMU request metadata:
dtlb.io.req.priv := effectivePriv
dtlb.io.req.sum := io.mstatus(Privileged.MSTATUS_SUM_BIT)
dtlb.io.req.mxr := io.mstatus(Privileged.MSTATUS_MXR_BIT)
mmu.io.req.priv := effectivePriv
mmu.io.req.sum := io.mstatus(Privileged.MSTATUS_SUM_BIT)
mmu.io.req.mxr := io.mstatus(Privileged.MSTATUS_MXR_BIT)
- Step 5: Wire backend to LSU
In OoOBackend.scala, add:
lsu.io.currentPriv := io.currentPriv
lsu.io.mstatus := csr.io.mstatus
- Step 6: Update tests
Append to Sv39Spec.scala:
it should "generate LSU effective privilege translation controls" in {
val verilog = ChiselStage.emitSystemVerilog(new LSU())
verilog should include("io_currentPriv")
verilog should include("io_mstatus")
verilog should include("io_mstatus[17]")
}
- Step 7: Run tests and dirty binary
Run:
sbt "testOnly Sv39Spec"
make -C sim/verilator compile
timeout 5s sim/verilator/obj_dir/VCore ../../riscv-tests/isa/rv64si-p-dirty
Expected: rv64si-p-dirty exits 0. If it still fails, inspect whether the first TESTNUM=2 store raises store page fault and whether the M-mode handler reads page_table_1 physically.
Task 4: Add Instruction-Side Sv39 Translation
Files:
-
Modify:
src/main/scala/frontend/ITLB.scala -
Modify:
src/main/scala/frontend/Frontend.scala -
Modify:
src/main/scala/OoOBackend.scala -
Modify:
src/main/scala/Core.scala -
Test:
src/test/scala/memory/Sv39Spec.scala -
Step 1: Extend Frontend IO
Add to Frontend IO:
val satp = Input(UInt(p.xlen.W))
val currentPriv = Input(UInt(2.W))
- Step 2: Add an instruction MMU instance
In Frontend.scala, instantiate MMU:
val immu = Module(new MMU(p))
val fetchTranslate = io.satp(63, 60) =/= 0.U && io.currentPriv =/= Privileged.PRV_M
Set ITLB request valid only when translation is active:
itlb.io.req.valid := fetchTranslate
itlb.io.req.vaddr := pc
itlb.io.req.isStore := false.B
itlb.io.req.isFetch := true.B
itlb.io.req.priv := io.currentPriv
itlb.io.req.sum := false.B
itlb.io.req.mxr := false.B
Drive immu for ITLB misses:
immu.io.satp := io.satp
immu.io.req.valid := fetchTranslate && itlb.io.resp.miss
immu.io.req.vaddr := pc
immu.io.req.isStore := false.B
immu.io.req.isFetch := true.B
immu.io.req.priv := io.currentPriv
immu.io.req.sum := false.B
immu.io.req.mxr := false.B
itlb.io.refill := immu.io.refill
- Step 3: Add instruction PTW memory port
Add frontend IO:
val ptwMemReqValid = Output(Bool())
val ptwMemReqAddr = Output(UInt(p.xlen.W))
val ptwMemRespValid = Input(Bool())
val ptwMemRespData = Input(UInt(p.xlen.W))
Wire:
io.ptwMemReqValid := immu.io.ptwMemReq.valid
io.ptwMemReqAddr := immu.io.ptwMemReq.addr
immu.io.ptwMemResp.valid := io.ptwMemRespValid
immu.io.ptwMemResp.data := io.ptwMemRespData
In Core.scala, arbitrate instruction-side PTW reads onto the existing data memory read path. Add a one-bit outstanding register so the frontend only consumes the memory response that belongs to its PTW request:
val frontendPtwOutstanding = RegInit(false.B)
val frontendPtwFire = frontend.io.ptwMemReqValid
when(frontendPtwFire) {
frontendPtwOutstanding := true.B
}.elsewhen(io.dmem_resp_valid && frontendPtwOutstanding) {
frontendPtwOutstanding := false.B
}
io.dmem_req_valid := frontend.io.ptwMemReqValid || backend.io.dmemReqValid
io.dmem_req_bits_addr := Mux(frontend.io.ptwMemReqValid, frontend.io.ptwMemReqAddr, backend.io.dmemReq.addr)
io.dmem_req_bits_data := Mux(frontend.io.ptwMemReqValid, 0.U, backend.io.dmemReq.data)
io.dmem_req_bits_isStore := Mux(frontend.io.ptwMemReqValid, false.B, backend.io.dmemReq.isStore)
io.dmem_req_bits_size := Mux(frontend.io.ptwMemReqValid, 3.U, backend.io.dmemReq.size)
frontend.io.ptwMemRespValid := io.dmem_resp_valid && frontendPtwOutstanding
frontend.io.ptwMemRespData := io.dmem_resp_bits
- Step 4: Gate fetch while instruction translation is pending
Use translated physical address only when translation is ready:
val fetchTranslationReady = !fetchTranslate || itlb.io.resp.hit
val fetchTranslationFault = fetchTranslate && (itlb.io.resp.pageFault || immu.io.resp.pageFault)
val fetchPaddr = Mux(fetchTranslate, itlb.io.resp.paddr, pc)
icache.io.reqValid := fetchTranslationReady && !fetchTranslationFault && !instMisaligned && !faultPending
icache.io.reqAddr := fetchPaddr
Update fetchFault to include fetchTranslationFault.
- Step 5: Expose backend CSR
satp
In OoOBackend.scala, add an output:
val satpOut = Output(UInt(p.xlen.W))
Drive it from the CSR file:
io.satpOut := csr.io.satp
Keep the existing LSU connection:
lsu.io.satp := csr.io.satp
- Step 6: Wire frontend privilege inputs
In Core.scala:
frontend.io.satp := backend.io.satpOut
frontend.io.currentPriv := privCtrl.io.priv
- Step 7: Add generated-Verilog frontend test
Append to Sv39Spec.scala:
it should "generate frontend instruction-side translation ports" in {
val verilog = ChiselStage.emitSystemVerilog(new Frontend())
verilog should include("io_satp")
verilog should include("io_currentPriv")
verilog should include("ptwMemReq")
}
- Step 8: Run icache-alias
Run:
sbt "testOnly Sv39Spec"
make -C sim/verilator compile
timeout 5s sim/verilator/obj_dir/VCore ../../riscv-tests/isa/rv64si-p-icache-alias
Expected: no Bad imem fetch pc=0x0, no out-of-bounds fetch stream, exit 0.
Task 5: Full Privileged Regression
Files:
-
No source edits.
-
Verify:
sim/results/privileged_test_results.txt -
Step 1: Rebuild generated RTL/testbench
Run:
sbt "runMain CoreOoO"
make -C sim/verilator compile
Expected: both commands exit 0.
- Step 2: Run privileged suite
Run:
cd sim/scripts
TIMEOUT_SECONDS=10 ./run_privileged_tests.sh
Expected:
rv64mi-p-instret_overflow: exit=0 PASS
rv64mi-p-mcsr: exit=0 PASS
rv64si-p-dirty: exit=0 PASS
rv64si-p-icache-alias: exit=0 PASS
rv64ui-p-ma_data is not in this privileged script and remains acceptable as a known aligned-access limitation in the base UI suite.
- Step 3: Run base ISA suite
Run:
cd sim/scripts
./run_tests.sh
Expected: existing PASS count is preserved, with only rv64ui-p-ma_data failing if the processor intentionally does not support misaligned data access.
Self-Review
- Spec coverage:
rv64mi-p-instret_overflowis covered by Task 1 and Task 2;rv64mi-p-mcsris covered by Task 1 and Task 2;rv64si-p-dirtyis covered by Task 3;rv64si-p-icache-aliasis covered by Task 4. - Placeholder scan: no deferred design items are left in the plan; every task has concrete files, code snippets, commands, and expected outcomes.
- Type consistency: new
TlbReqfields are populated in LSU and Frontend; MMU consumes the same field names; Core/OoOBackend wiring is explicitly called out where ownership ofsatpcrosses module boundaries.