Be a security in 20, avoid 30 years of detours
RISC-V Access Protection Introduction
In a RISC-V core, we have three types of access control unit, the PMP (Physical Memory Protection) and MPU (Memory Protection Unit) and APU (Access Protection Unit) to distinguish & protect regions between User/Machine mode and Privilege/Unprivilege mode. In a general use case, the roles of PMP/MPU/APU are as follows:
- PMP: Protect machine mode region in user mode
- MPU: Separate privilege and unprivilege in user mode
- APU: Access control for RISC-V handling outside master access
PMP
Introduction
PMPs control the access privileges of physical memory including R/W and execute, normally it is used to regulate supervisor or user mode.
In a general RISC-V core, there are normally 16 PMP entries, which means we can divide 16 resource groups.
PMP configure
Address & range calculation
When we talk about memory protection, the first thing we consider is the protection range, the setting for PMP range is quite not initiative, here we basically introduce the range setting called NAPOT, it setting the range to an amount of Power of Two, it will first calculate a pmpaddr using some bit operations, then check the lower 1 number n
in this addr, and it will protect the region starting from pmpaddr & LOWER_MASK
with a range of $2^{n+3}$ bytes. The calculation rules are:
pmpaddr | pmpcfg.A | Match type and size |
---|---|---|
aaaaa…aaaa | NA4 | 4-byte NAPOT range |
aaaaa…aaa0 | NAPOT | 8-byte NAPOT range |
aaaaa…aa01 | NAPOT | 16-byte NAPOT range |
aaaaa…a011 | NAPOT | 32-byte NAPOT range |
aaa01…1111 | NAPOT | $2^{XLEN}$ byte NAPOT range |
Here we have a range of global uninit data which resides in .bss
section that we want to protect1
2
3
volatile uint32_t protected_global[PROTECTED_ARRAY_LENGTH] __attribute__(aligned(NAPOT_SIZE));
Now we start to calculate the pmpaddr
and range, assume that the protected_global
is at 0x30415880
, so we are going to protect the range 0x30415880 -- 0x304158FF
For the PMP addr reg, since it is 4bytes aligned, so first we omit the least two bits in the addr, and since the range is $2^{n+3}$, so we clear the bits corresponding with alignment.1
2
3
4
5
6
7
8/* PMP address are 4-bytes aligned, drop the bottom 2 bits */
size_t pmpaddr = ((size_t)&protected_global) >> 2; // 0x30415880 >> 2 = 0xC105620
/* Clear the bit corresponding with alignment */
protected_addr &= ~(NAPOT_SIZE >> 3); // 0x30415880 & 0xFFFFFFEF
/* Set the bits up to the alignment bit, this address will be set in pmpaddr */
protected_addr |= ((NAPOT_SIZE >> 3) - 1); // 0x3041588F
Finally we get an address of 0x3041588F, according to the NAPOT rule, we will protect $2^{4+3}$ bytes.
Exception handler register
The next step is to handle an exception handler so that we could go and handle the exception after exception happened, here please refer to spec 3.1.20 for the exception code.
1 | /* Register a handler for the store access fault exception */ |
Initialization and region config
Finally we do initialization and region config to finally open up the PMP module. The pmp will be sequentially used for protecting the physical region.
1 | /* Initialize PMPs */ |
Attempting to write the protected region will cause an exception:
1 | protected_global[0] = 6; |
APU
Introduction
In our trust engine, there’s an APU which protectes the access from masters to RISC-V, we can setting the start/end address and RD/WR enable bit to control the access behavior.
Configure
To configure the APU, we need to assign the left and right boundary ($a_l$ and $a_r$) to define the region, given an access region a
, it should satisfy that $a_l \le a \le a_r$.
If at least one APU is configured, then the space will become a white list mechanism, when master try to access the unprotected region, it will face an error.
1 | /* |