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