Access Protection

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 protect

1
2
3
#define PROTECTED_ATTAY_LENGTH 32
#define NAPOT_SIZE 128
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
2
3
4
5
6
/* Register a handler for the store access fault exception */
rc = metal_cpu_exception_register(cpu, ECODE_STORE_FAULT, store_access_fault_handler);
if(rc < 0) {
vv_msg(SEV_INFO,ST_FUNCTION,"Failed to register exception handler\n");
return 3;
}

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/* Initialize PMPs */
pmp = metal_pmp_get_device();
if(!pmp) {
vv_msg(SEV_INFO,ST_FUNCTION,"Unable to get PMP Device\n");
return 4;
}
metal_pmp_init(pmp);

/* Configure PMP 0 to only allow reads to protected_global. The
* PMP region is locked so that the configuration applies to M-mode
* accesses. */
struct metal_pmp_config config = {
.L = METAL_PMP_LOCKED, /*the orginal is UNLOCKED*/
.A = METAL_PMP_NAPOT, /* Naturally-aligned power of two */
.X = 0,
.W = 0,
.R = 1,
};

rc = metal_pmp_set_region(pmp, 0, config, protected_addr);
if(rc != 0) {
vv_msg(SEV_INFO,ST_FUNCTION,"Failed to configure PMP 0\n");
return 5;
}

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/* 
* Configuration for Front Port APU
**/
apu_glb_cfg global_cfg = {0};
apu_config apu_cfg = {0};

global_cfg.enable = 1;
global_cfg.lock = 0;

// Restrict the region
apu_cfg.start_address = &dplain;
apu_cfg.end_address = apu_cfg.start_address + sizeof(dplain);
apu_cfg.rg_no = 0;
apu_cfg.rg_en = 1;

apu_cfg.rg_rd_en = 1;
apu_cfg.rg_wr_en = 0;
apu_cfg.rg_lock = 0;
TE_apu_global_enable(global_cfg);
TE_riscv_configure_apu(apu_cfg);

Reference

0%