Learning Tock from the ground up · Chapter 6
‹ ContentsChapter 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.
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.
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.
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.
Here is one access. Change nothing about it except who is making it, and watch the answer turn over.
store 0x000000FF into 0x2000F000
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.
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.
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.
Which describes 0x20008000 through 0x200087FF — 2048 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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
Next
A process fenced this tightly cannot print, cannot read a button and cannot wait a second. Everything it does that reaches the world goes through one instruction and eight classes of request. On the way: how a buffer crosses a boundary that exists to stop exactly that.
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.
0xE000ED90 — arch/cortex-m33/src/lib.rs:20–21, and the datasheet's system control space table, which lists MPU_TYPE at offset 0x0ed900x08, +0x0C and +0x10 — arch/cortex-m33/src/mpu_v8m.rs:27–58CORTEXM_MIN_REGION_SIZE at :25, tested at :330align32, nine lines at :188–196, used at :468–469, :550–551 and :623kernel/src/platform/mpu.rs:11–17 and mpu_v8m.rs:335–343kernel/src/process_standard.rs:1783–1794initial_process_app_brk_size returning SVC_FRAME_SIZE, arch/cortex-m/src/syscall.rs:273–277 and :196APP_MEMORY_REGION_MAX_NUM at mpu_v8m.rs:251, written at :583 and :648, and skipped by unused_region_number at :288–292mpu_regions: [Cell<Option<mpu::Region>>; 6], kernel/src/process_standard.rs:548, filled at :927–935chips/rp2350/src/chip.rs:30 and :52arch/cortex-m33/src/lib.rs:17, "Cortex-M33 supports up to 16 regions"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 treearch/cortex-m/src/mpu.rs:468–471, with the branch it opens running to :534mpu_v8m.rs:407–413, with the same three in the older file at arch/cortex-m/src/mpu.rs:391–397mpu_v8m.rs:415–419kernel/src/kernel.rs:542–543, and disabled again at :560–562mpu_v8m.rs:654–672SHCSR table, MEMFAULTENA, reset 0x0; the same bit in Tock at arch/cortex-m/src/scb.rs:193, "MEMFAULTENA OFFSET(16) NUMBITS(1)"disable_memfault, arch/cortex-m/src/scb.rs:388–397, a function with no callers, "Disable the MemFault exception"arch/cortex-v7m/src/lib.rs:573–577, branching at :606–609APP_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–453unsafe, 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"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.