VexRiscv MPU implementation

A prototype implementation of the RISC-V MPU was developed for the VEDLIoT project, which can be found on the mpu branch of lindemer’s fork of VexRiscv, here. Note that the MPU features were integrated directly into the PMP plugin’s source code, because they share a significant amount of hardware logic. This implementation is based on a simplified version of the MPU proposal, which is described on this page. It provides the minimum features required to implement secure enclaves in three privelege levels (M/S/U).

Specification

The concept and motivation behind this extension is described in this paper and the accompanying presentation. Since the actual MPU specification is still a draft and constantly changing, we implemented only the key features.

In the VexRiscv implementation, the MPU behaves almost identically to PMP, except that the mpucfg and mpuaddr CSRs can be accessed by both M- and S-mode. The mpucfg CSRs have an L bit like pmpcfg, but the meaning is different. In pmpcfg, the L bit causes the permissions to be enforced on M-mode and prevents the region from being changed until a full device reset. The L bit in mpucfg can always be changed by both M- and S-mode, and it does not cause the permissions to be enforced on S-mode. Instead, it fully revokes S-mode’s access to that region, but continues to enforce the specified permissions on U-mode. This is similar to how an MMU prevents the kernel from executing code located in userspace RAM.

This implementation only supports NAPOT addressing, and the registers are not ordered by priority. If there are multiple matching regions corresponding to a memory access, the access will be granted if any of those regions allow it. The same limitations are present in VexRiscv’s PMP implementation. The mpucfg registers are numbered 0x900 to 0x903 and the mpuaddr registers are numbered 0x910 to 0x91f.

Programming guide

The MPU unit tests on VexRiscv can be found here. These provide examples on how to configure it in Assembly.

RISC-V privilege levels

Trap handling

By default, all exceptions will cause the CPU to jump to the M-mode trap handler, so on startup, one of the first actions is to declare its location. This is done by writing the mtvec CSR:

__start:
    la x1, mtrap
    csrw mtvec, x1

mtrap:
    csrr x1, mcause
    // do something
    csrw mepc, x2
    mret

Inside the trap handler, a few other CSRs are useful:

mret is a special instruction which tells the CPU to return and switch to the previous privilege level. For example, if U-mode caused the exception, mret will switch the CPU back to U-mode. It’s not possible to discover the current privilege level from software in RISC-V, which is an intentional design choice to enhance security.

Privilege override

The CPU uses the 2-bit MPP field in the mstatus register to determine which privilege level to switch to when mret is called. These bits are set automatically when an exception occurs, so trap handlers can simply call mret to return to the originating level. In order to switch to a specific privilege level, these bits must be set manually before calling mret. M-mode is 0b11, S-mode is 0b01, and U-mode is 0b00.

For example, after some startup configuration in M-mode, the CPU will typically need to jump into S-mode and boot the OS. This can be done with the following sequence (assuming x1 holds the start address of the OS kernel):

    csrw mepc, x1
    li x1, 0x1800
    csrc mstatus, x1
    li x1, 0x0800
    csrs mstatus, x1
    mret

This clears the MPP bits and sets only the LSB of that field, in order to indicate S-mode. From S-mode, the jump to U-mode is quite similar. (Refer to the specification for more detailed information.):

    csrw sepc, x1
    li x1, 0x80
    csrc sstatus, x1
    sret

Exception codes

The MPU is intended to be an alternative to the MMU, and has thus re-used its exception codes. These are:

Trap delegation

One of the key use cases for the RISC-V MPU is running an embedded OS in S-mode with its own exception handler. Indeed, an operating system needs an exception handler. This way, the OS can use the MPU for thread isolation and abort threads that violate their permissions. To do this, the CPU must be configured at startup to send MPU exceptions to the S-mode trap handler, but leave other exceptions (e.g., PMP violations) to the M-mode trap handler.

The mdeleg CSR indicates which exceptions will be handled by the next-highest privilege level, in this case S-mode. The MPU throws page fault exceptions, which are numbered 12, 13 and 15. To delegate these to the S-mode trap handler, the corresponding bits must be set in mdeleg (i.e., 0xb000). (Note that all CSRs starting with the letter m are accesible only to M-mode.) After entering S-mode, the trap handler for those exceptions is declared by writing stvec. (Note that most of the M-mode CSRs have siblings in S-mode, i.e., scause, sstatus, etc.)

Integration with LiteX

Adding this extension to LiteX is a fairly simple process:

  1. Clone lindemer’s fork of VexRiscv and checkout the mpu branch.
  2. Follow the instructions in this README to point LiteX’s VexRiscv build script to your local clone of VexRiscv.
  3. Rebuild the VexRiscv_Secure variant of the CPU. The easiest way to do this is to simply delete the existing files (e.g., rm VexRiscv_Secure*) and then run make.

The MPU plugin is integrated directly into the PMP plugin’s source code, and it re-uses the PMP plugin’s configuration values. For example, if you rebuild the CPU with the following command, you will get a variant where both the PMP and the MPU have 16 regions and 256-byte granularity. (Keep in mind that the granularity has to be a power of 2, minimum 8 bytes, because only the NAPOT mode is implemented. 0-16 regions are allowed.) It would be quite trivial to create a variant where the PMP and MPU have different settings, but I left it this way so that it stays consistent with the upstream VexRiscv API.

sbt compile "runMain vexriscv.GenCoreDefault --pmpRegions 16 --pmpGranularity 256 --csrPluginConfig secure --outputFile VexRiscv_Secure"