Skip to content

Commit d21ba4d

Browse files
authored
Merge pull request #32 from HeatCrab/pmp/memory-isolation
Enable PMP for memory isolation
2 parents 905d252 + e34fac0 commit d21ba4d

21 files changed

Lines changed: 2203 additions & 57 deletions

.ci/run-app-tests.sh

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,10 @@ test_app() {
2626
exit_code=$?
2727

2828
# Check phase
29-
if echo "$output" | grep -qiE "(trap|exception|fault|panic|illegal|segfault)"; then
29+
# Filter out expected PMP termination messages before crash detection
30+
local filtered_output
31+
filtered_output=$(echo "$output" | grep -v "\[PMP\] Task terminated")
32+
if echo "$filtered_output" | grep -qiE "(trap|exception|fault|panic|illegal|segfault)"; then
3033
echo "[!] Crash detected"
3134
return 1
3235
elif [ $exit_code -eq 124 ] || [ $exit_code -eq 0 ]; then

.ci/run-functional-tests.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ TOOLCHAIN_TYPE=${TOOLCHAIN_TYPE:-gnu}
1111
declare -A FUNCTIONAL_TESTS
1212
FUNCTIONAL_TESTS["mutex"]="Fairness: PASS,Mutual Exclusion: PASS,Data Consistency: PASS,Overall: PASS"
1313
FUNCTIONAL_TESTS["semaphore"]="Overall: PASS"
14-
FUNCTIONAL_TESTS["umode"]="PASS: sys_tid() returned,PASS: sys_uptime() returned,[EXCEPTION] Illegal instruction"
14+
FUNCTIONAL_TESTS["umode"]="[PASS] returned tid=,[PASS] returned uptime=,[EXCEPTION] Illegal instruction"
1515
#FUNCTIONAL_TESTS["test64"]="Unsigned Multiply: PASS,Unsigned Divide: PASS,Signed Multiply: PASS,Signed Divide: PASS,Left Shifts: PASS,Logical Right Shifts: PASS,Arithmetic Right Shifts: PASS,Overall: PASS"
1616
#FUNCTIONAL_TESTS["suspend"]="Suspend: PASS,Resume: PASS,Self-Suspend: PASS,Overall: PASS"
1717

Documentation/hal-riscv-context-switch.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -165,7 +165,7 @@ void *hal_build_initial_frame(void *stack_top,
165165
frame[FRAME_EPC] = (uint32_t) task_entry;
166166
167167
/* SP value for when ISR returns (stored in frame[33]).
168-
* For U-mode: Set to user stack top.
168+
* For U-mode: Set to user stack top minus 256-byte guard zone.
169169
* For M-mode: Set to frame + ISR_STACK_FRAME_SIZE.
170170
*/
171171
if (user_mode && kernel_stack) {
Lines changed: 305 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,305 @@
1+
# PMP: Memory Protection
2+
3+
## Overview
4+
5+
Linmo operates entirely in Machine mode by default, with all tasks sharing the same physical address space.
6+
A misbehaving task can corrupt kernel data structures or interfere with other tasks, compromising system stability.
7+
8+
Physical Memory Protection provides hardware-enforced access control at the physical address level.
9+
Unlike an MMU, PMP requires no page tables or TLB management, making it suitable for resource-constrained RISC-V systems.
10+
PMP enforces read, write, and execute permissions for up to 16 configurable memory regions.
11+
12+
The design draws inspiration from the F9 microkernel, adopting a three-layer abstraction:
13+
- **Memory Pools** define static physical regions at boot time, derived from linker symbols.
14+
- **Flexpages** represent dynamically protected memory ranges with associated permissions.
15+
- **Memory Spaces** group flexpages into per-task protection domains.
16+
17+
## Architecture
18+
19+
### Memory Abstraction Layers
20+
21+
```mermaid
22+
graph TD
23+
classDef hw fill:#424242,stroke:#000,color:#fff,stroke-width:2px
24+
classDef static fill:#e1f5fe,stroke:#01579b,stroke-width:2px
25+
classDef dynamic fill:#fff3e0,stroke:#e65100,stroke-width:2px
26+
classDef container fill:#e8f5e9,stroke:#1b5e20,stroke-width:2px
27+
classDef task fill:#f3e5f5,stroke:#6a1b9a,stroke-width:2px
28+
29+
subgraph L0 ["Hardware"]
30+
PMP[PMP Registers]:::hw
31+
end
32+
33+
subgraph L1 ["Memory Pools"]
34+
MP["Static Regions<br/>(.text, .data, .bss)"]:::static
35+
end
36+
37+
subgraph L2 ["Flexpages"]
38+
FP["fpage_t<br/>base / size / rwx"]:::dynamic
39+
end
40+
41+
subgraph L3 ["Memory Spaces"]
42+
AS["memspace_t<br/>per-task domain"]:::container
43+
end
44+
45+
subgraph L4 ["Task"]
46+
TCB[TCB]:::task
47+
end
48+
49+
TCB -->|owns| AS
50+
AS -->|contains| FP
51+
MP -->|initializes| FP
52+
AS -->|configures| PMP
53+
```
54+
55+
The core structures:
56+
57+
```c
58+
typedef struct fpage {
59+
struct fpage *as_next; /* Next in address space list */
60+
struct fpage *map_next; /* Next in mapping chain */
61+
struct fpage *pmp_next; /* Next in PMP queue */
62+
uint32_t base; /* Physical base address */
63+
uint32_t size; /* Region size */
64+
uint32_t rwx; /* R/W/X permission bits */
65+
uint32_t pmp_id; /* PMP region index */
66+
uint32_t flags; /* Status flags */
67+
uint32_t priority; /* Eviction priority */
68+
int used; /* Usage counter */
69+
} fpage_t;
70+
```
71+
```c
72+
typedef struct memspace {
73+
uint32_t as_id; /* Memory space identifier */
74+
struct fpage *first; /* Head of flexpage list */
75+
struct fpage *pmp_first; /* Head of PMP-loaded list */
76+
struct fpage *pmp_stack; /* Stack regions */
77+
uint32_t shared; /* Shared flag */
78+
} memspace_t;
79+
```
80+
81+
### TOR Mode and Paired Entries
82+
83+
TOR (Top Of Range) mode defines region *i* as `[pmpaddr[i-1], pmpaddr[i])`.
84+
This works well for contiguous kernel regions where boundaries naturally chain together.
85+
86+
For dynamically allocated user regions at arbitrary addresses, Linmo uses paired entries:
87+
88+
```
89+
┌─────────────────────────────────────────┐
90+
│ Entry N: base_addr (disabled) │
91+
│ Entry N+1: top_addr (TOR, R|W) │
92+
│ │
93+
│ Region N+1 = [base_addr, top_addr) │
94+
└─────────────────────────────────────────┘
95+
```
96+
97+
The first entry sets the lower bound with permissions disabled.
98+
The second entry defines the upper bound with TOR mode and the desired permissions.
99+
This consumes two hardware slots per user region but allows non-contiguous regions at arbitrary addresses.
100+
101+
### Kernel and User Regions
102+
103+
Kernel regions protect `.text`, `.data`, and `.bss` sections:
104+
105+
```c
106+
static const mempool_t kernel_mempools[] = {
107+
DECLARE_MEMPOOL("kernel_text",
108+
&_stext, &_etext,
109+
PMPCFG_PERM_RX,
110+
PMP_PRIORITY_KERNEL),
111+
DECLARE_MEMPOOL("kernel_data",
112+
&_sdata, &_edata,
113+
PMPCFG_PERM_RW,
114+
PMP_PRIORITY_KERNEL),
115+
DECLARE_MEMPOOL("kernel_bss",
116+
&_sbss, &_ebss,
117+
PMPCFG_PERM_RW,
118+
PMP_PRIORITY_KERNEL),
119+
};
120+
```
121+
122+
Kernel heap and stack are intentionally excluded—PMP is ineffective for M-mode, and kernel heap/stack is only used in M-mode.
123+
This keeps Regions 0-2 for kernel, leaving Region 3+ available for user dynamic regions with correct TOR address ordering.
124+
125+
Kernel regions use a hybrid lock strategy:
126+
127+
| Lock Type | Location | Effect |
128+
|-----------|---------------------------|-------------------------|
129+
| Software | `regions[i].locked = 1` | Allocator skips slot |
130+
| Hardware | `PMPCFG_L` NOT set | M-mode access preserved |
131+
132+
Setting the hardware lock bit would deny M-mode access.
133+
134+
User regions protect task stacks and are dynamically loaded during context switches.
135+
When PMP slots are exhausted, user regions can be evicted and reloaded on demand.
136+
137+
## Memory Isolation
138+
139+
### Context Switching
140+
141+
Context switching reconfigures PMP in two phases:
142+
143+
```mermaid
144+
flowchart LR
145+
subgraph Eviction
146+
E1[Iterate pmp_first] --> E2[Disable region in hardware]
147+
E2 --> E3["Set pmp_id = INVALID"]
148+
end
149+
subgraph Loading
150+
L1[Reset pmp_first = NULL] --> L2{Already loaded?}
151+
L2 -->|Yes| L3[Add to tracking list]
152+
L2 -->|No| L4[Find free slot]
153+
L4 --> L5[Load to hardware]
154+
L5 --> L3
155+
end
156+
Eviction --> Loading
157+
```
158+
159+
**Eviction phase** iterates the outgoing task's `pmp_first` linked list.
160+
Each flexpage is disabled in hardware, and `pmp_id` is set to `PMP_INVALID_REGION (0xFF)` to mark it as unloaded.
161+
162+
**Loading phase** rebuilds `pmp_first` from scratch.
163+
This prevents circular references—if `pmp_first` is not cleared, reloading a flexpage could create a self-loop in the linked list.
164+
For each flexpage in the incoming task's memory space:
165+
- **Already loaded** (shared regions): Add directly to tracking list
166+
- **Not loaded**: Find a free slot via `find_free_region_slot()` and load
167+
168+
If all slots are occupied, remaining regions load on-demand through the fault handler (lazy loading).
169+
170+
### Per-Task Kernel Stack
171+
172+
U-mode trap handling requires a kernel stack to save context.
173+
If multiple U-mode tasks share a single kernel stack, Task A's context frame is overwritten when Task B traps—the ISR writes to the same position on the shared stack.
174+
175+
Linmo allocates a dedicated 512-byte kernel stack for each U-mode task:
176+
177+
```c
178+
typedef struct tcb {
179+
/* ... */
180+
void *kernel_stack; /* Base address of kernel stack (NULL for M-mode) */
181+
size_t kernel_stack_size; /* Size of kernel stack in bytes (0 for M-mode) */
182+
} tcb_t;
183+
```
184+
185+
M-mode tasks do not require a separate kernel stack—they use the task stack directly without privilege transition.
186+
187+
During context switch, the scheduler saves the incoming task's kernel stack top to a global variable.
188+
The ISR restore path loads this value into `mscratch`, enabling the next U-mode trap to use the correct per-task kernel stack.
189+
190+
### Fault Handling and Task Termination
191+
192+
PMP access faults occur when a U-mode task attempts to access memory outside its loaded regions.
193+
The trap handler routes these faults to the PMP fault handler, which attempts recovery or terminates the task.
194+
195+
The fault handler first searches the task's memory space for a flexpage containing the faulting address.
196+
If found and the flexpage is not currently loaded in hardware, it loads the region and returns to the faulting instruction.
197+
This enables lazy loading—regions not loaded during context switch are loaded on first access.
198+
199+
If no matching flexpage exists, the access is unauthorized (e.g., kernel memory or another task's stack).
200+
If the flexpage is already loaded but still faulted, recovery is impossible.
201+
In either case, the handler marks the task as `TASK_ZOMBIE` and returns a termination code.
202+
203+
```mermaid
204+
flowchart TD
205+
A[Find flexpage for fault_addr] --> B{Flexpage found?}
206+
B -->|No| F[Unauthorized access]
207+
B -->|Yes| C{Already loaded in hardware?}
208+
C -->|No| D[Load to hardware]
209+
D --> E[Return RECOVERED]
210+
C -->|Yes| F
211+
F --> G[Mark TASK_ZOMBIE]
212+
G --> H[Return TERMINATE]
213+
```
214+
215+
The trap handler interprets the return value:
216+
217+
| Return Code | Action |
218+
|-------------------------|-----------------------------------------------|
219+
| `PMP_FAULT_RECOVERED` | Resume execution at faulting instruction |
220+
| `PMP_FAULT_TERMINATE` | Print diagnostic, invoke dispatcher |
221+
| `PMP_FAULT_UNHANDLED` | Fall through to default exception handler |
222+
223+
Terminated tasks are not immediately destroyed.
224+
The dispatcher calls a cleanup routine before selecting the next runnable task.
225+
This routine iterates zombie tasks, evicts their PMP regions, frees their memory spaces and stacks, and removes them from the task list.
226+
Deferring cleanup to the dispatcher avoids modifying task structures from within interrupt context.
227+
228+
## Best Practices
229+
230+
### Hardware Limitations
231+
232+
PMP provides 16 hardware slots shared between kernel and user regions.
233+
Kernel regions occupy slots 0-2 and cannot be evicted.
234+
Each user region requires two slots (paired entries for TOR mode).
235+
236+
| Resource | Limit |
237+
|-----------------------------|----------------------------|
238+
| Total PMP slots | 16 |
239+
| Kernel slots | 3 (fixed at boot) |
240+
| Slots per user region | 2 (paired entries) |
241+
| Max concurrent user regions | 6 (13 ÷ 2) |
242+
243+
With only 6 concurrent user regions, systems spawning many U-mode tasks rely on lazy loading through the fault handler.
244+
This incurs runtime overhead as regions are dynamically reloaded during context switches.
245+
Applications with many concurrent U-mode tasks should consider this tradeoff between isolation granularity and performance.
246+
247+
### Task Creation Guidelines
248+
249+
U-mode tasks receive automatic PMP protection.
250+
The kernel allocates a memory space and registers the task stack as a protected flexpage.
251+
252+
Applications use `mo_task_spawn()`, which automatically creates tasks in the appropriate privilege mode based on the build configuration:
253+
254+
```c
255+
/* Standard usage - automatically inherits parent's privilege mode */
256+
mo_task_spawn(task_func, stack_size);
257+
```
258+
259+
Privileged applications may need explicit control over task privilege modes, such as when testing mixed-privilege scheduling or implementing system services that manage both trusted and isolated tasks.
260+
For these scenarios, use the explicit creation functions:
261+
262+
```c
263+
/* Explicitly create M-mode task: trusted code, full memory access */
264+
mo_task_spawn_kernel(task_func, stack_size);
265+
266+
/* Explicitly create U-mode task: isolated execution, PMP protected */
267+
mo_task_spawn_user(task_func, stack_size);
268+
```
269+
270+
Choose the appropriate privilege level:
271+
- **M-mode**: Trusted kernel components requiring unrestricted memory access
272+
- **U-mode**: Application tasks where memory isolation is desired
273+
274+
### Common Pitfalls
275+
276+
1. Assuming PMP protects the kernel
277+
278+
PMP only restricts Supervisor and User modes.
279+
Machine mode has unrestricted access regardless of PMP configuration.
280+
This is intentional—the kernel must access all memory to manage protection.
281+
282+
```c
283+
/* This code in M-mode bypasses PMP entirely */
284+
void kernel_func(void) {
285+
volatile uint32_t *user_stack = (uint32_t *)0x80007000;
286+
*user_stack = 0; /* No fault—M-mode ignores PMP */
287+
}
288+
```
289+
290+
PMP protects user tasks from each other but does not protect the kernel from itself.
291+
292+
2. Exhausting PMP slots
293+
294+
With only ~6 user regions available, spawning many U-mode tasks causes PMP slot exhaustion.
295+
Subsequent tasks rely entirely on lazy loading, degrading performance.
296+
297+
3. Mixing M-mode and U-mode incorrectly
298+
299+
M-mode tasks spawned with `mo_task_spawn()` do not receive memory spaces.
300+
PMP-related functions check for NULL memory spaces and return early, so calling them on M-mode tasks has no effect.
301+
302+
## References
303+
304+
- [RISC-V Privileged Architecture](https://riscv.github.io/riscv-isa-manual/snapshot/privileged/)
305+
- [Memory Protection for Embedded RISC-V Systems](https://nva.sikt.no/registration/0198eb345173-b2a7ef5c-8e7e-4b98-bd3e-ff9c469ce36d)

Makefile

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,8 @@ BUILD_LIB_DIR := $(BUILD_DIR)/lib
1313
# All other apps run in U-mode by default (secure)
1414
MMODE_APPS := cond coop cpubench echo hello mqueues mutex \
1515
pipes pipes_small pipes_struct prodcons progress \
16-
rtsched semaphore suspend test64 test_libc timer timer_kill
16+
rtsched semaphore suspend test64 test_libc timer timer_kill \
17+
privilege_switch
1718

1819
# Auto-detect: if building an M-mode app, enable CONFIG_PRIVILEGED
1920
ifneq ($(filter $(MAKECMDGOALS),$(MMODE_APPS)),)
@@ -28,7 +29,7 @@ include arch/$(ARCH)/build.mk
2829
INC_DIRS += -I $(SRC_DIR)/include \
2930
-I $(SRC_DIR)/include/lib
3031

31-
KERNEL_OBJS := timer.o mqueue.o pipe.o semaphore.o mutex.o logger.o error.o syscall.o task.o main.o
32+
KERNEL_OBJS := timer.o mqueue.o pipe.o semaphore.o mutex.o logger.o error.o syscall.o task.o memprot.o main.o
3233
KERNEL_OBJS := $(addprefix $(BUILD_KERNEL_DIR)/,$(KERNEL_OBJS))
3334
deps += $(KERNEL_OBJS:%.o=%.o.d)
3435

@@ -40,7 +41,7 @@ deps += $(LIB_OBJS:%.o=%.o.d)
4041
APPS := coop echo hello mqueues semaphore mutex cond \
4142
pipes pipes_small pipes_struct prodcons progress \
4243
rtsched suspend test64 timer timer_kill \
43-
cpubench test_libc umode
44+
cpubench test_libc umode privilege_switch pmp
4445

4546
# Output files for __link target
4647
IMAGE_BASE := $(BUILD_DIR)/image

app/mutex.c

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -133,9 +133,8 @@ void monitor_task(void)
133133
mo_task_yield();
134134
}
135135

136-
/* Wait a bit for tasks to fully complete */
137-
for (int i = 0; i < 50; i++)
138-
mo_task_yield();
136+
/* Wait a bit for tasks to fully complete and logs to flush */
137+
sys_tdelay(100);
139138

140139
/* Final report */
141140
printf("\n=== FINAL RESULTS ===\n");
@@ -190,10 +189,10 @@ int32_t app_main(void)
190189

191190
printf("Binary semaphore created successfully\n");
192191

193-
/* Create tasks */
194-
int32_t task_a_id = mo_task_spawn(task_a, 1024);
195-
int32_t task_b_id = mo_task_spawn(task_b, 1024);
196-
int32_t monitor_id = mo_task_spawn(monitor_task, 1024);
192+
/* Create tasks with larger stack for PMP/printf overhead */
193+
int32_t task_a_id = mo_task_spawn(task_a, 2048);
194+
int32_t task_b_id = mo_task_spawn(task_b, 2048);
195+
int32_t monitor_id = mo_task_spawn(monitor_task, 2048);
197196
int32_t idle_id = mo_task_spawn(idle_task, 512);
198197

199198
if (task_a_id < 0 || task_b_id < 0 || monitor_id < 0 || idle_id < 0) {

0 commit comments

Comments
 (0)