User Mode I: The Wall, the Trampoline, and the Trapframe¶
Overview¶
Every line of rv6 you have written so far ran with total power over the machine.
The shell, the filesystem, the drivers, the scheduler — all kernel code, and
kernel code can read any address, write any CSR, and halt the hart. That is
tolerable while you write all the code. An operating system exists to run other
people's programs, and those crash, scribble, and loop. This session builds the
wall that makes running them safe: user mode, the CPU's weakest privilege
level, plus a per-process page table in which the kernel is not addressable
at all. Then the two structures that make crossing the wall possible — the
trampoline, one page mapped at the same virtual address in every address
space so satp can change without the CPU losing its footing, and the
trapframe, the parking lot for 31 user registers that sscratch bootstraps
access to. This is the concept behind exercise 48k_user_mode (Friday,
November 13); the system-call ABI on top of it is L23, next Tuesday. See the
Sv39 paging guide for the page-table mechanics
assumed throughout.
Learning Objectives¶
- Explain why privilege levels alone do not isolate a process, and what the page table adds.
- Enumerate what user mode forbids, and name the trap each violation raises.
- Describe the contents of an rv6 user address space and the role of
PTE_Uin both directions. - State the trampoline problem precisely: why the instruction after
csrw satpis the one that kills you. - Justify mapping one page at an identical virtual address in every page
table, and explain why that page is mapped without
PTE_U. - Trace
uservecinstruction by instruction, naming the contents ofa0andsscratchat every step. - Derive why the kernel cannot push user registers onto a stack on entry,
and what
sscratchsolves. - Compare rv6's trampoline with Linux's flat kernel mapping before and after the 2018 KPTI patches.
Prerequisites¶
- L12 Virtual Memory I and the
Sv39 paging guide — three-level walks, PTE flags,
satp,sfence.vma. This lecture is unreadable without them. - L18 Traps, Privilege Modes, and Interrupts and exercise
43k_traps—stvec,sepc,scause,sstatus, and themretthat dropped M → S. - L13 Processes and the PCB and exercise
34k_processes—Proc, the fixedPROCStable, and the per-process page table you have been allocating since then but never used. - L14 The Context Switch and exercise
35k_context_switch—swtch, which rv6 reuses to enter and leave a running user program. - The RISC-V guide for the register file, and
Unsafe Rust and no_std for
global_asm!and raw pointers.
1. Why a Wall¶
An OS makes a promise: run this program, and if it misbehaves only the program dies. That is not achievable in software. If a program can execute any instruction the kernel can, it can rewrite the kernel; if it can name any address the kernel can, it can read the kernel's secrets and forge its data structures. The promise is enforceable only if the hardware refuses on the kernel's behalf, every cycle, at no cost.
Two hardware mechanisms do that, and you have already built both halves.
| Mechanism | What it restricts | Where you met it |
|---|---|---|
| Privilege levels | Which instructions are legal | exercise 43k_traps |
| The MMU + page tables | Which addresses exist at all | exercises 33k_paging, 39k_virtual_memory |
Neither suffices alone. Privilege levels without paging leave a program able to
read the kernel's data with an ordinary ld — no privileged instruction needed.
Paging without privilege levels leaves it able to csrw satp and install a page
table of its own, which is the same as owning the machine. Isolation is the
conjunction: a weak privilege level, plus an address space with no kernel in
it.
This is old. Multics (1965) generalized it to eight nested rings; the Intel 80286 shipped four, of which every mainstream OS used two, because the middle rings cost complexity and bought nothing. ARM calls them exception levels, EL0–EL3. RISC-V, with forty years of hindsight, ships three — machine (M), supervisor (S), and user (U) — plus an optional hypervisor extension for the case that really needed a fourth. Today you meet the last one.
Historical note: the two-ring convention is not laziness. Rings model a linear order of trust, and real systems have needed a lattice — mutually untrusting peers — which rings cannot express. Modern systems get that from separate address spaces instead, which is the mechanism in section 3.
2. The Third Privilege Level¶
In exercise 43k_traps your kernel dropped from M-mode to S-mode with an mret,
after setting mstatus.MPP to say where it was going. Dropping to user mode is
the same move one level down: clear sstatus.SPP (bit 8) to 0, put the target
address in sepc, and execute sret. rv6 does exactly this in usertrapret
(usermode.rs:455, usermode.rs:459).
What changes when SPP is 0 and sret retires:
| U-mode may not | Result if it tries |
|---|---|
Execute csrr/csrw on any supervisor CSR |
Illegal instruction, scause = 2 |
Execute sret, sfence.vma, wfi |
Illegal instruction, scause = 2 |
Touch a page whose PTE lacks PTE_U |
Page fault, scause = 12, 13, or 15 |
| Touch an unmapped address | Page fault, same causes |
Notice what is not on the list: arithmetic, loads, stores, branches, and jumps are all legal. User mode is not a sandbox that inspects the program. It is a set of hardware refusals that cost zero cycles when the program behaves.
The CSR ban is total, including reads: a program cannot read sstatus to learn
what mode it is in, satp to find its page table, or stvec to find the
kernel's entry point. The wall is opaque from the far side — which is why
getpid() must be a system call. A process's own identity is a fact only the
kernel holds.
stateDiagram-v2
[*] --> M: reset
M --> S: mret, MPP=S
S --> U: sret, SPP=U
U --> S: ecall
U --> S: interrupt
U --> S: fault
S --> S: kernel trap
There are exactly two doors from U back to S, and the program only controls one
of them. It can ask — ecall, which raises scause = 8, "environment call
from U-mode" — or the hardware can force the issue with an interrupt or a fault.
Both arrive at the address in stvec, which user code cannot read, let alone
write. The kernel therefore decides where every entry lands; the program only
decides when.
Key distinction:
ecallis not a jump and not a call. It is a synchronous exception. Nothing about it is special-cased in hardware beyond the cause code — the same vector, the samesepc, the samesstatusbookkeeping as a page fault. A system call is a fault the program raises on purpose.
3. The Private Address Space¶
Privilege alone still leaves the kernel readable, so each process gets its own
page table. You have been allocating one per process since exercise
34k_processes (proc.rs:116); this is the exercise where satp finally points
at it.
The layout in memlayout.rs:29-75 is deliberately spartan:
kernel page table a user page table
(satp while in the kernel) (satp while this process runs)
0x40_0000_0000 MAXVA ────────────────────────────────────────────
0x3F_FFFF_F000 TRAMPOLINE R X 0x3F_FFFF_F000 TRAMPOLINE R X
(no U) (no U) ← same page
0x3F_FFFF_E000 (unmapped) 0x3F_FFFF_E000 TRAPFRAME R W
(no U) ← this process
... ...
0x8800_0000 PHYSTOP
0x8000_0000 KERNBASE R W X (nothing at all up here)
0x0C00_0000 PLIC R W
0x1000_0000 UART0 R W 0x0001_1000 ← initial sp
0x0010_0000 TEST R W 0x0001_0000 stack page R W U
... (guard gap, unmapped)
0x0000_0000 (unmapped) 0x0000_0000 program image R X U
Three things deserve attention.
PTE_U is the wall (vm.rs:17-23). One bit in each leaf PTE decides whether
user mode may touch that page. The kernel's own mappings — the UART, the PLIC,
all 128 MiB of RAM at KERNBASE — are simply absent from the user's table, but
the two entries that are present (trampoline and trapframe) sit inside the
user's address space with PTE_U clear (proc.rs:164-165). They are visible to
the MMU and invisible to the program.
The bit works in the other direction too, which surprises people. When
sstatus.SUM is 0 — rv6 leaves it 0 — supervisor mode may not load or store
through a PTE_U page either. That is not an accident of the encoding; it is a
deliberate guard against the kernel dereferencing a user pointer by mistake,
which is the single most productive bug class in OS history. It is also why
walkaddr (vm.rs:252-261) exists: the kernel translates user addresses by
hand, through the user's page table, and refuses anything without PTE_U
(vm.rs:257). We will use it in L23.
Address 0 is an ordinary address. In a fresh, private address space nothing
is sacred about zero, and it is where xv6 loads programs. Hosted Unix leaves the
first page unmapped so null-pointer dereferences fault; rv6 gets that luxury in
exercise 49k_exec, not here.
MAXVA is 1 << 38, not 1 << 39 (memlayout.rs:49). Sv39 gives 39 bits,
but bits 63:39 of any valid virtual address must all equal bit 38 — the
sign-extension rule. Stopping one bit short means rv6 never has to think about
it. The trampoline at MAXVA - PGSIZE is therefore the highest page that is
unambiguously "positive", and it lands at page-table indices (255, 511, 511).
Comparison: Linux does the opposite. The kernel lives in the high half of every address space, mapped with the supervisor-only bit, so entering the kernel historically required no page-table switch at all — just a privilege change. That was faster and simpler, and it survived until January 2018, when Meltdown proved that speculative execution could leak the contents of supervisor-only pages that were merely mapped. The fix, KPTI, unmaps the kernel from user page tables and switches
CR3on entry — and the entry code that performs the switch must live at an address mapped in both tables. Linux calls that page the entry trampoline. It is the design you are about to build, arrived at from the opposite direction and twenty years later.
4. The Trampoline Problem¶
Here is the puzzle at the center of this material. Take it slowly; it is the one idea in the course that cannot be understood by analogy.
Entering the kernel from user mode means changing satp, because the user's
page table does not map the kernel. But satp is not data — it is the map by
which every address is interpreted, including the address the CPU is about to
fetch the next instruction from. Consider three consecutive instructions in some
hypothetical entry routine at virtual address 0x8000_5000:
va 0x8000_5000: ld t1, 0(a0) # t1 = kernel satp value
va 0x8000_5004: csrw satp, t1 # <- the world changes HERE
va 0x8000_5008: jr t0 # <- fetched through the NEW table
The csrw at ...5004 retires. The MMU is now consulting a different tree.
The CPU increments the PC to ...5008 and issues an instruction fetch — and
that fetch is translated by the new table. If the new table maps 0x8000_5008
to a different physical page, the CPU executes whatever bytes happen to live
there. If it maps nothing there, the CPU takes an instruction page fault whose
handler is reached through stvec, at an address that may itself now be
unmapped, and the hart wedges. The switch cannot be "undone" by the next
instruction, because there is no next instruction you can trust.
Key distinction: this is not a TLB problem.
sfence.vmafixes stale translations; it does not help when the translation is fresh, correct, and points somewhere else. The problem is that the code doing the switching has moved under its own feet.
The resolution¶
State the requirement exactly: the instructions that write satp must live at
a virtual address that means the same thing in both the old table and the new
one. If the very page holding the switching code is mapped at the same VA, to
the same physical page, in both tables, then the fetch at ...5008 resolves to
the same bytes before and after. The switch becomes invisible to the fetch
stream.
That page is the trampoline. rv6 puts it at TRAMPOLINE, the top page of
every address space (memlayout.rs:53), and maps it into the kernel table at
boot (vm.rs:169) and into every process's table at creation
(proc.rs:164).
graph TB
KR["kernel page table root"] --> KE["leaf PTE\nindex 255 / 511 / 511\nR X V, no U"]
P1R["process 1 page table root"] --> P1E["leaf PTE\nindex 255 / 511 / 511\nR X V, no U"]
P2R["process 2 page table root"] --> P2E["leaf PTE\nindex 255 / 511 / 511\nR X V, no U"]
KE --> PHYS["ONE physical page\nTRAMP_PAGE\nuservec + userret"]
P1E --> PHYS
P2E --> PHYS
PHYS -.-> VA["every table maps it at\nVA 0x3F_FFFF_F000"]
Every table, one page. The virtual address is a constant of the design, not a
per-process value, which is why userret can materialize the trapframe address
with a bare li (usermode.rs:144) — TRAPFRAME is likewise the same VA
everywhere.
Three consequences worth stating¶
The trampoline gets its own physical page, copied at boot. The assembly is
linked into the kernel image alongside everything else, so its physical page is
shared with unrelated kernel code. Mapping that page at TRAMPOLINE would
expose whatever else is on it. So kvmmake allocates a fresh page, copies the
bytes from trampoline to trampoline_end onto it, and maps the copy
(vm.rs:158-172). The fence.i at vm.rs:168 matters: the kernel just wrote
instructions through the data path, and RISC-V does not promise the
instruction fetch path sees them without it.
It is mapped without PTE_U, even in the user's table (vm.rs:169,
proc.rs:164). The trampoline is kernel code that happens to be addressable in
the user's address space. If the user could execute it, it could jump to the
middle of userret, past the csrw satp, and reload registers of its choosing
— or simply read the kernel's satp value out of the trapframe. Without
PTE_U, the only way to land on that page is a trap, and a trap has already
raised privilege to S before the first byte is fetched. The page is
simultaneously in the address space and unreachable from it.
sfence.vma brackets every satp write (usermode.rs:133-135,
usermode.rs:140-142). The fence before flushes stale entries from the table
you are leaving; the fence after guarantees the MMU consults the new one. QEMU
often forgives their absence; hardware with a real TLB does not, and the failure
is intermittent, which is worse.
Design note: an alternative exists — map the kernel's text into the user table as well, so no switch is needed on entry. That flat mapping is what Meltdown charged Linux for: mapped means speculatively reachable, and reachable means leakable. One page without
PTE_Uis a much smaller thing to defend.
5. The Trapframe¶
The ecall retires. The CPU is in S-mode at stvec, and every general-purpose
register still holds the user program's value. The kernel cannot execute one
line of Rust without destroying them, and it must hand all 31 back, bit-exact,
when the program resumes.
Why not a stack?¶
A kernel trap solves this by pushing. Look at kernelvec (trap.rs:90-107): it
does addi sp, sp, -128 and stores sixteen registers. That works because a
kernel trap interrupts kernel code, so sp already points at a valid kernel
stack.
From user mode, neither half of that sentence holds:
spholds a user value. The program chose it. It may point at its code page, at an unmapped address, or — this is the interesting one — at the trapframe. Pushing through it would be the kernel writing wherever the program said to.- Even a good
sppoints into the user's address space, andsatpstill holds the user's page table. There is no valid kernel stack pointer in any register at the momentuservecbegins.
So rv6 gives each process a dedicated page — the trapframe — and each
process a separate kernel stack (proc.rs:117-118). The trapframe is mapped
at TRAPFRAME in that process's page table, R|W and no U (proc.rs:165).
It is a fixed-layout structure (usermode.rs:33-71), #[repr(C)] so that the
field offsets in the Rust struct and the byte offsets in the assembly are the
same numbers:
offset field who writes it
------ ------------ ---------------------------------------
0 kernel_satp usertrapret, before leaving (usermode.rs:449)
8 kernel_sp usertrapret (usermode.rs:450)
16 kernel_trap usertrapret: address of usertrap (:451)
24 epc usertrap on entry; +4 for ecall (:397, :401)
32 kernel_hartid unused here; keeps the xv6 layout
40 ra 48 sp 56 gp 64 tp
72 t0 ... 112 a0 ... 168 a7 ... 280 t6
Offsets 40 through 280 are the 31 registers. Offsets 0 through 32 are notes the
kernel leaves for itself — the three values uservec needs before it can run
any Rust, deposited on the way out by usertrapret so they are waiting on the
way in.
Key distinction: a trapframe is not a context.
Context(exercise35k_context_switch) holds 14 callee-saved registers, becauseswtchis an ordinary function call and the ABI already spilled the rest. A trapframe holds 31, because a trap is not a function call — it can strike between any two instructions, and the ABI has promised nothing about that moment.
The chicken and the egg: sscratch¶
uservec needs a register to hold the trapframe's address so it can start
storing. But to get an address into a register it must first destroy a register,
and every register holds user state that is not yet saved anywhere.
The escape is sscratch, a supervisor CSR that exists for exactly this and
nothing else, and the instruction csrrw, which swaps a register with a CSR
in one atomic step. If sscratch holds TRAPFRAME when the trap arrives, then
leaves a0 = TRAPFRAME and sscratch = the user's a0. Nothing was lost: the
user's a0 is parked in a CSR that user mode cannot read, and the kernel has
the one pointer it needs. One instruction, zero memory accesses, no stack.
Who arms sscratch the first time? userret does, on its way out
(usermode.rs:180): its last act before sret is another csrrw a0, sscratch,
a0, which restores the user's a0 and leaves TRAPFRAME in sscratch,
ready for the next trap. The exit path arms the entry path. Since a process can
only reach user mode through userret, the invariant "sscratch holds
TRAPFRAME whenever U-mode code is running" holds from the very first
instruction the program ever executes.
6. uservec, Line by Line¶
stvec points here while user code runs (usermode.rs:443-445). Read
usermode.rs:92-137 alongside this. Four phases:
graph LR
A["entry\nswap a0 / sscratch"] --> B["park 31 registers\ninto TRAPFRAME"]
B --> C["load kernel_sp\nkernel_trap\nkernel_satp"]
C --> D["sfence + csrw satp\n+ sfence"]
D --> E["jr t0\ninto usertrap"]
Phase 1 — get a pointer (usermode.rs:94).
The state of the machine right now: privilege is S, satp is still the user's
page table, sepc holds the user PC, scause holds the reason, and 30 of 31
registers still hold user values. The trapframe is reachable because it is
mapped in the user's table.
Phase 2 — park everything (usermode.rs:96-127). Thirty sd instructions
at fixed offsets from a0, a0 itself skipped. Then the tail:
t0 was already saved at offset 72, so it is free to clobber — which is why
these two lines come last. sp is saved at offset 48 like any other register:
the kernel will not use the user's stack, but it must give it back.
Phase 3 — pick up the kernel's notes (usermode.rs:129-131).
ld sp, 8(a0) # kernel_sp: this process's kernel stack top
ld t0, 16(a0) # kernel_trap: the address of usertrap()
ld t1, 0(a0) # kernel_satp
All three were written by usertrapret before the last sret
(usermode.rs:449-451). The trick is worth naming: the kernel cannot look
anything up on entry, so it leaves itself a note on exit. The process's own
trapframe is the only memory reachable at this instant, so everything needed is
already in it. Note the ordering — sp now holds a kernel address that is
unmapped in the table currently installed, so uservec loads it but must not
touch it.
Phase 4 — cross (usermode.rs:133-137).
sfence.vma zero, zero
csrw satp, t1 # the world changes
sfence.vma zero, zero
jr t0 # into usertrap(), never returns here
The csrw at usermode.rs:134 is the instruction that made this whole page
necessary. The jr at line 137 is fetched through the new table and lands
correctly, because we are standing on the trampoline; t0 and sp now hold
kernel addresses that finally mean something. The next thing that runs is Rust:
usertrap (usermode.rs:385), on the process's own kernel stack, with all 31
user registers safely in memory.
7. The Road Back¶
usertrapret (usermode.rs:440-466) is the mirror image, and it runs in the
kernel with full addressability, so it does its work in Rust:
- Point
stvecback at the trampoline'suservec, computed asTRAMPOLINE + (uservec - trampoline)(usermode.rs:443-445) — the offset within the page is the same wherever the page is mapped. Symmetrically,usertrapaimsstvecatkernelvecon entry (usermode.rs:387): a trap taken in the kernel must not go throughuservec. - Write the three notes into the trapframe for next time (
usermode.rs:449-451). - Clear
sstatus.SPPsosretgoes to U-mode, setSPIEso interrupts are enabled once there (usermode.rs:455-456). - Set
sepcfrom the trapframe's savedepc(usermode.rs:459). - Compute the user's
satpvalue (usermode.rs:461) and calluserretat its trampoline address, passing that value ina0(usermode.rs:463-466).
userret (usermode.rs:139-181) then switches satp to the user table
(line 141) — legal, because it stands on the trampoline — materializes
TRAPFRAME with li a0, {trapframe} (line 144), stashes the user's a0 in
sscratch (lines 146-147), reloads the other 30 registers, and finishes with
the swap that both restores a0 and re-arms sscratch (line 180) before
sret.
There is exactly one way out of the kernel. Every return to user mode — first
entry, a system-call return, a timer-interrupt return, the child's first breath
after fork — goes through usertrapret and then userret. That
single path is why the sscratch invariant holds, and why 51k_fork_wait gets a
working child by copying the parent's trapframe and zeroing one field.
Where this goes next: L23 picks up at
usertrap, wherescause == 8means a system call — the a7/a0-a2 ABI, theepc += 4that steps over theecall,dispatch, andcopyin.
Key Concepts¶
| Concept | Definition | Example |
|---|---|---|
| User mode (U) | The weakest RISC-V privilege level: no CSR access, no privileged instructions, no non-PTE_U pages |
Entered by sret with sstatus.SPP = 0 (usermode.rs:455) |
PTE_U |
Leaf-PTE bit permitting user-mode access; with SUM = 0 it also bars kernel access |
PTE_U = 1 << 4 (vm.rs:23) |
| Per-process page table | The address space a process sees; the kernel is simply absent from it | (*p).pagetable, allocated in allocproc (proc.rs:116) |
MAXVA |
One past the highest VA rv6 uses; 1 << 38, one bit short of Sv39's 39 |
memlayout.rs:49 |
TRAMPOLINE |
The top page of every address space, holding uservec/userret |
0x3F_FFFF_F000 (memlayout.rs:53) |
| Trampoline page | One physical page mapped at the same VA in every table, so satp can change mid-stream |
Copied and mapped in kvmmake (vm.rs:158-172) |
TRAPFRAME |
The page below the trampoline; this process's register parking lot | 0x3F_FFFF_E000 (memlayout.rs:57) |
| Trapframe | 31 saved user registers plus four notes the kernel leaves itself | struct Trapframe, #[repr(C)] (usermode.rs:33-71) |
sscratch |
A supervisor CSR holding TRAPFRAME while user code runs; bootstraps register access |
csrrw a0, sscratch, a0 (usermode.rs:94) |
Kernel stack (kstack) |
A per-process page the kernel runs on after a trap, never the user's sp |
Loaded as kernel_sp (usermode.rs:129) |
uservec / userret |
The two halves of the trampoline: in from U, out to U | usermode.rs:93, usermode.rs:139 |
sfence.vma |
Flushes stale address translations; brackets every satp write |
usermode.rs:133-135 |
Practice Problems¶
Problem 1: Decode the trampoline's page-table indices¶
TRAMPOLINE = 0x3F_FFFF_F000 and TRAPFRAME = 0x3F_FFFF_E000. Give the L2, L1,
and L0 indices for both. Then encode the leaf PTE that maps TRAMPOLINE to
physical page 0x8020_1000 with the permissions rv6 actually uses.
Click to reveal solution
`px(level, va) = (va >> (12 + level * 9)) & 0x1ff` (`vm.rs:44-46`). `TRAMPOLINE = 2^38 - 4096`: - L2 = `va >> 30 & 0x1ff`: `2^38 / 2^30 = 256`, minus a partial page → 255. - L1 = `va >> 21 & 0x1ff`: `(2^38 - 2^12) >> 21 = 131071 = 0x1FFFF`; `& 0x1ff` = 511. - L0 = `va >> 12 & 0x1ff`: `0x3FF_FFFF & 0x1ff` = 511. So **(255, 511, 511)**. `TRAPFRAME` is one page lower, so only L0 changes: **(255, 511, 510)**. They share both upper-level page-table pages — the walk allocates two interior tables and both entries land in the same L0 table. The PTE (`vm.rs:30-32`): `((pa >> 12) << 10) | flags`. `0x8020_1000 >> 12 = 0x80201`, `<< 10 = 0x2008_0400`. Flags for the trampoline are `PTE_R | PTE_X | PTE_V` = `2 | 8 | 1` = `0xB` — deliberately **no** `PTE_U` and no `PTE_W`. Result: **`0x2008_040B`**.Problem 2: The instruction after csrw satp¶
A student decides the trampoline is overengineering and moves the satp switch
into an ordinary kernel function at virtual address 0x8000_9040:
The user page table maps only pages at VAs 0, 0x1_0000, 0x3F_FFFF_E000, and
0x3F_FFFF_F000. This routine is reached from user mode via stvec. Predict, in
order, exactly what the hardware does starting at 0x8000_9040.
Click to reveal solution
It never reaches `0x8000_9040`. The trap itself sets `pc = stvec = 0x8000_9040` while `satp` still holds the *user* table, and `0x8000_9040` is not mapped there. The very first instruction fetch takes an **instruction page fault**, `scause = 12`, `stval = 0x8000_9040`. That fault is delivered to `stvec` — which is `0x8000_9040`. Fetch, fault, deliver, fetch, fault: the hart loops on the same exception forever, and no handler ever runs. If you *reach* the routine — say the first two instructions happen to be mapped in the user table — the failure moves one instruction later: `csrw` retires, then the fetch of `0x8000_9048` is translated by the new table, so the CPU either executes unrelated bytes or double-faults as above. The switch is safe only if the *fetch address* is invariant across it.Problem 3: Trace uservec¶
A user program executes ecall with a0 = 1, a1 = 0x0000_0028, a2 = 21,
a7 = 16, sp = 0x0001_1000. Fill in a0, sscratch, and satp immediately
before each of these points in usermode.rs: (a) line 94, (b) line 96, (c) line
127, (d) line 137. Then: what breaks if line 94 is replaced with
csrr a0, sscratch?
Click to reveal solution
| Point | `a0` | `sscratch` | `satp` | |---|---|---|---| | (a) before line 94 | 1 (user value) | `0x3F_FFFF_E000` | user table | | (b) before line 96 | `0x3F_FFFF_E000` | 1 | user table | | (c) before line 127 | `0x3F_FFFF_E000` | 1 | user table | | (d) before line 137 | `0x3F_FFFF_E000` | 1 | **kernel table** | `a0` stays at `TRAPFRAME` for the whole routine — it is the base register for all 30 stores. `satp` changes only at line 134. With `csrr a0, sscratch`, the read succeeds and `a0` becomes `TRAPFRAME`, but the user's `a0` — the value `1` — is **overwritten and gone**. Nothing else holds it, so offset 112 gets garbage and the program resumes with a corrupted `a0`. For this program the corruption is invisible (it overwrites `a0` with the syscall return anyway), which is worse: the bug ships. The `csrrw` swap is required precisely because it saves and loads in the same instruction.Problem 4: PTE_U in both directions¶
Two independent one-bit mistakes. For each, state the exact symptom and the
scause value.
(a) proc_pagetable maps the trampoline with PTE_R | PTE_X | PTE_U.
(b) The program's code page is mapped with PTE_R | PTE_X and no PTE_U.
Click to reveal solution
**(a)** Everything works — and that is the problem: no test fails. But the program can now jump into the middle of `userret` at `TRAMPOLINE + offset`. The privileged `csrw satp` there would still fault (`scause = 2`), so the prize is reconnaissance rather than takeover: it can read the kernel's entry code, and if the trapframe were ever mapped `PTE_U` too it could read `kernel_satp` and `kernel_sp` and forge a trapframe. No kernel structure is ever `PTE_U`, and it has to be a *rule* precisely because violating it is silent. **(b)** The program never executes an instruction. `sret` retires, the CPU is in U-mode with `pc = 0`, and the very first fetch hits a PTE without `PTE_U`: **instruction page fault, `scause = 12`, `stval = 0`**. rv6's `usertrap` takes the `else` branch (`usermode.rs:428-433`), records `FAULTED`, and kills the process. This is the most common failure in exercise `48k_user_mode` and the error message names the address: `0`.Problem 5: Why not the user's stack?¶
Suppose uservec were written to push the 31 registers onto the user's stack
instead of into a trapframe:
Construct a user program that turns this into arbitrary kernel memory
corruption, and explain what the second mechanism (satp) contributes.
Click to reveal solution
Set `sp = TRAPFRAME + 256` and `ecall`. The kernel, in S-mode with the user's table still installed, writes 256 bytes wherever the program pointed — over its own trapframe, or over a shared page, or over any interior page-table page that was ever mapped `PTE_U`. The `satp` half limits today's damage: while `uservec` runs only the user's own pages are addressable, so a hostile `sp` vandalizes memory the process already owns. But the kernel is *executing a store whose address it did not choose*, which is the definition of losing control — and the moment `satp` becomes the kernel's, one instruction later, that same stale `sp` reaches everything. The fixed-address trapframe removes the program from the decision: `TRAPFRAME` is a constant it cannot influence.Problem 6: Order the steps, and find the bootstrap¶
Put these in the order they execute for a process's very first entry into user
mode, and identify which step establishes the sscratch invariant:
A. csrw satp, a0 in userret
B. mappages(pt, TRAMPOLINE, ...) in proc_pagetable
C. csrrw a0, sscratch, a0 at the end of userret
D. sret
E. (*tf).kernel_satp = ... in usertrapret
F. ptr::copy_nonoverlapping(src, tramp, len) in kvmmake
G. csrw stvec, tramp_uservec in usertrapret
Click to reveal solution
**F → B → G → E → A → C → D.** - **F** (`vm.rs:167`) happens once at boot, when `kvmmake` copies the trampoline onto its own page. - **B** (`proc.rs:164`) happens when the process is created, mapping that same physical page into the new table. - **G** (`usermode.rs:445`) and **E** (`usermode.rs:449`) run in `usertrapret`: aim `stvec` at `uservec`, then leave the notes. - **A** (`usermode.rs:141`) is the first instruction group of `userret`, standing on the trampoline. - **C** (`usermode.rs:180`) is the last instruction before `sret`. - **D** (`usermode.rs:181`) enters U-mode. **C establishes the invariant.** At that moment `a0` holds `TRAPFRAME` (put there by `li` at line 144) and `sscratch` holds the user's `a0`; the swap exchanges them, leaving `sscratch = TRAPFRAME` for a trap that has not happened yet. Because `userret` is the *only* road into user mode, `sscratch` is correct before the program's first instruction — no separate initialization exists anywhere in rv6, and none is needed.Further Reading¶
- Sv39 Paging — the three-level walk, PTE flag
encoding, and
satpfields this lecture assumes throughout. - RISC-V Guide — the register file, CSR instructions
(
csrr,csrw,csrrw), and the calling convention. - Memory Map — the QEMU
virtphysical layout the kernel page table mirrors. - rv6 Architecture — where
usermode.rs,vm.rs, andproc.rssit in the whole. - Exam Prep — Midterm 2 covers this material directly.
- xv6: a simple, Unix-like teaching operating system, chapter 4 ("Traps and system calls"). rv6's trampoline is xv6's, nearly instruction for instruction.
- The RISC-V Instruction Set Manual, Volume II: Privileged Architecture,
sections on
sstatus,sscratch,stvec, and Sv39. TheSUMbit is worth reading in the original. - The Linux kernel's
arch/x86/entry/entry_64.Sentry-trampoline code — the same idea, retrofitted after Meltdown onto a system that had gone twenty-five years without needing it.
Summary¶
-
Isolation is a conjunction, not a single feature. Privilege levels restrict which instructions are legal; page tables restrict which addresses exist. Either one alone leaves the kernel wide open.
-
User mode is defined by refusals. No CSR access at all, no privileged instructions, and no page whose PTE lacks
PTE_U. Ordinary computation is untouched and costs nothing. -
PTE_Uis the wall, and it cuts both ways. Withsstatus.SUM = 0the kernel may not dereference user pages either, which is why the kernel translates user addresses by hand throughwalkaddr(vm.rs:252-261). -
Every process gets its own page table, and the kernel is not in it. Only two kernel pages are mapped — the trampoline and this process's trapframe — both without
PTE_U(proc.rs:164-165). -
The trampoline exists because of the instruction after
csrw satp. That fetch is translated by the new table. Code that switchessatpmust live at a VA that means the same thing in both tables, or there is no next instruction. -
One physical page, one virtual address, every table.
kvmmakecopies the trampoline onto a private page (vm.rs:158-172); every page table maps it atTRAMPOLINEwithR|Xand noU, so only a trap can land there. -
The trapframe exists because there is no usable stack on entry.
spholds a user value in a user address space;kernelveccan push anduserveccannot. Each process gets a fixed-address trapframe and a separate kernel stack. -
sscratchbreaks the chicken-and-egg, anduserretarms it. Onecsrrw a0, sscratch, a0(usermode.rs:94) trades a user register for the trapframe pointer with nothing lost; the identical swap atusermode.rs:180re-arms it on the way out, which is why the invariant holds from the program's first instruction.