Home › Embedded, industrial & energy › Firmware & peripherals
algorithm low

Memory protection unit

Problem Firmware that runs several tasks, or trusted and untrusted code, in one flat address space on a small microcontroller has no virtual memory. A buggy or hostile task can overwrite a neighbour's data, and a stack overflow corrupts memory silently.

A handful of base/size regions with permissions, checked on every access by the CPU, give microcontroller tasks isolation and stack-overflow traps without an MMU.

hardware check O(1) per access (regions compared in parallel); a context switch rewrites a few region registers, O(regions)
firmwareisolationrtosstack-overflowprivilegecortex-mrisc-v
Overview

A microcontroller has no page tables, yet an RTOS still wants each task to touch only its own stack and buffers. The MPU is a small table (8 or 16 entries on Cortex-M) of address ranges with access rights, checked by hardware on every load, store and fetch. A violation raises a fault instead of corrupting memory. The kernel rewrites task-specific entries on each context switch, and a no-access guard below a stack turns a silent overflow into a trap.

Key facts

Use when

  • An RTOS must stop tasks corrupting each other or the kernel: FreeRTOS-MPU, Zephyr user mode.
  • A safety case needs freedom from interference between functions of different integrity level (automotive, industrial, medical firmware).
  • Secure IoT firmware separates application, network stack and third-party libraries.

Avoid when

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

  1. Reset: the MPU is off. Select a region in MPU_RNR, write MPU_RBAR and MPU_RASR (ARMv7-M) or RBAR and RLAR (ARMv8-M), then set MPU_CTRL.ENABLE.
  2. 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.
  3. The matching region's access permissions are checked against privilege (handlers and privileged threads versus unprivileged threads); XN forbids instruction fetch.
  4. No match: with PRIVDEFENA set, privileged code falls back to the default memory map (the background region); unprivileged code faults.
  5. A violation raises MemManage (escalated to HardFault if it is not enabled). The handler reads MMFSR (in CFSR): DACCVIOL, IACCVIOL, stacking errors MSTKERR/MUNSTKERR, and MMARVALID, which says MMFAR holds the faulting address.
  6. Context switch: PendSV writes the incoming task's regions (stack plus a few configured ranges), with the MPU briefly disabled.
  7. Stack guard: a no-access region of 32 bytes at the low end of a stack, numbered higher than the stack's own region.
  8. 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 (Zephyr BUILTIN_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 MMARVALID before reading MMFAR; after a stacking error MMFAR may 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

ARMv7-M MPUpower-of-two regions; Cortex-M3/M4/M7.
ARMv8-M MPUbase/limit, 32-byte granule, MAIR attributes; Cortex-M23/M33/M55.
RISC-V PMPordered NAPOT/TOR entries; the choice on RISC-V microcontrollers.
MMUper-process address spaces; heavier, see guard pages.

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

namelangrepoinstallverdictlicencehealthas_ofnotes
FreeRTOS-Kernel MPU portsCFreeRTOS/FreeRTOS-Kernelgit submodule————portable/GCC/ARM_CM4_MPU (ARMv7-M), portable/GCC/ARM_CM33 (ARMv8-M, configENABLE_MPU)
Zephyr MPU driversCzephyrproject-rtos/zephyrwest init (Zephyr getting started)————arch/arm/core/mpu: v7, v8, stack guard, memory domains
cortex-mRustrust-embedded/cortex-mcargo 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

Used by

safety monitor architecture · plannedIndependent doer/checker safety monitor that forces a safe state when the main channel misbehaves

Systems (5):

Alternatives and related

Preemptive priority schedulingFirmware or a real-time OS runs several activities on one CPU, some with deadlines of microseconds and others of seconds. Something must decide, at every event, which activity gets the processor, cheaply and predictably.Watchdog timerUnattended devices (field sensors, motor controllers, vehicle ECUs) eventually hang and must recover without a human. This detects a stuck program within a bounded time and forces a reset to a defined state.Safety-critical coding standardsTeams writing firmware or control code whose failure could cause harm, or whose certification is audited, must decide which coding rules to adopt. This guide answers what the common standards enforce and how.Context switchingA machine has more runnable tasks than cores, yet each task expects to own the processor. Something must pause one task and resume another exactly where it stopped, without either noticing, and do it often.
fault detection isolation recovery · plannedMonitor/response tables, fault isolation and safe-mode recovery hierarchies for autonomous systems
Virtual memory and the TLBPrograms that jump around large in-memory data (hash tables, graphs, model weights) can run far slower than expected because every address must be translated. This guide shows when translation becomes the bottleneck and when huge pages fix it.
guard pages memory sandboxing · plannedReserve large virtual regions with guard pages so bounds checks become page faults
Least privilege and isolationTeams deploying software that touches user data, money or other systems must decide how much access each component, service and person gets, so one compromised part does not expose everything. This guide covers that decision.Interrupt handlingA CPU busy with its own work must notice events from devices and timers promptly. Notifying it per event costs more than the work when events arrive in thousands per second, but waiting too long adds delay.