-
Notifications
You must be signed in to change notification settings - Fork 26
Description
Background
Linmo currently lacks memory protection. All tasks share the same physical address space with no isolation - any task can access kernel memory or corrupt other tasks' stacks, either accidentally or maliciously.
This issue proposes implementing RISC-V Physical Memory Protection (PMP) to provide hardware-enforced memory isolation. The implementation is based on a master's thesis provided by Jserv, which describes porting F9 Microkernel to RISC-V using PMP. The design considers future MMU implementation (see #1), ensuring the abstractions can support both PMP and MMU-based memory management.
Proposed Solution
Implement RISC-V PMP using TOR (Top Of Range) addressing mode to protect kernel memory and isolate task stacks. Introduce memory management abstractions (memory pools, flex pages, address spaces) that work with PMP hardware.
Goals
- Kernel Protection: Protect kernel code and data from unauthorized task access
- Task Isolation: Provide per-task stack regions with hardware enforcement
- Memory Abstractions: Implement flex pages and address spaces for memory management
- PMP Infrastructure: Create CSR access functions and region management
- Fault Handling: Handle PMP violations appropriately
Detailed Requirements
1. Memory Management Abstractions
Three core abstractions form the foundation of memory protection:
Memory Pools: Define static memory regions with attributes. Each pool specifies a memory range and its access permissions, used to initialize PMP regions at boot time.
Flex Pages: Describe contiguous memory regions with protection attributes. Unlike traditional L4 flex pages for virtual memory, these represent physical memory regions enforced by PMP. RISC-V TOR mode allows arbitrary base addresses and sizes without alignment constraints, eliminating the need to split regions into power-of-two chunks.
Address Spaces: Organize multiple flex pages into a cohesive view of memory accessible to a task. Each task has an associated address space, and multiple tasks can share the same address space.
2. Data Structures
Flex Page Structure:
typedef struct fpage {
struct fpage *as_next; /* Next fpage in address space */
struct fpage *map_next; /* Next in mapping chain */
struct fpage *pmp_next; /* Next in PMP queue */
uint32_t base; /* Base physical address */
uint32_t size; /* Region size */
uint32_t rwx; /* Read/Write/Execute permissions */
uint32_t pmp_id; /* Assigned PMP region index */
uint32_t flags; /* Status flags */
uint32_t priority; /* Eviction priority */
int used; /* Usage counter */
} fpage_t;Address Space Structure:
typedef struct {
uint32_t as_id; /* Address space identifier */
struct fpage *first; /* First fpage in address space */
struct fpage *pmp_first; /* First fpage in PMP list */
struct fpage *pmp_stack; /* Stack-specific fpages */
uint32_t shared; /* Shared address space flag */
} as_t;Memory Pool Structure:
typedef struct {
const char *name; /* Pool name */
uintptr_t start; /* Start address */
uintptr_t end; /* End address */
uint32_t flags; /* Access permissions */
uint32_t tag; /* Pool type tag */
} mempool_t;TCB Extension:
typedef struct tcb {
/* ... existing fields ... */
as_t *as; /* Address space pointer */
} tcb_t;3. TOR Addressing Mode
Use TOR (Top Of Range) mode exclusively for PMP configuration:
/* TOR mode configuration */
#define PMP_A_TOR 0x08 /* Address mode: TOR */
#define PMP_R 0x01 /* Read permission */
#define PMP_W 0x02 /* Write permission */
#define PMP_X 0x04 /* Execute permission */
#define PMP_L 0x80 /* Lock region */
/* Configure a TOR region (uses two pmpaddr registers) */
void pmp_set_tor_region(uint8_t idx, uintptr_t base, uintptr_t top, uint8_t rwx)
{
pmpaddr_write[idx - 1](base >> 2);
pmpaddr_write[idx](top >> 2);
uint8_t cfg = (rwx & 0x7) | PMP_A_TOR;
pmpcfg_set_region(idx, cfg);
}TOR mode advantages:
- No alignment restrictions on base address or size
- Single flex page can cover any memory region
- Simpler kernel code for region management
Trade-off: Uses 2 pmpaddr registers per region (vs 1 for NAPOT), limiting to approximately 8 task regions after kernel regions.
4. Memory Pool Declaration
Define static memory regions using linker symbols:
/* Linker script symbols */
extern uint32_t _text_start, _text_end;
extern uint32_t _data_start, _data_end;
extern uint32_t _rodata_start, _rodata_end;
/* Memory pool definitions */
static mempool_t memmap[] = {
{
.name = "KTEXT",
.start = (uintptr_t)&_text_start,
.end = (uintptr_t)&_text_end,
.flags = MP_KR | MP_KX | MP_NO_FPAGE,
.tag = MPT_KERNEL_TEXT
},
{
.name = "KDATA",
.start = (uintptr_t)&_data_start,
.end = (uintptr_t)&_data_end,
.flags = MP_KR | MP_KW | MP_NO_FPAGE,
.tag = MPT_KERNEL_DATA
},
{
.name = "KRODATA",
.start = (uintptr_t)&_rodata_start,
.end = (uintptr_t)&_rodata_end,
.flags = MP_KR | MP_NO_FPAGE,
.tag = MPT_KERNEL_RODATA
}
};5. PMP Region Management
With 16 PMP regions and TOR mode requiring 2 registers per region, implement priority-based management:
Priority Levels:
#define PMP_PRIO_KERNEL 0 /* Highest - never evict */
#define PMP_PRIO_STACK 1 /* High - current task stack */
#define PMP_PRIO_SHARED 2 /* Medium - shared regions */
#define PMP_PRIO_TEMP 3 /* Lowest - evictable */Region Selection:
fpage_t* select_victim_fpage(void)
{
fpage_t *victim = NULL;
uint32_t lowest_prio = 0;
for (int i = 0; i < num_loaded_fpages; i++) {
if (loaded_fpages[i].priority > lowest_prio &&
loaded_fpages[i].flags & FPAGE_EVICTABLE) {
victim = &loaded_fpages[i];
lowest_prio = loaded_fpages[i].priority;
}
}
return victim;
}6. Address Space Switching
During context switch, configure PMP for the new task's address space:
void pmp_switch_context(as_t *next_as)
{
if (!next_as)
return;
/* Prioritize stack fpages */
fpage_t *fpage = next_as->pmp_stack;
while (fpage) {
pmp_load_fpage(fpage);
fpage = fpage->pmp_next;
}
/* Load remaining fpages */
fpage = next_as->pmp_first;
while (fpage) {
if (pmp_regions_available()) {
pmp_load_fpage(fpage);
} else {
fpage_t *victim = select_victim_fpage();
if (victim) {
pmp_evict_fpage(victim);
pmp_load_fpage(fpage);
}
}
fpage = fpage->pmp_next;
}
}Integrate into dispatcher:
void dispatch(void)
{
/* ... save context ... */
/* Switch PMP configuration */
tcb_t *next_task = (tcb_t *)kcb->task_current->data;
pmp_switch_context(next_task->as);
/* ... restore context ... */
}7. PMP Fault Handling
Handle PMP violations by checking mcause values:
void do_trap(uint32_t cause, uint32_t epc)
{
if (MCAUSE_IS_INTERRUPT(cause)) {
/* ... interrupt handling ... */
} else {
uint32_t code = MCAUSE_GET_CODE(cause);
/* Check for PMP-related exceptions */
if (code == 1 || code == 5 || code == 7) {
/* Instruction/Load/Store access fault */
handle_pmp_fault(code, epc, read_csr(mtval));
} else {
/* Other exceptions */
/* ... */
}
}
}
void handle_pmp_fault(uint32_t code, uint32_t epc, uint32_t mtval)
{
tcb_t *current = (tcb_t *)kcb->task_current->data;
printf("\n====================================\n");
printf("PMP VIOLATION\n");
printf("====================================\n");
printf("Task ID: %u\n", current->id);
printf("Exception: %s\n",
code == 1 ? "Instruction access fault" :
code == 5 ? "Load access fault" :
"Store/AMO access fault");
printf("PC: 0x%08x\n", epc);
printf("Address: 0x%08x\n", mtval);
printf("====================================\n");
/* Terminate task */
/* TODO: Implement task termination */
hal_panic();
}8. PMP CSR Access
Provide functions for dynamic PMP register access:
/* Function pointer arrays for pmpaddr registers */
static void (*pmpaddr_write[])(uint32_t) = {
write_pmpaddr0, write_pmpaddr1, write_pmpaddr2, write_pmpaddr3,
write_pmpaddr4, write_pmpaddr5, write_pmpaddr6, write_pmpaddr7,
write_pmpaddr8, write_pmpaddr9, write_pmpaddr10, write_pmpaddr11,
write_pmpaddr12, write_pmpaddr13, write_pmpaddr14, write_pmpaddr15
};
static uint32_t (*pmpaddr_read[])() = {
read_pmpaddr0, read_pmpaddr1, read_pmpaddr2, read_pmpaddr3,
read_pmpaddr4, read_pmpaddr5, read_pmpaddr6, read_pmpaddr7,
read_pmpaddr8, read_pmpaddr9, read_pmpaddr10, read_pmpaddr11,
read_pmpaddr12, read_pmpaddr13, read_pmpaddr14, read_pmpaddr15
};
/* CSR access macros */
#define write_pmpaddrN(N) \
static inline void write_pmpaddr##N(uint32_t val) { \
write_csr(pmpaddr##N, val); \
}
#define read_pmpaddrN(N) \
static inline uint32_t read_pmpaddr##N(void) { \
return read_csr(pmpaddr##N); \
}
/* Generate all 16 functions */
write_pmpaddrN(0) read_pmpaddrN(0)
write_pmpaddrN(1) read_pmpaddrN(1)
/* ... repeat for 2-15 ... */9. Configuration Options
Add PMP configuration to config.h:
/* Physical Memory Protection (PMP) Configuration */
#ifndef CONFIG_PMP
#define CONFIG_PMP 1
#endif
#ifndef CONFIG_PMP_REGIONS
#define CONFIG_PMP_REGIONS 16
#endif
#ifndef CONFIG_PMP_PRIORITY_MGMT
#define CONFIG_PMP_PRIORITY_MGMT 1
#endifImplementation Plan
File Organization
Create separate PMP implementation files:
arch/riscv/pmp.c- PMP implementationarch/riscv/pmp.h- PMP interface
Implementation Phases
Phase 1: Basic Infrastructure
- Define fpage, address space, and memory pool structures
- Implement PMP CSR access functions
- Create memory pool declarations from linker symbols
- Add address space pointer to TCB
Phase 2: Region Management
- Implement flex page allocation/deallocation
- Implement priority-based region selection
- Implement PMP region loading/eviction
- Create address space initialization functions
Phase 3: Context Switching
- Integrate PMP switching into dispatcher
- Implement pmp_switch_context function
- Test basic task isolation
Phase 4: Fault Handling
- Implement PMP fault detection in do_trap
- Add fault handler with diagnostic output
- Add task termination for non-recoverable faults
Phase 5: Testing
- Create test cases for memory violations
- Test kernel protection
- Test inter-task isolation
- Measure performance overhead
Technical Considerations
Memory Overhead:
- Flex page structures: ~40 bytes each
- Address space structures: ~24 bytes each
- Memory pool array: ~32 bytes per pool
- Minimal runtime overhead
Performance Impact:
- PMP configuration during context switch: ~50-100 cycles
- Priority-based selection: O(n) where n = loaded fpages
- Acceptable overhead for improved security
Limitations:
- Maximum ~8 concurrent tasks with unique stacks (TOR uses 2 regions each)
- Priority-based eviction may evict important regions under pressure
- No support for dynamic region resizing
Linker Script Updates:
The linker script must export memory region symbols:
_text_start = .;
/* text sections */
_text_end = .;
_rodata_start = .;
/* rodata sections */
_rodata_end = .;
_data_start = .;
/* data sections */
_data_end = .;Testing Strategy
Create test applications in app/ to verify PMP functionality:
Test 1: Kernel Protection
- Attempt to write to kernel code/data regions
- Verify PMP fault is triggered
Test 2: Stack Isolation
- Task attempts to access another task's stack
- Verify PMP fault prevents access
Test 3: Valid Operations
- Normal task operations (stack usage, heap allocation)
- Verify no false positives
Test 4: Context Switch
- Multiple tasks with different PMP configurations
- Verify correct PMP switching during context switches
Acceptance Criteria
- Kernel code and data are protected from task access
- Each task's stack is isolated from other tasks
- PMP violations are detected and reported with diagnostic information
- Context switches correctly configure PMP for the new task
- Priority-based region management works correctly
- No performance degradation during normal operation
- Test cases pass successfully