Deep dive
How it works
In: an access (address, read/write/execute, privileged or not) and the region table · Out: allow, or a MemManage fault carrying the address · State: region table (base, size, rights), enable bits, one saved table per task · Cost: O(1) per access in hardware; O(regions) per context switch
- Reset: the MPU is off. Select a region in
MPU_RNR, writeMPU_RBARandMPU_RASR(ARMv7-M) orRBARandRLAR(ARMv8-M), then setMPU_CTRL.ENABLE. - Each access is compared with all enabled regions. On ARMv7-M the highest-numbered match decides, even if a lower region would allow it; a disabled subregion counts as no match. On ARMv8-M an overlap faults.
- The matching region's access permissions are checked against privilege (handlers and privileged threads versus unprivileged threads); XN forbids instruction fetch.
- No match: with
PRIVDEFENAset, privileged code falls back to the default memory map (the background region); unprivileged code faults. - A violation raises MemManage (escalated to HardFault if it is not enabled). The handler reads
MMFSR(inCFSR):DACCVIOL,IACCVIOL, stacking errorsMSTKERR/MUNSTKERR, andMMARVALID, which saysMMFARholds the faulting address. - Context switch: PendSV writes the incoming task's regions (stack plus a few configured ranges), with the MPU briefly disabled.
- Stack guard: a no-access region of 32 bytes at the low end of a stack, numbered higher than the stack's own region.
- RISC-V PMP: ordered entries in
pmpcfg/pmpaddr(NAPOT or TOR ranges); the lowest match wins, and M-mode is exempt unless the entry is locked.
Code
A std Rust model: ARMv7-M rules (size, alignment, subregions, highest region wins, privilege, XN), per-task reprogramming with a stack guard, and ARMv8-M base/limit with overlap faults.
#[derive(Clone, Copy, PartialEq)]
enum Ap { None, PrivRw, Rw, Ro } // subset of the ARMv7-M AP encodings
#[derive(Clone, Copy, PartialEq)]
enum Acc { Read, Write, Exec }
type Check = Result<(), u64>; // Err(addr) = MemManage fault, addr = MMFAR
fn allows(ap: Ap, xn: bool, acc: Acc, privileged: bool) -> bool {
let read = ap != Ap::None && (privileged || ap != Ap::PrivRw);
let write = ap == Ap::Rw || (ap == Ap::PrivRw && privileged);
match acc { Acc::Read => read, Acc::Write => write, Acc::Exec => read && !xn }
}
/// ARMv7-M region: size 2^n >= 32, base aligned to size, 8 subregions (SRD) if size >= 256.
#[derive(Clone, Copy)]
struct V7 { base: u64, size: u64, srd: u8, ap: Ap, xn: bool }
impl V7 {
fn new(base: u64, size: u64, srd: u8, ap: Ap, xn: bool) -> Option<V7> {
let ok = size.is_power_of_two() && size >= 32 && base % size == 0 && (srd == 0 || size >= 256);
ok.then_some(V7 { base, size, srd, ap, xn })
}
fn hits(&self, a: u64) -> bool {
a >= self.base && a - self.base < self.size
&& self.srd >> ((a - self.base) / (self.size / 8)) & 1 == 0 // disabled subregion: no match
}
}
struct Mpu7 { regions: [Option<V7>; 8], privdefena: bool }
impl Mpu7 {
fn check(&self, a: u64, acc: Acc, privileged: bool) -> Check {
// the highest-numbered matching region wins, whatever its permissions
match self.regions.iter().rev().flatten().find(|r| r.hits(a)) {
Some(r) if allows(r.ap, r.xn, acc, privileged) => Ok(()),
None if privileged && self.privdefena => Ok(()), // background map, privileged only
_ => Err(a),
}
}
}
/// ARMv8-M region: 32-byte granularity, base and inclusive limit; overlapping regions fault.
struct V8 { base: u64, limit: u64, ap: Ap }
fn check_v8(regions: &[V8], a: u64, acc: Acc, privileged: bool) -> Check {
let mut hit = regions.iter().filter(|r| a >= r.base && a <= r.limit);
match (hit.next(), hit.next()) {
(Some(r), None) if allows(r.ap, true, acc, privileged) => Ok(()),
_ => Err(a), // no match (no background map here) or overlap
}
}
const RAM: u64 = 0x2000_0000;
fn switch_to(m: &mut Mpu7, stack: u64) { // what PendSV does: rewrite regions 6 and 7
m.regions[6] = V7::new(stack, 1024, 0, Ap::Rw, true);
m.regions[7] = V7::new(stack, 32, 0, Ap::None, true); // guard: lowest 32 bytes
}
fn main() {
assert!(V7::new(RAM + 0x100, 1024, 0, Ap::Rw, true).is_none()); // misaligned
assert!(V7::new(RAM, 1000, 0, Ap::Rw, true).is_none()); // not 2^n
let mut m = Mpu7 { regions: [None; 8], privdefena: true };
m.regions[0] = V7::new(RAM, 64 * 1024, 0, Ap::PrivRw, true); // kernel RAM
let (a, b) = (RAM + 0x400, RAM + 0x800); // two 1 KiB task stacks
switch_to(&mut m, a);
assert!(m.check(a + 0x40, Acc::Write, false).is_ok());
assert!(m.check(a + 0x40, Acc::Exec, false).is_err()); // XN
assert!(m.check(b + 0x40, Acc::Read, false).is_err()); // other task: region 0 is privileged-only
assert!(m.check(b + 0x40, Acc::Read, true).is_ok()); // kernel: allowed by region 0
let mut sp = a + 1024; // the stack grows down until the guard (region 7) beats region 6
let fault = loop { sp -= 4; if let Err(f) = m.check(sp, Acc::Write, false) { break f; } };
assert_eq!(fault, a + 28);
switch_to(&mut m, b);
assert!(m.check(a + 0x40, Acc::Write, false).is_err());
// subregion disable: 2 KiB region, lowest eighth (256 B) switched off
let mut s = Mpu7 { regions: [None; 8], privdefena: false };
s.regions[0] = V7::new(RAM, 2048, 0b1, Ap::Ro, true);
assert!(s.check(RAM + 255, Acc::Read, false).is_err() && s.check(RAM + 256, Acc::Read, false).is_ok());
// ARMv8-M: any 32-byte multiple; overlap faults
let mut v8 = vec![V8 { base: RAM + 0x20, limit: RAM + 0x9F, ap: Ap::Rw }];
assert!(check_v8(&v8, RAM + 0x9F, Acc::Write, false).is_ok());
assert!(check_v8(&v8, RAM + 0xA0, Acc::Write, false).is_err());
v8.push(V8 { base: RAM + 0x80, limit: RAM + 0xFF, ap: Ap::Ro });
assert!(check_v8(&v8, RAM + 0x90, Acc::Read, false).is_err()); // in both regions
assert!(check_v8(&v8, RAM + 0xC0, Acc::Read, false).is_ok());
println!("all MPU checks passed");
}
Compiled and run: prints all MPU checks passed.
Complexity and constants
- Hardware compares all regions in parallel as part of the access.
- Region count: 8 on most Cortex-M; the FreeRTOS ports accept 8 or 16 (
configTOTAL_MPU_REGIONS); RISC-V allows 0, 16 or 64 PMP entries. - Switch: FreeRTOS's Cortex-M4 MPU port writes four regions with one load-multiple and one store-multiple through the alias registers (12 on 16-region parts).
Efficiency
- Cost: a protected access adds no instruction; any extra latency is core-specific (see the core's TRM; not measured here). A context switch adds a few region-register stores and barriers (cycle count not measured). Bound: the switch path and the region budget, not bandwidth.
- Levers:
- use the background map for privileged code — frees regions for tasks (Zephyr docs) — trusted kernel
- write regions in one store-multiple through the alias registers — fewer bus writes per switch (FreeRTOS CM4 MPU
port.c) — Cortex-M with RBAR/RASR aliases - disable a subregion instead of adding a guard region — one region does two jobs — ARMv7-M, regions of 256 bytes and up
- stack-limit registers (
PSPLIM/MSPLIM) — no region needed for the guard (ZephyrBUILTIN_STACK_GUARD) — ARMv8-M Mainline
- Naive → standard → state of the art:
- no protection, canaries only · cheapest; detects overflow late and only some of it
- regions rewritten per task, stack guard as a region · four register pairs per switch
- ARMv8-M stack limits plus base/limit regions · no guard region, no power-of-two padding
Implementation notes
- Align each protected stack to its power-of-two size (linker script); FreeRTOS rounds a size up to the next power of two (
prvGetMPURegionSizeSetting). - Disable the MPU while writing, as FreeRTOS does; finish with
DSB/ISB. - Keep kernel, vector table and MPU code privileged-only, or a task can rewrite the MPU.
- Enable MemManage and test
MMARVALIDbefore readingMMFAR; after a stacking errorMMFARmay be invalid (Zephyr's fault code handles this). - Zephyr's minimum region size defaults to 32 bytes (256 on ARMv6-M).
Pitfalls
- A stack frame bigger than the guard (a large local array) can jump over it; make the guard larger or use a stack-limit register.
- Priority rules differ: highest region wins on ARMv7-M, lowest entry on RISC-V, overlap faults on ARMv8-M.
- A high-numbered permissive region silently overriding a deny region below it.
- DMA masters and the debugger can bypass the MPU (from memory).
- After a supervisor stack overflow the system state is not trustworthy (Zephyr docs); restart rather than resume.
Variants and alternatives
In production
- RTOS kernels: FreeRTOS-MPU ports for Cortex-M4 and M33, with unprivileged tasks and per-task regions; Zephyr user mode with memory domains and
CONFIG_MPU_STACK_GUARD. - Safety-certified firmware: partitions of different integrity level, alongside coding standards (from memory).
- Secure IoT: isolating application, network and crypto code in TrustZone-based designs (from memory).
- Pairs with a watchdog: if the fault handler itself fails, the watchdog restarts the system.
Implementations
| name | lang | repo | install | verdict | licence | health | as_of | notes |
|---|---|---|---|---|---|---|---|---|
| FreeRTOS-Kernel MPU ports | C | FreeRTOS/FreeRTOS-Kernel | git submodule | — | — | — | — | portable/GCC/ARM_CM4_MPU (ARMv7-M), portable/GCC/ARM_CM33 (ARMv8-M, configENABLE_MPU) |
| Zephyr MPU drivers | C | zephyrproject-rtos/zephyr | west init (Zephyr getting started) | — | — | — | — | arch/arm/core/mpu: v7, v8, stack guard, memory domains |
| cortex-m | Rust | rust-embedded/cortex-m | cargo add cortex-m | — | — | — | — | register block for MPU (ctrl, rnr, rbar, rasr, aliases) |
Sources
- ARM, ARMv7-M Architecture Reference Manual, DDI 0403 (MPU chapter). No link: developer.arm.com returned 403 to the link checker.
- ARM, Armv8-M Architecture Reference Manual, DDI 0553 (PMSAv8). No link, same reason.
- RISC-V International, Privileged Architecture, Physical Memory Protection. source
- FreeRTOS-Kernel, ARM_CM4_MPU
port.c. code - FreeRTOS-Kernel, ARM_CM33 non-secure
port.c. code - Zephyr Project, Memory Protection Design. docs
- Zephyr,
arch/arm/core/mpu. code - cortex-m crate,
peripheral::MPU. docs