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);