Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 59 additions & 1 deletion arch/riscv/core/pmp.c
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,8 @@ static void print_pmp_entries(unsigned int pmp_start, unsigned int pmp_end,
* @param pmp_cfg Pointer to the array where the CSR contents will be stored.
* @param pmp_cfg_size The size of the pmp_cfg array, measured in unsigned long entries.
*/
static inline void z_riscv_pmp_read_config(unsigned long *pmp_cfg, size_t pmp_cfg_size)
IF_DISABLED(CONFIG_ZTEST, (static inline)) void z_riscv_pmp_read_config(unsigned long *pmp_cfg,
size_t pmp_cfg_size)
{
__ASSERT(pmp_cfg_size == (size_t)(CONFIG_PMP_SLOTS / PMPCFG_STRIDE),
"Invalid PMP config array size");
Expand All @@ -136,6 +137,40 @@ static inline void z_riscv_pmp_read_config(unsigned long *pmp_cfg, size_t pmp_cf
#endif
}

/**
* @brief Writes the PMP configuration CSRs (pmpcfgX) based on architecture and slot count.
*
* This helper function abstracts the logic required to write the correct Control and Status
* Registers (CSRs)—pmpcfg0, pmpcfg1, etc.—by accounting for whether the system is 32-bit or
* 64-bit and the total number of PMP slots configured. It handles the different register
* packing schemes between RV32 and RV64.
*
* @param pmp_cfg Pointer to the array containing the PMP configuration values to be written
* to the CSRs.
* @param pmp_cfg_size The size of the pmp_cfg array, measured in unsigned long entries.
*/
static inline void z_riscv_pmp_write_config(unsigned long *pmp_cfg, size_t pmp_cfg_size)
{
__ASSERT(pmp_cfg_size == (size_t)(CONFIG_PMP_SLOTS / PMPCFG_STRIDE),
"Invalid PMP config array size");

#ifdef CONFIG_64BIT
/* RV64: pmpcfg0 holds entries 0-7; pmpcfg2 holds entries 8-15. */
csr_write(pmpcfg0, pmp_cfg[0]);
#if CONFIG_PMP_SLOTS > 8
csr_write(pmpcfg2, pmp_cfg[1]);
#endif
#else
/* RV32: Each pmpcfg register holds 4 entries. */
csr_write(pmpcfg0, pmp_cfg[0]);
csr_write(pmpcfg1, pmp_cfg[1]);
#if CONFIG_PMP_SLOTS > 8
csr_write(pmpcfg2, pmp_cfg[2]);
csr_write(pmpcfg3, pmp_cfg[3]);
#endif
#endif
}

static void dump_pmp_regs(const char *banner)
{
unsigned long pmp_addr[CONFIG_PMP_SLOTS];
Expand Down Expand Up @@ -366,6 +401,29 @@ static unsigned long global_pmp_last_addr;
/* End of global PMP entry range */
static unsigned int global_pmp_end_index;

void riscv_pmp_clear_all(void)
{
/*
* Ensure we are in M-mode and that memory accesses use M-mode privileges
* (MPRV=0). We also set MPP to M-mode to establish a predictable prior privilege level.
*/
csr_clear(mstatus, MSTATUS_MPRV);
csr_set(mstatus, MSTATUS_MPP);

const size_t num_pmpcfg_reg = CONFIG_PMP_SLOTS / PMPCFG_STRIDE;
unsigned long pmp_cfg[num_pmpcfg_reg];

z_riscv_pmp_read_config(pmp_cfg, num_pmpcfg_reg);

uint8_t *pmp_n_cfg = (uint8_t *)pmp_cfg;

for (int index = 0; index < CONFIG_PMP_SLOTS; ++index) {
pmp_n_cfg[index] = 0x0;
}

z_riscv_pmp_write_config(pmp_cfg, num_pmpcfg_reg);
}

/**
* @Brief Initialize the PMP with global entries on each CPU
*/
Expand Down
27 changes: 27 additions & 0 deletions include/zephyr/arch/riscv/pmp.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/*
* Copyright (c) 2025 Alphabet
*
* SPDX-License-Identifier: Apache-2.0
*/

#ifndef ZEPHYR_INCLUDE_RISCV_PMP_H_
#define ZEPHYR_INCLUDE_RISCV_PMP_H_

#include <zephyr/arch/riscv/arch.h>

#ifdef CONFIG_RISCV_PMP

/**
* @brief Resets all unlocked PMP entries to OFF mode (Null Region).
*
* This function is used to securely clear the PMP configuration. It first
* ensures the execution context is M-mode by setting MSTATUS_MPRV=0 and
* MSTATUS_MPP=M-mode. It then reads all pmpcfgX CSRs, iterates through
* the configuration bytes, and clears the Address Matching Mode bits (PMP_A)
* for any entry that is not locked (PMP_L is clear), effectively disabling the region.
*/
void riscv_pmp_clear_all(void);

#endif

#endif /* ZEPHYR_INCLUDE_RISCV_PMP_H_ */
13 changes: 13 additions & 0 deletions tests/arch/riscv/pmp/clear-pmp-unlocked-entries/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# SPDX-License-Identifier: Apache-2.0

cmake_minimum_required(VERSION 3.20.0)
find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE})
project(riscv_pmp)

FILE(GLOB app_sources src/*.c)
target_sources(app PRIVATE ${app_sources})

target_include_directories(app PRIVATE
${ZEPHYR_BASE}/kernel/include
${ZEPHYR_BASE}/arch/${ARCH}/include
)
2 changes: 2 additions & 0 deletions tests/arch/riscv/pmp/clear-pmp-unlocked-entries/prj.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
CONFIG_ZTEST=y
CONFIG_MULTITHREADING=y
109 changes: 109 additions & 0 deletions tests/arch/riscv/pmp/clear-pmp-unlocked-entries/src/main.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
/* Copyright (c) 2022 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/


#include <kernel_internal.h>
#include <zephyr/arch/riscv/pmp.h>
#include <zephyr/tc_util.h>
#include <zephyr/ztest.h>

#define MSTATUS_MPP_SHIFT 11

void z_riscv_pmp_read_config(unsigned long *pmp_cfg, size_t pmp_cfg_size);

/* The logic to read MPRV value (Bit 17) of the mstatus register. */
static unsigned long read_mprv_value(void)
{
return (csr_read(mstatus) & MSTATUS_MPRV) ? 1 : 0;
}

/* The logic to read MPP value (Bits 11:12) of the mstatus register. */
static unsigned long read_mpp_value(void)
{
return ((csr_read(mstatus) & MSTATUS_MPP) >> MSTATUS_MPP_SHIFT);
}

/**
* @brief Test selective PMP cleanup: only clear unlocked entries.
*
* This test verifies that the system function `riscv_pmp_clear_all()` correctly
* zeros out Physical Memory Protection (PMP) entries that are **unlocked** (PMP_L bit clear),
* while scrupulously preserving the state of all **locked** entries (PMP_L bit set).
*/
ZTEST(riscv_pmp_clear_unlocked_entries, test_riscv_pmp_clear_unlocked_entries)
{
/* Calculate the number of full 'unsigned long' CSRs required to hold all PMP slots.
* PMP entries are 8-bit, so the total number of bytes is CONFIG_PMP_SLOTS.
* This must be aligned to the size of the PMP Configuration CSRs (unsigned long).
*/
const size_t num_pmpcfg_regs = CONFIG_PMP_SLOTS / sizeof(unsigned long);

/* Arrays to store the PMP configuration state before and after the clear operation. */
unsigned long pmp_cfg_before[num_pmpcfg_regs];
unsigned long pmp_cfg_after[num_pmpcfg_regs];

/* --- Pre-Clear M-Status Checks (Expected State for Setup) --- */
/* Expected: MPRV is 1 (Enabled). When 1, M-mode loads/stores use the privilege defined by
* MPP.
*/
zassert_equal(
read_mprv_value(), 1,
"MPRV should be enabled (1) to use the privilege specified by the MPP field.");

/* Expected: MPP is 0x00 (Binary 00, for U-Mode). This configuration is used before dropping
* to User mode.
*/
zassert_equal(read_mpp_value(), 0x00,
"MPP should be set to 0x00 (U-Mode) before execution, not 0x%lx.",
read_mpp_value());

/* 1. Capture the initial state of all PMP Configuration CSRs. */
z_riscv_pmp_read_config(pmp_cfg_before, num_pmpcfg_regs);

/* 2. Execute the function under test. This should clear all UNLOCKED entries. */
riscv_pmp_clear_all();

/* 3. Capture the final state for comparison. */
z_riscv_pmp_read_config(pmp_cfg_after, num_pmpcfg_regs);

/* Cast the configuration arrays to an array of bytes (uint8_t *).
* This is crucial because each PMP entry (pmpcfgX) is an 8-bit field,
* regardless of the XLEN (32-bit or 64-bit) of the RISC-V architecture.
*/
const uint8_t *const pmp_entries_initial = (const uint8_t *)pmp_cfg_before;
const uint8_t *const pmp_entries_final = (const uint8_t *)pmp_cfg_after;

/* Iterate over all available PMP slots (0 up to CONFIG_PMP_SLOTS - 1). */
for (int index = 0; index < CONFIG_PMP_SLOTS; ++index) {
const uint8_t initial_entry = pmp_entries_initial[index];
const uint8_t final_entry = pmp_entries_final[index];

/* Check if the PMP Lock bit (PMP_L) was set in the initial state. */
if (initial_entry & PMP_L) {
/* If LOCKED: The entry MUST remain completely unchanged. */
zassert_equal(initial_entry, final_entry,
"PMP Entry %d (LOCKED) changed: Initial=0x%x, Final=0x%x. "
"Locked entries must be preserved.",
index, initial_entry, final_entry);
} else {
/* If UNLOCKED: The entry MUST be cleared to 0 (PMP_A_NA: Non-Active). */
zassert_equal(final_entry, 0,
"PMP Entry %d (UNLOCKED) was not cleared to 0x0. "
"Initial=0x%x, Final=0x%x. Unlocked entries must be cleared.",
index, initial_entry, final_entry);
}
}

/* --- Post-Clear M-Status Checks (Expected Return State) --- */
/* Expected: MPRV is 0 (Disabled). When 0, M-mode loads/stores use M-mode privilege. */
zassert_equal(read_mprv_value(), 0,
"MPRV should be disabled (0) to ensure M-mode memory accesses use M-mode "
"privilege.");

/* Expected: MPP is 0x3 (Binary 11, for M-Mode). This is the default mode after boot. */
zassert_equal(read_mpp_value(), 0x3,
"MPP should be set to 0x3 (M-Mode) after boot, not 0x%lx.", read_mpp_value());
}

ZTEST_SUITE(riscv_pmp_clear_unlocked_entries, NULL, NULL, NULL, NULL, NULL);
10 changes: 10 additions & 0 deletions tests/arch/riscv/pmp/clear-pmp-unlocked-entries/testcase.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
common:
platform_allow:
- qemu_riscv32
- qemu_riscv32e
- qemu_riscv64
filter: CONFIG_RISCV_PMP
ignore_faults: true

tests:
arch.riscv.pmp.clear.unlocked.entries: {}