Summary
OpenC910 accepts A=NA4 in pmpcfg, but a higher-priority locked NA4 deny entry can be bypassed by a lower-priority broader allow entry.
The minimal software-visible POC is a locked M-mode lw:
PMP0 = locked NA4 deny on one word
PMP1 = locked 4KB NAPOT RWX allow on the containing page
lw denied_word
Expected result:
Actual result on OpenC910:
- the load retires successfully
Testcase idea
This testcase constructs two overlapping PMP entries on the same target word:
PMP0: a higher-priority NA4 entry that denies the 4-byte word
PMP1: a lower-priority 4KB NAPOT entry that allows the whole page
Then it executes exactly one instruction on the denied word:
If NA4 is implemented correctly, the load must trap with mcause = 5 (load access fault), because PMP0 has higher priority.
If the bug is present, the load retires successfully and the testcase prints BUG.
Seed Code
.include "openc910_min_common.inc"
.equ PMP0_NA4_DENY_LOCK, 0x90
.equ PMP1_NAPOT_RWX_LOCK, 0x9f
.text
.align 6
.global main
main:
la t0, trap_handler
csrw mtvec, t0
# PMP0: higher-priority locked NA4 deny on denied_word.
la t0, denied_word
srli t0, t0, 2
csrw pmpaddr0, t0
# PMP1: lower-priority locked 4KB NAPOT RWX allow on the containing page.
la t0, target_page
srli t0, t0, 2
li t1, 0x1ff
or t0, t0, t1
csrw pmpaddr1, t0
li t0, (PMP1_NAPOT_RWX_LOCK << 8) | PMP0_NA4_DENY_LOCK
csrw pmpcfg0, t0
fence
# This load should raise mcause=5 if NA4 is implemented correctly.
la t0, denied_word
lw t1, 0(t0)
la a0, msg_bug
jal ra, oracle_puts
1:
j 1b
trap_handler:
csrr t0, mcause
li t1, 5
beq t0, t1, expected_trap
la a0, msg_wrong
jal ra, oracle_puts
2:
j 2b
expected_trap:
la a0, msg_trap
jal ra, oracle_puts
3:
j 3b
.section .rodata
msg_bug:
.asciz "BUG\n"
msg_trap:
.asciz "TRAP\n"
msg_wrong:
.asciz "WRONG\n"
.data
.align 12
target_page:
.space 2048
denied_word:
.word 0x55667788
.space 4096 - 2048 - 4
How to reproduce
Build OpenC910's native Verilator simulator
Run this once:
cd openc910/smart_run
make compile SIM=verilator THREADS=8
make buildVerilator
Compile the testcase
/opt/riscv/bin/riscv64-unknown-elf-gcc -c -march=rv64gc_zicsr_zifencei -mabi=lp64d -O2 openc910_min_crt0.S -o openc910_min_crt0.o
/opt/riscv/bin/riscv64-unknown-elf-gcc -c -march=rv64gc_zicsr_zifencei -mabi=lp64d -O2 openc910_na4_load_bypass_min.S -o openc910_na4_load_bypass_min.o
/opt/riscv/bin/riscv64-unknown-elf-gcc -nostdlib -nostartfiles -march=rv64gc_zicsr_zifencei -mabi=lp64d -T openc910_min_linker.ld openc910_min_crt0.o openc910_na4_load_bypass_min.o -o openc910_na4_load_bypass_min.elf
Convert the ELF into OpenC910's inst.pat / data.pat
/opt/riscv/bin/riscv64-unknown-elf-objcopy -O srec openc910_na4_load_bypass_min.elf openc910_na4_load_bypass_min_inst.hex -j .text* -j .rodata* -j .eh_frame*
/opt/riscv/bin/riscv64-unknown-elf-objcopy -O srec openc910_na4_load_bypass_min.elf openc910_na4_load_bypass_min_data.hex -j .data* -j .bss -j .sbss -j .COMMON
../tests/bin/Srec2vmem openc910_na4_load_bypass_min_inst.hex inst.pat
../tests/bin/Srec2vmem openc910_na4_load_bypass_min_data.hex data.pat
Run OpenC910's native simulator
Observed output:
Meaning:
BUG = the denied load succeeded instead of trapping
TRAP = correct behavior; the load raised mcause = 5
WRONG = some unexpected trap happened, but not the expected load access fault
The seed intentionally self-spins after printing one of these strings, so a local run typically ends at the simulation cutoff afterward.
Test Case: test-case.zip
Summary
OpenC910 accepts
A=NA4inpmpcfg, but a higher-priority lockedNA4deny entry can be bypassed by a lower-priority broader allow entry.The minimal software-visible POC is a locked M-mode
lw:PMP0 = locked NA4 denyon one wordPMP1 = locked 4KB NAPOT RWX allowon the containing pagelw denied_wordExpected result:
load access faultActual result on OpenC910:
Testcase idea
This testcase constructs two overlapping PMP entries on the same target word:
PMP0: a higher-priorityNA4entry that denies the 4-byte wordPMP1: a lower-priority4KB NAPOTentry that allows the whole pageThen it executes exactly one instruction on the denied word:
If
NA4is implemented correctly, the load must trap withmcause = 5(load access fault), becausePMP0has higher priority.If the bug is present, the load retires successfully and the testcase prints
BUG.Seed Code
How to reproduce
Build OpenC910's native Verilator simulator
Run this once:
cd openc910/smart_run make compile SIM=verilator THREADS=8 make buildVerilatorCompile the testcase
Convert the ELF into OpenC910's
inst.pat/data.patRun OpenC910's native simulator
Observed output:
BUGMeaning:
BUG= the denied load succeeded instead of trappingTRAP= correct behavior; the load raisedmcause = 5WRONG= some unexpected trap happened, but not the expected load access faultThe seed intentionally self-spins after printing one of these strings, so a local run typically ends at the simulation cutoff afterward.
Test Case: test-case.zip