Skip to content

Part 3 · Firmware Systems31 min read

Memory Protection, Stack Safety, and Robustness

A microcontroller has no operating system watching over memory. A stack that grows one word too far, a memcpy with a bad length, or a wild pointer silently overwrites whatever happens to live next door, and the symptom appears minutes later in unrelated code. This lesson is about turning that silence into noise. You will configure the Cortex-M4’s Memory Protection Unit so that a stack overflow faults at the exact instruction that caused it, make RAM non-executable, see how privileged and unprivileged code are separated, measure stack usage properly, choose a heap policy that cannot fail at 3 a.m., and use canaries, checksums and watchpoints to find the code that wrote where it should not.

The Memory Protection Unit (MPU) checks every memory access the core makes against a small table of regions. Each region has a base address, a size, access permissions for privileged and unprivileged code, an execute-never bit, and memory type attributes. An access that violates its region’s rules is blocked and raises a MemManage fault before the write lands. It is not an MMU: there is no address translation and no virtual memory, only checks.

The STM32F407’s Cortex-M4 implements the Armv7-M MPU (called PMSAv7) with 8 regions. Its registers sit in the System Control Space:

RegisterAddressPurpose
MPU_TYPE0xE000ED90read-only; DREGION (bits 15:8) = number of regions, 8 here
MPU_CTRL0xE000ED94bit 0 ENABLE, bit 1 HFNMIENA, bit 2 PRIVDEFENA
MPU_RNR0xE000ED98selects which region RBAR and RASR refer to
MPU_RBAR0xE000ED9Cregion base address (bits 31:5), plus VALID (bit 4) and REGION (bits 3:0)
MPU_RASR0xE000EDA0region attributes, size and enable

CMSIS exposes them as MPU->TYPE, MPU->CTRL, MPU->RNR, MPU->RBAR and MPU->RASR. The attribute register packs everything else:

MPU_RASR
31 29 28 27 26 24 23 22 21 19 18 17 16 15 8 7 6 5 1 0
+-------+---+--+------+-----+------+---+---+---+-----------+-----+--------+---+
| res. |XN | | AP | res.| TEX | S | C | B | SRD | res.| SIZE | EN|
+-------+---+--+------+-----+------+---+---+---+-----------+-----+--------+---+
XN 1 = never execute from this region
AP access permissions (table below)
TEX/S/C/B memory type and cacheability
SRD subregion disable, one bit per eighth of the region
SIZE region size = 2^(SIZE+1) bytes, minimum SIZE = 4 (32 bytes)
EN region enable
APPrivilegedUnprivileged
000no accessno access
001read/writeno access
010read/writeread-only
011read/writeread/write
100reservedreserved
101read-onlyno access
110read-onlyread-only
111read-onlyread-only
  • Size is a power of two, from 32 bytes to 4 GB. SIZE holds log2(size) minus 1. You cannot make a 48 KB region.
  • The base address must be aligned to the size. A 64 KB region must start on a 64 KB boundary. The low bits of RBAR below the size are ignored, so a misaligned base silently protects the wrong range.
  • Overlaps are allowed, and the highest-numbered region wins. This is what makes guard regions easy: one large region for all of SRAM, and a small region with a higher number carved out of it.
  • Subregions. A region of 256 bytes or more is split into eight equal subregions, and each bit of SRD disables one. An access to a disabled subregion behaves as if that region did not cover it, and falls through to lower-numbered regions or the background. For example, a 128 KB region over the STM32F407’s SRAM has 16 KB subregions, and the top one is exactly SRAM2 (0x2001C000); setting SRD = 0x80 leaves SRAM2 to a different region with different rules.
  • The background region. With PRIVDEFENA = 1, privileged accesses that hit no region use the default memory map as if the MPU were off. Unprivileged accesses that hit no region always fault. With PRIVDEFENA = 0, every access must hit a region or it faults.
  • HFNMIENA. When 0 (the usual choice), the MPU is disabled while running the HardFault and NMI handlers, so a fault handler can still access memory to report what happened.

TEX, C, B and S describe the memory type. The Cortex-M4 in the STM32F4 has no data cache, so these bits change little in practice, but they should still describe the memory honestly, and code that moves to a Cortex-M7 depends on them. Common choices:

MemoryTEXCBSType
Internal flash000100normal, write-through
Internal SRAM000111normal, write-back, shareable
Peripherals000011shareable device

Here is an MPU setup for the STM32F407 that makes flash read-only, makes SRAM non-executable, and reserves the highest-priority region for a stack guard. It uses the CMSIS register definitions and bit masks from core_cm4.h, included through stm32f4xx.h.

mpu.c
#include <stdint.h>
#include "stm32f4xx.h" /* MPU, SCB, MPU_RASR_*, MPU_CTRL_*, __DMB, __DSB, __ISB */
#include "mpu.h"
/* RASR.AP values */
#define AP_NONE 0u /* privileged: none, unprivileged: none */
#define AP_PRIV_RW 1u /* privileged: RW, unprivileged: none */
#define AP_FULL_RW 3u /* privileged: RW, unprivileged: RW */
#define AP_RO 6u /* privileged: RO, unprivileged: RO */
/* Memory types for STM32F4 internal memories */
#define ATTR_FLASH (MPU_RASR_C_Msk)
#define ATTR_SRAM (MPU_RASR_C_Msk | MPU_RASR_B_Msk | MPU_RASR_S_Msk)
#define REGION_FLASH 0u
#define REGION_SRAM 1u
#define REGION_GUARD 7u /* highest number: overrides REGION_SRAM where they overlap */
static uint32_t rasr(uint32_t size_log2, uint32_t ap, uint32_t xn, uint32_t attr)
{
return (xn << MPU_RASR_XN_Pos) | (ap << MPU_RASR_AP_Pos) | attr |
((size_log2 - 1u) << MPU_RASR_SIZE_Pos) | MPU_RASR_ENABLE_Msk;
}
static void region_set(uint32_t region, uint32_t base, uint32_t rasr_value)
{
MPU->RNR = region;
MPU->RBAR = base; /* VALID = 0, so RNR selects the region */
MPU->RASR = rasr_value;
}
void mpu_init(void)
{
__DMB(); /* finish accesses made under the old map */
MPU->CTRL = 0u; /* disable while reprogramming */
for (uint32_t r = 0; r < 8u; r++) { /* clear anything a bootloader left */
MPU->RNR = r;
MPU->RASR = 0u;
}
/* Region 0: 1 MB flash at 0x08000000, read-only for all, executable. */
region_set(REGION_FLASH, 0x08000000u, rasr(20u, AP_RO, 0u, ATTR_FLASH));
/* Region 1: 128 KB SRAM at 0x20000000, read/write for all, never executable. */
region_set(REGION_SRAM, 0x20000000u, rasr(17u, AP_FULL_RW, 1u, ATTR_SRAM));
SCB->SHCSR |= SCB_SHCSR_MEMFAULTENA_Msk; /* MemManage_Handler instead of HardFault */
MPU->CTRL = MPU_CTRL_PRIVDEFENA_Msk | MPU_CTRL_ENABLE_Msk;
__DSB(); /* MPU enable has completed... */
__ISB(); /* ...and later instructions see it */
}
void mpu_set_stack_guard(uint32_t guard_base) /* must be 32-byte aligned */
{
region_set(REGION_GUARD, guard_base, rasr(5u, AP_NONE, 1u, ATTR_SRAM));
__DSB();
__ISB();
}

Line by line, the important parts:

  • rasr() builds the attribute word. Sizes are passed as log2: 20 for 1 MB, 17 for 128 KB, 5 for 32 bytes, and the function stores size_log2 - 1 in the SIZE field, as the hardware expects.
  • The MPU is disabled while regions change. Otherwise a half-configured region could fault the code doing the configuring. The DMB before ensures earlier accesses complete under the old rules.
  • All eight regions are cleared so that nothing left over from a bootloader stays active.
  • Region 0, flash, is read-only with execution allowed. A stray write to flash (a bad pointer into code) now faults immediately instead of being ignored by the flash controller.
  • Region 1, SRAM, is read/write but XN. If a corrupted function pointer or return address sends the PC into RAM, the first instruction fetch faults with an instruction access violation instead of executing data as code. That is the single cheapest defense against code-injection attacks on an MCU.
  • MEMFAULTENA routes MPU violations to MemManage_Handler rather than escalating to HardFault, which keeps the cause clear.
  • PRIVDEFENA = 1. Everything not covered (peripherals, the system ROM, CCM RAM at 0x10000000, the core’s own registers at 0xE0000000) stays accessible to privileged code through the default map.
  • DSB then ISB after enabling. The DSB waits for the MPU_CTRL write to complete; the ISB flushes instructions already fetched under the old configuration.

Stacks grow downward. A stack guard is a small no-access region placed directly below the lowest address of a stack. The first push, call or local variable that goes past the bottom of the stack touches the guard, and the MPU raises a MemManage fault on that instruction, before a single byte of the neighbor is corrupted.

higher addresses
+---------------------------+ <- top of task 2's stack
| task 2 stack, grows down |
| | |
| v |
+---------------------------+
| GUARD: 32 bytes, AP=000 | <- MPU region 7; any access faults
+---------------------------+ <- top of task 1's stack
| task 1 stack |
| (saved context lives |
| near the top) |
lower addresses

The natural place for guards is per-task stacks in an RTOS. Tasks run on PSP, so when a task overflows, the MemManage exception is taken on MSP, which is healthy, and the handler can report the fault cleanly. With only eight regions you cannot keep a guard for every task permanently, but you do not need to: only the running task can overflow its stack. Reprogram one region on every context switch to guard the incoming task’s stack.

Applying this to the scheduler from RTOS fundamentals: tasks and the context switch takes three changes:

/* 1. Align every stack to 32 bytes so its lowest 32 bytes can be an MPU region. */
static uint32_t stacks[MAX_TASKS][STACK_WORDS] __attribute__((aligned(32)));
/* 2. In os_switch_context(), after choosing the next task (interrupts are disabled there): */
mpu_set_stack_guard((uint32_t)(uintptr_t)&stacks[current_index][0]);
/* 3. In main(), before os_start(): */
mpu_init();

Each row of stacks is 1024 bytes, so with 32-byte alignment of the array every row starts on a 32-byte boundary, and the bottom 32 bytes of each row become that task’s guard when it runs. The usable stack shrinks to 992 bytes. Region 7 overlaps region 1 and wins, so the guard is inaccessible even though the rest of SRAM is read/write.

#include <stdint.h>
#include "stm32f4xx.h"
#include "crash.h" /* crash_record_save(), from Watchdogs, faults, and recovering from them */
void MemManage_Handler(void)
{
const uint32_t cfsr = SCB->CFSR;
const uint32_t mmfar = SCB->MMFAR; /* valid only if MMARVALID is set */
crash_record_save(CRASH_MEMMANAGE, cfsr, mmfar);
NVIC_SystemReset();
}

The MemManage status bits are the low byte of SCB->CFSR. For a stack guard they read like this:

Bits setMeaning
DACCVIOL (bit 1) and MMARVALID (bit 7), MMFAR inside the guardthe task’s own code pushed, called or wrote a local into the guard: a clean catch, the stacked PC is the guilty instruction
MSTKERR (bit 4)the overflow happened while the hardware was stacking an exception frame for an interrupt; the task’s frame is incomplete and it cannot be resumed
MLSPERR (bit 5)lazy floating-point state preservation hit the guard
IACCVIOL (bit 0)an instruction fetch from an XN or no-access region, typically a corrupted return address or function pointer jumping into RAM

In an RTOS, the handler can also record current_tcb->name, which tells you which task overflowed. Watchdogs, faults, and recovering from them shows how to dump the full stacked frame. Compare this with the unguarded version: task 2 overwrites task 1’s saved context, and the fault happens much later when task 1 is resumed with a garbage PC.

The guard has to be wider than any single frame

Section titled “The guard has to be wider than any single frame”

A guard only catches accesses that land on it. A function with a large local array moves the stack pointer by the whole frame size in one instruction:

void parse_packet(void)
{
uint8_t scratch[512]; /* sub sp, sp, #512: SP jumps far past a 32-byte guard */
scratch[400] = 0; /* this write lands below the guard: not caught */
/* ... */
}

If the task’s stack is nearly full, SP can jump straight over a 32-byte guard, and the first writes go into the next task’s stack without touching the guard at all. Defenses: make the guard larger than the largest frame in any task (check the .su files from -fstack-usage), keep big buffers out of stack frames entirely (static or pool-allocated), or use a core with hardware stack limit registers, described below, which check the SP value itself.

The main stack can get a guard the same way, by reserving an aligned guard area below it in the linker script:

_Min_Stack_Size = 0x1000;
.stack (NOLOAD) :
{
. = ALIGN(32);
_stack_guard = .; /* 32 bytes, no access once the MPU is on */
. = . + 32;
_sstack = .; /* lowest usable main stack address */
. = . + _Min_Stack_Size;
. = ALIGN(8);
_estack = .; /* initial MSP, vector table entry 0 */
} > RAM

This replaces the _estack = ORIGIN(RAM) + LENGTH(RAM) definition from Linker scripts, and in a bare-metal application you declare extern uint32_t _stack_guard[]; and call mpu_set_stack_guard((uint32_t)(uintptr_t)_stack_guard) after mpu_init(). There is a catch: when MSP itself overflows, the fault handler also needs MSP to run, and its exception frame would be pushed into the guard too. The stacking fault escalates and the handler may not get a usable stack. Make the main stack guard larger than an exception frame plus what the handler needs, or have the handler switch to a small dedicated emergency stack before doing anything else. A useful alternative on many STM32 parts is to place the main stack at the very bottom of SRAM, so that an overflow walks below 0x20000000 into reserved address space and causes a bus fault without corrupting anything.

Beyond stack guards, firmware uses the MPU to:

  • Make RAM non-executable (XN on SRAM), as above. It stops a whole class of exploits and turns “jumped to garbage” bugs into an immediate, precise fault.
  • Make flash read-only, catching writes through pointers that should have pointed to RAM.
  • Fence off peripherals from unprivileged code. With PRIVDEFENA = 1 and no region covering 0x40000000, unprivileged code cannot touch any peripheral; you then grant exactly the ranges each component needs. A sensor task gets I2C1 and nothing else.
  • Isolate tasks. FreeRTOS-MPU, the MPU-enabled FreeRTOS ports, runs tasks created with xTaskCreateRestricted in unprivileged mode, each with its own stack region and a few configurable regions for the buffers and peripherals it may touch. Kernel code and data live in privileged sections, and API calls from unprivileged tasks go through wrappers that switch to privileged mode for the duration of the call. Zephyr offers the same idea as user-mode threads with memory domains.

Cortex-M code runs either privileged (full access, including system registers such as SCB, MPU and NVIC) or unprivileged. Handler mode is always privileged. Thread mode is privileged after reset and becomes unprivileged when CONTROL.nPRIV is set:

__set_CONTROL(__get_CONTROL() | CONTROL_nPRIV_Msk);
__ISB(); /* following instructions run unprivileged */

This is a one-way door. Unprivileged code cannot clear nPRIV; only an exception handler can. Unprivileged code also cannot write PRIMASK, BASEPRI or FAULTMASK (the writes are ignored) and cannot access the System Control Space (those accesses fault). So how does an unprivileged task ask for something it is not allowed to do, like reprogramming a clock? It makes a system call with SVC, which enters a privileged handler that validates the request and performs it.

#include <stdint.h>
#include "stm32f4xx.h"
int32_t sys_led_set(uint32_t on); /* privileged implementation, defined elsewhere */
__attribute__((naked)) void SVC_Handler(void)
{
__asm volatile (
" tst lr, #4 \n" /* EXC_RETURN bit 2: frame on MSP or PSP? */
" ite eq \n"
" mrseq r0, msp \n"
" mrsne r0, psp \n"
" b svc_dispatch \n" /* tail call; its return performs the exception return */
);
}
__attribute__((used)) void svc_dispatch(uint32_t *frame)
{
const uint16_t *ret = (const uint16_t *)(uintptr_t)frame[6]; /* stacked PC: after the SVC */
const uint8_t number = (uint8_t)(ret[-1] & 0xFFu); /* SVC #n is encoded 0xDFnn */
switch (number) {
case 1:
frame[0] = (uint32_t)sys_led_set(frame[0]); /* r0 in, result back in r0 */
break;
default:
frame[0] = (uint32_t)-1; /* unknown call */
break;
}
}
/* The unprivileged side: */
static inline int32_t led_set(uint32_t on)
{
register uint32_t r0 __asm__("r0") = on;
__asm volatile ("svc 1" : "+r"(r0) : : "memory");
return (int32_t)r0;
}

The assembly picks whichever stack holds the caller’s exception frame, then branches (not calls) to the C dispatcher, leaving EXC_RETURN in lr so that when svc_dispatch returns, the return is the exception return. The call number is the 8-bit immediate inside the 16-bit SVC instruction, found just before the stacked return address. Arguments arrive in the stacked r0 to r3, and writing frame[0] sets the value the caller sees in r0.

A real system-call layer must treat every argument as hostile: a pointer passed by an unprivileged task must be checked against the regions that task is allowed to access before the privileged handler dereferences it, or the syscall becomes a way around the MPU. This is a sketch of the mechanism; RTOS kernels with MPU support implement the full version. The SVC that starts the first task in the scheduler lesson is the same exception used for a different purpose. TrustZone-M on the Cortex-M33 adds a second, hardware-enforced split between secure and non-secure worlds; Firmware security covers it.

Cortex-M23 and M33 cores implement Armv8-M, and their MPU (PMSAv8) fixes most of PMSAv7’s annoyances:

  • Regions are defined by a base and a limit (RBAR and RLAR) with 32-byte granularity. No power-of-two sizes, no alignment to the size, no subregions.
  • Regions must not overlap. An access that matches two regions faults, so the “big region plus higher-numbered guard” trick is replaced by laying regions out side by side.
  • Memory types are indices into two attribute registers (MPU_MAIR0 and MPU_MAIR1) instead of TEX/C/B/S per region.
  • With TrustZone, the secure and non-secure worlds each have their own MPU.

Armv8-M Mainline cores such as the M33 also add stack limit registers, MSPLIM and PSPLIM. Any instruction that would move SP below the limit raises a UsageFault with the STKOF flag set, before the access happens. Because the check is on the stack pointer value rather than on the memory touched, it catches the large-frame case that a guard region misses, and it costs no MPU region. RTOS ports for the M33 load PSPLIM on every context switch. (On the M23, Armv8-M Baseline, the limit registers exist only in the secure state.)

The MPU catches overflows; it does not tell you how close you are to one. For that, fill the stack with a known pattern at boot and later count how much is still untouched, as the scheduler lesson did for task stacks. For the main stack:

#include <stdint.h>
#include "stm32f4xx.h" /* __get_MSP */
#define STACK_PAINT 0xA5A5A5A5u
extern uint32_t _sstack[]; /* lowest usable main stack address, from the linker script */
extern uint32_t _estack[]; /* top of the main stack */
void stack_paint(void) /* call first thing in main, with interrupts still disabled */
{
uint32_t *p = _sstack;
const uintptr_t limit = __get_MSP() - 64u; /* stay clear of this function's own frame */
while ((uintptr_t)p < limit) {
*p++ = STACK_PAINT;
}
}
uint32_t stack_unused_bytes(void)
{
const uint32_t *p = _sstack;
while ((uintptr_t)p < (uintptr_t)_estack && *p == STACK_PAINT) {
p++;
}
return (uint32_t)((uintptr_t)p - (uintptr_t)_sstack);
}

stack_paint fills from the bottom of the stack up to a little below the current stack pointer, never touching live frames. The best place for it is the startup code, before main has used much stack. stack_unused_bytes scans upward from the bottom and stops at the first overwritten word; everything below that point has never been used.

The measurement is only as good as the test: the high-water mark records the deepest point reached during this run, so exercise every feature, every error path and the worst interrupt nesting before trusting it. Keep at least 25 percent margin. FreeRTOS gives the same number per task from uxTaskGetStackHighWaterMark(), and configCHECK_FOR_STACK_OVERFLOW = 2 makes the kernel check the last 16 bytes of the pattern at every context switch and call vApplicationStackOverflowHook. That check is cheap and useful, but it runs only at switches, so it detects an overflow after the damage. The MPU guard detects it at the instruction.

Memory: where everything lives explained why malloc is risky in firmware: fragmentation, non-deterministic timing, and failure at unpredictable moments. The practical policies, from safest:

StrategyFrees memory?FragmentationTimingUse it for
No heap at alln/anonedeterministicthe default for most firmware
Allocate at boot onlynononedeterministic after initbuffers sized from configuration read at startup
Fixed-block poolsyesnone within a poolO(1)messages, packets, events
FreeRTOS heap_1nononedeterministicsystems that create every task and queue at startup
FreeRTOS heap_2yes, no coalescinggrows over timenot O(1)legacy; superseded by heap_4
FreeRTOS heap_3yesthe C library’sthe C library’swrapping libc malloc with the scheduler suspended
FreeRTOS heap_4yes, merges adjacent free blockslimitednot O(1)general use when objects are created and deleted at runtime
FreeRTOS heap_5as heap_4limitednot O(1)heaps spread over several RAM areas (vPortDefineHeapRegions)

Two practical notes. On the STM32F407, heap_5 can span SRAM and the 64 KB CCM RAM, but DMA cannot reach CCM, so never let a DMA buffer come from that part of the heap. And FreeRTOS’s static creation functions (xTaskCreateStatic, xQueueCreateStatic, enabled with configSUPPORT_STATIC_ALLOCATION) let you build a complete system with no heap at all.

Whatever the strategy, decide the failure policy before the first allocation fails: every allocation’s return value is checked; the system either degrades gracefully (drop the packet, count it) or records a crash and resets; it never continues with a null pointer. FreeRTOS calls vApplicationMallocFailedHook() when configUSE_MALLOC_FAILED_HOOK is 1, a good place to log and stop in development builds.

If the application must never use the C library heap, make that a link-time fact rather than a convention:

#include <errno.h>
#include <stddef.h>
void *_sbrk(ptrdiff_t incr) /* newlib's heap growth hook */
{
(void)incr;
errno = ENOMEM;
return (void *)-1; /* every malloc now returns NULL */
}

Any hidden malloc (some printf implementations allocate stdio buffers on first use) now fails visibly in testing instead of slowly eating RAM in the field.

When something overwrites memory it does not own, you want to know two things: that it happened, as early as possible, and who did it.

Canaries. Put a known value at both ends of important buffers and structures and check it periodically, before use, and in the fault handler:

#include <stdbool.h>
#include <stdint.h>
#define CANARY 0xC0FFEE11u
typedef struct {
uint32_t head;
uint8_t data[128];
uint32_t tail;
} guarded_buf_t;
static guarded_buf_t rx = { .head = CANARY, .tail = CANARY };
bool rx_intact(void)
{
return rx.head == CANARY && rx.tail == CANARY;
}

An overflow of data by even one word changes tail. Canaries do not prevent corruption, but they shorten the distance between cause and detection from “somewhere, sometime” to “this buffer, since the last check”. GCC’s -fstack-protector applies the same idea to stack frames automatically.

Checksums over important RAM. Configuration copied from flash into RAM at boot can carry a CRC computed when it was loaded. Verify it before using the configuration and periodically; on mismatch, reload from flash and log the event. A corrupted config is a symptom; the log entry with a timestamp helps find the cause.

Hardware watchpoints. Once you know which address gets corrupted, let the CPU catch the writer. The Cortex-M4’s DWT unit has four comparators, and gdb uses them for watch:

(gdb) watch *(uint32_t *)0x20001a40
Hardware watchpoint 2: *(uint32_t *)0x20001a40
(gdb) continue
Hardware watchpoint 2: *(uint32_t *)0x20001a40
Old value = 3
New value = 1953066601
(gdb) bt

The core halts on the write, and the backtrace shows who did it. This one technique finds most “who wrote here” bugs in minutes.

Watchpoints without a debugger. In the field there is no gdb, but the DWT can still fire. With monitor-mode debugging enabled, a watchpoint match raises the DebugMonitor exception instead of halting the core:

#include <stdint.h>
#include "stm32f4xx.h"
void watch_writes(const volatile void *addr) /* addr must be 4-byte aligned */
{
CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk | CoreDebug_DEMCR_MON_EN_Msk;
DWT->COMP0 = (uint32_t)(uintptr_t)addr;
DWT->MASK0 = 2u; /* ignore the low 2 address bits: watch one word */
DWT->FUNCTION0 = 6u; /* data address match on write */
}

DebugMon_Handler (vector offset 0x30) then reads the stacked PC, exactly as the HardFault handler does, and records it in the crash log. The PC points at or shortly after the instruction that wrote. Monitor mode only operates when no halting debugger is attached, so it does not interfere with gdb sessions.

The MPU and watchpoints catch mistakes. Coding habits prevent them:

  • Bound every buffer write. Every array index from outside data is checked against the array size before use.
  • Length-checked copies. Pass the destination size to every copy function and refuse oversize input:
#include <stddef.h>
#include <stdint.h>
#include <string.h>
#include "err.h"
err_t copy_payload(uint8_t *dst, size_t dst_size, const uint8_t *src, size_t len)
{
if (dst == NULL || src == NULL || len > dst_size) {
return ERR_INVALID_ARG; /* never trust a length that came from outside */
}
memcpy(dst, src, len);
return ERR_OK;
}
  • Assert invariants. If a state variable can only be 0 to 3, assert it. An assertion that records the file and line and resets is far better than continuing with corrupted state.
  • Check every return code. Mark functions whose result must not be ignored with __attribute__((warn_unused_result)), and the compiler enforces it.
  • Avoid big stack frames and recursion. They make stack depth hard to bound and defeat small guard regions.

Design patterns for embedded C and Safety, standards, and coding guidelines turn these habits into project-wide policy.

  • A region base not aligned to its size. The low address bits are ignored and the wrong range is protected; align the base to the region size, and assert it at runtime.
  • Forgetting DSB and ISB after enabling or changing the MPU. The next few instructions run under the old rules; always follow MPU changes with both.
  • Making SRAM XN while code runs from RAM. Flash programming routines in .ramfunc fault on their first instruction; give them their own executable region.
  • A guard smaller than the biggest stack frame. A large local array jumps the guard and corrupts the neighbor anyway; size the guard from -fstack-usage or move big buffers off the stack.
  • Not enabling MEMFAULTENA. Every MPU violation arrives as a HardFault and the cause is harder to see; enable the MemManage handler.
  • Trusting a high-water mark from a light test. The deepest path never ran; measure under worst-case load with every feature active.
  • Ignoring allocation failures. A null pointer is dereferenced much later; check every allocation and define the failure policy up front.
  • A system call that dereferences unchecked pointers. Unprivileged code uses it to bypass the MPU; validate every pointer argument against the caller’s regions.
  1. Add mpu_init() to your Part 2 blinky and confirm it still runs. Then add a deliberate write to *(volatile uint32_t *)0x08004000 and confirm you land in MemManage_Handler with DACCVIOL and MMARVALID set and MMFAR = 0x08004000.
  2. Make SRAM executable for a moment to prove the point: copy a two-instruction Thumb function (movs r0, #42 then bx lr, encoded 0x202A 0x4770) into a RAM array and call it through a function pointer with bit 0 set. With region 1 marked XN, observe IACCVIOL; with XN cleared, observe the call returning 42.
  3. Add per-task guards to the scheduler from RTOS fundamentals: tasks and the context switch. Give one task a recursive function with a depth controlled from a button press, and confirm that the fault happens at the recursion, with MMFAR inside that task’s guard, rather than later in a different task. Then repeat with the MPU disabled and describe the difference.
  4. Compile a module containing a function with a 512-byte local array using -fstack-usage, and read the .su output. Explain why a 32-byte guard does not protect a task that calls it, and change the code so it would.
  5. Use watch_writes() on a canary at the end of a buffer and deliberately overflow the buffer from a function called through a few layers. In DebugMon_Handler, record the stacked PC, then use arm-none-eabi-addr2line -e firmware.elf on it to find the guilty line, all without a debugger attached.

Question 1:You configure a 64 KB MPU region with base 0x20004000. What range is actually protected?

Question 2:Region 1 covers all SRAM as read/write. Region 7 is a 32-byte no-access guard inside it. A task writes to the guard. What happens?

Question 3:A task with a 32-byte stack guard calls a function whose frame has a 1 KB local buffer while its stack is nearly full. Why might the overflow go undetected?

Question 4:After mpu_init() marks SRAM as XN, a corrupted return address points into a RAM buffer. What does the CPU do on return?

Question 5:Unprivileged thread code executes __set_PRIMASK(1) to disable interrupts. What happens?

  • The Cortex-M4 MPU has 8 regions of power-of-two size, aligned to their size; overlaps are allowed and the highest-numbered region wins.
  • Enable it with PRIVDEFENA so privileged code keeps the default map, enable MEMFAULTENA for a dedicated handler, and follow every change with DSB and ISB.
  • A no-access guard below each task stack, reprogrammed on every context switch, turns silent stack overflow into a fault at the guilty instruction; it must be wider than the largest frame.
  • Mark SRAM execute-never and flash read-only; give RAM-resident code and flash programming explicit exceptions.
  • Unprivileged code cannot touch system registers or masks and reaches privileged services through SVC, whose handler must validate every argument.
  • Armv8-M replaces power-of-two regions with base and limit, forbids overlaps, and adds MSPLIM and PSPLIM stack limit checks.
  • Measure stacks with painting under worst-case load, choose the simplest heap policy that works, and use canaries, checksums and DWT watchpoints to catch whoever corrupts memory.

Next: Bootloaders and firmware update, where you design the code that runs before your application and replaces it safely in the field.