Prologue
RISC-V is an open and modular architecture that enables customizable and efficient processor designs. When secure processing is of interest, it is necessary to understand the memory protection mechanisms that are part of the architecture. RISC-V supports an optional physical memory protection (PMP) unit, designed to protect machine secrets, that provides per-hart memory protection mechanisms. In this blog, we explore the intricacies of the PMP specification.
This article goes pretty deep into the Physical Memory Protection (PMP) specification of RISC-V, described in detail in Section 3.7 of the RISC-V Privileged Specification. It is recommended to have a basic understanding of RISC-V before reading this article. That being said, grab your fins, and deflate!
(* equalize *)

Descending to 10 meters (0 to 1 Bar)
Introduction to PMPs
Simply put, PMPs are just a bunch of registers and gates. They’re an optional hardware unit, that allow you regulate memory access privileges such as read, write, and execute, over the physical address space.
Typically, these are used to regulate accesses whose effective privilege mode is Supervisor or User. However, they may also be used to regulate accesses to the Machine mode, but in this case the PMP registers themselves are locked and even M-mode software can’t change them until the hart is reset.
From the spec: At any time, a RISC-V hardware thread (hart) is running at some privilege level encoded as a mode in one or more CSRs (control and status registers). Three RISC-V privilege levels are currently defined in the increasing order of privilege as: User (U), Supervisor (S) and Machine (M).
Wait a minute, I said “PMP registers”. Let’s talk about them for a minute ?
(* equalize *)
PMP Registers
In order to protect a certain region of physical memory, we need to define the start/end addresses, and the permissions that apply on them. The RISC-V architecture specifies a set of dedicated CSRs for encoding the address space, and another for encoding the permissions. The former being called pmpaddr and the latter, pmpcfg. There can be a maximum of 64 PMP entries. Hence, 64 pmpaddr and 16 pmpcfg registers.
You can simply use 2 entries to denote the base/bound addresses (TOR). The spec defines a smart technique to encode the base address and the size of the region in a single 32-bit register, if your region is an aligned natural power of 2. Given that the minimum region size is 4 bytes, we can already ignore the lower 2 bits of an address. Then, you can simply set all the bits with lower significance to indicate the size of the region. Confused? here’s an example 1:
/* encode PMP address */
if (log2len == PMP_SHIFT) {
pmpaddr = (addr >> PMP_SHIFT);
} else {
if (log2len == __riscv_xlen) {
pmpaddr = -1UL;
} else {
addrmask = (1UL << (log2len - PMP_SHIFT)) - 1;
pmpaddr = ((addr >> PMP_SHIFT) & ~addrmask);
pmpaddr |= (addrmask >> 1);
}
}
The pmpcfg registers, are even smaller. Just 3 bits to indicate the “rwx” permissions, a couple of bits to indicate the address matching mode, and another bit to indicate a locked state. Aligning this number to a power of 2, we say we need 8 bits to define a pmpcfg entry.
We’re at about 6 to 8 meters below the sea level. Exciting, right ? By the way, here’s some marine life at this depth.

Let’s dive a little deeper, shall we?
(* equalizes *)
(* clears face mask *)
10 to 20 meters (1 to 2 Bar)
Address Matching Modes
Let’s talk a little more about this. We said that we need 2 bits to encode the address matching mode. As it happens, the spec defines 4 modes to match addresses. They are:
- Off — No address matching
- TOR — Top of Range
- NA4 — Naturally Aligned Four-byte region
- NAPOT — Naturally Aligned Power-Of-Two region, ≥ 8 bytes.
Each pmpcfg register holds 4 PMP entries. In case of a 64-bit machine, the odd-numbered indices act in continuity of the even-numbered indices.
In the TOR mode, there is no restriction on the alignment of the base/bound addresses. In the NAPOT mode however, both the base address must be aligned to the power of two which the size of the region is. This must also be a multiple of the granularity. For example, if your granularity is 32 bytes, then protecting a region starting at 0x80000010 is illegal in the NAPOT mode.
Setting permissions
In order to protect a 32-byte region starting at 0x80000000 against fetches in S/U modes, and set this to be of the first priority while matching addresses, you would write the value 0x20000003 into the pmpaddr0 register. Before you do that, you would set the pmpcfg0 register to contain the value 0x0000001B. In order to apply these permissions to machine mode (M) as well, you would now lock this PMP entry by setting the pmpcfg0 register to contain the value 0x0000009B. Alas, now you can never fetch the first 32 bytes starting at 0x80000000.You also can’t change this value until you reset your hart since the lock bit is sticky. Haha 🙂
Now wait, I mentioned priority. Where did that come from ?
Check your pressure guage for how much air is left in your oxygen tank, we’re going deeper. Heads up, my dive computer says we’re at about 17 meters. If we go any deeper, we may have to perform a decompression stop. We’ll make a safety-stop at 5 meters anyways 🙂

20 to 30 meters (2 to 3 Bar)
Priority and Matching Logic
Let’s slow down a bit now. The spec says that a lower-numbered PMP entry has higher priority. This is used to break ties when there’s multiple PMP regions that match the same memory access. Here are some corner-case observations.
If a memory access is not fully by the highest (in priority) PMP entry, then the access is considered to be faulty. Even if the access is fully covered by the second-highest priority PMP entry, it is still considered to be faulty. If the access does not match any PMP entry, then too, it is considered to be faulty. In the case that an access from a low-privilege mode does not match any PMP entry, it is still considered to be faulty. In other words, a memory access is considered to be faulty if it the (priority-based) matching entry does not match the access fully. However, the spec allows you to split a 4-byte access into 4 separate byte accesses, which may now be fully covered by different PMP regions that allow the access to go through.
For the case of atomics, the region on which an AMO op is desired, must allow both read and write accesses. Irrespective of which permission is defined, the AMO op will fault with the cause store-access-fault. The spec also prevents you from protecting a physical memory region against reads if you’re allowing writes to it. However, the spec says that if an AMO op is split into multiple memory accesses in the micro-architecture, then possible side-effects are allowed but the access muse still fault.
That’s not all.
30 to 40 meters (3 to 4 Bar)
Smepmp
The RISC-V Privileged Specification defines an ISA extension for PMP Enhancements.
From the spec: Being able to access the memory of a process running at a high privileged execution mode, such as the Supervisor or Machine mode, from a lower privileged mode such as the User mode, introduces an obvious attack vector since it allows for an attacker to perform privilege escalation, and tamper with the code and/or data of that process. A less obvious attack vector exists when the reverse happens, in which case an attacker instead of tampering with code and/or data that belong to a high-privileged process, can tamper with the memory of an unprivileged / less-privileged process and trick the high-privileged process to use or execute it. To prevent this attack vector, two mechanisms known as Supervisor Memory Access Prevention (SMAP) and Supervisor Memory Execution Prevention (SMEP) were introduced in recent systems. The first one prevents the OS from accessing the memory of an unprivileged process unless a specific code path is followed, and the second one prevents the OS from executing the memory of an unprivileged process at all times. RISC-V already includes support for SMAP, through the sstatus.SUM bit, and for SMEP by always denying execution of virtual memory pages marked with the U bit, with Supervisor mode (OS) privileges, as mandated on the Privilege Spec.
This extension defines a Rule Locking Bypass (RLB) mechanism, essentially allowing you to bypass the locked nature of PMP entries, a Machine-Mode Whitelist/Allowlist Policy (MMWP) which when set changes the default PMP policy for M-mode when accessing memory regions that don’t have a matching PMP rule, to denied instead of ignored, and a Machine-Mode Lockdown (MML) which changes the meaning of pmpcfg.L to mean that a rule is now M-mode-only when set, and S/U-mode-only when unset.
We can’t stay at this depth for too long, maybe another dive some other day. Let’s ascend, shall we ?

Monitor your ascent rate. Ensure that you don’t go up faster than the last air bubble! Remember to make a decompression stop and connect with the author: Karthik B.
References:
[2]: https://github.com/riscv/riscv-isa-manual/releases/tag/riscv-isa-release-7004057-2025-02-14