From 952dcd4236e204df0194697ebfb3cb00247a1baf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20Desbiens?= Date: Mon, 17 Aug 2026 17:52:14 -0400 Subject: [PATCH] Gave each thread its own MPU window, and made a violation fault First step towards a ThreadX module port for this core: establish that PMSAv8-R regions can be switched per thread on this part, what that costs, and that a violation actually faults. Those are the questions worth answering before writing a module manager on top of them. Two threads each own a 4 KB window at the top of DRAM2. The windows are carved out of the broad data region in mpu.c, because isolation is only meaningful in memory no other region already covers -- every other region in that map is a wide RW window, so a private buffer inside one of them would be reachable by every thread whatever else was programmed. Each thread writes its own window, which must succeed, and then reaches for the other thread's, which must fault. The second half is the part that matters: a test that only shows a thread reaching its own memory would pass just as well with no protection at all. Measured on the S32Z280-594EVB, reproducible across three runs: thread 0 window 0x3187E000 own: reachable other: faulted thread 1 window 0x3187F000 own: reachable other: faulted region switch cost: 562 to 604 cycles The cost is worth noting for the module port to come. A context switch on this part is about 1400 cycles, so switching one region adds roughly 40% to it, and most of that is the dsb and isb rather than the register writes. A module switch programming several regions should therefore batch the barriers once at the end rather than per region. Scope, stated plainly. The window is applied by the thread calling thread_mpu_activate, not by the scheduler. The port's scheduler does call _tx_execution_thread_enter under TX_ENABLE_EXECUTION_CHANGE_NOTIFY, which would make it automatic, but that macro is read by port assembly compiled into the shared threadx library, so enabling it would oblige all nine example targets in this port to supply the four execution hooks. A ThreadX module port carries its own copies of the port assembly for exactly that reason, and that is where the switch belongs. There is no user mode, no syscall boundary and no loader here. The fault is survivable the same way the boot probes make it survivable: fault_expected tells the data abort handler to record the violation and resume after the faulting access. That works in thread context because the handler returns where it came from rather than to a fixed recovery point. Assisted-by: Claude Code (Opus 5) --- .../example_build/s32z280_evb/CMakeLists.txt | 1 + .../example_build/s32z280_evb/demo_s32z280.c | 124 +++++++++++ .../gnu/example_build/s32z280_evb/mpu.c | 7 +- .../gnu/example_build/s32z280_evb/platform.h | 12 ++ .../example_build/s32z280_evb/thread_mpu.c | 197 ++++++++++++++++++ .../example_build/s32z280_evb/thread_mpu.h | 79 +++++++ 6 files changed, 419 insertions(+), 1 deletion(-) create mode 100644 ports/cortex_r52/gnu/example_build/s32z280_evb/thread_mpu.c create mode 100644 ports/cortex_r52/gnu/example_build/s32z280_evb/thread_mpu.h diff --git a/ports/cortex_r52/gnu/example_build/s32z280_evb/CMakeLists.txt b/ports/cortex_r52/gnu/example_build/s32z280_evb/CMakeLists.txt index 94fe43301..7f216700c 100644 --- a/ports/cortex_r52/gnu/example_build/s32z280_evb/CMakeLists.txt +++ b/ports/cortex_r52/gnu/example_build/s32z280_evb/CMakeLists.txt @@ -106,6 +106,7 @@ add_executable(s32z280_demo.elf EXCLUDE_FROM_ALL ${EVB_DIR}/gic_probe.c ${EVB_DIR}/cache.c ${EVB_DIR}/tcm.c + ${EVB_DIR}/thread_mpu.c ${EVB_DIR}/demo_s32z280.c ) diff --git a/ports/cortex_r52/gnu/example_build/s32z280_evb/demo_s32z280.c b/ports/cortex_r52/gnu/example_build/s32z280_evb/demo_s32z280.c index 474786e32..11637ad92 100644 --- a/ports/cortex_r52/gnu/example_build/s32z280_evb/demo_s32z280.c +++ b/ports/cortex_r52/gnu/example_build/s32z280_evb/demo_s32z280.c @@ -51,6 +51,7 @@ #include "linflexd.h" #include "timer.h" #include "cache.h" +#include "thread_mpu.h" #include "board.h" #define DEMO_STACK_SIZE 2048 @@ -388,6 +389,73 @@ static void deep_entry(ULONG which) deep_done[which] = 1U; } +/**************************************************************************/ +/* Per-thread memory protection, demonstrated rather than asserted. */ +/* */ +/* Two threads each own a 4 KB window at the top of DRAM2, carved out of */ +/* the broad data region in mpu.c so that nothing else grants access to */ +/* them. Each thread writes its own window, which must succeed, and then */ +/* reaches for the other thread's, which must fault. */ +/* */ +/* The second half is the part that matters. A test that only shows a */ +/* thread reaching its own memory proves nothing about isolation: it */ +/* would pass just as well with no protection at all. */ +/* */ +/* The fault is made survivable the same way the boot probes do it -- */ +/* fault_expected tells the data abort handler to record the violation */ +/* and resume at the instruction after the faulting access, rather than */ +/* treating it as fatal. That works in thread context because the */ +/* handler returns to where it came from rather than to a fixed recovery */ +/* point. */ +/**************************************************************************/ + +#define ISO_THREADS 2U + +static TX_THREAD iso_thread[ISO_THREADS]; + +static unsigned char iso_stack[ISO_THREADS][DEMO_STACK_SIZE]; + +static volatile unsigned int iso_done[ISO_THREADS]; +static unsigned int iso_own_ok[ISO_THREADS]; +static unsigned int iso_other_faulted[ISO_THREADS]; +static unsigned long iso_own_base[ISO_THREADS]; + +static void iso_entry(ULONG which) +{ + extern unsigned int fault_expected; + extern unsigned int fault_taken; + + unsigned long own = thread_mpu_window_base((unsigned int) which); + unsigned long other = thread_mpu_window_base((unsigned int) (1UL - which)); + unsigned int before; + unsigned int pattern = 0xC0DE0000U + (unsigned int) which; + + iso_own_base[which] = own; + + /* Install this thread's window. */ + + thread_mpu_activate(tx_thread_identify()); + + /* Its own window: must be reachable. */ + + *((volatile unsigned int *) own) = pattern; + iso_own_ok[which] = + (*((volatile unsigned int *) own) == pattern) ? 1U : 0U; + + /* The other thread's window: must not be. Only the write is attempted -- + reading it back would fault a second time, and the point is already + made. */ + + before = fault_taken; + fault_expected = 1U; + *((volatile unsigned int *) other) = 0xBADU; + fault_expected = 0U; + + iso_other_faulted[which] = (fault_taken > before) ? 1U : 0U; + + iso_done[which] = 1U; +} + static void sleeper_entry(ULONG input) { unsigned long spinner_before; @@ -443,6 +511,46 @@ static void judge_entry(ULONG input) linflexd_puts("\n=== ThreadX on S32Z280: results ===\n"); + linflexd_puts("per-thread MPU isolation\n"); + { + unsigned int w; + unsigned int all_ok = 1U; + + for (w = 0U; w < ISO_THREADS; w++) + { + linflexd_puts(" thread "); + demo_dec(w); + linflexd_puts(" window 0x"); + linflexd_put_hex32((unsigned int) iso_own_base[w]); + if (iso_done[w] == 0U) + { + linflexd_puts(" did not finish\n"); + all_ok = 0U; + } + else + { + linflexd_puts(iso_own_ok[w] ? " own: reachable" : " own: UNREACHABLE"); + linflexd_puts(iso_other_faulted[w] ? " other: faulted\n" + : " other: REACHED\n"); + if ((iso_own_ok[w] == 0U) || (iso_other_faulted[w] == 0U)) + { + all_ok = 0U; + } + } + } + + linflexd_puts(all_ok ? " PASS each thread reached its own window and faulted on the other\n" + : " FAIL isolation not demonstrated\n"); + + linflexd_puts(" region switch cost, cycles: min "); + demo_dec(thread_mpu_min_cycles()); + linflexd_puts(" max "); + demo_dec(thread_mpu_max_cycles()); + linflexd_puts(" switches "); + demo_dec(thread_mpu_switch_count()); + linflexd_puts("\n"); + } + linflexd_puts("stack-heavy work, cycles (24 frames, cold cache)\n"); { static const char *const dwhere[CTX_PAIRS] = { "stack in BTCM ", @@ -592,6 +700,22 @@ void tx_application_define(void *first_unused_memory) ctx_dram_a_stack, DEMO_STACK_SIZE, 3U, 3U, TX_NO_TIME_SLICE, TX_AUTO_START); + /* Isolation pair. Priority 6 and 7, after the measurement threads. */ + + { + unsigned int w; + + for (w = 0U; w < ISO_THREADS; w++) + { + (void) tx_thread_create(&iso_thread[w], "iso", iso_entry, + (ULONG) w, + iso_stack[w], DEMO_STACK_SIZE, + 6U + w, 6U + w, + TX_NO_TIME_SLICE, TX_AUTO_START); + (void) thread_mpu_grant(&iso_thread[w], w); + } + } + /* Stack-heavy pair, after the context-switch pairs. */ (void) tx_thread_create(&deep_btcm_thread, "deep btcm", deep_entry, 0UL, diff --git a/ports/cortex_r52/gnu/example_build/s32z280_evb/mpu.c b/ports/cortex_r52/gnu/example_build/s32z280_evb/mpu.c index 0af4ee94d..2073d0db5 100644 --- a/ports/cortex_r52/gnu/example_build/s32z280_evb/mpu.c +++ b/ports/cortex_r52/gnu/example_build/s32z280_evb/mpu.c @@ -353,8 +353,13 @@ static void build_table(void) bottom of the region -- no fault, just two objects on one address. */ mpu_regions[5].mpu_region_base = S32Z_DRAM2_BASE; + /* Stops short of the per-thread windows at the top of the bank. Those are + mapped one at a time by the thread-entry hook; leaving them inside this + region would make them reachable from every thread and the isolation + below would prove nothing. */ + mpu_regions[5].mpu_region_limit = S32Z_DRAM2_BASE - + S32Z_DRAM2_SIZE - 1UL; + + S32Z_DRAM2_SHARED_SIZE - 1UL; mpu_regions[5].mpu_region_ap = MPU_AP_RW_EL1; mpu_regions[5].mpu_region_execute_never = 1U; mpu_regions[5].mpu_region_shareability = MPU_SH_NON; diff --git a/ports/cortex_r52/gnu/example_build/s32z280_evb/platform.h b/ports/cortex_r52/gnu/example_build/s32z280_evb/platform.h index 37f2e9e4a..4adbe55ec 100644 --- a/ports/cortex_r52/gnu/example_build/s32z280_evb/platform.h +++ b/ports/cortex_r52/gnu/example_build/s32z280_evb/platform.h @@ -130,6 +130,18 @@ #define S32Z_DRAM0_SIZE 0x00040000UL #define S32Z_DRAM1_BASE 0x317C0000UL #define S32Z_DRAM1_SIZE 0x00040000UL +/* The top 8 KB of DRAM2 is deliberately left out of the broad data region in + mpu.c and mapped one window at a time, per thread, instead. Isolation is only + meaningful in memory that no other region already grants access to, and every + other region in that map is a wide RW window -- a private buffer placed inside + one of them would be reachable by every thread no matter what else was + programmed. */ + +#define S32Z_THREAD_WINDOW_BASE 0x3187E000UL +#define S32Z_THREAD_WINDOW_SIZE 0x00001000UL /* 4 KB per thread */ +#define S32Z_THREAD_WINDOW_COUNT 2UL +#define S32Z_DRAM2_SHARED_SIZE (S32Z_THREAD_WINDOW_BASE - S32Z_DRAM2_BASE) + #define S32Z_DRAM2_BASE 0x31800000UL #define S32Z_DRAM2_SIZE 0x00080000UL diff --git a/ports/cortex_r52/gnu/example_build/s32z280_evb/thread_mpu.c b/ports/cortex_r52/gnu/example_build/s32z280_evb/thread_mpu.c new file mode 100644 index 000000000..9042aeb98 --- /dev/null +++ b/ports/cortex_r52/gnu/example_build/s32z280_evb/thread_mpu.c @@ -0,0 +1,197 @@ +/*************************************************************************** + * Copyright (c) 2026 Eclipse ThreadX contributors + * + * This program and the accompanying materials are made available under the + * terms of the MIT License which is available at + * https://opensource.org/licenses/MIT. + * + * AI Disclosure: This file was largely AI-generated by Claude Code (Opus 5). + * The AI-generated portions may be considered public domain (CC0-1.0) + * and not subject to the project's licence. The human contributor has + * reviewed and verified that the code is correct. + * + * SPDX-License-Identifier: MIT and CC0-1.0 + **************************************************************************/ + +/**************************************************************************/ +/* */ +/* BOARD SUPPORT RELEASE */ +/* */ +/* thread_mpu.c Cortex-R52/GNU */ +/* 6.5.2 */ +/* AUTHOR */ +/* */ +/* Frédéric Desbiens, Eclipse Foundation */ +/* */ +/* DESCRIPTION */ +/* */ +/* Per-thread memory protection. See thread_mpu.h for what this is */ +/* and is not. */ +/* */ +/* MISRA C:2012 deviations (justified) */ +/* */ +/* Directive 4.3 -- the MPU is only reachable through CP15; every */ +/* access is encapsulated in a one-line accessor below. */ +/* */ +/**************************************************************************/ + +#include "tx_api.h" +#include "platform.h" +#include "mpu.h" +#include "timer.h" +#include "thread_mpu.h" + +/* The region reprogrammed on every thread entry. mpu.c programs 0 to 7, and + MPUIR on this part reports 20, so 8 is free. Chosen high on purpose: the + shared regions keep their indices, so a thread that reaches this hook with no + window still has code, stacks, console and GIC exactly as before. */ + +#define THREAD_MPU_REGION 8UL + +typedef struct +{ + TX_THREAD *owner; + unsigned long base; +} thread_window_t; + +static thread_window_t windows[S32Z_THREAD_WINDOW_COUNT]; + +static unsigned long switch_count; +static unsigned int last_cycles; +static unsigned int min_cycles = 0xFFFFFFFFU; +static unsigned int max_cycles; + + +/**************************************************************************/ +/* CP15 accessors. */ +/**************************************************************************/ + +static void write_prselr(unsigned long value) +{ + __asm__ volatile("mcr p15, 0, %0, c6, c2, 1" : : "r"(value) : "memory"); +} + +static void write_prbar(unsigned long value) +{ + __asm__ volatile("mcr p15, 0, %0, c6, c3, 0" : : "r"(value) : "memory"); +} + +static void write_prlar(unsigned long value) +{ + __asm__ volatile("mcr p15, 0, %0, c6, c3, 1" : : "r"(value) : "memory"); +} + + +/**************************************************************************/ +/* Registration. */ +/**************************************************************************/ + +unsigned long thread_mpu_window_base(unsigned int window) +{ + if ((unsigned long) window >= S32Z_THREAD_WINDOW_COUNT) + { + return 0UL; + } + + return S32Z_THREAD_WINDOW_BASE + + ((unsigned long) window * S32Z_THREAD_WINDOW_SIZE); +} + +unsigned long thread_mpu_grant(TX_THREAD *thread_ptr, unsigned int window) +{ + unsigned long base = thread_mpu_window_base(window); + + if ((base == 0UL) || (thread_ptr == TX_NULL)) + { + return 0UL; + } + + windows[window].owner = thread_ptr; + windows[window].base = base; + + return base; +} + + +/**************************************************************************/ +/* Activation. */ +/* */ +/* Called explicitly by a thread rather than from the scheduler, and that */ +/* is a deliberate limitation of this step rather than the intended */ +/* design. */ +/* */ +/* The port's scheduler does call _tx_execution_thread_enter under */ +/* TX_ENABLE_EXECUTION_CHANGE_NOTIFY, which would make this automatic on */ +/* every switch. That macro is read by the port assembly compiled into */ +/* the shared threadx library, so enabling it would oblige all nine */ +/* example targets in this port -- including the FVP ones -- to supply */ +/* the four execution hooks. A ThreadX module port carries its own */ +/* copies of the port assembly for exactly this reason, and that is where */ +/* the switch belongs. */ +/* */ +/* What this step establishes without that machinery: that a PMSAv8-R */ +/* region can be reprogrammed per thread on this part, what it costs, and */ +/* that a violation faults. Those are the questions worth answering */ +/* before writing a module manager on top of them. */ +/**************************************************************************/ + +void thread_mpu_activate(TX_THREAD *thread_ptr) +{ + unsigned int before; + unsigned int after; + unsigned int delta; + unsigned int i; + unsigned long base = 0UL; + + before = timer_read_cycles(); + + for (i = 0U; (unsigned long) i < S32Z_THREAD_WINDOW_COUNT; i++) + { + if (windows[i].owner == thread_ptr) + { + base = windows[i].base; + break; + } + } + + write_prselr(THREAD_MPU_REGION); + __asm__ volatile("isb" ::: "memory"); + + if (base == 0UL) + { + /* No window for this thread: disable the region rather than leaving the + previous thread's window in place. Forgetting this is how per-thread + protection silently becomes no protection -- the last thread to run + would leave its window open to whatever ran next. */ + + write_prlar(0UL); + } + else + { + write_prbar((base & ~0x3FUL) + | ((unsigned long) MPU_SH_NON << 3) + | ((unsigned long) MPU_AP_RW_EL1 << 1) + | 1UL); /* XN: data only */ + + write_prlar(((base + S32Z_THREAD_WINDOW_SIZE - 1UL) & ~0x3FUL) + | ((unsigned long) MPU_ATTR_NORMAL_WB << 1) + | 1UL); /* EN */ + } + + __asm__ volatile("dsb sy" ::: "memory"); + __asm__ volatile("isb" ::: "memory"); + + after = timer_read_cycles(); + delta = after - before; + + switch_count++; + last_cycles = delta; + if (delta < min_cycles) { min_cycles = delta; } + if (delta > max_cycles) { max_cycles = delta; } +} + + +unsigned long thread_mpu_switch_count(void) { return switch_count; } +unsigned int thread_mpu_last_cycles(void) { return last_cycles; } +unsigned int thread_mpu_min_cycles(void) { return min_cycles; } +unsigned int thread_mpu_max_cycles(void) { return max_cycles; } diff --git a/ports/cortex_r52/gnu/example_build/s32z280_evb/thread_mpu.h b/ports/cortex_r52/gnu/example_build/s32z280_evb/thread_mpu.h new file mode 100644 index 000000000..cba6226e5 --- /dev/null +++ b/ports/cortex_r52/gnu/example_build/s32z280_evb/thread_mpu.h @@ -0,0 +1,79 @@ +/*************************************************************************** + * Copyright (c) 2026 Eclipse ThreadX contributors + * + * This program and the accompanying materials are made available under the + * terms of the MIT License which is available at + * https://opensource.org/licenses/MIT. + * + * AI Disclosure: This file was largely AI-generated by Claude Code (Opus 5). + * The AI-generated portions may be considered public domain (CC0-1.0) + * and not subject to the project's licence. The human contributor has + * reviewed and verified that the code is correct. + * + * SPDX-License-Identifier: MIT and CC0-1.0 + **************************************************************************/ + +/**************************************************************************/ +/* */ +/* BOARD SUPPORT RELEASE */ +/* */ +/* thread_mpu.h Cortex-R52/GNU */ +/* 6.5.2 */ +/* AUTHOR */ +/* */ +/* Frédéric Desbiens, Eclipse Foundation */ +/* */ +/* DESCRIPTION */ +/* */ +/* Per-thread memory protection for the NXP S32Z280-594EVB. */ +/* */ +/* One MPU region is reprogrammed on every thread entry to grant the */ +/* incoming thread access to its own window and nothing else. A thread */ +/* with no window registered runs with that region disabled, so it */ +/* reaches none of them. */ +/* */ +/* This is a step towards a ThreadX module port for this core, not a */ +/* substitute for one. It demonstrates that PMSAv8-R regions can be */ +/* switched per thread on this part, at a measured cost, and that a */ +/* violation faults -- which are the questions worth answering before */ +/* building a module manager on top of them. There is no user mode, no */ +/* syscall boundary and no loader here. */ +/* */ +/* The hook is _tx_execution_thread_enter, which the port's scheduler */ +/* already calls under TX_ENABLE_EXECUTION_CHANGE_NOTIFY, after the */ +/* stack pointer has been switched. No port assembly is modified. */ +/* */ +/**************************************************************************/ + +#ifndef THREAD_MPU_H +#define THREAD_MPU_H + +#include "tx_api.h" + +/* Apply a thread's window, or disable the region if it has none. Called by the + thread itself in this step; see the comment in thread_mpu.c for why it is not + yet driven from the scheduler. */ + +void thread_mpu_activate(TX_THREAD *thread_ptr); + +/* Give a thread exclusive access to one of the per-thread windows. The index + selects the window, 0 to S32Z_THREAD_WINDOW_COUNT - 1. Returns the window's + base address, or 0 if the index is out of range or the table is full. */ + +unsigned long thread_mpu_grant(TX_THREAD *thread_ptr, unsigned int window); + +/* Base address of a window, whether or not it is granted to anyone. Lets a + test reach for a window it does not own. */ + +unsigned long thread_mpu_window_base(unsigned int window); + +/* How many thread entries have reprogrammed the region, and what the last one + cost in cycles. The cost is the price of per-thread protection on this core + and is the reason the hook records it. */ + +unsigned long thread_mpu_switch_count(void); +unsigned int thread_mpu_last_cycles(void); +unsigned int thread_mpu_min_cycles(void); +unsigned int thread_mpu_max_cycles(void); + +#endif