MPU Support & Memory Protection¶
Verification note for this module: the Memory Protection Unit is real
silicon (ARMv7-M/ARMv8-M MPU peripheral) with no meaningful host-side
simulation — the POSIX port used elsewhere in this course has no MPU
equivalent (there is no memory protection concept in a POSIX process the way
FreeRTOS-MPU means it). Nothing in this module was built or run; it is a
careful technical review against the FreeRTOS-Kernel MPU ports
(GCC/ARM_CM4_MPU, GCC/ARM_CM33/secure etc.) and ARM's own MPU
architecture reference.
What FreeRTOS-MPU actually protects against¶
Standard FreeRTOS has no memory isolation between tasks at all — any task
can read or write any other task's stack, the kernel's own data structures,
or any peripheral register, because everything runs in a single flat address
space at the CPU's most privileged level. FreeRTOS-MPU (configENABLE_MPU)
changes this by giving each task an MPU region configuration: a task can
be restricted to only its own stack plus explicitly-granted regions, and
demoted to "unprivileged" execution so it cannot itself reconfigure the MPU
or execute privileged-only instructions.
// FreeRTOSConfig.h
#define configENABLE_MPU 1
#define configTOTAL_MPU_REGIONS 8
#define configTEX_S_C_B_FLASH 0x07UL
#define configTEX_S_C_B_SRAM 0x07UL
#define configENFORCE_SYSTEM_CALLS_FROM_KERNEL_ONLY 1
// Region access rights, packed into an enum per FreeRTOS-MPU's API:
typedef struct MPU_REGION_INFO {
void *pvBaseAddress;
uint32_t ulLengthInBytes;
uint32_t ulRegionPermissions; // e.g. tskMPU_READ_PERMISSION | tskMPU_WRITE_PERMISSION
} MemoryRegion_t;
Privileged vs. unprivileged tasks¶
TaskParameters_t xSensorTaskParams = {
.pvTaskCode = vSensorTask,
.pcName = "Sensor",
.usStackDepth = 256,
.pvParameters = NULL,
.uxPriority = 2 | portPRIVILEGE_BIT, // OR this bit in to run privileged
.puxStackBuffer = sensorStack,
.xRegions = {
{ (void *)0x20001000, 256, tskMPU_READ_PERMISSION | tskMPU_WRITE_PERMISSION },
{ (void *)ADC_PERIPH_BASE, 0x1000, tskMPU_READ_PERMISSION | tskMPU_WRITE_PERMISSION },
{ 0, 0, 0 }, // unused regions must be zeroed
},
};
xTaskCreateRestricted(&xSensorTaskParams, &sensorHandle);
A task created via xTaskCreateRestricted without portPRIVILEGE_BIT runs
in unprivileged mode: it can only access its own stack (region 0, always
auto-configured by the kernel), plus whatever's listed in xRegions — one
peripheral's registers, one shared buffer, nothing else. Trying to touch
memory outside its configured regions faults immediately (a Cortex-M
MemManage fault) rather than silently corrupting whatever it hit. This
converts an entire class of bug — a stray pointer write clobbering another
task's stack or the kernel's TCB list from Module 1 — from silent corruption
discovered hours later into an immediate, precisely-located hardware fault
at the moment of the bad access.
Why this is not "security" in the general sense¶
FreeRTOS-MPU is a fault-isolation and bug-containment mechanism, not a security boundary against a determined attacker with code-execution capability inside the "protected" region. A few concrete limits:
- The kernel itself, and any privileged task, can reconfigure the MPU or touch anything — MPU regions constrain unprivileged code only.
- System calls (
MPU_xQueueSendetc. — the MPU-wrapped API variants unprivileged tasks must use instead of the raw kernel calls) briefly raise privilege to execute the real kernel function, then drop back down; a bug in kernel code reachable through a syscall is not contained by the task's own restricted regions. - Region granularity and alignment are hardware-constrained (ARMv7-M MPU regions must be power-of-two sized and aligned to their own size; ARMv8-M's MPU is more flexible but still has real minimum granularity) — you cannot always give a task exactly the memory it needs and nothing more; padding to the next valid region size/alignment is normal and can waste RAM on small targets.
- This is a materially different, much lighter-weight mechanism than TrustZone (Level 4 Module 5), which provides a hardware-enforced secure/ non-secure world split even privileged code in the non-secure world cannot cross — MPU protects tasks from each other; TrustZone protects a secure world from an entire non-secure OS including its kernel.
The stack-overflow-detection connection¶
MPU-backed builds get a stronger version of Level 1's stack overflow
detection: instead of configCHECK_FOR_STACK_OVERFLOW's pattern/watermark
checks (which only detect overflow after it already wrote past the stack,
if it happens to hit the watermark region), an MPU region placed immediately
past a task's stack with no access rights turns any stack overflow into an
instant MemManage fault at the exact instruction that overran — a strictly
stronger and earlier guarantee than the pattern-based check alone, at the
cost of consuming one of the limited MPU regions per task.
How It Actually Works (the region-swap on every switch)¶
FreeRTOS-MPU works by reprogramming the Cortex-M's Memory Protection Unit
region registers as part of every context switch to an unprivileged task —
each MPU-aware TCB carries its own small table of region base/size/
permission settings (its private stack, plus any explicitly shared regions),
and vTaskSwitchContext's successor in the MPU port writes that table into
the MPU hardware registers before the task resumes, so one task's stack
memory is physically inaccessible (a hard fault, not a silent bug) to
another task's unprivileged code even though both live in the same flat
physical address space. A task only escalates to privileged mode through a
narrow, deliberate gate — a supervisor call (svc instruction) trapping into
a handler the kernel controls — which is precisely why this is "protection
against accidental corruption," not general security: any code that finds a
way to trigger that same trap, or that was already linked as privileged, sees
the whole address space. The stack-overflow connection is mechanical: a
correctly configured MPU region around a task's stack turns an overflow that
would otherwise silently corrupt an adjacent TCB into an immediate, precisely
attributable memory-fault exception at the moment the boundary is crossed.
Traps¶
- Assuming
configENABLE_MPUprovides isolation without also auditing every unprivileged task's use of raw (non-MPU_-prefixed) kernel API calls. Unprivileged tasks must go through theMPU_-wrapped syscall layer; calling raw kernel functions directly from unprivileged code is either blocked by the port or undefined depending on configuration — check your specific port's enforcement. - Running out of MPU regions. ARMv7-M typically provides 8 regions
total; the kernel reserves some (flash, kernel RAM, the task's own stack)
before the application gets any —
configTOTAL_MPU_REGIONSbudgets this explicitly, and a design with many distinct shared buffers per task can exhaust it fast. Consolidate shared regions rather than one region per buffer where possible. - Forgetting alignment/size constraints. An
xRegionsentry with a base address or length that doesn't satisfy the underlying MPU hardware's alignment rules either fails silently to the nearest valid boundary or is rejected outright depending on the port — always round explicitly rather than relying on the port to do the "obviously correct" thing. - Treating MPU protection as equivalent to TrustZone-grade security. As above: MPU faults contain bugs (a wild pointer, a stack overflow) between cooperating-but-imperfect application tasks. It does not stop a task that has arbitrary code execution rights within its own region from calling legitimate syscalls to manipulate shared kernel state it's allowed to reach, and it provides no protection at all against a hostile actor with physical or debug-port access.
- Not testing the fault path. It's easy to test that a well-behaved MPU-restricted task runs correctly and skip deliberately triggering an out-of-region access to confirm the fault handler actually catches it (rather than, say, silently succeeding due to a misconfigured region) — the whole value of this feature is in the fault path, so it needs its own explicit test, not just happy-path testing.
Cheat sheet¶
| Concept | Standard FreeRTOS | FreeRTOS-MPU |
|---|---|---|
| Address space | Flat, every task sees everything | Per-task restricted regions |
| Privilege | All tasks run privileged | Privileged / unprivileged distinction |
| Bad memory access | Silent corruption | Immediate MemManage fault |
| Kernel API from tasks | Direct calls | MPU_-wrapped syscalls for unprivileged tasks |
| Stack overflow detection | Pattern/watermark (after the fact) | Optional guard region (at the instant of overflow) |
| Region limits | N/A | Hardware-fixed count + alignment rules |
| Security boundary strength | None | Bug containment — not attacker-resistant, not TrustZone-equivalent |
| Verified on this machine | N/A | N/A — no MPU-capable simulation exists; source/spec review only |
Exercise¶
- Read ARM's Cortex-M4 (ARMv7-M) MPU architecture reference (region
alignment/size rules specifically) and design an
xRegionslayout for a task needing access to a 300-byte buffer and a peripheral with a 0x400-byte register block — work out what actual region sizes and alignments the hardware requires, since neither input size is already a valid MPU region size. - On an MPU-capable Cortex-M board, deliberately write a task that overflows its stack while running unprivileged with a guard region configured, and confirm you get an immediate fault rather than silent corruption — capture and interpret the fault status register contents.
- Write down, in your own words, three specific attacks or bugs FreeRTOS-MPU does contain and three it explicitly does not — and explain why TrustZone (Level 4 Module 5) is needed for the ones MPU alone can't cover.