diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4d4b8c3..0e9d5d7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -87,7 +87,7 @@ jobs: - name: Build PMP CSR test firmware run: | - riscv64-unknown-elf-gcc -nostdlib -march=rv64gc -mabi=lp64 \ + riscv64-unknown-elf-gcc -nostdlib -march=rv64gc -mabi=lp64 -I contrib/fw \ -Wl,-Ttext=0x10000,--no-dynamic-linker \ -o pmp_csr_test \ contrib/fw/pmp-csr-test/pmp_csr_test.S @@ -102,7 +102,7 @@ jobs: - name: Build PMP enforcement test firmware run: | - riscv64-unknown-elf-gcc -nostdlib -march=rv64gc -mabi=lp64 \ + riscv64-unknown-elf-gcc -nostdlib -march=rv64gc -mabi=lp64 -I contrib/fw \ -Wl,-Ttext=0x10000,--no-dynamic-linker \ -o pmp_enforce_test \ contrib/fw/pmp-enforce-test/pmp_enforce_test.S @@ -112,7 +112,7 @@ jobs: - name: Build PMP shift test firmware run: | - riscv64-unknown-elf-gcc -nostdlib -march=rv64gc -mabi=lp64 \ + riscv64-unknown-elf-gcc -nostdlib -march=rv64gc -mabi=lp64 -I contrib/fw \ -Wl,-Ttext=0x10000,--no-dynamic-linker \ -o pmp_shift_test \ contrib/fw/pmp-shift-test/pmp_shift_test.S @@ -122,7 +122,7 @@ jobs: - name: Build PMP upper-cfg test firmware run: | - riscv64-unknown-elf-gcc -nostdlib -march=rv64gc -mabi=lp64 \ + riscv64-unknown-elf-gcc -nostdlib -march=rv64gc -mabi=lp64 -I contrib/fw \ -Wl,-Ttext=0x10000,--no-dynamic-linker \ -o pmp_upper_cfg_test \ contrib/fw/pmp-upper-cfg-test/pmp_upper_cfg_test.S @@ -132,7 +132,7 @@ jobs: - name: Build PMP cfg2 test firmware run: | - riscv64-unknown-elf-gcc -nostdlib -march=rv64gc -mabi=lp64 \ + riscv64-unknown-elf-gcc -nostdlib -march=rv64gc -mabi=lp64 -I contrib/fw \ -Wl,-Ttext=0x10000,--no-dynamic-linker \ -o pmp_cfg2_test \ contrib/fw/pmp-cfg2-test/pmp_cfg2_test.S @@ -142,7 +142,7 @@ jobs: - name: Build PMP TOR test firmware run: | - riscv64-unknown-elf-gcc -nostdlib -march=rv64gc -mabi=lp64 \ + riscv64-unknown-elf-gcc -nostdlib -march=rv64gc -mabi=lp64 -I contrib/fw \ -Wl,-Ttext=0x10000,--no-dynamic-linker \ -o pmp_tor_test \ contrib/fw/pmp-tor-test/pmp_tor_test.S @@ -152,7 +152,7 @@ jobs: - name: Build PMP 64-entry pmpaddr test firmware run: | - riscv64-unknown-elf-gcc -nostdlib -march=rv64gc -mabi=lp64 \ + riscv64-unknown-elf-gcc -nostdlib -march=rv64gc -mabi=lp64 -I contrib/fw \ -Wl,-Ttext=0x10000,--no-dynamic-linker \ -o pmp_64entry_addr_test \ contrib/fw/pmp-64entry-addr-test/pmp_64entry_addr_test.S @@ -162,7 +162,7 @@ jobs: - name: Build PMP 64-entry pmpcfg test firmware run: | - riscv64-unknown-elf-gcc -nostdlib -march=rv64gc -mabi=lp64 \ + riscv64-unknown-elf-gcc -nostdlib -march=rv64gc -mabi=lp64 -I contrib/fw \ -Wl,-Ttext=0x10000,--no-dynamic-linker \ -o pmp_64entry_cfg_test \ contrib/fw/pmp-64entry-cfg-test/pmp_64entry_cfg_test.S @@ -172,10 +172,50 @@ jobs: - name: Build PMP 8-entry guard test firmware (VP/S5 model) run: | - riscv64-unknown-elf-gcc -nostdlib -march=rv64gc -mabi=lp64 \ + riscv64-unknown-elf-gcc -nostdlib -march=rv64gc -mabi=lp64 -I contrib/fw \ -Wl,-Ttext=0x10000,--no-dynamic-linker \ -o pmp_8entry_guard_test \ contrib/fw/pmp-8entry-guard-test/pmp_8entry_guard_test.S - name: rv64gc_mp_8 8-entry enforcement test - interp (VP/S5 model) run: LD_LIBRARY_PATH=. ./riscv-sim -f pmp_8entry_guard_test --isa rv64gc_mp_8 --backend interp + + - name: Build PMP fetch deny test firmware + run: | + riscv64-unknown-elf-gcc -nostdlib -march=rv64gc -mabi=lp64 -I contrib/fw \ + -Wl,-Ttext=0x10000,--no-dynamic-linker \ + -o pmp_fetch_deny_test \ + contrib/fw/pmp-fetch-deny-test/pmp_fetch_deny_test.S + + - name: rv64gc_mp_64 fetch execute-permission test - interp + run: LD_LIBRARY_PATH=. ./riscv-sim -f pmp_fetch_deny_test --isa rv64gc_mp_64 --backend interp + + - name: Build PMP fetch straddle test firmware + run: | + riscv64-unknown-elf-gcc -nostdlib -march=rv64gc -mabi=lp64 -I contrib/fw \ + -Wl,-Ttext=0x10000,--no-dynamic-linker \ + -o pmp_fetch_straddle_test \ + contrib/fw/pmp-fetch-straddle-test/pmp_fetch_straddle_test.S + + - name: rv64gc_mp_64 straddling-fetch sector test - interp + run: LD_LIBRARY_PATH=. ./riscv-sim -f pmp_fetch_straddle_test --isa rv64gc_mp_64 --backend interp + + - name: Build PMP lock test firmware + run: | + riscv64-unknown-elf-gcc -nostdlib -march=rv64gc -mabi=lp64 -I contrib/fw \ + -Wl,-Ttext=0x10000,--no-dynamic-linker \ + -o pmp_lock_test \ + contrib/fw/pmp-lock-test/pmp_lock_test.S + + - name: rv64gc_mp_64 lock-bit test - interp + run: LD_LIBRARY_PATH=. ./riscv-sim -f pmp_lock_test --isa rv64gc_mp_64 --backend interp + + - name: Build PMP no-entry U-mode test firmware + run: | + riscv64-unknown-elf-gcc -nostdlib -march=rv64gc -mabi=lp64 -I contrib/fw \ + -Wl,-Ttext=0x10000,--no-dynamic-linker \ + -o pmp_noentry_umode_test \ + contrib/fw/pmp-noentry-umode-test/pmp_noentry_umode_test.S + + - name: rv64gc_mup no-entry U-mode denial test - interp + run: LD_LIBRARY_PATH=. ./riscv-sim -f pmp_noentry_umode_test --isa rv64gc_mup --backend interp diff --git a/contrib/fw/pmp-64entry-addr-test/pmp_64entry_addr_test.S b/contrib/fw/pmp-64entry-addr-test/pmp_64entry_addr_test.S index 64fe595..670ea9d 100644 --- a/contrib/fw/pmp-64entry-addr-test/pmp_64entry_addr_test.S +++ b/contrib/fw/pmp-64entry-addr-test/pmp_64entry_addr_test.S @@ -2,6 +2,8 @@ // Pass: readback matches written sentinel -> j . (exit 0) // Fail: any trap (unregistered CSR) or readback mismatch -> semihosting SYS_EXIT (exit 2) +#include "pmp_test_common.h" + .section .text .globl _start _start: @@ -15,12 +17,5 @@ _start: j . // pass -fail: - li a0, 0x18 - .option push - .option norvc - slli zero, zero, 0x1f - ebreak - srai zero, zero, 7 - .option pop - j . +trap_entry fail + semihosting_fail diff --git a/contrib/fw/pmp-64entry-cfg-test/pmp_64entry_cfg_test.S b/contrib/fw/pmp-64entry-cfg-test/pmp_64entry_cfg_test.S index 0336554..545d69b 100644 --- a/contrib/fw/pmp-64entry-cfg-test/pmp_64entry_cfg_test.S +++ b/contrib/fw/pmp-64entry-cfg-test/pmp_64entry_cfg_test.S @@ -1,8 +1,12 @@ // Verify pmpcfg4 (CSR 0x3A4, holds entries 32-39 on RV64) is accessible and persistent. // Tests 64-entry pmpcfg registration. -// Pass: write 0x9F -> read back 0x9F -> write 0 -> read back 0 -> j . (exit 0) +// The probe value deliberately leaves L (0x80) clear: locking entry 32 would freeze its +// config byte, so the clear below could not be observed. +// Pass: write 0x1F -> read back 0x1F -> write 0 -> read back 0 -> j . (exit 0) // Fail: any trap or readback mismatch -> semihosting SYS_EXIT (exit 2) +#include "pmp_test_common.h" + .section .text .globl _start _start: @@ -10,7 +14,7 @@ _start: csrw mtvec, t0 // Write and verify a non-zero value - li t0, 0x9F + li t0, 0x1F csrw 0x3A4, t0 // pmpcfg4 write csrr t1, 0x3A4 // pmpcfg4 read bne t0, t1, fail // must match @@ -23,12 +27,5 @@ _start: j . // pass -fail: - li a0, 0x18 - .option push - .option norvc - slli zero, zero, 0x1f - ebreak - srai zero, zero, 7 - .option pop - j . +trap_entry fail + semihosting_fail diff --git a/contrib/fw/pmp-8entry-guard-test/pmp_8entry_guard_test.S b/contrib/fw/pmp-8entry-guard-test/pmp_8entry_guard_test.S index a739801..1bc9081 100644 --- a/contrib/fw/pmp-8entry-guard-test/pmp_8entry_guard_test.S +++ b/contrib/fw/pmp-8entry-guard-test/pmp_8entry_guard_test.S @@ -5,16 +5,7 @@ // Pass: phase2_trap fires (load denied) -> j . (exit 0) // Fail: CSR trap, load succeeds, or unexpected behavior -> semihosting SYS_EXIT (exit 2) -.macro semihosting_fail - li a0, 0x18 - .option push - .option norvc - slli zero, zero, 0x1f - ebreak - srai zero, zero, 7 - .option pop - j . -.endm +#include "pmp_test_common.h" .section .text .globl _start @@ -39,10 +30,10 @@ _start: semihosting_fail // no fault -> FAIL -phase1_trap: +trap_entry phase1_trap semihosting_fail // CSR trap -> FAIL -phase2_trap: +trap_entry phase2_trap j . // load denied as expected -> PASS .align 2 diff --git a/contrib/fw/pmp-cfg2-test/pmp_cfg2_test.S b/contrib/fw/pmp-cfg2-test/pmp_cfg2_test.S index 7a125a0..bcbe967 100644 --- a/contrib/fw/pmp-cfg2-test/pmp_cfg2_test.S +++ b/contrib/fw/pmp-cfg2-test/pmp_cfg2_test.S @@ -4,6 +4,8 @@ // Pass: pmpcfg2 readback == written value -> j . (exit 0) // Fail: pmpcfg2 write traps OR readback wrong -> semihosting SYS_EXIT (exit 2) +#include "pmp_test_common.h" + .section .text .globl _start _start: @@ -18,12 +20,5 @@ _start: j . // pass: ISS detects j . and exits 0 -fail: - li a0, 0x18 - .option push - .option norvc - slli zero, zero, 0x1f - ebreak - srai zero, zero, 7 - .option pop - j . +trap_entry fail + semihosting_fail diff --git a/contrib/fw/pmp-csr-test/pmp_csr_test.S b/contrib/fw/pmp-csr-test/pmp_csr_test.S index e363642..0a603c0 100644 --- a/contrib/fw/pmp-csr-test/pmp_csr_test.S +++ b/contrib/fw/pmp-csr-test/pmp_csr_test.S @@ -2,6 +2,8 @@ // Pass: j . detected as JUMP_TO_SELF by ISS, exits 0. // Fail: semihosting SYS_EXIT sequence, ISS exits non-zero. +#include "pmp_test_common.h" + .section .text .globl _start _start: @@ -18,15 +20,8 @@ _start: j . // pass: ISS detects j . and exits 0 -fail: +trap_entry fail // Semihosting SYS_EXIT: the ISS checks for slli+ebreak+srai and intercepts // before dispatching to mtvec. Must use 4-byte ebreak (not c.ebreak) so the // check lands at the correct offsets (-4 and +4 from the ebreak address). - li a0, 0x18 // SYS_EXIT - .option push - .option norvc // force 4-byte ebreak (0x00100073), not 2-byte c.ebreak - slli zero, zero, 0x1f - ebreak - srai zero, zero, 7 - .option pop - j . // fallback if semihosting is not configured + semihosting_fail diff --git a/contrib/fw/pmp-enforce-test/pmp_enforce_test.S b/contrib/fw/pmp-enforce-test/pmp_enforce_test.S index 549f133..0b8a62c 100644 --- a/contrib/fw/pmp-enforce-test/pmp_enforce_test.S +++ b/contrib/fw/pmp-enforce-test/pmp_enforce_test.S @@ -6,16 +6,7 @@ // Pass: PMP denies load -> load fault fires -> j . (exit 0) // Fail: any CSR trap, OR load succeeds without fault -> semihosting SYS_EXIT (exit 2) -.macro semihosting_fail - li a0, 0x18 - .option push - .option norvc - slli zero, zero, 0x1f - ebreak - srai zero, zero, 7 - .option pop - j . -.endm +#include "pmp_test_common.h" .section .text .globl _start @@ -41,10 +32,10 @@ _start: semihosting_fail // no fault: enforcement did not fire -> FAIL -phase1_trap: +trap_entry phase1_trap semihosting_fail // CSR trapped: PMP unavailable -> FAIL -phase2_trap: +trap_entry phase2_trap j . // load denied as expected -> PASS .align 2 diff --git a/contrib/fw/pmp-fetch-deny-test/pmp_fetch_deny_test.S b/contrib/fw/pmp-fetch-deny-test/pmp_fetch_deny_test.S new file mode 100644 index 0000000..8966ad1 --- /dev/null +++ b/contrib/fw/pmp-fetch-deny-test/pmp_fetch_deny_test.S @@ -0,0 +1,53 @@ +// PMP execute-permission enforcement on instruction fetch. +// +// Instruction fetches use the IMEM address space rather than MEM, and no other +// PMP test exercises the fetch path or the X permission bit at all. This test +// pins that behaviour so the address-space guard in pmp.h read_mem cannot be +// narrowed to MEM alone without a test failing. +// +// Entry 0 is an NA4 region covering the 4 bytes at target, locked and with no +// X permission. L=1 is required because pmp_check grants M-mode unconditionally +// when L=0. +// +// Pass: fetch of target is denied and reports an instruction access fault -> j . (exit 0) +// Fail: any CSR trap, the fetch succeeds, or the cause is wrong -> semihosting SYS_EXIT (exit 2) + +#include "pmp_test_common.h" + +.section .text +.globl _start +_start: + // A CSR trap here means PMP is unavailable -> FAIL + la t0, csr_trap + csrw mtvec, t0 + + // pmpaddr0 = target >> 2 (NA4: covers exactly the 4 bytes at target) + la t5, target + srli t1, t5, 2 + csrw pmpaddr0, t1 + + // pmpcfg0 byte 0 = 0x90 = NA4 (0x10) | L (0x80), no R/W/X + li t1, 0x90 + csrw pmpcfg0, t1 + + la t0, pmp_fault + csrw mtvec, t0 + + jr t5 // fetch from target: PMP must deny (no X) + +trap_entry csr_trap + semihosting_fail // CSR trapped: PMP unavailable -> FAIL + +trap_entry pmp_fault + // A denied fetch is an instruction access fault, not a load access fault. + csrr t1, mcause + li t2, 1 // RV_CAUSE_FETCH_ACCESS + bne t1, t2, fail + j . // fetch denied with the correct cause -> PASS + +fail: + semihosting_fail + + .align 2 // 4-byte align so the NA4 region covers this exactly +target: + semihosting_fail // executed only if the fetch was allowed -> FAIL diff --git a/contrib/fw/pmp-fetch-straddle-test/pmp_fetch_straddle_test.S b/contrib/fw/pmp-fetch-straddle-test/pmp_fetch_straddle_test.S new file mode 100644 index 0000000..4a6b18c --- /dev/null +++ b/contrib/fw/pmp-fetch-straddle-test/pmp_fetch_straddle_test.S @@ -0,0 +1,68 @@ +// PMP sector iteration must cover every 4-byte sector an access touches. +// +// pmp_check walked sectors with `for(offset = 0; offset < len; offset += 4)`, +// which visits only floor(len/4) sectors starting at addr. When addr is not +// 4-byte aligned the final touched sector is never examined. +// +// Instruction fetch is the reachable case: the hart rejects misaligned data +// accesses with MISALIGNED_LOAD before they reach PMP, but fetch_ins always +// requests 4 bytes and the fetch alignment is 2 on a compressed ISA, so a +// 4-byte instruction at PC%4==2 spans two sectors. +// +// Layout at straddle_base (4-byte aligned): +// base+0..base+1 c.nop sector base+0 +// base+2..base+5 4-byte j sectors base+0 and base+4 +// Entry 0 is an NA4 deny region covering only sector base+4, so the fetch at +// base+2 partially overlaps it. Partial overlap must be denied. +// +// Buggy: only sector base+0 is checked, no entry matches, the M-mode +// fall-through allows the fetch, the jump runs -> FAIL. +// Fixed: sector base+4 matches, the overlap is partial -> denied -> PASS. +// +// Pass: the straddling fetch is denied and reports an instruction access fault -> j . (exit 0) +// Fail: any CSR trap, the fetch succeeds, or the cause is wrong -> semihosting SYS_EXIT (exit 2) + +#include "pmp_test_common.h" + +.section .text +.globl _start +_start: + // A CSR trap here means PMP is unavailable -> FAIL + la t0, csr_trap + csrw mtvec, t0 + + // pmpaddr0 = (straddle_base + 4) >> 2: NA4 over the upper sector only + la t5, straddle_base + addi t1, t5, 4 + srli t1, t1, 2 + csrw pmpaddr0, t1 + + // pmpcfg0 byte 0 = 0x90 = NA4 (0x10) | L (0x80), no R/W/X + li t1, 0x90 + csrw pmpcfg0, t1 + + la t0, pmp_fault + csrw mtvec, t0 + + jr t5 // enter at straddle_base, which is 4-byte aligned + +trap_entry csr_trap + semihosting_fail // CSR trapped: PMP unavailable -> FAIL + +trap_entry pmp_fault + // A denied fetch is an instruction access fault, not a load access fault. + csrr t1, mcause + li t2, 1 // RV_CAUSE_FETCH_ACCESS + bne t1, t2, fetch_allowed + j . // straddling fetch denied as expected -> PASS + +fetch_allowed: + semihosting_fail // the straddling fetch was allowed -> FAIL + + .align 2 // 4-byte align: straddle_base % 4 == 0 +straddle_base: + c.nop // 2 bytes, leaves PC at base+2 + .option push + .option norvc // force a 4-byte jump so the fetch spans two sectors + j fetch_allowed // occupies base+2..base+5 + .option pop diff --git a/contrib/fw/pmp-lock-test/pmp_lock_test.S b/contrib/fw/pmp-lock-test/pmp_lock_test.S new file mode 100644 index 0000000..13240c0 --- /dev/null +++ b/contrib/fw/pmp-lock-test/pmp_lock_test.S @@ -0,0 +1,66 @@ +// PMP lock bit (L, bit 7 of a cfg byte) must be enforced. +// +// Per the RISC-V privileged spec, once a PMP entry is locked only a reset can +// unlock it: L=1 freezes both pmpicfg and the associated pmpiaddr, and if a +// locked entry uses A=TOR it additionally freezes pmpaddr[i-1], the region's +// lower bound. write_pmpcfg and write_pmpaddr overwrote their targets +// unconditionally, so a locked configuration could be relaxed at will. +// +// The locked entries here use A=OFF (and an empty TOR range) so no region is +// ever matched. This keeps the test purely about CSR retention and removes any +// chance of the test denying its own instruction fetches. +// +// Writes to locked entries are ignored silently rather than trapping, so every +// check is a CSR write followed by a readback compare. +// +// Pass: all three locked values are retained -> j . (exit 0) +// Fail: any CSR trap, OR any locked value changed -> semihosting SYS_EXIT (exit 2) + +#include "pmp_test_common.h" + +.section .text +.globl _start +_start: + // A CSR trap here means PMP is unavailable -> FAIL + la t0, fail + csrw mtvec, t0 + + // Entry 0: set an address, then lock the entry with A=OFF so it is locked + // but inactive. + li t1, 0x1111 + csrw pmpaddr0, t1 + li t1, 0x80 // L (0x80), A=OFF + csrw pmpcfg0, t1 + csrr t2, pmpcfg0 + li t3, 0x80 + bne t2, t3, fail // lock bit did not store at all + + // A locked cfg byte must survive an attempt to clear it. + csrw pmpcfg0, zero + csrr t2, pmpcfg0 + bne t2, t3, fail + + // A locked entry's pmpaddr must survive an attempt to change it. + li t1, 0x2222 + csrw pmpaddr0, t1 + csrr t2, pmpaddr0 + li t3, 0x1111 + bne t2, t3, fail + + // Entry 3 as locked TOR freezes pmpaddr2, its lower bound, even though + // entry 2 is itself unlocked. pmpaddr3 stays 0 so the range is empty. + li t1, 0x3333 + csrw pmpaddr2, t1 + li t1, 0x88 // L (0x80) | A=TOR (0x08) + slli t1, t1, 24 // place in cfg byte 3 -> entry 3 + csrw pmpcfg0, t1 + li t1, 0x4444 + csrw pmpaddr2, t1 + csrr t2, pmpaddr2 + li t3, 0x3333 + bne t2, t3, fail + + j . // every locked value retained -> PASS + +trap_entry fail + semihosting_fail diff --git a/contrib/fw/pmp-noentry-umode-test/pmp_noentry_umode_test.S b/contrib/fw/pmp-noentry-umode-test/pmp_noentry_umode_test.S new file mode 100644 index 0000000..e5eef41 --- /dev/null +++ b/contrib/fw/pmp-noentry-umode-test/pmp_noentry_umode_test.S @@ -0,0 +1,45 @@ +// With PMP entries implemented but none enabled, an S/U-mode access must fail. +// +// The privileged spec says that if no PMP entry matches an M-mode access the +// access succeeds, but if no entry matches an S/U-mode access while at least one +// entry is implemented, the access fails. pmp_check short-circuited on +// `if(!any_active) return true;`, which allowed the access at any privilege +// level, contradicting the fall-through at the end of the function that already +// returned PRIV == PRIV_M. +// +// This needs a hart that implements U mode: the M-only wrappers pin PRIV to +// PRIV_M, where the buggy and correct results coincide. Run with rv64gc_mup. +// +// No PMP entry is programmed here, so any_active stays false. After mret the +// first U-mode instruction fetch must already be denied. +// +// Pass: the U-mode access traps back to M mode -> j . (exit 0) +// Fail: U mode runs unrestricted -> semihosting SYS_EXIT (exit 2) + +#include "pmp_test_common.h" + +.section .text +.globl _start +_start: + // Any trap from U mode lands here and means PMP denied the access -> PASS + la t0, pmp_fault + csrw mtvec, t0 + + // mstatus.MPP = U (0) so mret drops to user mode + li t0, 3 + slli t0, t0, 11 // MPP mask, bits 12:11 + csrc mstatus, t0 + + la t0, umode_entry + csrw mepc, t0 + mret + + // Only reached if mret did not transfer control at all + semihosting_fail + +trap_entry pmp_fault + j . // U-mode access denied as expected -> PASS + +umode_entry: + // Reached only if U mode was allowed to run with no PMP entry enabled. + semihosting_fail diff --git a/contrib/fw/pmp-shift-test/pmp_shift_test.S b/contrib/fw/pmp-shift-test/pmp_shift_test.S index bbb15bb..2c13f3f 100644 --- a/contrib/fw/pmp-shift-test/pmp_shift_test.S +++ b/contrib/fw/pmp-shift-test/pmp_shift_test.S @@ -8,16 +8,7 @@ // Pass: PMP denies load -> load fault fires -> j . (exit 0) // Fail: any CSR trap, OR load succeeds without fault -> semihosting SYS_EXIT (exit 2) -.macro semihosting_fail - li a0, 0x18 - .option push - .option norvc - slli zero, zero, 0x1f - ebreak - srai zero, zero, 7 - .option pop - j . -.endm +#include "pmp_test_common.h" .section .text .globl _start @@ -43,10 +34,10 @@ _start: semihosting_fail // no fault: enforcement did not fire -> FAIL -phase1_trap: +trap_entry phase1_trap semihosting_fail // CSR trapped: PMP unavailable -> FAIL -phase2_trap: +trap_entry phase2_trap j . // load denied as expected -> PASS .align 2 diff --git a/contrib/fw/pmp-tor-test/pmp_tor_test.S b/contrib/fw/pmp-tor-test/pmp_tor_test.S index b9001a9..02de520 100644 --- a/contrib/fw/pmp-tor-test/pmp_tor_test.S +++ b/contrib/fw/pmp-tor-test/pmp_tor_test.S @@ -8,6 +8,8 @@ // Pass: load fault fires -> j . (exit 0) // Fail: load succeeds without fault -> semihosting SYS_EXIT (exit 2) +#include "pmp_test_common.h" + .section .text .globl _start _start: @@ -37,17 +39,10 @@ _start: ld t2, 0(t5) // No fault fired -> bug present -fail: - li a0, 0x18 - .option push - .option norvc - slli zero, zero, 0x1f - ebreak - srai zero, zero, 7 - .option pop - j . - -pmp_fault: +trap_entry fail + semihosting_fail + +trap_entry pmp_fault j . // load denied as expected -> PASS .align 3 // 8-byte align for ld diff --git a/contrib/fw/pmp-upper-cfg-test/pmp_upper_cfg_test.S b/contrib/fw/pmp-upper-cfg-test/pmp_upper_cfg_test.S index 7c7e524..8334724 100644 --- a/contrib/fw/pmp-upper-cfg-test/pmp_upper_cfg_test.S +++ b/contrib/fw/pmp-upper-cfg-test/pmp_upper_cfg_test.S @@ -5,6 +5,8 @@ // Pass: readback == written value -> j . (exit 0) // Fail: readback != written value -> semihosting SYS_EXIT (exit 2) +#include "pmp_test_common.h" + .section .text .globl _start _start: @@ -17,11 +19,4 @@ _start: j . // pass: ISS detects j . and exits 0 fail: - li a0, 0x18 - .option push - .option norvc - slli zero, zero, 0x1f - ebreak - srai zero, zero, 7 - .option pop - j . + semihosting_fail diff --git a/contrib/fw/pmp_test_common.h b/contrib/fw/pmp_test_common.h new file mode 100644 index 0000000..a430e95 --- /dev/null +++ b/contrib/fw/pmp_test_common.h @@ -0,0 +1,36 @@ +// Shared helpers for the PMP firmware tests in contrib/fw/pmp-*. +// +// Result convention used by every PMP test: +// Pass: reach a `j .` self-loop, which trips the ISS JUMP_TO_SELF finish +// condition and exits 0. +// Fail: semihosting SYS_EXIT, which makes riscv-sim exit 2. + +#ifndef PMP_TEST_COMMON_H +#define PMP_TEST_COMMON_H + +// Terminate the run as a failure via ARM-style semihosting SYS_EXIT. +// The ebreak must be the 4-byte encoding rather than c.ebreak, because the ISS +// identifies the semihosting call by the magic instructions at -4 and +4 around +// it, so norvc is required here even when the caller allows compressed code. +.macro semihosting_fail + li a0, 0x18 // SYS_EXIT + .option push + .option norvc + slli zero, zero, 0x1f + ebreak + srai zero, zero, 7 + .option pop + j . // fallback if semihosting is not configured +.endm + +// Start a trap handler. mtvec's low two bits hold the vectoring mode, so a +// handler that is only 2-byte aligned has its address truncated and the hart +// vectors two bytes early. That lands inside a preceding semihosting_fail, whose +// trailing `j .` reads as the pass condition, so a misaligned handler makes a +// test pass without its body ever running. Always declare handlers with this. +.macro trap_entry name + .align 2 +\name\(): +.endm + +#endif // PMP_TEST_COMMON_H diff --git a/src/iss/arch/riscv_hart_m_p.h b/src/iss/arch/riscv_hart_m_p.h index de83a35..bb7fb09 100644 --- a/src/iss/arch/riscv_hart_m_p.h +++ b/src/iss/arch/riscv_hart_m_p.h @@ -187,7 +187,8 @@ iss::status riscv_hart_m_p::read(const addr_t& a, const unsigned len } auto res = this->memory.rd_mem({address_type::PHYSICAL, a.access, a.space, a.val}, length, data); if(unlikely(res != iss::Ok && (access & access_type::DEBUG) == 0)) { - this->reg.trap_state = (1UL << 31) | traits::RV_CAUSE_LOAD_ACCESS << 16; + auto trap_id = is_fetch(a.access) ? traits::RV_CAUSE_FETCH_ACCESS : traits::RV_CAUSE_LOAD_ACCESS; + this->reg.trap_state = (1UL << 31) | trap_id << 16; this->fault_data = addr; } return res; diff --git a/src/iss/mem/pmp.h b/src/iss/mem/pmp.h index b00e00b..680b9ba 100644 --- a/src/iss/mem/pmp.h +++ b/src/iss/mem/pmp.h @@ -90,10 +90,10 @@ template struct pmp : public memory_ele iss::status read_mem(const addr_t& addr, unsigned length, uint8_t* data) { assert((addr.type == iss::address_type::PHYSICAL || is_debug(addr.access)) && "Only physical addresses are expected in pmp"); - if(likely(addr.space == arch::traits::MEM || std::numeric_limits::max()) && + // debug accesses bypass PMP so a debugger can inspect protected memory + // instruction fetches arrive in the IMEM space, data reads in MEM; both are subject to PMP + if(likely(addr.space == arch::traits::MEM || addr.space == arch::traits::IMEM) && !pmp_check(addr.access, addr.val, length) && !is_debug(addr.access)) { - if(is_debug(addr.access)) - throw trap_access(0, addr.val); // trap is raised in privilege wrapper return iss::Err; } @@ -102,9 +102,8 @@ template struct pmp : public memory_ele iss::status write_mem(const addr_t& addr, unsigned length, uint8_t const* data) { assert((addr.type == iss::address_type::PHYSICAL || is_debug(addr.access)) && "Only physical addresses are expected in pmp"); + // debug accesses bypass PMP so a debugger can modify protected memory if(likely(addr.space == arch::traits::MEM) && !pmp_check(addr.access, addr.val, length) && !is_debug(addr.access)) { - if(is_debug(addr.access)) - throw trap_access(0, addr.val); // trap is raised in privilege wrapper, so we just return error return iss::Err; } @@ -121,12 +120,20 @@ template struct pmp : public memory_ele iss::status write_pmpaddr(unsigned addr, reg_t const& val) { if(addr >= arch::pmpaddr0 && addr < arch::pmpaddr0 + NUM_ENTRIES) { - pmpaddr[addr - arch::pmpaddr0] = val; + auto i = addr - arch::pmpaddr0; + // L freezes the entry's own address; a locked TOR entry additionally freezes + // the preceding address register, which supplies its lower bound + auto locked_as_tor_base = i + 1 < NUM_ENTRIES && (cfg_byte(i + 1) & PMP_L) && ((cfg_byte(i + 1) & PMP_A) >> 3) == PMP_TOR; + if(!(cfg_byte(i) & PMP_L) && !locked_as_tor_base) + pmpaddr[i] = val; return iss::Ok; } return iss::Err; } + // config byte of entry i, extracted from the cfg register that packs it + uint8_t cfg_byte(size_t i) const { return (pmpcfg[i / cfg_reg_size] >> ((i % cfg_reg_size) * 8)) & 0xff; } + iss::status read_pmpcfg(unsigned addr, reg_t& val) { if(addr >= arch::pmpcfg0 && addr < arch::pmpcfg0 + (NUM_ENTRIES / cfg_reg_size) * pmpcfg_stride) { val = pmpcfg[(addr - arch::pmpcfg0) / pmpcfg_stride]; @@ -136,12 +143,16 @@ template struct pmp : public memory_ele } iss::status write_pmpcfg(unsigned addr, reg_t val) { if(addr >= arch::pmpcfg0 && addr < arch::pmpcfg0 + (NUM_ENTRIES / cfg_reg_size) * pmpcfg_stride) { - pmpcfg[(addr - arch::pmpcfg0) / pmpcfg_stride] = val & cfg_valid_mask; + auto reg = (addr - arch::pmpcfg0) / pmpcfg_stride; + // a locked entry keeps its config byte until reset, so retain those bytes + reg_t locked = 0; + for(size_t b = 0; b < cfg_reg_size; b++) + if(cfg_byte(reg * cfg_reg_size + b) & PMP_L) + locked |= reg_t(0xff) << (b * 8); + pmpcfg[reg] = (pmpcfg[reg] & locked) | ((val & cfg_valid_mask) & ~locked); any_active = false; - for(size_t i = 0; i < NUM_ENTRIES; i++) { - auto cfg = pmpcfg[i / cfg_reg_size] >> ((i % cfg_reg_size) * 8); - any_active |= cfg & PMP_A; - } + for(size_t i = 0; i < NUM_ENTRIES; i++) + any_active |= cfg_byte(i) & PMP_A; return iss::Ok; } return iss::Err; @@ -156,23 +167,31 @@ template struct pmp : public memory_ele }; template bool pmp::pmp_check(access_type type, uint64_t addr, unsigned len) { + // No entry can match, so this is the no-match outcome: M mode is unrestricted while + // S/U mode fails, since entries are implemented. Matches the fall-through below. if(!any_active) - return true; + return hart_if.PRIV == arch::PRIV_M; reg_t base = 0; for(size_t i = 0; i < NUM_ENTRIES; i++) { reg_t tor = pmpaddr[i] << PMP_SHIFT; - reg_t cfg = pmpcfg[i / cfg_reg_size] >> ((i % cfg_reg_size) * 8); + reg_t cfg = cfg_byte(i); if(cfg & PMP_A) { auto pmp_a = (cfg & PMP_A) >> 3; auto is_tor = pmp_a == PMP_TOR; auto is_na4 = pmp_a == PMP_NA4; reg_t mask = (pmpaddr[i] << 1) | (!is_na4); mask = ~(mask & ~(mask + 1)) << PMP_SHIFT; - // Check each 4-byte sector of the access + // Check every 4-byte sector the access touches. Counting offsets up to len + // skips the last sector whenever addr is not sector aligned, which fetches can + // be: fetch_ins always asks for 4 bytes and the fetch alignment is 2 on a + // compressed ISA, so an instruction at addr%4==2 spans two sectors. Note this + // also inspects the 2 bytes the ISS over-reads past a compressed instruction, + // so a fetch at the very end of an executable region is denied conservatively. auto any_match = false; auto all_match = true; - for(reg_t offset = 0; offset < len; offset += 1 << PMP_SHIFT) { - reg_t cur_addr = addr + offset; + constexpr reg_t sector_size = 1 << PMP_SHIFT; + reg_t last_sector = (addr + len - 1) & ~(sector_size - 1); + for(reg_t cur_addr = addr & ~(sector_size - 1); cur_addr <= last_sector; cur_addr += sector_size) { auto napot_match = ((cur_addr ^ tor) & mask) == 0; auto tor_match = base <= cur_addr && cur_addr < tor; auto match = is_tor ? tor_match : napot_match; @@ -189,37 +208,6 @@ template bool pmp::pmp_ch } base = tor; } - // constexpr auto pmp_num_regs = 16; - // reg_t tor_base = 0; - // auto any_active = false; - // auto lower_addr = addr >>2; - // auto upper_addr = (addr+len-1)>>2; - // for (size_t i = 0; i < pmp_num_regs; i++) { - // uint8_t cfg = csr[pmpcfg0+(i/4)]>>(i%4); - // uint8_t cfg_next = i==(pmp_num_regs-1)? 0 : csr[pmpcfg0+((i+1)/4)]>>((i+1)%4); - // auto pmpaddr = csr[pmpaddr0+i]; - // if (cfg & PMP_A) { - // any_active=true; - // auto is_tor = bit_sub<3, 2>(cfg) == PMP_TOR; - // auto is_napot = bit_sub<4, 1>(cfg) && bit_sub<3, 2>(cfg_next)!= PMP_TOR; - // if(is_napot) { - // reg_t mask = bit_sub<3, 1>(cfg)?~( pmpaddr & ~(pmpaddr + 1)): 0x3fffffff; - // auto mpmpaddr = pmpaddr & mask; - // if((lower_addr&mask) == mpmpaddr && (upper_addr&mask)==mpmpaddr) - // return (hart_if.reg.PRIV == PRIV_M && !(cfg & PMP_L)) || - // (type == access_type::READ && (cfg & PMP_R)) || - // (type == access_type::WRITE && (cfg & PMP_W)) || - // (type == access_type::FETCH && (cfg & PMP_X)); - // } else if(is_tor) { - // if(lower_addr>=tor_base && upper_addr<=pmpaddr) - // return (hart_if.reg.PRIV == PRIV_M && !(cfg & PMP_L)) || - // (type == access_type::READ && (cfg & PMP_R)) || - // (type == access_type::WRITE && (cfg & PMP_W)) || - // (type == access_type::FETCH && (cfg & PMP_X)); - // } - // } - // tor_base = pmpaddr; - // } return hart_if.PRIV == arch::PRIV_M; } diff --git a/src/sysc/register_cores.cpp b/src/sysc/register_cores.cpp index db74446..721c7d5 100644 --- a/src/sysc/register_cores.cpp +++ b/src/sysc/register_cores.cpp @@ -50,7 +50,7 @@ namespace iss { namespace interp { using namespace sysc; -__attribute__((used)) volatile std::array riscv_init = { +__attribute__((used)) volatile std::array riscv_init = { iss_factory::instance().register_creator("rv32i_m:interp", [](unsigned gdb_port, sysc::riscv::core_complex_if* cc) -> iss_factory::base_t { auto* cpu = new core2sc_adapter>(cc); @@ -151,6 +151,13 @@ __attribute__((used)) volatile std::array riscv_init = { auto* cpu = new core2sc_adapter>(cc); return {sysc::core_ptr{cpu}, vm_ptr{create(static_cast(cpu), gdb_port)}}; }), + iss_factory::instance().register_creator("rv64gc_mup:interp", // rv64gc_mu with PMP + [](unsigned gdb_port, sysc::riscv::core_complex_if* cc) -> iss_factory::base_t { + auto* cpu = new core2sc_adapter>(cc); + cpu->memories.insert_before_last( + std::make_unique>(cpu->get_priv_if())); + return {sysc::core_ptr{cpu}, vm_ptr{create(static_cast(cpu), gdb_port)}}; + }), iss_factory::instance().register_creator("rv64gc_msu:interp", [](unsigned gdb_port, sysc::riscv::core_complex_if* cc) -> iss_factory::base_t { auto* cpu = new core2sc_adapter>(cc); diff --git a/src/vm/interp/vm_rv64gc_mp.cpp b/src/vm/interp/vm_rv64gc_mp.cpp index 86eefaf..73d50b4 100644 --- a/src/vm/interp/vm_rv64gc_mp.cpp +++ b/src/vm/interp/vm_rv64gc_mp.cpp @@ -3,6 +3,7 @@ #include #include #include +#include #include #include #include @@ -24,7 +25,7 @@ struct rv64gc_mp_hart : public arch::riscv_hart_m_p { } }; -// rv64gc_mp_8_hart: same as rv64gc_mp but with only 8 PMP entries (models SiFive S5). +// rv64gc_mp_64_hart: same wiring, with the full 64 PMP entries. struct rv64gc_mp_64_hart : public arch::riscv_hart_m_p { mem::pmp pmp_obj{this->get_priv_if()}; @@ -34,6 +35,7 @@ struct rv64gc_mp_64_hart : public arch::riscv_hart_m_p { } }; +// rv64gc_mp_8_hart: only 8 PMP entries (models SiFive S5). struct rv64gc_mp_8_hart : public arch::riscv_hart_m_p { mem::pmp pmp_obj{this->get_priv_if()}; @@ -43,9 +45,21 @@ struct rv64gc_mp_8_hart : public arch::riscv_hart_m_p { } }; +// rv64gc_mup_hart: PMP on a hart that also implements U mode, so the privilege +// dependent parts of pmp_check are reachable. The M-only wrappers pin PRIV to +// PRIV_M, which makes any S/U behaviour untestable. +struct rv64gc_mup_hart : public arch::riscv_hart_mu_p { + mem::pmp pmp_obj{this->get_priv_if()}; + + rv64gc_mup_hart() { + pmp_obj.set_next(this->default_mem.get_mem_if()); + memory = pmp_obj.get_mem_if(); + } +}; + namespace { -volatile std::array rv64gc_mp_dummy = { +volatile std::array rv64gc_mp_dummy = { core_factory::instance().register_creator("rv64gc_mp:interp", [](unsigned port, void* init_data) -> std::tuple { auto* cpu = new rv64gc_mp_hart(); @@ -66,8 +80,18 @@ volatile std::array rv64gc_mp_dummy = { } return {cpu_ptr{cpu}, vm_ptr{iss::interp::create(cpu, port, false)}}; }), - core_factory::instance().register_creator("rv64gc_mp_8:interp", [](unsigned port, void* init_data) -> std::tuple { - auto* cpu = new rv64gc_mp_8_hart(); + core_factory::instance().register_creator("rv64gc_mp_8:interp", + [](unsigned port, void* init_data) -> std::tuple { + auto* cpu = new rv64gc_mp_8_hart(); + if(init_data) { + auto* cb = + reinterpret_cast::reg_t>*>(init_data); + cpu->set_semihosting_callback(*cb); + } + return {cpu_ptr{cpu}, vm_ptr{iss::interp::create(cpu, port, false)}}; + }), + core_factory::instance().register_creator("rv64gc_mup:interp", [](unsigned port, void* init_data) -> std::tuple { + auto* cpu = new rv64gc_mup_hart(); if(init_data) { auto* cb = reinterpret_cast::reg_t>*>(init_data); cpu->set_semihosting_callback(*cb);