The Memory Protection Unit

Learning Tock from the ground up · Chapter 6

‹ Contents

The Memory Protection Unit

Chapter 5 ended on a sentence with a hole in it. Every edge it drew was a number the kernel was holding: where a process starts, where its memory ends, how far up it may reach. Not one of them stops an instruction. This chapter is about the thing that does.

What you'll be able to do at the end

  • Say what the memory protection unit checks on every access, and the one thing it never sees.
  • Read one region out of the two registers that describe it, and get back the addresses the kernel put in.
  • Say how many regions this board has, which two are spoken for before your process starts, and what runs out first.
  • Follow one forbidden store from the instruction to the stopped process, naming every step in between.
  • Say what the older Cortex-M units cost that this one does not, in address space and in alignment.

Plan on forty minutes, and plan on dragging things: most of this chapter is built to be handled rather than read. Chapter 5 is assumed: what a process is, and the seven boundaries through its memory. Chapter 1 comes back too, as a register with fields in it — except that this time the fields govern what other addresses may be touched.

In the book: course/root-of-trust/userspace-attack, which puts the unit to work stopping an attack. This chapter is about what it is before it is about what it stops.

Nine words this chapter needs

Each is one sentence now and repeated in context below. Two of them are the names of Cortex-M exceptions, and only one of the two ever fires on this board.

memory protection unit
Hardware inside the processor that checks the address of every access before the access is allowed to finish.
privileged
The mode the kernel runs in. Tock configures the unit not to constrain it, which is what Figure 8 is about.
unprivileged
The mode a process runs in, and the only mode Tock has the rules enforced against.
permissions
What may be done at an address: read it, write it, execute it, or some of the three.
region
One run of addresses, described by two registers, carrying one set of permissions.
execute-never
A bit forbidding instruction fetch from a region. There are two: one aimed at the process, one at the kernel.
fault
The exception the hardware raises when an access breaks the rules. Chapter 5 used the word; this is where it comes from.
MemManage
The exception a protection violation raises, on a chip where it has been switched on. On this board it has not.
HardFault
The catch-all exception everything else escalates into, and the one this board actually handles.

The gap chapter 5 left

Chapter 5 drew a process's memory as seven boundaries and named the field behind each one. app_break, kernel_memory_break, memory_start. They are all real, and the kernel holds all of them. But every one is a number in a struct.

A number in a struct is a note about what should happen. The processor running a store never reads it. Between the instruction a process issues and the memory that changes, no kernel code runs at all. That is what running means.

So the enforcement cannot live in the kernel. It lives in the processor, right beside the thing doing the accessing, and it is the one piece of this series that is not software. The RP2350 datasheet gives it three jobs: "enforce privilege rules", "separate processes", "manage memory attributes".

The first two jobs are this chapter. The third comes down to one line the kernel writes on every configure and never changes.

Figure 1 Five things the unit is asked, and one it never sees

Here is one access. Change nothing about it except who is making it, and watch the answer turn over.

store 0x000000FF into 0x2000F000

Address0x2000F000examined
Verbstoreexamined
Whoa processexamined
Value0x000000FFnever looked at
RefusedAllowed

Unprivileged, and 0x2000F000 is outside every region this process was given. The store never reaches memory.

Privileged, and the same store to the same address goes through. The regions were never pointed at the kernel: the line that turns the unit on also hands privileged code the whole default memory map. Nothing about the access changed. Only who was making it.

Before the access completes, on every load, every store and every instruction fetch. Not on a sample of them and not afterwards. There is no window in which a forbidden access half-happens.

Only unprivileged code, which on this board means only processes. The line that turns the unit on also hands privileged code the whole default memory map, and the kernel runs privileged. Figure 8 is that line.

The address, against the enabled regions. This chip gives Tock eight of them, and each is a start, an end, and a set of permissions.

Allowed, or not allowed, for the verb being attempted. Reading, writing and executing are three separate answers, so a region can be readable and not writable, or executable and not readable.

The value. A store of the right bytes to the wrong address and a store of the wrong bytes to the right address are indistinguishable here. Nothing in this hardware knows what a program is trying to do — only where.

Notice what that last one rules out. This unit cannot tell a bug from an attack, and cannot tell a correct program from an incorrect one. It answers a question about addresses, and every guarantee built on it is a guarantee about addresses.

One region, two registers

Chapter 1 spent itself on a register with fields packed into it: bits that meant something to the hardware rather than to the program. Same shape here, different job. Instead of one bit per pin, these fields describe a stretch of addresses and what may be done inside it.

One warning about the word. Chapter 5 used region for the four areas a linker script cuts the chip into. That is not this sense. From here on it means what the glossary above says: one run of addresses, described by two registers, carrying one set of permissions.

The registers sit at 0xE000ED90. Three of them describe one region. MPU_RNR at 0xE000ED98 picks which of the eight you are talking about. Then MPU_RBAR and MPU_RLAR, at 0xE000ED9C and 0xE000EDA0, describe it.

Figure 2 Two registers, seven fields, one region

Pick a permission and drag the size. The two registers rebuild as you go — watch which fields move and which never do. Click any field to read what it is.

2048 bytes
MPU_RBAR 0x20008003
MPU_RLAR 0x200087F1

Which describes 0x20008000 through 0x200087FF2048 bytes, read back out of the two registers above.

The first address of the region, with its bottom five bits missing. The kernel writes logical_start >> 5, so what the hardware stores is the address in units of 32 bytes. Those five absent bits are half the size rule on this chip: a region may start on any multiple of 32. The limit field in the pair below carries the other half. Drag the size and this field never moves.

Two bits, four values, and only two of them get used for a process: ReadWrite and ReadOnly. The other two are the privileged-only pair, and they grant unprivileged code nothing at all. Watch this field as you press the four buttons above: it is one of only two things they change.

One bit, and the reason this chapter has the phrase execute-never in it. Set the bit and no instruction may be fetched from the region. The names in the Tock source read backwards at first glance: XN::Disable is the value that sets the bit, because what it disables is execution.

The last address of the region, inclusive, in the same 32-byte units. The kernel writes (logical_end - 1) >> 5. The limit counts the last byte that is in, so passing the end address itself would stretch the region 32 bytes past where it should stop. This is the one address field the size slider moves.

The other execute-never bit, and the only field in the pair that speaks about nothing but privileged code. The source calls it "privileged execute-never. Defines whether code can be executed from this privileged region", and the value names read backwards here too: Tock writes PXN::Disable into every region it builds, which sets the bit. That is the 0x10 this figure adds to every limit it shows.

Shareability, and the field this page can say least about. Tock writes zero into it on every region it builds and never varies, so nothing you can do above will move it.

Three bits pointing at one of eight attribute slots. Every region Tock builds points at slot 0, and the only line that ever writes those slots sets slot 0 to normal, non-cacheable memory. It is set on every configure and never varies.

The switch. A region with this bit clear is ignored no matter what else is in the pair, which is how the unit holds eight regions when a process only needs two. An unused slot is written as a base of zero and this bit cleared.

Notice how little of the pair the permission buttons touch: two bits out of sixty-four. Everything else is either an address or a constant Tock writes the same way every time. And notice where the low bits of each address went. The pair has no room for bits 4 to 0 of either address, because those ten positions are carrying the fields in the rows above. A region beginning at an odd address cannot be described, so the kernel is never allowed to want one.

Build a region yourself

That is enough to do the arithmetic yourself. Take a process whose memory the kernel has placed at 0x20008000, with read and write permission and no execute — the permission every process's RAM gets. Chapter 5 said the accessible part starts at 32 bytes and grows when the process asks. The figure below is that process. Drag how much it asks for, then drag a probe across the edge and see where the answer changes. One caveat on that base: it is picked to make the arithmetic easy to follow, not read off this board like every other address in this series. Where a process's memory really starts depends on how much RAM the kernel's own data took.

A process asks the kernel for 100 bytes. The unit hands it a region. How much can the process actually reach?

That is what the process is told, and it is not what it gets. The limit field records a boundary in units of 32 bytes, so it has nowhere to put the number 100.

Right, and the process is never told. align32 rounds the far edge up because the limit field cannot record a finer boundary, so the process quietly finds it can reach twenty-eight bytes further than it asked to. The rounding always goes the process's way.

That is the answer for the older unit, and it is the instinct this chapter exists to correct. On Armv7-M a region really did have to be a power of two, aligned to its own size, and the waste could be enormous. This chip has the newer unit: a base and a limit, in units of 32 bytes, and no power-of-two rule anywhere.

Nothing refuses it. There is no table of legal sizes to fail against. The only constraint is that both edges land on a 32-byte boundary, and the kernel arranges that by moving the far edge rather than by saying no.

Figure 3 Grow the region, then try to reach past it

Drag the top slider to ask for memory, and the bottom one to point at an address. Then press a verb. Move the probe one byte at a time across the right-hand edge and watch where the answer actually changes.

2049 bytes
Reachable 2080 bytes
Handed over unasked 31 bytes
Last address inside 0x2000881F
MPU_RLAR 0x20008811
0x20007FC0 0x20009040
0x20008000
AllowedRefused

Inside the region, and the access field reads ReadWrite. A process may read its own memory, which is the least surprising thing on this page.

Inside the region, and the access field reads ReadWrite. The store lands. This is the only one of the three verbs that both permission bits have to agree on.

Inside the region, and refused anyway. A process's RAM is asked for as ReadWriteOnly, which sets the execute-never bit, so no instruction may be fetched from here. A buffer that arrives full of instructions is still only a buffer.

Below the base. The address does not fall inside any enabled region, and an address that matches nothing is refused rather than allowed. This is the end the process cannot grow towards: its own memory starts here and stays here.

Past the limit. The store never reaches memory, no kernel code has run yet, and the processor is about to raise the fault Figure 7 follows to the end. Drag back a byte at a time to find the exact address where this becomes allowed.

Notice that the edge does not move where you would expect. A request that is not already a multiple of 32 comes back rounded up to the next whole multiple, because the limit field has nowhere to record a finer boundary. The rounding always goes the process's way, it is never reported, and the process simply finds it can reach a little further than it asked to. Notice too which number never changes: a process's memory cannot be moved once it is running, because its own compiled addresses point into it, and chapter 5's four arguments were how it learned them. Growth happens at one end only.

Five permissions, and the two Tock actually uses

The kernel never writes those bit patterns itself. It names a permission, and the chip's own file turns that name into a pair of fields. There are five names, and Tock asks for two of them.

Figure 4 Five names, and the two fields each becomes

Read down the two bit columns first, and count how many bits it takes to tell the five names apart. Then click a row for what it is for.

Access field ReadWrite, execute allowed: everything permitted. A region that is both writable and executable is the arrangement every other mechanism in this chapter exists to avoid.

Access field ReadWrite, execute-never bit set. This is what a process's RAM gets. A process may keep whatever it likes in its own memory and may not run it, so a buffer that arrives full of instructions is still only a buffer.

Access field ReadOnly, execute allowed. This is what a process's flash gets, asked for by name when the process is loaded. A process cannot rewrite its own code, which is the other half of the pair above.

Access field ReadOnly, execute-never. Readable data that is neither writable nor runnable.

The odd one, and the reason its last cell is a dash rather than a yes. It sets the access field to ReadOnlyPrivilegedOnly and leaves the execute-never bit clear. That access field is a privileged-only one, so a process may not read them back. Whether a process may run them is a question the Tock source cannot answer on its own, and nothing in the tree asks for this variant, so nothing here settles it. It exists because the hardware can express it.

Notice that the two the loader uses are the two that split writing from running. Flash: read and execute, never write. RAM: read and write, never execute. Between them there is no address a process can both write and later run. The other three names have no caller anywhere in the tree.

You get eight regions. Two are gone before you start.

The datasheet says the unit "supports 16 memory regions: 8 secure and 8 non-secure" — two separate sets of eight rather than sixteen in one pool. Tock asks for eight, in one line of the chip crate: cortexm33::mpu::MPU<8>.

By the time a process runs its first instruction, two of those eight are already taken. One of the two was never on offer in the first place.

Tock's Cortex-M33 code works with eight memory regions. Where did the number eight come from?

The hardware does report it, and the trait offers a method that asks. Four architectures implement that method. No line in this tree calls it.

Right, and that is worth sitting with. The number is a constant in Tock's own code. A chip with sixteen regions would have eight of them ignored, and a chip with four would be a bug nothing detects at compile time.

It does not. The specification allows a range, and implementations differ. That is exactly why the trait has a method to ask.

There is no such setting. The boards in this tree do not get a say, which is the point of the note under the figure.

Figure 5 Where the eight go, and the request that gets nothing

Press the button until the kernel says no. Count how many times you got a slot, and watch which one it never offers you.

Two slots are gone before the process runs a single instruction. Six are left. Granted. The allocator walked up from region 1 and took the first slot that was free. Refused. Every slot the allocator is willing to hand out is taken, and it never offers region 0 — that one belongs to the process's own memory and its limit moves while the process runs.

granted 0 of 6

Not chosen. A constant named APP_MEMORY_REGION_MAX_NUM is set to 0. The routine that places a process's memory writes region 0 and nothing else, and the routine that hands out spare regions skips index 0 on every pass. The one region whose limit moves while the process runs is the one region nobody else can be given.

The first thing asked for when a process is loaded, before its RAM is even placed: the whole of its flash, read and execute, no write. It goes into region 1 for no better reason than that region 1 is the first slot the allocator will hand out.

Whatever else a process is given access to later — a buffer shared with a driver, a second stretch of flash — comes out of these. Six, and no more, because the array in the kernel that remembers them is declared six long.

There is no seventh. The request returns nothing and the caller is told no. Two separate limits produce that answer on this chip: seven allocatable hardware slots minus the one flash took is six, and the kernel's own array is six. They agree exactly, and nothing in the source makes them agree.

Notice that the number eight is written by hand. The trait offers a method that asks the hardware how many regions it really has; four architectures implement it, and no line in this tree calls it.

The same request on an older chip

Everything above is one implementation of a trait. The kernel never touches MPU_RBAR. It asks for a region of at least so many bytes, with these permissions, somewhere inside this stretch of free memory. A file under arch/ works out the rest. That file is very different on the older Cortex-M cores, and the difference is not a detail.

Figure 6 One request, two hardware answers

Drag the request and read the two columns against each other. Watch the last row especially — it is the one that decides where in memory a process can be put at all.

3072 bytes
Address space taken3072 bytes4096 bytes
The process gets3072 bytes3072 bytes
Smallest step it can grow by32 bytes512 bytes
Must start on a multiple of32 bytes4096 bytes

Round the start up to a multiple of 32, round the end up to a multiple of 32, write both. 3 kB is already a multiple of 32, so the region is 3 kB and the process gets what it asked for. The whole size rule is one nine-line function that rounds a pointer up.

A region's size must be a power of two, and it must start on a multiple of its own size. 3 kB is not a power of two, so the first condition already fails. The older code answers by allocating a larger region — the next power of two — cutting it into eight equal subregions, and switching off the ones the process should not have. That branch runs to sixty-four lines. The newer file's whole size rule is a nine-line function that rounds a pointer up.

Notice which one this board has. Most Tock boards run the older unit, where the size rule really does shape how much memory an application can be given. On the RP2350 it does not: the granularity is 32 bytes and the rounding is invisible.

What happens the instant a process gets it wrong

A process stores one byte past the end of its region. The fault that follows is the point of everything above, and it takes six steps to turn into a stopped process. The chip's own documentation names the first stop on that route, and this board never goes there.

Figure 7 Six steps from a refused store to a stopped process

Walk it forward one step at a time and watch the four readings change. Step 3 is the one that explains why this board has no handler named after the thing that went wrong.

Runningthe process
Modeunprivileged
MMFARnothing yet
APP_HARD_FAULTclear
step 1 of 6

No kernel code has run yet and none is about to. The address did not match an enabled region, or matched one that forbids writing, and the write never reaches memory. There is no line of source behind this step, because there is no software in it.

The datasheet is exact about it: "MPU mismatches and permission violations invoke the MemManage handler." That is the architecture's answer, and it is what would happen on a board that had asked for it.

This board has not. The enable bit for MemManage sits in SHCSR and resets to zero; no line in this kernel sets it. Tock's own comment on the routine that clears that bit says what the consequence is: it "escalates the exception to a HardFault instead". So the HardFault handler runs, and MemManage never does.

The handler's first job is to find out who faulted, and it is settled by bit 2 of the link register: tst lr, #4. Set means the process's stack was in use, clear means the kernel's. Clear branches to a handler that never returns and takes the board down. Set carries on to step 5.

Five words are copied into a global before anything else can overwrite them: CCR, CFSR, HFSR, MMFAR, BFAR. MMFAR is the one that matters here — it holds the address that was refused. Then a flag is set, the processor is put back into privileged mode, and the handler returns into the kernel.

Back in the kernel, one if reads that flag. It takes priority over everything else, and the switch reason comes out as Fault. From here it is chapter 5's story: the board's fault policy decides what it costs, and on the Pico 2 that means the whole machine stops.

Notice that step 4 is where a memory bug in a process and a memory bug in the kernel stop being the same event. One bit of one register decides whether the board keeps running, and the process's own code cannot reach it.

Turning the unit on is three bits

The regions go into the hardware before a switch into a process, and the unit is turned on immediately after. Turning it on is a single write of three fields to MPU_CTRL, and two of the three are about who is not being protected against.

The regions do not always go in. Each process's configuration carries an id and a dirty flag, and the routine that loads it compares both against what the hardware already holds. Come back to the same process with its regions unchanged and the eight pairs are left alone.

The three fields are written every time, and they are worth one comparison. The older unit from Figure 6 is a different design end to end, and its own enable routine writes the identical three, in the same order, under the same comment. Whatever else changed between those two chips, the answer to "who is this for" did not.

Figure 8 One write, three fields, two exemptions

Flip the three switches and read what each one costs. Tock's own setting is the one they start in; every other combination is a board that behaves differently.

MPU_CTRL 0x00000005 what Tock writes
  • The regions are live, and every access a process makes is checked against them.The regions are inert. Every pair still holds what the kernel wrote, and none of it is consulted — this is the state the board boots into.
  • Privileged code keeps the default memory map behind the regions, so the kernel reaches anything the regions do not cover.The kernel is now fenced by the same eight regions as the process. It would fault on its first access outside them, which is why nothing in this tree clears this bit.
  • The unit stays on inside a HardFault or an NMI. The handler that runs after a violation is fenced too, so it could not reach into the process's stack to read out what happened.Inside a HardFault, or an NMI, the unit does not apply. That is exactly what lets the handler reach into the process's own stack and read out what happened.

The unit is on. Until this bit is set the regions are inert, which is the state the board boots into and the state the kernel returns to the moment a process stops running.

Privileged code gets the default memory map behind the regions, so anything the regions do not cover is still reachable from the kernel. The comment on the line calls it "allow privileged code access to all unprotected memory". So turning the unit off for the kernel has nothing to undo. That routine clears the enable bit and stops, under a comment reading "the MPU is not enabled for privileged mode, so we don't have to do anything".

Inside a HardFault, or an NMI — a non-maskable interrupt, the one kind the processor will not put off — the unit does not apply. The handler that runs because a process reached somewhere it should not have runs with the regions switched off. That is exactly what lets it reach into the process's own stack and read out what happened.

Notice that this is chapter 4's conclusion again, in hardware this time. Nothing here is protecting the board from the kernel. The unit was only ever pointed one way.

What the unit does not protect

A process cannot reach the kernel's memory, cannot reach another process's memory, cannot write its own code and cannot run its own data. That is a real, hardware-backed list, and it is worth being clear about what is not on it.

Figure 9 Three things the fence is not, and what each one means

Click each. None of these is a flaw; each is a boundary of what this mechanism was for.

The regions constrain unprivileged code and nothing else. A bug in a capsule can write any address on the chip, exactly as chapter 4 said, and this hardware will not notice. What the unit adds is a second kind of code that the first kind is protected from.

It fences the process, not the process's memory. The kernel reads and writes inside a process's allocation as a matter of course, and chapter 5's three structures live up there. Chapter 8 is entirely about a driver keeping state inside a process. The barrier only ever faces outward.

Eight is a number written by hand. MPU_TYPE would report what the hardware really has, the trait offers a method that reads it, four architectures implement that method, and nothing in the tree calls it. If that hand-written eight and the silicon disagreed, nothing here would say so.

Notice that the first two are design and the third is an absence. The trait's own safety note asks implementors to ensure "applications only have access to the configured MPU regions and no kernel memory" — a promise made in a comment, checked by review.

Check yourself

A region's MPU_RBAR reads 0x20008003. What is the first address the process may touch?

The second. Only bits 31 down to 5 are the address, so the bottom five bits belong to other fields — here the two access bits reading 0b01 for read-write, and the execute-never bit set. Mask them off and the base is 0x20008000. The third answer is the mistake worth naming: the stored value is the address already, not a number waiting to be shifted.

A process asks the kernel to move its break up by one byte. How much more memory can it reach?

Thirty-two. The new end is rounded up to the next multiple of 32 before the region is rebuilt, because the limit field has nowhere to record a finer boundary. The request is not refused and the rounding is not reported — the process simply finds it can reach a little further than it asked to.

Which of these does the unit refuse on this board?

The first. A process's flash is asked for as read-and-execute, so writing it is refused. Reading it is allowed, which is why the second is not the answer. And the third is not refused at all: a capsule runs privileged, where the regions do not apply.

Why does this board handle HardFault rather than MemManage, when the datasheet names MemManage?

The second. The chip implements it perfectly well, and Tock has a routine that would clear the enable bit, but nothing calls that routine. The bit simply resets to zero and is never written, and a fault with no enabled handler of its own escalates to HardFault.

What you can say now

A process is confined by two pairs of registers inside the processor, checked on every access it makes. The unit holds eight pairs; two is what a process gets. Both are spoken for before it starts: its RAM, read-write and never executable, and its flash, read-execute and never writable. The addresses in those registers are stored in units of 32 bytes, which is the only size rule this chip has, and it rounds in the process's favour.

When a process reaches past them the store never happens. A handler works out from one bit whether the kernel or the process was running, records the address that was refused, and hands the decision to the policy chapter 5 described.

None of it applies to the kernel. That was never an oversight. It is the shape of the whole design, and it is why the door in this wall has to be built so carefully.

Everything above, checked against source

Every claim on this page comes from the Tock tree at commit 83bad9388, the same commit chapters 4 and 5 were pinned to. Hardware claims come from the RP2350 datasheet, build of 2025-07-29.

  • What the unit is for, in three lines — RP2350 datasheet §3.7.4.7: "enforce privilege rules", "separate processes", "manage memory attributes"
  • "MPU mismatches and permission violations invoke the MemManage handler" — the same section
  • "The MPU supports 16 memory regions: 8 secure and 8 non-secure" — the same section
  • The register group at 0xE000ED90arch/cortex-m33/src/lib.rs:20–21, and the datasheet's system control space table, which lists MPU_TYPE at offset 0x0ed90
  • The layout of that group, so RNR, RBAR and RLAR land at +0x08, +0x0C and +0x10arch/cortex-m33/src/mpu_v8m.rs:27–58
  • Every field in Figure 2, with its offset and width — the same file, :80–110, "Defines whether code can be executed from this privileged region"
  • The base being written as logical_start >> 5:346, "let rbar_value = MPU_RBAR::BASE.val((logical_start as u32) >> 5)"
  • The limit being written as (logical_end - 1) >> 5, and the comment saying why — :358–363
  • The end address having to be a multiple of 32, checked before the region is built — :353–356
  • The smallest region being 32 bytes — CORTEXM_MIN_REGION_SIZE at :25, tested at :330
  • Rounding a pointer up to the next multiple of 32, which is the whole of the size rule here — align32, nine lines at :188–196, used at :468–469, :550–551 and :623
  • The five permission names and the two fields each becomes — kernel/src/platform/mpu.rs:11–17 and mpu_v8m.rs:335–343
  • ExecuteOnly mapping to privileged-only read with execution left on — :340–342
  • A process's flash being asked for as read-and-execute, before its RAM is placed — kernel/src/process_standard.rs:1783–1794
  • A process's RAM being asked for as read-write — the same file, :1928–1935
  • The accessible part starting at 32 bytes — initial_process_app_brk_size returning SVC_FRAME_SIZE, arch/cortex-m/src/syscall.rs:273–277 and :196
  • Region 0 being reserved for a process's RAM — APP_MEMORY_REGION_MAX_NUM at mpu_v8m.rs:251, written at :583 and :648, and skipped by unused_region_number at :288–292
  • Six slots for everything else — mpu_regions: [Cell<Option<mpu::Region>>; 6], kernel/src/process_standard.rs:548, filled at :927–935
  • Eight regions asked for by the chip crate — chips/rp2350/src/chip.rs:30 and :52
  • The Cortex-M33 supporting more than that — the comment at arch/cortex-m33/src/lib.rs:17, "Cortex-M33 supports up to 16 regions"
  • A method that would ask the hardware how many regions exist — number_total_regions, declared at kernel/src/platform/mpu.rs:136, implemented at mpu_v8m.rs:421, arch/cortex-m/src/mpu.rs:405, arch/riscv/src/pmp.rs:1058 and arch/x86/src/mpu.rs:308, and called nowhere in the tree
  • The older unit needing power-of-two sizes aligned to themselves, and using subregions when that fails — arch/cortex-m/src/mpu.rs:468–471, with the branch it opens running to :534
  • The three fields written to turn the unit on — mpu_v8m.rs:407–413, with the same three in the older file at arch/cortex-m/src/mpu.rs:391–397
  • Turning it off being one line, because it was never on for the kernel — mpu_v8m.rs:415–419
  • The regions being written before the switch and the unit enabled after — kernel/src/kernel.rs:542–543, and disabled again at :560–562
  • The hardware write being skipped when the same configuration is already loaded and unchanged — mpu_v8m.rs:654–672
  • Attribute slot 0 being set to normal, non-cacheable memory on every configure — :655–658, with every region pointing at slot 0 at :365
  • MemManage's enable bit resetting to zero — the datasheet's SHCSR table, MEMFAULTENA, reset 0x0; the same bit in Tock at arch/cortex-m/src/scb.rs:193, "MEMFAULTENA OFFSET(16) NUMBITS(1)"
  • Clearing that bit escalating the exception instead of removing it — the note on disable_memfault, arch/cortex-m/src/scb.rs:388–397, a function with no callers, "Disable the MemFault exception"
  • The handler deciding between kernel and process on bit 2 of the link register — arch/cortex-v7m/src/lib.rs:573–577, branching at :606–609
  • The five status registers copied out on a process fault, MMFAR among them — the same file, :612–623
  • The flag the kernel reads afterwards — APP_HARD_FAULT set at :625–627, declared at arch/cortex-m/src/syscall.rs:39, read at :443 and turned into a fault at :450–453
  • The trait being unsafe, and what it asks of implementors — kernel/src/platform/mpu.rs:82–89, "applications only have access to the configured MPU regions and no kernel memory"
  • The board's fault policy being what decides the cost — boards/raspberry_pi_pico_2/src/main.rs:71, "fn scheduler(&self) -> &Self::Scheduler {"

Text, diagrams and interactive figures © Jon Hillesheim 2026, licensed CC BY-SA 4.0 — share and adapt freely with credit, under the same license. Tock source excerpts quoted above remain under their own Apache-2.0 OR MIT license and are not relicensed here. The RP2350 datasheet is quoted in short passages with attribution and is not relicensed here either.

Every line reference below links to that commit on the fork it was read from, at the lines it names.