C-Class PMP is enforced on the cacheable path only — MMIO/uncacheable accesses bypass PMP

Target: shaktiproject/cores/c-class (umbrella). The affected source is in the caches_mmu submodule (caches_mmu/src/dcache/dcache1rw.bsv). Type: Finding / report (a spec-compliance gap). No patch attached — the cacheable-only behaviour is structural to the dcache; a suggested upstream fix is described below. Affected: c-class v5.5.0 = 84083b2 (== master HEAD); caches_mmu submodule 164569d.

Summary

The C-Class data cache runs fn_pmp_lookup and raises a PMP fault only on the cacheable rule (rl_ram_check). The MMIO / uncacheable path (rl_initiate_io) launches the bus request with no PMP term and no fault gate. So a U/S-mode load/store to a decoded MMIO window is never PMP-checked — even with a correct PMP map, and even after a fail-closed base-PMP fix, U/S MMIO is not fenced by PMP. This is independent of (and additional to) the base-PMP divergences in the related PMPFIX report.

Spec reference

RISC-V Privileged Architecture Vol II §3.7.1 — PMP applies to all S/U (and locked-M) accesses, independent of cacheability / memory attribute. The check must not be conditioned on the access being cacheable.

Evidence (re-confirmed live at caches_mmu HEAD 164569d)

In caches_mmu/src/dcache/dcache1rw.bsv: rule rl_ram_check (line 635) — the cacheable rule — calls PMP and raises the fault:

657:  let {pmp_err, pmp_cause} = fn_pmp_lookup(pmpreq, unpack(req.priv), …);
659:  if (!lv_access_fault && pmp_err) begin lv_access_fault = True; … end
716:  if (lv_access_fault) begin wr_fault <= True; …

But rule rl_initiate_io (line 1146) — the MMIO rule — has no fn_pmp_lookup and no wr_fault in its guard or body; the IO request is forwarded unconditionally. (Verified: fn_pmp_lookup does not appear anywhere between rule rl_initiate_io and its endrule.)

Generated Verilog (build/hw/verilog/mkdcache.v) corroborates — the fault term is AND-ed with the cacheable rule:

3117: assign wr_fault_whas = WILL_FIRE_RL_rl_ram_check &&
3119:      (fn_pmp_lookup___d252[7] || ff_from_tlb_rv_port1__read[72] || ff_from_tlb_rv_port1__read[64]);

— while the IO-launch rule carries no PMP/fault term:

2508: assign CAN_FIRE_RL_rl_initiate_io =
2509:      m_iobuffer_RDY_mv_io_head && ff_mem_io_request_FULL_N && m_iobuffer_mv_io_head_valid &&
2512:      !m_iobuffer_mv_io_empty && !rg_io_busy ;
2513: assign WILL_FIRE_RL_rl_initiate_io = CAN_FIRE_RL_rl_initiate_io ;

Reproduce

Map a U-mode process with a PMP map that denies a decoded MMIO/peripheral window, then issue a U-mode load/store to that window. Observed: the access completes (no PMP fault) — it routes through rl_initiate_io. Control: a cacheable (DTCM) deny over the same TOR range does fault on the same core — isolating the gap to the IO path.

Suggested upstream fix

Wire fn_pmp_lookup into the IO path. Either (a) compute the PMP result for the IO physical address and gate CAN_FIRE_RL_rl_initiate_io on !pmp_err, raising wr_fault / returning an access-fault response instead of issuing the bus transaction; or (b) factor the PMP check out of rl_ram_check into a shared pre-issue stage covering both the cacheable and IO paths. Use the same Load_access_fault / Store_access_fault cause as the cacheable path. Option (b) is cleaner (single PMP evaluation point, no path divergence) but is the larger change.

Verify (plan for the upstream fix)

(1) U-mode load/store to a PMP-denied MMIO window → access fault (was: completes). (2) U-mode access to a PMP-permitted MMIO window → succeeds (no regression). (3) Cacheable deny/permit unchanged (regression guard on rl_ram_check). (4) M-mode (and locked-region) MMIO behaviour matches the cacheable path.

Note

We hit this during an integration where the exposure was mitigated downstream at the SoC-fabric level (an AxPROT[0]-based per-privilege deny on both cacheable and IO beats), so this is offered as a finding rather than a patch. The core-level fix above is the correct upstream remedy.

Edited by siju-felsite