Skip to content

Conversation

@fsammoura1980
Copy link
Contributor

@fsammoura1980 fsammoura1980 commented Sep 18, 2025

This series significantly upgrades the RISC-V Physical Memory Protection (PMP) implementation by introducing dynamic configuration and Device Tree integration.

Key changes:

  • DT Integration: PMP entries can now be configured early and flexibly using the zephyr,memattr Device Tree property. The PMP domain resync logic is updated to respect these DT-defined regions.
  • Dynamic Mode: Introduce CONFIG_PMP_KERNEL_MODE_DYNAMIC to allow dynamic configuration and activation of Machine mode PMP entries for better context switching support.
  • Mode-Specific State: Split global PMP state variables (index and last address) into mode-specific counterparts for M-mode (kernel) and U-mode (userspace). This ensures correct per-thread PMP initialization and accurate tracking of global PMP ranges across privilege levels.
  • SMP Fix: Correct the PMP address indexing during SMP configuration to properly manage per-CPU IRQ stack guards.
  • Refactoring: Rename internal PMP functions (e.g., stackguard to kernelmode) to reflect their general kernel configuration role.
  • Modularity: Extract PMP region address decoding logic into a new helper function, improving code readability and enabling direct unit testing of address calculation modes (TOR, NA4, NAPOT).
  • Testing: Add a new unit test suite to validate the DT memory attribute configuration and PMP state.

@fkokosinski
Copy link
Member

Hi, thanks for the PR!

I see that we're limiting the user to just one custom protected memory region. Is this due to limitations imposed by Kconfig? Could we use devicetree for defining those regions instead? That would allow the user to specify more than one protected memory region.

@fsammoura1980
Copy link
Contributor Author

Hi, thanks for the PR!

I see that we're limiting the user to just one custom protected memory region. Is this due to limitations imposed by Kconfig? Could we use devicetree for defining those regions instead? That would allow the user to specify more than one protected memory region.

I am just defining only a new one because this is something that our application requires. users of the entry can get the config values from the device tree. Do you have any specific suggestion of how to generalize extracting things from a device tree early in the abstraction phase?

@fkokosinski
Copy link
Member

Do you have any specific suggestion of how to generalize extracting things from a device tree early in the abstraction phase?

We could start using zephyr,memory-attr for declaring PMP regions. IIRC the Zephyr ARM port uses them for MPU regions.

@fsammoura1980 fsammoura1980 force-pushed the custom_pmp branch 2 times, most recently from 61fc494 to 7219bdd Compare October 6, 2025 17:28
@fsammoura1980
Copy link
Contributor Author

Do you have any specific suggestion of how to generalize extracting things from a device tree early in the abstraction phase?

We could start using zephyr,memory-attr for declaring PMP regions. IIRC the Zephyr ARM port uses them for MPU regions.

ok, I implemented it using zephyr,memattr approach.

@fsammoura1980 fsammoura1980 force-pushed the custom_pmp branch 2 times, most recently from 4e71970 to 7bf83a8 Compare October 7, 2025 16:43
@fsammoura1980
Copy link
Contributor Author

@jimmyzhe Your concerns has been addresses. Your review is appreciated.

@fsammoura1980
Copy link
Contributor Author

@fkokosinski I responded to all reviewers feedback. Your review is appreciated.

@fsammoura1980 fsammoura1980 force-pushed the custom_pmp branch 2 times, most recently from 15085c2 to f299e3f Compare November 4, 2025 01:51
Rename the `z_riscv_pmp_stackguard_*` functions to
`z_riscv_pmp_kernelmode_*`. This change better reflects that
these functions are used for general kernel mode PMP configuration,
not strictly limited to stack guard purposes.

Call sites in fatal.c, isr.S, and switch.S have been updated accordingly.

Signed-off-by: Firas Sammoura <[email protected]>
@fsammoura1980 fsammoura1980 force-pushed the custom_pmp branch 2 times, most recently from e9f0bc6 to dc54c8d Compare November 4, 2025 23:35
Introduce `CONFIG_PMP_KERNEL_MODE_DYNAMIC` to enable dynamic
configuration and activation of Machine mode PMP entries. This allows
PMP settings to be managed efficiently during transitions between
kernel and thread contexts.

Signed-off-by: Firas Sammoura <[email protected]>
jimmyzhe
jimmyzhe previously approved these changes Nov 5, 2025
Copy link
Contributor

@jimmyzhe jimmyzhe left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM. Thanks.

Copy link

@npitre npitre left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some comments:

The commit log says: "the pmp_cfg array in z_riscv_pmp_init() is
initialized to zero to prevent writing uninitialized stack data to unused
PMP entries." This is unnecessary as that's what the third argument to
write_pmp_entries() is already meant for.

You do:

-       pmp_addr[global_pmp_end_index - 1] = global_pmp_last_addr;
+       pmp_addr[pmp_end_index - 1] = (pmp_end_index == global_pmp_m_end_index)
+                                             ? global_pmp_m_last_addr
+                                             : global_pmp_u_last_addr;

This is getting somewhat ugly. Suggestion:

enum {
    M_MODE = 0,
#if defined(CONFIG_USERSPACE)
    U_MODE,
#endif
    MODE_TOTAL
} pmp_mode;

[...]

static unsigned long global_pmp_last_addr[MODE_TOTAL];

static unsigned int global_pmp_end_index[MODE_TOTAL];

[...]

static inline unsigned int z_riscv_pmp_thread_init(enum pmp_mode mode,
                                                   unsigned long *pmp_addr,
                                                   unsigned long *pmp_cfg,
                                                   unsigned int index_limit)
{
    unsigned int pmp_end_index = global_pmp_end_index[mode];

    [...]
    pmp_addr[pmp_end_index - 1] = global_pmp_last_addr[mode];
    return pmp_end_index;
}

[...]

void z_riscv_pmp_kernelmode_prepare(struct k_thread *thread)
{
    unsigned int index = z_riscv_pmp_thread_init(M_MODE, PMP_M_MODE(thread));
    [...]

In z_riscv_pmp_thread_init() you have:

#if defined(CONFIG_MEM_ATTR) && defined(CONFIG_USERSPACE)
        /*
         * Retrieve the memory attribute region's pmpaddr entries to handle the
         * switch back from user mode.
         */
        memcpy(&pmp_addr[global_pmp_u_end_index], mem_attr_pmp_addr,
               (global_pmp_m_end_index - global_pmp_u_end_index) * PMPCFG_STRIDE);
#endif

This should be done only for mode == M_MODE.

In arch/riscv/core/fatal.c you do:

-#ifdef CONFIG_PMP_STACK_GUARD
+#if defined(CONFIG_PMP_STACK_GUARD) && defined(CONFIG_MULTITHREADING)

What's the reason for this?

In z_riscv_pmp_kernelmode_prepare():

+#if defined(CONFIG_PMP_STACK_GUARD) && defined(CONFIG_MULTITHREADING)

Can't we protect against overflow even when multithreading is disabled?

Split global PMP state variables (index and last address) into
mode-specific counterparts to correctly track the end of global PMP
ranges for both M-mode (kernel) and U-mode (userspace).

This ensures correct per-thread PMP initialization when configuring
mode-specific dynamic PMP entries.

Signed-off-by: Firas Sammoura <[email protected]>
When CONFIG_SMP is enabled, per-CPU IRQ stack guards are added. To prevent
unintended TOR (Top of Range) entry sharing, the PMP address entry
preceding each guard region in `pmp_addr` is marked with -1L.

The previously used index to access `pmp_addr` could become stale, as
additional PMP entries may be allocated after its initial calculation
but before the SMP loop for IRQ guards.

Signed-off-by: Firas Sammoura <[email protected]>
…utes

The Physical Memory Protection (PMP) initialization is updated to support
custom entries defined in the Device Tree (DT) using the `zephyr,memattr`
property, contingent on `CONFIG_MEM_ATTR` being enabled. A new function,
`set_pmp_mem_attr()`, iterates over DT-defined regions and programs PMP
entries in `z_riscv_pmp_init()`, allowing for early, flexible, and
hardware-specific R/W/X protection for critical memory areas. DT-based
entries are also installed in `z_riscv_pmp_kernelmode_prepare()` for
thread-specific configuration. The logic for the temporary PMP "catch-all"
entry is adjusted to account for new DT entries. Furthermore, the PMP
domain resync logic now masks user partition permissions against DT-defined
region permissions, preventing privilege escalation. `CONFIG_RISCV_PMP` is
updated to select `PMP_KERNEL_MODE_DYNAMIC` if `MEM_ATTR`. Finally, the
`pmp_cfg` array in `z_riscv_pmp_init()` is initialized to zero to prevent
writing uninitialized stack data to unused PMP entries.

Signed-off-by: Firas Sammoura <[email protected]>
The logic to decode PMP addressing modes (**TOR**, **NA4**, **NAPOT**) into
physical start and end addresses was previously embedded in
`print_pmp_entries()`.

Extract this calculation into a new static helper function,
`pmp_decode_region()`, to significantly improve the readability and
modularity of the PMP debug printing code.

The new helper function is fully self-contained and exposes a defined API
for the PMP address decoding logic. This enables **direct reuse** in
**unit tests** (e.g., using **Ztest**) to verify the core address
calculation accuracy for all PMP modes and boundary conditions, independent
of the main PMP initialization or logging path.

Signed-off-by: Firas Sammoura <[email protected]>
…state

This commit implements a new unit test suite to validate the
integration of Device Tree memory attributes (`zephyr,memory-attr`)
with the RISC-V Physical Memory Protection (PMP) hardware.

The test suite includes:
1. **`test_pmp_devicetree_memattr_config`**: Verifies that the PMP
   Control and Status Registers (CSRs) are programmed correctly based
   on the memory regions defined with `zephyr,memory-attr` in the
   Device Tree. It iterates through the active PMP entries and
   asserts a match against the expected DT-defined regions.
2. **`test_riscv_mprv_mpp_config`**: Checks the initial state of the
   Machine Privilege Register Virtualization (MPRV) bit and Machine
   Previous Privilege (MPP) field in the `mstatus` CSR to ensure PMP
   is configured for correct privilege level switching during boot.
3. **`test_dt_pmp_perm_conversion`**: Validates the
   `DT_MEM_RISCV_TO_PMP_PERM` macro to ensure the conversion from
   Device Tree memory attribute flags to RISC-V PMP permission bits
   (R/W/X) is correct.

Signed-off-by: Firas Sammoura <[email protected]>
@sonarqubecloud
Copy link

sonarqubecloud bot commented Nov 5, 2025

@fsammoura1980
Copy link
Contributor Author

Thanks Nicolas for the feedback. Here is my response:

The commit log says: "the pmp_cfg array in z_riscv_pmp_init() is initialized to zero to prevent writing uninitialized stack data to unused PMP entries." This is unnecessary as that's what the third argument to write_pmp_entries() is already meant for.

[FS] I believe this question was discussed in this comment with @jimmyzhe

You do:

-       pmp_addr[global_pmp_end_index - 1] = global_pmp_last_addr;
+       pmp_addr[pmp_end_index - 1] = (pmp_end_index == global_pmp_m_end_index)
+                                             ? global_pmp_m_last_addr
+                                             : global_pmp_u_last_addr;

This is getting somewhat ugly. Suggestion:

enum {
    M_MODE = 0,
#if defined(CONFIG_USERSPACE)
    U_MODE,
#endif
    MODE_TOTAL
} pmp_mode;

[...]

static unsigned long global_pmp_last_addr[MODE_TOTAL];

static unsigned int global_pmp_end_index[MODE_TOTAL];

[...]

static inline unsigned int z_riscv_pmp_thread_init(enum pmp_mode mode,
                                                   unsigned long *pmp_addr,
                                                   unsigned long *pmp_cfg,
                                                   unsigned int index_limit)
{
    unsigned int pmp_end_index = global_pmp_end_index[mode];

    [...]
    pmp_addr[pmp_end_index - 1] = global_pmp_last_addr[mode];
    return pmp_end_index;
}

[...]

void z_riscv_pmp_kernelmode_prepare(struct k_thread *thread)
{
    unsigned int index = z_riscv_pmp_thread_init(M_MODE, PMP_M_MODE(thread));
    [...]

[FS] Thanks for the suggestion! I agree that using an enum cleans up the code significantly.

I've implemented this by creating the pmp_mode enum and leveraging it to unify the handling of M-mode and U-mode PMP entries, just as you outlined.

The new structure is:

enum pmp_mode {
	M_MODE = 0,
#if defined(CONFIG_USERSPACE)
	U_MODE,
#endif /* CONFIG_USERSPACE */
	MODE_TOTAL
};

A modified PR has been uploaded with these changes.

In z_riscv_pmp_thread_init() you have:

#if defined(CONFIG_MEM_ATTR) && defined(CONFIG_USERSPACE)
        /*
         * Retrieve the memory attribute region's pmpaddr entries to handle the
         * switch back from user mode.
         */
        memcpy(&pmp_addr[global_pmp_u_end_index], mem_attr_pmp_addr,
               (global_pmp_m_end_index - global_pmp_u_end_index) * PMPCFG_STRIDE);
#endif

This should be done only for mode == M_MODE.

[FS] Acknowledged. This has been implemented for M-mode.

#if defined(CONFIG_MEM_ATTR) && defined(CONFIG_USERSPACE)
	/*
	 * This block restores the PMP entries used for memory attributes (set in
	 * mem_attr_pmp_addr) that were overwritten when switching from user mode
	 * back to kernel mode. It only applies when running in M_MODE pmp mode.
	 */
	if (mode == M_MODE) {
		memcpy(&pmp_addr[global_pmp_end_index[U_MODE]], mem_attr_pmp_addr,
		       (global_pmp_end_index[M_MODE] - global_pmp_end_index[U_MODE]) *
			       PMPCFG_STRIDE);
	}
#endif

In arch/riscv/core/fatal.c you do:

-#ifdef CONFIG_PMP_STACK_GUARD
+#if defined(CONFIG_PMP_STACK_GUARD) && defined(CONFIG_MULTITHREADING)

What's the reason for this?

[FS] This code block is intended to remove the per-thread stack guard PMP entry following a thread stack overflow event, a requirement tracked in a related issue (e.g., #75960).

This logic is only relevant when CONFIG_MULTITHREADING=y. When multithreading is disabled (CONFIG_MULTITHREADING=n), the main thread's stack guard is handled by a global, locked PMP entry set during system initialization (pmp.c#L443-L446). Since this locked entry cannot be removed, adding a conditional check for CONFIG_MULTITHREADING enhances correctness by ensuring the removal attempt only occurs in the per-thread context where it is necessary and possible.

In z_riscv_pmp_kernelmode_prepare():

+#if defined(CONFIG_PMP_STACK_GUARD) && defined(CONFIG_MULTITHREADING)

Can't we protect against overflow even when multithreading is disabled?

[FS] Per-thread stack guards are only required in a multithreaded context. If CONFIG_MULTITHREADING is disabled, the main thread's stack is already protected by a global, locked PMP entry set up during initialization (see pmp.c#L443-L446). We explicitly check both CONFIG_PMP_STACK_GUARD and CONFIG_MULTITHREADING for clarity, even though z_riscv_pmp_kernelmode_prepare() (the function calling this code) is only invoked when multithreading is enabled.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area: Architectures area: RISCV RISCV Architecture (32-bit & 64-bit) area: Tests Issues related to a particular existing or missing test

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants