From 7811ef1ebb1b65f1be9ce83e0ee53d1786137ad0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20Desbiens?= Date: Tue, 18 Aug 2026 09:02:54 -0400 Subject: [PATCH 01/33] Started a ThreadX module port for the Cortex-R52, headers first No GNU module port exists for any R-profile core, so there is no close template to copy. This adds the two headers a module port needs and records the design decisions the rest of it will rest on. Seeded from cortex_m33 rather than cortex_r4, which is the important choice here. The R4 port is the other R-profile one, but it is PMSAv7: power-of-two region sizes, and 541 lines of register setup mostly spent juggling them plus an alignment adjustment pass to match. PMSAv8-R uses base and limit pairs with a 64-byte granule, which is the Armv8-M model, so the M33 port's structure transfers and that whole class of complexity does not arise. The attribute encodings are bit-identical: shareability in PRBAR[4:3], access permission in PRBAR[2:1], execute-never in PRBAR[0], attribute index in PRLAR[3:1], region enable in PRLAR[0]. What differs from the M33, and is handled: Region budget. MPUIR on the S32Z280 reports 20 EL1 regions where an M33 has 8, and that changes the design rather than the numbers. On an M33 the kernel and a module compete for the same eight; here the kernel keeps its own and the manager owns a separate block from TXM_MODULE_MPU_FIRST_REGION upward, so a module switch reprograms only the module's regions and never rebuilds the kernel's. Granule. 64 bytes, not 32. This one is silent when wrong: the low bits of PRBAR and PRLAR hold attributes, so an under-aligned base does not fault, it changes the region's shareability and permissions instead. Fault reporting. Armv8-M reports a memory fault through SHCSR, CFSR, MMFAR and BFAR. This core splits it by access type, so the fault info structure carries DFSR and DFAR for data aborts and IFSR and IFAR for prefetch aborts. Both pairs are needed: a module can fault either by writing outside its data region or by branching outside its code region. SPSR replaces xPSR, because the mode the faulting code was in is what says whether it was the module in user mode or the kernel. No secure-stack extension. Those calls are the Armv8-M security extension, which this core does not have; privilege here is EL1 against EL0 and there is no secure world to allocate a second stack in. tx_thread_vfp_enable is kept at the front of TX_THREAD_EXTENSION_2, ahead of the module fields. The base port holds the floating-point lazy-enable flag there, and dropping it while adding the module fields would leave VFP threads with nowhere to record that they have used the unit -- which would surface as corrupted floating-point state inside a module, a long way from its cause. Also recorded, from the measurement in #637: a region switch on this part costs 562 to 604 cycles against a context switch of about 1400, and most of that is the dsb and isb rather than the register writes. A module switch programming several regions should therefore issue one barrier pair at the end rather than one per region, and the header says so where the register info structure is defined. Still to come: the module manager sources. Its own copies of the port assembly so the region switch happens in the scheduler, the PMSAv8-R region setup, a memory fault handler, user-mode entry, the port dispatch, and a sample module with its build. Assisted-by: Claude Code (Opus 5) --- ports_module/cortex_r52/gnu/inc/tx_port.h | 368 +++++++++++++++++ .../cortex_r52/gnu/inc/txm_module_port.h | 388 ++++++++++++++++++ 2 files changed, 756 insertions(+) create mode 100644 ports_module/cortex_r52/gnu/inc/tx_port.h create mode 100644 ports_module/cortex_r52/gnu/inc/txm_module_port.h diff --git a/ports_module/cortex_r52/gnu/inc/tx_port.h b/ports_module/cortex_r52/gnu/inc/tx_port.h new file mode 100644 index 000000000..bac4428cc --- /dev/null +++ b/ports_module/cortex_r52/gnu/inc/tx_port.h @@ -0,0 +1,368 @@ +/*************************************************************************** + * Copyright (c) 2024 Microsoft Corporation + * 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. + * + * SPDX-License-Identifier: MIT + **************************************************************************/ +// Some portions generated by Claude Code (Opus 5). + + +/**************************************************************************/ +/**************************************************************************/ +/** */ +/** ThreadX Component */ +/** */ +/** Port Specific */ +/** */ +/**************************************************************************/ +/**************************************************************************/ + + +/**************************************************************************/ +/* */ +/* PORT SPECIFIC C INFORMATION RELEASE */ +/* */ +/* tx_port.h Cortex-R52/GNU */ +/* 6.5.2 */ +/* */ +/* AUTHOR */ +/* */ +/* Frédéric Desbiens, Eclipse Foundation */ +/* */ +/* Derived from the Cortex-R5/GNU port originally written by */ +/* William E. Lamie, Microsoft Corporation. */ +/* */ +/* DESCRIPTION */ +/* */ +/* This file contains data type definitions that make the ThreadX */ +/* real-time kernel function identically on a variety of different */ +/* processor architectures. For example, the size or number of bits */ +/* in an "int" data type vary between microprocessor architectures and */ +/* even C compilers for the same microprocessor. ThreadX does not */ +/* directly use native C data types. Instead, ThreadX creates its */ +/* own special types that can be mapped to actual data types by this */ +/* file to guarantee consistency in the interface and functionality. */ +/* */ +/**************************************************************************/ + +#ifndef TX_PORT_H +#define TX_PORT_H + + +/* Determine if the optional ThreadX user define file should be used. */ + +#ifdef TX_INCLUDE_USER_DEFINE_FILE + + +/* Yes, include the user defines in tx_user.h. The defines in this file may + alternately be defined on the command line. */ + +#include "tx_user.h" +#endif + + +/* Define compiler library include files. */ + +#include +#include + + +/* Define ThreadX basic types for this port. */ + +#define VOID void +typedef char CHAR; +typedef unsigned char UCHAR; +typedef int INT; +typedef unsigned int UINT; +typedef long LONG; +typedef unsigned long ULONG; +typedef short SHORT; +typedef unsigned short USHORT; + + +/* Define the priority levels for ThreadX. Legal values range + from 32 to 1024 and MUST be evenly divisible by 32. */ + +#ifndef TX_MAX_PRIORITIES +#define TX_MAX_PRIORITIES 32 +#endif + + +/* Define the minimum stack for a ThreadX thread on this processor. If the size supplied during + thread creation is less than this value, the thread create call will return an error. */ + +#ifndef TX_MINIMUM_STACK +#define TX_MINIMUM_STACK 200 /* Minimum stack size for this port */ +#endif + + +/* Define the system timer thread's default stack size and priority. These are only applicable + if TX_TIMER_PROCESS_IN_ISR is not defined. */ + +#ifndef TX_TIMER_THREAD_STACK_SIZE +#define TX_TIMER_THREAD_STACK_SIZE 1024 /* Default timer thread stack size */ +#endif + +#ifndef TX_TIMER_THREAD_PRIORITY +#define TX_TIMER_THREAD_PRIORITY 0 /* Default timer thread priority */ +#endif + + +/* Define various constants for the ThreadX ARM port. */ + +#ifdef TX_ENABLE_FIQ_SUPPORT +#define TX_INT_DISABLE 0xC0 /* Disable IRQ & FIQ interrupts */ +#else +#define TX_INT_DISABLE 0x80 /* Disable IRQ interrupts */ +#endif +#define TX_INT_ENABLE 0x00 /* Enable IRQ interrupts */ + + +/* Define the clock source for trace event entry time stamp. The following two item are port specific. + For example, if the time source is at the address 0x0a800024 and is 16-bits in size, the clock + source constants would be: + +#define TX_TRACE_TIME_SOURCE *((ULONG *) 0x0a800024) +#define TX_TRACE_TIME_MASK 0x0000FFFFUL + +*/ + +#ifndef TX_TRACE_TIME_SOURCE +#define TX_TRACE_TIME_SOURCE ++_tx_trace_simulated_time +#endif +#ifndef TX_TRACE_TIME_MASK +#define TX_TRACE_TIME_MASK 0xFFFFFFFFUL +#endif + + +/* Define the port specific options for the _tx_build_options variable. This variable indicates + how the ThreadX library was built. */ + +#ifdef TX_ENABLE_FIQ_SUPPORT +#define TX_FIQ_ENABLED 1 +#else +#define TX_FIQ_ENABLED 0 +#endif + +#ifdef TX_ENABLE_IRQ_NESTING +#define TX_IRQ_NESTING_ENABLED 2 +#else +#define TX_IRQ_NESTING_ENABLED 0 +#endif + +#ifdef TX_ENABLE_FIQ_NESTING +#define TX_FIQ_NESTING_ENABLED 4 +#else +#define TX_FIQ_NESTING_ENABLED 0 +#endif + +#define TX_PORT_SPECIFIC_BUILD_OPTIONS TX_FIQ_ENABLED | TX_IRQ_NESTING_ENABLED | TX_FIQ_NESTING_ENABLED + + +/* Define the in-line initialization constant so that modules with in-line + initialization capabilities can prevent their initialization from being + a function call. */ + +#define TX_INLINE_INITIALIZATION + + +/* Determine whether or not stack checking is enabled. By default, ThreadX stack checking is + disabled. When the following is defined, ThreadX thread stack checking is enabled. If stack + checking is enabled (TX_ENABLE_STACK_CHECKING is defined), the TX_DISABLE_STACK_FILLING + define is negated, thereby forcing the stack fill which is necessary for the stack checking + logic. */ + +#ifdef TX_ENABLE_STACK_CHECKING +#undef TX_DISABLE_STACK_FILLING +#endif + + +/* Define the TX_THREAD control block extensions for this port. The main reason + for the multiple macros is so that backward compatibility can be maintained with + existing ThreadX kernel awareness modules. */ + +/* TX_THREAD_EXTENSION_2 carries the per-thread VFP enable flag used by the + lazy floating-point save and restore in tx_thread_schedule.S, + tx_thread_system_return.S and tx_thread_context_restore.S. + + It is defined UNCONDITIONALLY, not under TX_ENABLE_VFP_SUPPORT, and that is + deliberate: the assembly reaches this field through a hard-coded structure + offset, so making the field conditional would move every following member + between build configurations and leave the offset correct in only one of + them. Keeping it always present makes the layout independent of the + floating-point build options. The offset is checked at compile time in + tx_port_offset_check.c, which turns a wrong offset into a build failure + instead of silent corruption of an unrelated thread field. */ + +#define TX_THREAD_EXTENSION_0 +#define TX_THREAD_EXTENSION_1 +/* The module thread extension. A thread that belongs to a module carries a + pointer to its module instance, its entry information, the user-mode state to + restore when it is scheduled, and two stacks: the module's own stack for user + mode and a kernel stack for the privileged side of a system call. + + Two stacks rather than one is not an implementation detail that can be + simplified away. A module thread runs in user mode on memory the module owns; + the moment it enters the kernel through a system call, the kernel must not be + writing its own state onto memory the module can also write, or the module + could corrupt the kernel by scribbling on its own stack. */ + +/* tx_thread_vfp_enable comes first and is not optional here. The Cortex-R52 + port keeps the floating-point lazy-enable flag in this extension, and dropping + it while adding the module fields would leave VFP threads with nowhere to + record that they have used the unit -- the kind of breakage that shows up as + corrupted floating-point state in a module, a long way from its cause. */ + +#define TX_THREAD_EXTENSION_2 ULONG tx_thread_vfp_enable; \ + VOID *tx_thread_module_instance_ptr; \ + VOID *tx_thread_module_entry_info_ptr; \ + ULONG tx_thread_module_current_user_mode; \ + ULONG tx_thread_module_user_mode; \ + ULONG tx_thread_module_saved_lr; \ + VOID *tx_thread_module_kernel_stack_start; \ + VOID *tx_thread_module_kernel_stack_end; \ + ULONG tx_thread_module_kernel_stack_size; \ + VOID *tx_thread_module_stack_ptr; \ + VOID *tx_thread_module_stack_start; \ + VOID *tx_thread_module_stack_end; \ + ULONG tx_thread_module_stack_size; \ + VOID *tx_thread_module_reserved; +#define TX_THREAD_EXTENSION_3 + + +/* Define the port extensions of the remaining ThreadX objects. */ + +#define TX_BLOCK_POOL_EXTENSION +#define TX_BYTE_POOL_EXTENSION +#define TX_EVENT_FLAGS_GROUP_EXTENSION +#define TX_MUTEX_EXTENSION +#define TX_QUEUE_EXTENSION +#define TX_SEMAPHORE_EXTENSION +#define TX_TIMER_EXTENSION + + +/* Define the user extension field of the thread control block. Nothing + additional is needed for this port so it is defined as white space. */ + +#ifndef TX_THREAD_USER_EXTENSION +#define TX_THREAD_USER_EXTENSION +#endif + + +/* Define the macros for processing extensions in tx_thread_create, tx_thread_delete, + tx_thread_shell_entry, and tx_thread_terminate. */ + + +#define TX_THREAD_CREATE_EXTENSION(thread_ptr) +#define TX_THREAD_DELETE_EXTENSION(thread_ptr) +#define TX_THREAD_COMPLETED_EXTENSION(thread_ptr) +#define TX_THREAD_TERMINATED_EXTENSION(thread_ptr) + + +/* Define the ThreadX object creation extensions for remaining objects. */ + +#define TX_BLOCK_POOL_CREATE_EXTENSION(pool_ptr) +#define TX_BYTE_POOL_CREATE_EXTENSION(pool_ptr) +#define TX_EVENT_FLAGS_GROUP_CREATE_EXTENSION(group_ptr) +#define TX_MUTEX_CREATE_EXTENSION(mutex_ptr) +#define TX_QUEUE_CREATE_EXTENSION(queue_ptr) +#define TX_SEMAPHORE_CREATE_EXTENSION(semaphore_ptr) +#define TX_TIMER_CREATE_EXTENSION(timer_ptr) + + +/* Define the ThreadX object deletion extensions for remaining objects. */ + +#define TX_BLOCK_POOL_DELETE_EXTENSION(pool_ptr) +#define TX_BYTE_POOL_DELETE_EXTENSION(pool_ptr) +#define TX_EVENT_FLAGS_GROUP_DELETE_EXTENSION(group_ptr) +#define TX_MUTEX_DELETE_EXTENSION(mutex_ptr) +#define TX_QUEUE_DELETE_EXTENSION(queue_ptr) +#define TX_SEMAPHORE_DELETE_EXTENSION(semaphore_ptr) +#define TX_TIMER_DELETE_EXTENSION(timer_ptr) + + +/* Determine if the ARM architecture has the CLZ instruction. This is available on + architectures v5 and above. If available, redefine the macro for calculating the + lowest bit set. */ + +#if __TARGET_ARCH_ARM > 4 + +#ifndef __thumb__ + +#define TX_LOWEST_SET_BIT_CALCULATE(m, b) m = m & ((ULONG) (-((LONG) m))); \ + asm volatile (" CLZ %0,%1 ": "=r" (b) : "r" (m) ); \ + b = 31 - b; +#endif +#endif + + +/* Define ThreadX interrupt lockout and restore macros for protection on + access of critical kernel information. The restore interrupt macro must + restore the interrupt posture of the running thread prior to the value + present prior to the disable macro. In most cases, the save area macro + is used to define a local function save area for the disable and restore + macros. */ + +/* Per-thread floating-point control. Implemented in tx_thread_schedule.S and + available only when the library is built with TX_ENABLE_VFP_SUPPORT. A + thread's floating-point context is saved and restored lazily: only threads + that have called tx_thread_vfp_enable() pay for it. */ + +#ifdef TX_ENABLE_VFP_SUPPORT +void tx_thread_vfp_enable(void); +void tx_thread_vfp_disable(void); +#endif + + +#ifdef __thumb__ + +unsigned int _tx_thread_interrupt_disable(void); +unsigned int _tx_thread_interrupt_restore(UINT old_posture); + + +#define TX_INTERRUPT_SAVE_AREA UINT interrupt_save; + +#define TX_DISABLE interrupt_save = _tx_thread_interrupt_disable(); +#define TX_RESTORE _tx_thread_interrupt_restore(interrupt_save); + +#else + +#define TX_INTERRUPT_SAVE_AREA UINT interrupt_save; + +#ifdef TX_ENABLE_FIQ_SUPPORT +#define TX_DISABLE asm volatile (" MRS %0,CPSR; CPSID if ": "=r" (interrupt_save) ); +#else +#define TX_DISABLE asm volatile (" MRS %0,CPSR; CPSID i ": "=r" (interrupt_save) ); +#endif + +#define TX_RESTORE asm volatile (" MSR CPSR_c,%0 "::"r" (interrupt_save) ); + +#endif + + +/* Define the interrupt lockout macros for each ThreadX object. */ + +#define TX_BLOCK_POOL_DISABLE TX_DISABLE +#define TX_BYTE_POOL_DISABLE TX_DISABLE +#define TX_EVENT_FLAGS_GROUP_DISABLE TX_DISABLE +#define TX_MUTEX_DISABLE TX_DISABLE +#define TX_QUEUE_DISABLE TX_DISABLE +#define TX_SEMAPHORE_DISABLE TX_DISABLE + + +/* Define the version ID of ThreadX. This may be used by the application.*/ + +#ifdef TX_THREAD_INIT +CHAR _tx_version_id[] = + "(c) 2024 Microsoft Corp. (c) 2026-present Eclipse ThreadX contributors. * ThreadXCortex-R52/GNU Version 6.5.1.202602a *"; +#else +extern CHAR _tx_version_id[]; +#endif + + +#endif + diff --git a/ports_module/cortex_r52/gnu/inc/txm_module_port.h b/ports_module/cortex_r52/gnu/inc/txm_module_port.h new file mode 100644 index 000000000..55f00aa57 --- /dev/null +++ b/ports_module/cortex_r52/gnu/inc/txm_module_port.h @@ -0,0 +1,388 @@ +/*************************************************************************** + * Copyright (c) 2024 Microsoft Corporation + * Copyright (c) 2026-present 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. + * + * SPDX-License-Identifier: MIT + **************************************************************************/ + + +/**************************************************************************/ +/**************************************************************************/ +/** */ +/** ThreadX Component */ +/** */ +/** Module */ +/** */ +/**************************************************************************/ +/**************************************************************************/ + + +/**************************************************************************/ +/* */ +/* APPLICATION INTERFACE DEFINITION RELEASE */ +/* */ +/* txm_module_port.h Cortex-R52/GNU */ +/* 6.1.10 */ +/* AUTHOR */ +/* */ +/* Scott Larson, Microsoft Corporation */ +/* */ +/* DESCRIPTION */ +/* */ +/* This file defines the basic module constants, interface structures, */ +/* and function prototypes. */ +/* */ +/**************************************************************************/ + +#ifndef TXM_MODULE_PORT_H +#define TXM_MODULE_PORT_H + +/* Determine if the optional Modules user define file should be used. */ + +#ifdef TXM_MODULE_INCLUDE_USER_DEFINE_FILE + +/* Yes, include the user defines in txm_module_user.h. The defines in this file may + alternately be defined on the command line. */ + +#include "txm_module_user.h" +#endif + +/* It is assumed that the base ThreadX tx_port.h file has been modified to add the + following extensions to the ThreadX thread control block (this code should replace + the corresponding macro define in tx_port.h): + +#define TX_THREAD_EXTENSION_2 VOID *tx_thread_module_instance_ptr; \ + VOID *tx_thread_module_entry_info_ptr; \ + ULONG tx_thread_module_current_user_mode; \ + ULONG tx_thread_module_user_mode; \ + ULONG tx_thread_module_saved_lr; \ + VOID *tx_thread_module_kernel_stack_start; \ + VOID *tx_thread_module_kernel_stack_end; \ + ULONG tx_thread_module_kernel_stack_size; \ + VOID *tx_thread_module_stack_ptr; \ + VOID *tx_thread_module_stack_start; \ + VOID *tx_thread_module_stack_end; \ + ULONG tx_thread_module_stack_size; \ + VOID *tx_thread_module_reserved; + +The following extensions must also be defined in tx_port.h: + +#define TX_EVENT_FLAGS_GROUP_EXTENSION VOID *tx_event_flags_group_module_instance; \ + VOID (*tx_event_flags_group_set_module_notify)(struct TX_EVENT_FLAGS_GROUP_STRUCT *group_ptr); + +#define TX_QUEUE_EXTENSION VOID *tx_queue_module_instance; \ + VOID (*tx_queue_send_module_notify)(struct TX_QUEUE_STRUCT *queue_ptr); + +#define TX_SEMAPHORE_EXTENSION VOID *tx_semaphore_module_instance; \ + VOID (*tx_semaphore_put_module_notify)(struct TX_SEMAPHORE_STRUCT *semaphore_ptr); + +#define TX_TIMER_EXTENSION VOID *tx_timer_module_instance; \ + VOID (*tx_timer_module_expiration_function)(ULONG id); +*/ + +/* Define the kernel stack size for a module thread. */ +#ifndef TXM_MODULE_KERNEL_STACK_SIZE +#define TXM_MODULE_KERNEL_STACK_SIZE 768 +#endif + +/* Define constants specific to the tools the module can be built with for this particular modules port. */ + +#define TXM_MODULE_IAR_COMPILER 0x00000000 +#define TXM_MODULE_RVDS_COMPILER 0x01000000 +#define TXM_MODULE_GNU_COMPILER 0x02000000 +#define TXM_MODULE_COMPILER_MASK 0xFF000000 +#define TXM_MODULE_OPTIONS_MASK 0x000000FF + + +/* Define the properties for this particular module port. */ + +#define TXM_MODULE_PORT_DISPATCH + +#define TXM_MODULE_MEMORY_PROTECTION_ENABLED + +#ifdef TXM_MODULE_MEMORY_PROTECTION_ENABLED +#define TXM_MODULE_REQUIRE_ALLOCATED_OBJECT_MEMORY +#else +#define TXM_MODULE_REQUIRE_LOCAL_OBJECT_MEMORY +#endif + +#define TXM_MODULE_USER_MODE 0x00000001 +#define TXM_MODULE_MEMORY_PROTECTION 0x00000002 +#define TXM_MODULE_SHARED_EXTERNAL_MEMORY_ACCESS 0x00000004 + + +/* Define the supported options for this module. */ + +#define TXM_MODULE_MANAGER_SUPPORTED_OPTIONS (TXM_MODULE_USER_MODE | TXM_MODULE_MEMORY_PROTECTION | TXM_MODULE_SHARED_EXTERNAL_MEMORY_ACCESS) +#define TXM_MODULE_MANAGER_REQUIRED_OPTIONS 0 + + +/* Define offset adjustments according to the compiler used to build the module. */ + +#define TXM_MODULE_IAR_SHELL_ADJUST 24 +#define TXM_MODULE_IAR_START_ADJUST 28 +#define TXM_MODULE_IAR_STOP_ADJUST 32 +#define TXM_MODULE_IAR_CALLBACK_ADJUST 44 + +#define TXM_MODULE_RVDS_SHELL_ADJUST 0 +#define TXM_MODULE_RVDS_START_ADJUST 0 +#define TXM_MODULE_RVDS_STOP_ADJUST 0 +#define TXM_MODULE_RVDS_CALLBACK_ADJUST 0 + +#define TXM_MODULE_GNU_SHELL_ADJUST 24 +#define TXM_MODULE_GNU_START_ADJUST 28 +#define TXM_MODULE_GNU_STOP_ADJUST 32 +#define TXM_MODULE_GNU_CALLBACK_ADJUST 44 + + +/* Define other module port-specific constants. */ + +/* Define INLINE_DECLARE to inline for ARM compiler. */ + +#define INLINE_DECLARE inline + +/* Define the number of MPU entries assigned to the code and data sections. + + PMSAv8-R gives this core many more regions than Armv8-M gives an M33, and that + changes the design rather than just the numbers. MPUIR on the S32Z280 reports + 20 EL1 regions, where an M33 has 8 in total and the kernel and a module must + therefore share them. Here the kernel's map can keep its own regions + permanently and the manager owns a separate block, so a module switch + reprograms only the module's regions and never rebuilds the kernel's. + + Eight entries are reserved, laid out as on the M33 -- one for the kernel entry + function, one for module code, one for module data, five for shared memory -- + starting at TXM_MODULE_MPU_FIRST_REGION. The indices below are relative to + that base. + + TXM_MODULE_MPU_FIRST_REGION must sit above every region the board support + package programs. The S32Z280 example uses 0 through 8, so 9 is the first + free one; a board that uses more must raise this. */ + +#define TXM_MODULE_MPU_FIRST_REGION 9 +#define TXM_MODULE_MPU_TOTAL_ENTRIES 8 +#define TXM_MODULE_MPU_KERNEL_ENTRY_INDEX 0 +#define TXM_MODULE_MPU_CODE_INDEX 1 +#define TXM_MODULE_MPU_DATA_INDEX 2 + +#define TXM_MODULE_MPU_SHARED_INDEX 3 +#define TXM_MODULE_MPU_SHARED_ENTRIES 5 + +#define TXM_MODULE_ATTRIBUTE_NON_SHAREABLE 0x00 +#define TXM_MODULE_ATTRIBUTE_OUTER_SHAREABLE 0x10 +#define TXM_MODULE_ATTRIBUTE_INNER_SHAREABLE 0x18 +#define TXM_MODULE_ATTRIBUTE_READ_WRITE 0x02 +#define TXM_MODULE_ATTRIBUTE_READ_ONLY 0x06 +#define TXM_MODULE_ATTRIBUTE_EXECUTE_NEVER 0x01 +#define TXM_MODULE_ATTRIBUTE_REGION_ENABLE 0x01 +#define TXM_MODULE_ATTRIBUTE_MASK 0x1E +#define TXM_MODULE_ATTRIBUTE_INDEX 0x00 + +/* The attribute encodings above are the PMSAv8 field layout and are the same on + this core as on an M33: shareability in PRBAR[4:3], access permission in + PRBAR[2:1], execute-never in PRBAR[0], attribute index in PRLAR[3:1] and the + region enable in PRLAR[0]. What differs is the granule -- 64 bytes here + against 32 on Armv8-M, so the base and limit fields are [31:6] rather than + [31:5] -- and that the registers are reached through CP15 with PRSELR + selecting the region, rather than being memory mapped. */ + +/* MAIR_ATTR settings +Device-nGnRE : 0b [Outer]0000 [Inner]0100 +Code WT RA : 0b [Outer]1010 [Inner]1010 +DATA WBWA RA : 0b [Outer]1111 [Inner]1111 +*/ + +/* Data aligned to 8 bytes (stacks must be 8-byte aligned). */ +#define TXM_MODULE_DATA_ALIGNMENT 8 + +/* MPU regions must be 64-byte aligned on PMSAv8-R, where Armv8-M requires 32. + Getting this wrong is silent: the low bits of PRBAR and PRLAR hold attributes, + so an under-aligned base does not fault, it changes the shareability and + permissions of the region instead. */ +#define TXM_MODULE_MPU_ALIGNMENT 64 + +/* No secure-stack extension calls. Those are the Armv8-M security extension, + which this core does not have: privilege here is the ARM mode, EL1 against + EL0, and there is no secure world to allocate a second stack in. */ + +/* Two registers set up each MPU region: PRBAR and PRLAR, selected by PRSELR. + Held per module so a switch is a sequence of register writes with nothing to + compute -- the cost measured on this part is dominated by the barriers, not + the writes, so a switch should program every region and issue one dsb and isb + at the end rather than a pair per region. */ +typedef struct TXM_MODULE_MPU_INFO_STRUCT +{ + ULONG txm_module_mpu_region_base_address; + ULONG txm_module_mpu_region_limit_address; +} TXM_MODULE_MPU_INFO; + +/* Define the port-extensions to the module manager instance structure. */ +#define TXM_MODULE_MANAGER_PORT_EXTENSION \ + TXM_MODULE_MPU_INFO txm_module_instance_mpu_registers[TXM_MODULE_MPU_TOTAL_ENTRIES]; \ + ULONG txm_module_instance_shared_memory_count; \ + ULONG txm_module_instance_shared_memory_address[TXM_MODULE_MPU_SHARED_ENTRIES]; \ + ULONG txm_module_instance_shared_memory_length[TXM_MODULE_MPU_SHARED_ENTRIES]; + + +/* Define the memory fault information structure that is populated when a memory fault occurs. */ + +typedef struct TXM_MODULE_MANAGER_MEMORY_FAULT_INFO_STRUCT +{ + TX_THREAD *txm_module_manager_memory_fault_info_thread_ptr; + VOID *txm_module_manager_memory_fault_info_code_location; + /* Fault status and address, read through CP15. Armv8-M reports a memory + fault in SHCSR, CFSR, MMFAR and BFAR; this core splits it by access type + instead -- DFSR and DFAR for a data abort, IFSR and IFAR for a prefetch + abort -- and both pairs are recorded because a module can fault either + way: writing outside its data region, or branching outside its code + region. */ + + ULONG txm_module_manager_memory_fault_info_dfsr; + ULONG txm_module_manager_memory_fault_info_dfar; + ULONG txm_module_manager_memory_fault_info_ifsr; + ULONG txm_module_manager_memory_fault_info_ifar; + ULONG txm_module_manager_memory_fault_info_sp; + ULONG txm_module_manager_memory_fault_info_r0; + ULONG txm_module_manager_memory_fault_info_r1; + ULONG txm_module_manager_memory_fault_info_r2; + ULONG txm_module_manager_memory_fault_info_r3; + ULONG txm_module_manager_memory_fault_info_r4; + ULONG txm_module_manager_memory_fault_info_r5; + ULONG txm_module_manager_memory_fault_info_r6; + ULONG txm_module_manager_memory_fault_info_r7; + ULONG txm_module_manager_memory_fault_info_r8; + ULONG txm_module_manager_memory_fault_info_r9; + ULONG txm_module_manager_memory_fault_info_r10; + ULONG txm_module_manager_memory_fault_info_r11; + ULONG txm_module_manager_memory_fault_info_r12; + ULONG txm_module_manager_memory_fault_info_lr; + /* SPSR rather than xPSR: the mode the faulting code was running in is what + says whether it was the module in user mode or the kernel. */ + + ULONG txm_module_manager_memory_fault_info_spsr; +} TXM_MODULE_MANAGER_MEMORY_FAULT_INFO; + + +#define TXM_MODULE_MANAGER_FAULT_INFO \ + TXM_MODULE_MANAGER_MEMORY_FAULT_INFO _txm_module_manager_memory_fault_info; + + +/* Define the macro to check the code alignment. */ + +#define TXM_MODULE_MANAGER_CHECK_CODE_ALIGNMENT(module_location, code_alignment) \ + { \ + ULONG temp; \ + temp = (ULONG) module_location; \ + temp = temp & (code_alignment - 1); \ + if (temp) \ + { \ + _tx_mutex_put(&_txm_module_manager_mutex); \ + return(TXM_MODULE_ALIGNMENT_ERROR); \ + } \ + } + + +/* Define the macro to adjust the alignment and size for code/data areas. */ + +#define TXM_MODULE_MANAGER_ALIGNMENT_ADJUST(module_preamble, code_size, code_alignment, data_size, data_alignment) _txm_module_manager_alignment_adjust(module_preamble, &code_size, &code_alignment, &data_size, &data_alignment); + + +/* Define the macro to adjust the symbols in the module preamble. */ + +#define TXM_MODULE_MANAGER_CALCULATE_ADJUSTMENTS(properties, shell_function_adjust, start_function_adjust, stop_function_adjust, callback_function_adjust) \ + if ((properties & TXM_MODULE_COMPILER_MASK) == TXM_MODULE_IAR_COMPILER) \ + { \ + shell_function_adjust = TXM_MODULE_IAR_SHELL_ADJUST; \ + start_function_adjust = TXM_MODULE_IAR_START_ADJUST; \ + stop_function_adjust = TXM_MODULE_IAR_STOP_ADJUST; \ + callback_function_adjust = TXM_MODULE_IAR_CALLBACK_ADJUST; \ + } \ + else if ((properties & TXM_MODULE_COMPILER_MASK) == TXM_MODULE_RVDS_COMPILER) \ + { \ + shell_function_adjust = TXM_MODULE_RVDS_SHELL_ADJUST; \ + start_function_adjust = TXM_MODULE_RVDS_START_ADJUST; \ + stop_function_adjust = TXM_MODULE_RVDS_STOP_ADJUST; \ + callback_function_adjust = TXM_MODULE_RVDS_CALLBACK_ADJUST; \ + } \ + else \ + { \ + shell_function_adjust = TXM_MODULE_GNU_SHELL_ADJUST; \ + start_function_adjust = TXM_MODULE_GNU_START_ADJUST; \ + stop_function_adjust = TXM_MODULE_GNU_STOP_ADJUST; \ + callback_function_adjust = TXM_MODULE_GNU_CALLBACK_ADJUST; \ + } + + +/* Define the macro to populate the thread control block with module port-specific information. + Check if the module is in user mode and set up txm_module_thread_entry_info_kernel_call_dispatcher accordingly. +*/ + +#define TXM_MODULE_MANAGER_THREAD_SETUP(thread_ptr, module_instance) \ + thread_ptr -> tx_thread_module_current_user_mode = module_instance -> txm_module_instance_property_flags & TXM_MODULE_USER_MODE; \ + thread_ptr -> tx_thread_module_user_mode = module_instance -> txm_module_instance_property_flags & TXM_MODULE_USER_MODE; \ + if (thread_ptr -> tx_thread_module_user_mode) \ + { \ + thread_entry_info -> txm_module_thread_entry_info_kernel_call_dispatcher = _txm_module_manager_user_mode_entry; \ + } \ + else \ + { \ + thread_entry_info -> txm_module_thread_entry_info_kernel_call_dispatcher = _txm_module_manager_kernel_dispatch; \ + } + + +/* Define the macro to populate the module control block with module port-specific information. + If memory protection is enabled, set up the MPU registers. +*/ +#define TXM_MODULE_MANAGER_MODULE_SETUP(module_instance) \ + if (module_instance -> txm_module_instance_property_flags & TXM_MODULE_USER_MODE) \ + { \ + if (module_instance -> txm_module_instance_property_flags & TXM_MODULE_MEMORY_PROTECTION) \ + { \ + _txm_module_manager_mm_register_setup(module_instance); \ + } \ + } \ + else \ + { \ + /* Do nothing. */ \ + } + +/* Define the macro to perform port-specific functions when unloading the module. */ +/* Nothing needs to be done for this port. */ +#define TXM_MODULE_MANAGER_MODULE_UNLOAD(module_instance) + + +/* Define the macros to perform port-specific checks when passing pointers to the kernel. */ + +/* Define macro to make sure object is inside the module's data. */ +#define TXM_MODULE_MANAGER_CHECK_INSIDE_DATA(module_instance, obj_ptr, obj_size) \ + _txm_module_manager_inside_data_check(module_instance, obj_ptr, obj_size) + +/* Define some internal prototypes to this module port. */ + +#ifndef TX_SOURCE_CODE +#define txm_module_manager_memory_fault_notify _txm_module_manager_memory_fault_notify +#endif + + +#ifdef TX_SOURCE_CODE + +#endif + +#define TXM_MODULE_MANAGER_ADDITIONAL_PROTOTYPES \ +VOID _txm_module_manager_alignment_adjust(TXM_MODULE_PREAMBLE *module_preamble, ULONG *code_size, ULONG *code_alignment, ULONG *data_size, ULONG *data_alignment); \ +VOID _txm_module_manager_memory_fault_handler(VOID); \ +UINT _txm_module_manager_memory_fault_notify(VOID (*notify_function)(TX_THREAD *, TXM_MODULE_INSTANCE *)); \ +VOID _txm_module_manager_mm_register_setup(TXM_MODULE_INSTANCE *module_instance); \ +UINT _txm_module_manager_inside_data_check(TXM_MODULE_INSTANCE *module_instance, ALIGN_TYPE obj_ptr, UINT obj_size); \ +ALIGN_TYPE _txm_module_manager_port_dispatch(TXM_MODULE_INSTANCE *module_instance, ULONG kernel_request, ALIGN_TYPE param_0, ALIGN_TYPE param_1, ALIGN_TYPE param_2); + + +#define TXM_MODULE_MANAGER_VERSION_ID \ +CHAR _txm_module_manager_version_id[] = \ + "Copyright (c) 2024 Microsoft Corporation. * ThreadX Module Cortex-R52/GNU Version 6.5.0.202601 *"; + +#endif From 315fb7264695b06cea00d3af001efb5fe09edb48 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20Desbiens?= Date: Tue, 18 Aug 2026 09:27:38 -0400 Subject: [PATCH 02/33] Added the PMSAv8-R region setup and fault handler for the module port Two of the module manager's port-specific sources, both derived from the Armv8-M versions for the reasons given in the previous commit. The region setup needed two substantive changes and they are the ones worth reviewing. The address mask is 64-byte where Armv8-M is 32-byte, and it is now a named constant, TXM_MODULE_MPU_ADDRESS_MASK, rather than a literal repeated six times. PRBAR and PRLAR hold attributes in the bits below the granule, so masking to the wrong boundary does not fault: it writes address bits into the shareability and permission fields, and the region comes up with attributes nobody asked for. That failure is silent, which is why the constant is named and the reason is recorded where the code uses it. Regions are non-shareable where the M33 port marks module memory inner-shareable. The RTU here is a single Cortex-R52 and every region in the board support package's map is non-shareable, so matching it keeps one memory model across the kernel and its modules. A multi-core RTU configuration would need to revisit this, and much else besides. The fault handler needed almost nothing, which was worth finding out. It is architecture-neutral: it terminates the faulting thread and calls the notification callback. The register capture happens before it, in the abort vector, and that part is still to be written -- DFSR, DFAR, IFSR and IFAR have to be read before anything else can fault and overwrite them, and on this core the abort is taken in Abort mode with its own banked lr and sp, so the capture belongs there rather than in C. The file now says so, so the next person does not look for it here. Both files compile clean against the module headers, which also confirms the header work: the new mask and the DFSR, DFAR, IFSR and IFAR fault fields all resolve. Assisted-by: Claude Code (Opus 5) --- .../cortex_r52/gnu/inc/txm_module_port.h | 6 + .../txm_module_manager_memory_fault_handler.c | 116 ++++++++++ .../txm_module_manager_mm_register_setup.c | 207 ++++++++++++++++++ 3 files changed, 329 insertions(+) create mode 100644 ports_module/cortex_r52/gnu/module_manager/src/txm_module_manager_memory_fault_handler.c create mode 100644 ports_module/cortex_r52/gnu/module_manager/src/txm_module_manager_mm_register_setup.c diff --git a/ports_module/cortex_r52/gnu/inc/txm_module_port.h b/ports_module/cortex_r52/gnu/inc/txm_module_port.h index 55f00aa57..164ac43f7 100644 --- a/ports_module/cortex_r52/gnu/inc/txm_module_port.h +++ b/ports_module/cortex_r52/gnu/inc/txm_module_port.h @@ -205,6 +205,12 @@ DATA WBWA RA : 0b [Outer]1111 [Inner]1111 permissions of the region instead. */ #define TXM_MODULE_MPU_ALIGNMENT 64 +/* Mask that keeps only the address bits of PRBAR and PRLAR. The low six bits + hold attributes on this core, so every base and limit written into the region + table must be masked with this and not with the Armv8-M 32-byte equivalent. */ + +#define TXM_MODULE_MPU_ADDRESS_MASK 0xFFFFFFC0 + /* No secure-stack extension calls. Those are the Armv8-M security extension, which this core does not have: privilege here is the ARM mode, EL1 against EL0, and there is no secure world to allocate a second stack in. */ diff --git a/ports_module/cortex_r52/gnu/module_manager/src/txm_module_manager_memory_fault_handler.c b/ports_module/cortex_r52/gnu/module_manager/src/txm_module_manager_memory_fault_handler.c new file mode 100644 index 000000000..03e92d696 --- /dev/null +++ b/ports_module/cortex_r52/gnu/module_manager/src/txm_module_manager_memory_fault_handler.c @@ -0,0 +1,116 @@ +/*************************************************************************** + * Copyright (c) 2024 Microsoft Corporation + * Copyright (c) 2026-present 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. + * + * SPDX-License-Identifier: MIT + **************************************************************************/ + + +/**************************************************************************/ +/**************************************************************************/ +/** */ +/** ThreadX Component */ +/** */ +/** Module Manager */ +/** */ +/**************************************************************************/ +/**************************************************************************/ + +#define TX_SOURCE_CODE + +#include "tx_api.h" +#include "tx_thread.h" +#include "txm_module.h" + + +/* This handler is architecture-neutral: it terminates the faulting thread and + calls the notification callback. The fault registers are captured before it + runs, in the abort vector, because DFSR, DFAR, IFSR and IFAR must be read + before anything else can fault and overwrite them -- and on this core the + abort is taken in Abort mode with its own banked lr and sp, so the capture has + to happen there rather than here. + + Data aborts and prefetch aborts both arrive here. A module can violate its + protection either way: writing outside its data region, or branching outside + its code region. Which pair of registers is meaningful depends on which it + was, and the fault info structure carries both. */ + +/* Define the user's fault notification callback function pointer. This is + setup via the txm_module_manager_memory_fault_notify API. */ + +VOID (*_txm_module_manager_fault_notify)(TX_THREAD *, TXM_MODULE_INSTANCE *); + + +/* Define a macro that can be used to allocate global variables useful to + store information about the last fault. This macro is defined in + txm_module_port.h and is usually populated in the assembly language + fault handling prior to the code calling _txm_module_manager_memory_fault_handler. */ + +TXM_MODULE_MANAGER_FAULT_INFO + + +/**************************************************************************/ +/* */ +/* FUNCTION RELEASE */ +/* */ +/* _txm_module_manager_memory_fault_handler Cortex-R52 */ +/* 6.1.8 */ +/* AUTHOR */ +/* */ +/* Scott Larson, Microsoft Corporation */ +/* */ +/* DESCRIPTION */ +/* */ +/* This function handles a fault associated with a memory protected */ +/* module. */ +/* */ +/* INPUT */ +/* */ +/* None */ +/* */ +/* OUTPUT */ +/* */ +/* None */ +/* */ +/* CALLS */ +/* */ +/* _tx_thread_terminate Terminate thread */ +/* */ +/* CALLED BY */ +/* */ +/* Fault handler */ +/* */ +/**************************************************************************/ +VOID _txm_module_manager_memory_fault_handler(VOID) +{ + +TXM_MODULE_INSTANCE *module_instance_ptr; +TX_THREAD *thread_ptr; + + /* Pickup the current thread. */ + thread_ptr = _tx_thread_current_ptr; + + /* Initialize the module instance pointer to NULL. */ + module_instance_ptr = TX_NULL; + + /* Is there a thread? */ + if (thread_ptr) + { + /* Pickup the module instance. */ + module_instance_ptr = thread_ptr -> tx_thread_module_instance_ptr; + + /* Terminate the current thread. */ + _tx_thread_terminate(_tx_thread_current_ptr); + } + + /* Determine if there is a user memory fault notification callback. */ + if (_txm_module_manager_fault_notify) + { + /* Yes, call the user's notification memory fault callback. */ + (_txm_module_manager_fault_notify)(thread_ptr, module_instance_ptr); + } +} diff --git a/ports_module/cortex_r52/gnu/module_manager/src/txm_module_manager_mm_register_setup.c b/ports_module/cortex_r52/gnu/module_manager/src/txm_module_manager_mm_register_setup.c new file mode 100644 index 000000000..5fc7edba8 --- /dev/null +++ b/ports_module/cortex_r52/gnu/module_manager/src/txm_module_manager_mm_register_setup.c @@ -0,0 +1,207 @@ +/*************************************************************************** + * Copyright (c) 2024 Microsoft Corporation + * Copyright (c) 2026-present 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. + * + * SPDX-License-Identifier: MIT + **************************************************************************/ + + +/**************************************************************************/ +/**************************************************************************/ +/** */ +/** ThreadX Component */ +/** */ +/** Module Manager */ +/** */ +/**************************************************************************/ +/**************************************************************************/ + +#define TX_SOURCE_CODE + +#include "tx_api.h" +#include "txm_module.h" + +/**************************************************************************/ +/* */ +/* FUNCTION RELEASE */ +/* */ +/* _txm_module_manager_mm_register_setup Cortex-R52 */ +/* 6.1.6 */ +/* AUTHOR */ +/* */ +/* Scott Larson, Microsoft Corporation */ +/* */ +/* DESCRIPTION */ +/* */ +/* This function sets up the Cortex-R52 MPU register definitions based */ +/* on the module's memory characteristics. */ +/* */ +/* INPUT */ +/* */ +/* module_instance Pointer to module instance */ +/* */ +/* OUTPUT */ +/* */ +/* MPU settings for the module in module_instance */ +/* */ +/* CALLS */ +/* */ +/* none */ +/* */ +/* CALLED BY */ +/* */ +/* _txm_module_manager_thread_create */ +/* */ +/**************************************************************************/ +/* Two departures from the Armv8-M version this was derived from, both + deliberate. + + The address mask is 64-byte, not 32-byte. PRBAR and PRLAR hold attributes in + the bits below the granule, so masking to the wrong boundary does not fault -- + it silently writes address bits into the shareability and permission fields + and the region comes up with attributes nobody asked for. + + Regions are non-shareable rather than inner-shareable. The M33 port marks + module memory inner-shareable; the RTU here is a single Cortex-R52 and every + region in the board support package's map is non-shareable, so matching it + keeps one memory model across the kernel and its modules. A multi-core RTU + configuration would need to revisit this, and would need to revisit far more + than this. */ + +VOID _txm_module_manager_mm_register_setup(TXM_MODULE_INSTANCE *module_instance) +{ + +ULONG data_size; +ULONG start_stop_stack_size; +ULONG callback_stack_size; + + /* Setup MPU region for kernel mode entry. */ + /* Set base address register to user mode entry function address, which is guaranteed to be at least 32-byte aligned. + Mask address to proper range, inner shareable, read only. */ + module_instance -> txm_module_instance_mpu_registers[TXM_MODULE_MPU_KERNEL_ENTRY_INDEX].txm_module_mpu_region_base_address = ((ULONG) _txm_module_manager_user_mode_entry & TXM_MODULE_MPU_ADDRESS_MASK) | TXM_MODULE_ATTRIBUTE_NON_SHAREABLE | TXM_MODULE_ATTRIBUTE_READ_ONLY; + /* Set the limit address, attribute index, and enable bit. */ + module_instance -> txm_module_instance_mpu_registers[TXM_MODULE_MPU_KERNEL_ENTRY_INDEX].txm_module_mpu_region_limit_address = ((ULONG) _txm_module_manager_user_mode_entry & TXM_MODULE_MPU_ADDRESS_MASK) | TXM_MODULE_ATTRIBUTE_INDEX | TXM_MODULE_ATTRIBUTE_REGION_ENABLE; + /* End of kernel mode entry setup. */ + + + /* Setup MPU region for module code protection. */ + /* Set base address register to module code address, which should be at least 32-byte aligned. + Mask address to proper range, inner shareable, read only. */ + module_instance -> txm_module_instance_mpu_registers[TXM_MODULE_MPU_CODE_INDEX].txm_module_mpu_region_base_address = ((ULONG) module_instance -> txm_module_instance_code_start & TXM_MODULE_MPU_ADDRESS_MASK) | TXM_MODULE_ATTRIBUTE_NON_SHAREABLE | TXM_MODULE_ATTRIBUTE_READ_ONLY; + /* Set the limit address (code start + code size-1), attribute index, and enable bit. */ + module_instance -> txm_module_instance_mpu_registers[TXM_MODULE_MPU_CODE_INDEX].txm_module_mpu_region_limit_address = (((ULONG) module_instance -> txm_module_instance_code_start + module_instance -> txm_module_instance_preamble_ptr -> txm_module_preamble_code_size - 1) & TXM_MODULE_MPU_ADDRESS_MASK) | TXM_MODULE_ATTRIBUTE_INDEX | TXM_MODULE_ATTRIBUTE_REGION_ENABLE; + /* End of module code protection. */ + + + /* Setup MPU region for module data protection. */ + /* Set base address register to module data address, which should be at least 32-byte aligned. + Mask address to proper range, inner shareable, read write, execute never. */ + module_instance -> txm_module_instance_mpu_registers[TXM_MODULE_MPU_DATA_INDEX].txm_module_mpu_region_base_address = ((ULONG) module_instance -> txm_module_instance_data_start & TXM_MODULE_MPU_ADDRESS_MASK) | TXM_MODULE_ATTRIBUTE_NON_SHAREABLE | TXM_MODULE_ATTRIBUTE_READ_WRITE | TXM_MODULE_ATTRIBUTE_EXECUTE_NEVER; + + /* Adjust the size of the module elements to be aligned to the default alignment. We do this + so that when we partition the allocated memory, we can simply place these regions right beside + each other without having to align their pointers. Note this only works when they all have + the same alignment. */ + + data_size = module_instance -> txm_module_instance_preamble_ptr -> txm_module_preamble_data_size; + start_stop_stack_size = module_instance -> txm_module_instance_preamble_ptr -> txm_module_preamble_start_stop_stack_size; + callback_stack_size = module_instance -> txm_module_instance_preamble_ptr -> txm_module_preamble_callback_stack_size; + + data_size = ((data_size + TXM_MODULE_DATA_ALIGNMENT - 1)/TXM_MODULE_DATA_ALIGNMENT) * TXM_MODULE_DATA_ALIGNMENT; + start_stop_stack_size = ((start_stop_stack_size + TXM_MODULE_DATA_ALIGNMENT - 1)/TXM_MODULE_DATA_ALIGNMENT) * TXM_MODULE_DATA_ALIGNMENT; + callback_stack_size = ((callback_stack_size + TXM_MODULE_DATA_ALIGNMENT - 1)/TXM_MODULE_DATA_ALIGNMENT) * TXM_MODULE_DATA_ALIGNMENT; + + /* Update the data size to include thread stacks. */ + data_size = data_size + start_stop_stack_size + callback_stack_size; + + /* Set the limit address (data start + data size-1), attribute index, and enable bit. */ + module_instance -> txm_module_instance_mpu_registers[TXM_MODULE_MPU_DATA_INDEX].txm_module_mpu_region_limit_address = (((ULONG) module_instance -> txm_module_instance_data_start + data_size - 1) & TXM_MODULE_MPU_ADDRESS_MASK) | TXM_MODULE_ATTRIBUTE_INDEX | TXM_MODULE_ATTRIBUTE_REGION_ENABLE; + /* End of module data protection. */ + + /* Remaining MPU entries are disabled for now and can be used for shared memory. */ +} + +/**************************************************************************/ +/* */ +/* FUNCTION RELEASE */ +/* */ +/* _txm_module_manager_inside_data_check Cortex-M33 */ +/* 6.1.6 */ +/* AUTHOR */ +/* */ +/* Scott Larson, Microsoft Corporation */ +/* */ +/* DESCRIPTION */ +/* */ +/* This function checks if the specified object is inside shared */ +/* memory. */ +/* */ +/* INPUT */ +/* */ +/* module_instance Pointer to module instance */ +/* obj_ptr Pointer to the object */ +/* obj_size Size of the object */ +/* */ +/* OUTPUT */ +/* */ +/* Whether the object is inside the shared memory region. */ +/* */ +/* CALLS */ +/* */ +/* None */ +/* */ +/* CALLED BY */ +/* */ +/* Module dispatch check functions */ +/* */ +/* RELEASE HISTORY */ +/* */ +/* DATE NAME DESCRIPTION */ +/* */ +/* 12-31-2020 Scott Larson Initial Version 6.1.3 */ +/* 04-02-2021 Scott Larson Modified comments, */ +/* resulting in version 6.1.6 */ +/* */ +/**************************************************************************/ +UINT _txm_module_manager_inside_data_check(TXM_MODULE_INSTANCE *module_instance, ALIGN_TYPE obj_ptr, UINT obj_size) +{ + +UINT shared_memory_index; +UINT num_shared_memory_mpu_entries; +ALIGN_TYPE shared_memory_address_start; +ALIGN_TYPE shared_memory_address_end; + + /* Check for overflow. */ + if ((obj_ptr) > ((obj_ptr) + (obj_size))) + { + return(TX_FALSE); + } + + /* Check if the object is inside the module data. */ + if ((obj_ptr >= (ALIGN_TYPE) module_instance -> txm_module_instance_data_start) && + ((obj_ptr + obj_size) <= ((ALIGN_TYPE) module_instance -> txm_module_instance_data_end + 1))) + { + return(TX_TRUE); + } + + /* Check if the object is inside the shared memory. */ + num_shared_memory_mpu_entries = module_instance -> txm_module_instance_shared_memory_count; + for (shared_memory_index = 0; shared_memory_index < num_shared_memory_mpu_entries; shared_memory_index++) + { + + shared_memory_address_start = (ALIGN_TYPE) module_instance -> txm_module_instance_shared_memory_address[shared_memory_index]; + shared_memory_address_end = shared_memory_address_start + module_instance -> txm_module_instance_shared_memory_length[shared_memory_index]; + + if ((obj_ptr >= (ALIGN_TYPE) shared_memory_address_start) && + ((obj_ptr + obj_size) <= (ALIGN_TYPE) shared_memory_address_end)) + { + return(TX_TRUE); + } + } + + return(TX_FALSE); +} From 21210e5cdd93fca05e22b6f6d830e7e7ab5fa753 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20Desbiens?= Date: Tue, 18 Aug 2026 09:31:54 -0400 Subject: [PATCH 03/33] Completed the portable half of the R52 module manager sources Three more port-specific sources: alignment adjustment, the fault notification API, and shared external memory enable. All five C files now compile clean against the module headers. alignment_adjust is worth a note because of how little it does. It rounds code and data size up to the 64-byte granule and sets both alignments to it, and that is the whole function. The Cortex-R4 port's equivalent runs to 183 lines, because PMSAv7 regions must be a power of two in size and aligned to their own size, so a module's code and data have to be grown and repositioned to fit the nearest legal region. Base and limit pairs have no such constraint: any 64-byte-aligned extent is a legal region. The comment says so, because the brevity looks like something missing otherwise. What remains is the assembly and an example, and that is the larger half: - its own copies of the port assembly -- schedule, context save and restore, stack build, system return -- so the region switch happens in the scheduler rather than being called by the thread as in #637 - the module thread stack build and user-mode entry - the abort vector's register capture, which has to read DFSR, DFAR, IFSR and IFAR before anything else can fault over them, in Abort mode with its own banked lr and sp - the port dispatch - a sample module and its build None of that is a copy of the M33 equivalents. Armv8-M scheduling is built on exception return with PSP and MSP and a 697-line scheduler; this core switches ARM modes between EL1 and EL0, and its base scheduler is far smaller. The assembly starts from the working Cortex-R52 port rather than from the M33 module port, with the region load added. Assisted-by: Claude Code (Opus 5) --- .../src/txm_module_manager_alignment_adjust.c | 86 ++++++++++ ...xm_module_manager_external_memory_enable.c | 158 ++++++++++++++++++ .../txm_module_manager_memory_fault_notify.c | 78 +++++++++ .../txm_module_manager_mm_register_setup.c | 2 +- 4 files changed, 323 insertions(+), 1 deletion(-) create mode 100644 ports_module/cortex_r52/gnu/module_manager/src/txm_module_manager_alignment_adjust.c create mode 100644 ports_module/cortex_r52/gnu/module_manager/src/txm_module_manager_external_memory_enable.c create mode 100644 ports_module/cortex_r52/gnu/module_manager/src/txm_module_manager_memory_fault_notify.c diff --git a/ports_module/cortex_r52/gnu/module_manager/src/txm_module_manager_alignment_adjust.c b/ports_module/cortex_r52/gnu/module_manager/src/txm_module_manager_alignment_adjust.c new file mode 100644 index 000000000..48d1eca5c --- /dev/null +++ b/ports_module/cortex_r52/gnu/module_manager/src/txm_module_manager_alignment_adjust.c @@ -0,0 +1,86 @@ +/*************************************************************************** + * Copyright (c) 2024 Microsoft Corporation + * Copyright (c) 2026-present 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. + * + * SPDX-License-Identifier: MIT + **************************************************************************/ + + +/**************************************************************************/ +/**************************************************************************/ +/** */ +/** ThreadX Component */ +/** */ +/** Module Manager */ +/** */ +/**************************************************************************/ +/**************************************************************************/ + +#define TX_SOURCE_CODE + +#include "tx_api.h" +#include "txm_module.h" + + +/**************************************************************************/ +/* */ +/* FUNCTION RELEASE */ +/* */ +/* _txm_module_manager_alignment_adjust Cortex-R52 */ +/* 6.1.8 */ +/* AUTHOR */ +/* */ +/* Scott Larson, Microsoft Corporation */ +/* */ +/* DESCRIPTION */ +/* */ +/* This function adjusts the alignment and size of the code and data */ +/* section for a given module implementation. */ +/* */ +/* INPUT */ +/* */ +/* module_preamble Pointer to module preamble */ +/* code_size Size of the code area (updated) */ +/* code_alignment Code area alignment (updated) */ +/* data_size Size of data area (updated) */ +/* data_alignment Data area alignment (updated) */ +/* */ +/* OUTPUT */ +/* */ +/* None */ +/* */ +/* CALLS */ +/* */ +/* None */ +/* */ +/* CALLED BY */ +/* */ +/* Initial thread stack frame */ +/* */ +/**************************************************************************/ +VOID _txm_module_manager_alignment_adjust(TXM_MODULE_PREAMBLE *module_preamble, + ULONG *code_size, + ULONG *code_alignment, + ULONG *data_size, + ULONG *data_alignment) +{ + + /* Rounding to the granule is all this has to do, and that is a property of + PMSAv8-R rather than a shortcut. The Cortex-R4 port's equivalent runs to + 183 lines because PMSAv7 regions must be a power of two in size and + aligned to their own size, so a module's code and data have to be grown + and repositioned to fit the nearest legal region. Base and limit pairs + have no such constraint: any 64-byte-aligned extent is a legal region. */ + + /* Round code and data size UP to TXM_MODULE_MPU_ALIGNMENT bytes. */ + *code_size = (*code_size + TXM_MODULE_MPU_ALIGNMENT - 1) & ~(TXM_MODULE_MPU_ALIGNMENT - 1); + *data_size = (*data_size + TXM_MODULE_MPU_ALIGNMENT - 1) & ~(TXM_MODULE_MPU_ALIGNMENT - 1); + + /* Alignment for code and data is TXM_MODULE_MPU_ALIGNMENT bytes. */ + *code_alignment = TXM_MODULE_MPU_ALIGNMENT; + *data_alignment = TXM_MODULE_MPU_ALIGNMENT; +} diff --git a/ports_module/cortex_r52/gnu/module_manager/src/txm_module_manager_external_memory_enable.c b/ports_module/cortex_r52/gnu/module_manager/src/txm_module_manager_external_memory_enable.c new file mode 100644 index 000000000..76a7f06ce --- /dev/null +++ b/ports_module/cortex_r52/gnu/module_manager/src/txm_module_manager_external_memory_enable.c @@ -0,0 +1,158 @@ +/*************************************************************************** + * Copyright (c) 2024 Microsoft Corporation + * Copyright (c) 2026-present 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. + * + * SPDX-License-Identifier: MIT + **************************************************************************/ + + +/**************************************************************************/ +/**************************************************************************/ +/** */ +/** ThreadX Component */ +/** */ +/** Module Manager */ +/** */ +/**************************************************************************/ +/**************************************************************************/ + +#define TX_SOURCE_CODE + +#include "tx_api.h" +#include "tx_mutex.h" +#include "tx_queue.h" +#include "tx_thread.h" +#include "txm_module.h" + + +/**************************************************************************/ +/* */ +/* FUNCTION RELEASE */ +/* */ +/* _txm_module_manager_external_memory_enable Cortex-R52 */ +/* 6.1.8 */ +/* AUTHOR */ +/* */ +/* Scott Larson, Microsoft Corporation */ +/* */ +/* DESCRIPTION */ +/* */ +/* This function creates an entry in the MPU table for a shared */ +/* memory space. The start_address must be 32-byte aligned. */ +/* */ +/* INPUT */ +/* */ +/* module_instance Module instance pointer */ +/* start_address Start address of memory */ +/* length Length of external memory */ +/* attributes Memory attributes (r/w) */ +/* */ +/* OUTPUT */ +/* */ +/* status Completion status */ +/* */ +/* CALLS */ +/* */ +/* _tx_mutex_get Get protection mutex */ +/* _tx_mutex_put Release protection mutex */ +/* */ +/* CALLED BY */ +/* */ +/* Application code */ +/* */ +/**************************************************************************/ +UINT _txm_module_manager_external_memory_enable(TXM_MODULE_INSTANCE *module_instance, + VOID *start_address, + ULONG length, + UINT attributes) +{ + +ULONG address; +ULONG shared_index; + + /* Determine if the module manager has not been initialized yet. */ + if (_txm_module_manager_ready != TX_TRUE) + { + /* Module manager has not been initialized. */ + return(TX_NOT_AVAILABLE); + } + + /* Determine if the module is valid. */ + if (module_instance == TX_NULL) + { + /* Invalid module pointer. */ + return(TX_PTR_ERROR); + } + + /* Get module manager protection mutex. */ + _tx_mutex_get(&_txm_module_manager_mutex, TX_WAIT_FOREVER); + + /* Determine if the module instance is valid. */ + if (module_instance -> txm_module_instance_id != TXM_MODULE_ID) + { + /* Release the protection mutex. */ + _tx_mutex_put(&_txm_module_manager_mutex); + + /* Invalid module pointer. */ + return(TX_PTR_ERROR); + } + + /* Determine if the module instance is in the loaded state. */ + if (module_instance -> txm_module_instance_state != TXM_MODULE_LOADED) + { + /* Release the protection mutex. */ + _tx_mutex_put(&_txm_module_manager_mutex); + + /* Return error if the module is not ready. */ + return(TX_START_ERROR); + } + + /* Determine if there are shared memory entries available. */ + if(module_instance -> txm_module_instance_shared_memory_count >= TXM_MODULE_MPU_SHARED_ENTRIES) + { + /* Release the protection mutex. */ + _tx_mutex_put(&_txm_module_manager_mutex); + + /* No more entries available. */ + return(TX_NO_MEMORY); + } + + /* Start address must adhere to Cortex-R52 MPU alignment. */ + address = (ULONG) start_address; + if(address != (address & ~(TXM_MODULE_MPU_ALIGNMENT - 1))) + { + /* Release the protection mutex. */ + _tx_mutex_put(&_txm_module_manager_mutex); + + /* Return alignment error. */ + return(TXM_MODULE_ALIGNMENT_ERROR); + } + + /* At this point, we have a valid address. Set up MPU registers. */ + + /* Pick up index into shared memory entries. */ + shared_index = TXM_MODULE_MPU_SHARED_INDEX + module_instance -> txm_module_instance_shared_memory_count; + + /* Set base address register with start address, sanitized attributes and execute never. */ + module_instance -> txm_module_instance_mpu_registers[shared_index].txm_module_mpu_region_base_address = address | (attributes & TXM_MODULE_ATTRIBUTE_MASK) | TXM_MODULE_ATTRIBUTE_EXECUTE_NEVER; + + /* Set the limit address (data start + length-1), attribute index, and enable bit. */ + module_instance -> txm_module_instance_mpu_registers[shared_index].txm_module_mpu_region_limit_address = (address + length-1) | TXM_MODULE_ATTRIBUTE_INDEX | TXM_MODULE_ATTRIBUTE_REGION_ENABLE; + + /* Keep track of shared memory address and length in module instance. */ + module_instance -> txm_module_instance_shared_memory_address[module_instance -> txm_module_instance_shared_memory_count] = address; + module_instance -> txm_module_instance_shared_memory_length[module_instance -> txm_module_instance_shared_memory_count] = length; + + /* Increment counter. */ + module_instance -> txm_module_instance_shared_memory_count++; + + /* Release the protection mutex. */ + _tx_mutex_put(&_txm_module_manager_mutex); + + /* Return success. */ + return(TX_SUCCESS); +} diff --git a/ports_module/cortex_r52/gnu/module_manager/src/txm_module_manager_memory_fault_notify.c b/ports_module/cortex_r52/gnu/module_manager/src/txm_module_manager_memory_fault_notify.c new file mode 100644 index 000000000..a86196876 --- /dev/null +++ b/ports_module/cortex_r52/gnu/module_manager/src/txm_module_manager_memory_fault_notify.c @@ -0,0 +1,78 @@ +/*************************************************************************** + * Copyright (c) 2024 Microsoft Corporation + * Copyright (c) 2026-present 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. + * + * SPDX-License-Identifier: MIT + **************************************************************************/ + + +/**************************************************************************/ +/**************************************************************************/ +/** */ +/** ThreadX Component */ +/** */ +/** Module Manager */ +/** */ +/**************************************************************************/ +/**************************************************************************/ + +#define TX_SOURCE_CODE + +#include "tx_api.h" +#include "tx_thread.h" +#include "txm_module.h" + + +/* Define the external user's fault notification callback function pointer. This is + setup via the txm_module_manager_memory_fault_notify API. */ + +extern VOID (*_txm_module_manager_fault_notify)(TX_THREAD *, TXM_MODULE_INSTANCE *); + + + +/**************************************************************************/ +/* */ +/* FUNCTION RELEASE */ +/* */ +/* _txm_module_manager_memory_fault_notify Cortex-R52 */ +/* 6.1.8 */ +/* AUTHOR */ +/* */ +/* Scott Larson, Microsoft Corporation */ +/* */ +/* DESCRIPTION */ +/* */ +/* This function registers an application callback when/if a memory */ +/* fault occurs. The supplied thread is automatically terminated, but */ +/* any other threads in the same module may still execute. */ +/* */ +/* INPUT */ +/* */ +/* notify_function Memory fault notification */ +/* function, NULL disables. */ +/* */ +/* OUTPUT */ +/* */ +/* status Completion status */ +/* */ +/* CALLS */ +/* */ +/* None */ +/* */ +/* CALLED BY */ +/* */ +/* Application Code */ +/* */ +/**************************************************************************/ +UINT _txm_module_manager_memory_fault_notify(VOID (*notify_function)(TX_THREAD *, TXM_MODULE_INSTANCE *)) +{ + /* Setup notification function. */ + _txm_module_manager_fault_notify = notify_function; + + /* Return success. */ + return(TX_SUCCESS); +} diff --git a/ports_module/cortex_r52/gnu/module_manager/src/txm_module_manager_mm_register_setup.c b/ports_module/cortex_r52/gnu/module_manager/src/txm_module_manager_mm_register_setup.c index 5fc7edba8..6e487fd56 100644 --- a/ports_module/cortex_r52/gnu/module_manager/src/txm_module_manager_mm_register_setup.c +++ b/ports_module/cortex_r52/gnu/module_manager/src/txm_module_manager_mm_register_setup.c @@ -129,7 +129,7 @@ ULONG callback_stack_size; /* */ /* FUNCTION RELEASE */ /* */ -/* _txm_module_manager_inside_data_check Cortex-M33 */ +/* _txm_module_manager_inside_data_check Cortex-R52 */ /* 6.1.6 */ /* AUTHOR */ /* */ From efe85afe666254a2244d3f7b06ec512cfacefc50 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20Desbiens?= Date: Tue, 18 Aug 2026 09:43:29 -0400 Subject: [PATCH 04/33] Programmed MPU regions directly instead of through PRSELR, and moved the module block The Cortex-R52 TRM provides direct access to PRBAR0 through PRBAR15 and PRLAR0 through PRLAR15 (3.3.85, 3.3.86), encoded CRn c6, CRm c8 + n/2, with opc2 0 and 1 for an even region and 4 and 5 for an odd one. PRBAR and PRLAR without a number are the indirect view that PRSELR selects, and PRSELR needs an ISB before the region registers can be written -- once per region. This was found while sizing the module manager's region switch and it changes the design, so it is applied in both places. Measured on the S32Z280-594EVB, same region and same isolation result either way: through PRSELR 542 to 604 cycles direct 434 to 470 cycles The isolation test still passes, which is what confirms the encoding reaches the region it is supposed to: each thread reaches its own window and faults on the other. A wrong CRm would have programmed some other region and the fault would have gone away. What remains of the 434 is mostly the closing dsb and isb, the table search and two counter reads rather than the two register writes. So a block of regions written directly with one barrier pair at the end costs far less than the per-region figure implies, where the PRSELR route pays an ISB every time. That is the difference between a module switch that is affordable and one that is not: eight regions through PRSELR would have added several thousand cycles to a context switch of about 1400. The module port's region block therefore moves from 9 through 16 to 8 through 15, so all eight entries sit inside the directly addressable range. The board support package's map occupies 0 through 7, so nothing has to move to make room. A board needing more than eight kernel regions must either shrink the module block or accept PRSELR for the overflow, and the header says so. thread_mpu.c uses the direct path as well, which makes the per-thread window from #637 about 22% cheaper. Its PRSELR accessors are kept and marked unused, because regions above 15 have no direct encoding and a later board may need them. Assisted-by: Claude Code (Opus 5) --- .../example_build/s32z280_evb/thread_mpu.c | 45 ++++++++++++++++--- .../cortex_r52/gnu/inc/txm_module_port.h | 24 +++++++--- 2 files changed, 58 insertions(+), 11 deletions(-) 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 index 9042aeb98..ebb2b736e 100644 --- a/ports/cortex_r52/gnu/example_build/s32z280_evb/thread_mpu.c +++ b/ports/cortex_r52/gnu/example_build/s32z280_evb/thread_mpu.c @@ -66,16 +66,52 @@ static unsigned int max_cycles; /* CP15 accessors. */ /**************************************************************************/ +/* Direct per-region access rather than PRSELR. + + The Cortex-R52 TRM 3.3.85 and 3.3.86 provide direct access to PRBAR0 through + PRBAR15 and PRLAR0 through PRLAR15, encoded CRn c6, CRm c8 + n/2, with opc2 0 + and 1 for an even region and 4 and 5 for an odd one. PRBAR and PRLAR without + a number are the indirect view selected by PRSELR. + + Region 8 is therefore CRm c12, opc2 0 and 1. Using it removes the PRSELR + write and, more importantly, the ISB that has to follow PRSELR before the + region registers can be written. + + Measured on this part: 542 to 604 cycles through PRSELR against 434 to 470 + direct, for the same region and the same isolation result. The saving matters + most where several regions are programmed at once, which is what a module + switch does -- there the PRSELR route pays an ISB per region while the direct + route pays one barrier pair for the whole block. + + Only regions 0 through 15 can be reached this way. Above that, PRSELR is the + only option. */ + +/* Kept for regions above 15, which have no direct encoding. Unused while the + per-thread window lives at region 8. */ + +__attribute__((unused)) static void write_prselr(unsigned long value) { __asm__ volatile("mcr p15, 0, %0, c6, c2, 1" : : "r"(value) : "memory"); } +static void write_prbar8_direct(unsigned long value) +{ + __asm__ volatile("mcr p15, 0, %0, c6, c12, 0" : : "r"(value) : "memory"); +} + +static void write_prlar8_direct(unsigned long value) +{ + __asm__ volatile("mcr p15, 0, %0, c6, c12, 1" : : "r"(value) : "memory"); +} + +__attribute__((unused)) static void write_prbar(unsigned long value) { __asm__ volatile("mcr p15, 0, %0, c6, c3, 0" : : "r"(value) : "memory"); } +__attribute__((unused)) static void write_prlar(unsigned long value) { __asm__ volatile("mcr p15, 0, %0, c6, c3, 1" : : "r"(value) : "memory"); @@ -154,9 +190,6 @@ void thread_mpu_activate(TX_THREAD *thread_ptr) } } - write_prselr(THREAD_MPU_REGION); - __asm__ volatile("isb" ::: "memory"); - if (base == 0UL) { /* No window for this thread: disable the region rather than leaving the @@ -164,16 +197,16 @@ void thread_mpu_activate(TX_THREAD *thread_ptr) protection silently becomes no protection -- the last thread to run would leave its window open to whatever ran next. */ - write_prlar(0UL); + write_prlar8_direct(0UL); } else { - write_prbar((base & ~0x3FUL) + write_prbar8_direct((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) + write_prlar8_direct(((base + S32Z_THREAD_WINDOW_SIZE - 1UL) & ~0x3FUL) | ((unsigned long) MPU_ATTR_NORMAL_WB << 1) | 1UL); /* EN */ } diff --git a/ports_module/cortex_r52/gnu/inc/txm_module_port.h b/ports_module/cortex_r52/gnu/inc/txm_module_port.h index 164ac43f7..5fd3aad5f 100644 --- a/ports_module/cortex_r52/gnu/inc/txm_module_port.h +++ b/ports_module/cortex_r52/gnu/inc/txm_module_port.h @@ -159,11 +159,25 @@ The following extensions must also be defined in tx_port.h: starting at TXM_MODULE_MPU_FIRST_REGION. The indices below are relative to that base. - TXM_MODULE_MPU_FIRST_REGION must sit above every region the board support - package programs. The S32Z280 example uses 0 through 8, so 9 is the first - free one; a board that uses more must raise this. */ - -#define TXM_MODULE_MPU_FIRST_REGION 9 + TXM_MODULE_MPU_FIRST_REGION is 8, and the choice is not arbitrary. The TRM + provides direct access to PRBAR0 through PRBAR15 and PRLAR0 through PRLAR15 + (3.3.85, 3.3.86), encoded CRn c6, CRm c8 + n/2, opc2 0 and 1 for an even + region and 4 and 5 for an odd one. Regions above 15 are reachable only + through PRSELR, which selects a region for the indirect PRBAR and PRLAR view + and needs an ISB before those registers can be written -- per region. + + Measured on this part: programming one region through PRSELR costs 542 to 604 + cycles, and the same region written directly costs 434 to 470. Most of what + remains is the closing dsb and isb rather than the writes, so a block of + regions written directly with one barrier pair at the end is far cheaper than + the per-region figure suggests, while the PRSELR route pays an ISB every time. + + Eight entries at 8 through 15 therefore sit entirely within the directly + addressable range, and the board support package's map occupies 0 through 7. + A board needing more than eight kernel regions has to either shrink this block + or accept PRSELR for the overflow. */ + +#define TXM_MODULE_MPU_FIRST_REGION 8 #define TXM_MODULE_MPU_TOTAL_ENTRIES 8 #define TXM_MODULE_MPU_KERNEL_ENTRY_INDEX 0 #define TXM_MODULE_MPU_CODE_INDEX 1 From c95e5942b45b85f7e9f15029d346c8178f139e13 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20Desbiens?= Date: Tue, 18 Aug 2026 09:54:41 -0400 Subject: [PATCH 05/33] Added the module scheduler's region switch, with the offsets checked at build time The scheduler is seeded from the working Cortex-R52 port rather than from the Armv8-M module port, because the two schedule differently: Armv8-M returns through an exception with PSP and MSP and needs 697 lines for it, where this core switches ARM modes and manages in 240. The region switch goes in after the current thread pointer is set and the stack is switched, and it is unrolled rather than looped. CRm and opc2 are encoded in the instruction rather than held in a register, so a loop is not possible for direct region access -- and unrolled is what is wanted anyway: sixteen writes and one barrier pair, where PRSELR would need an ISB per region. A thread that is not a module thread, or a module without protection configured, takes the other path and all eight regions are closed. That is not tidiness. The alternative is that whatever module ran last silently lends its memory to whatever runs next, and per-thread protection quietly becomes no protection at all. Only PRLAR is written there, since clearing the enable bit is what closes a region. The offsets the assembly hard-codes are now verified at compile time in txm_module_manager_offset_check.c, which emits no code. Hard-coded offsets in port assembly are a standing hazard -- issue #577 proposes exactly this check for every port -- and this port is a concrete case for it: the module instance pointer is at 0x94 here against 0x90 on Armv8-M, because the Cortex-R52 port keeps tx_thread_vfp_enable ahead of the module fields in TX_THREAD_EXTENSION_2. Copying the Armv8-M number would have built cleanly and misbehaved on the board. The check was tested in both directions. It passes against the real structures, and with the offset changed to the Armv8-M value it fails the build with the message naming what would go wrong. An assertion that only ever passes proves very little; one that provably rejects the mistake it exists to catch is worth having. It also guards the region-table shape the unrolled sequence depends on -- eight entries of two words each -- and that the block stays inside the directly addressable range, since regions above 15 have no PRBARn encoding. Assisted-by: Claude Code (Opus 5) --- .../module_manager/src/tx_thread_schedule.S | 325 ++++++++++++++++++ .../src/txm_module_manager_offset_check.c | 99 ++++++ 2 files changed, 424 insertions(+) create mode 100644 ports_module/cortex_r52/gnu/module_manager/src/tx_thread_schedule.S create mode 100644 ports_module/cortex_r52/gnu/module_manager/src/txm_module_manager_offset_check.c diff --git a/ports_module/cortex_r52/gnu/module_manager/src/tx_thread_schedule.S b/ports_module/cortex_r52/gnu/module_manager/src/tx_thread_schedule.S new file mode 100644 index 000000000..24d6ab27c --- /dev/null +++ b/ports_module/cortex_r52/gnu/module_manager/src/tx_thread_schedule.S @@ -0,0 +1,325 @@ +@/*************************************************************************** +@ * Copyright (c) 2024 Microsoft Corporation +@ * 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. +@ * +@ * SPDX-License-Identifier: MIT +@ **************************************************************************/ +@ Some portions generated by Claude Code (Opus 5). +@ +@ +@/**************************************************************************/ +@/**************************************************************************/ +@/** */ +@/** ThreadX Component */ +@/** */ +@/** Thread */ +@/** */ +@/**************************************************************************/ +@/**************************************************************************/ +#ifdef TX_INCLUDE_USER_DEFINE_FILE +#include "tx_user.h" +#endif + +@ Offsets into TX_THREAD and TXM_MODULE_INSTANCE. Hard-coded offsets in port +@ assembly are a standing hazard -- see issue #577 -- so these are checked +@ against offsetof at compile time in txm_module_manager_offset_check.c, and a +@ mismatch is a build failure rather than a fault at run time. +@ +@ They are not the same as the Armv8-M port's: there the module instance pointer +@ sits at 0x90, and here it is 0x94 because the Cortex-R52 port keeps +@ tx_thread_vfp_enable ahead of the module fields in TX_THREAD_EXTENSION_2. + + .equ TXM_THREAD_MODULE_INSTANCE, 0x94 + .equ TXM_INSTANCE_DATA_START, 0x2C + .equ TXM_INSTANCE_MPU_REGISTERS, 0x64 + + .global _tx_thread_execute_ptr + .global _tx_thread_current_ptr + .global _tx_timer_time_slice + .global _tx_execution_thread_enter +@ +@ +@/* Define the 16-bit Thumb mode veneer for _tx_thread_schedule for +@ applications calling this function from to 16-bit Thumb mode. */ +@ + .text + .align 2 + .global $_tx_thread_schedule + .type $_tx_thread_schedule,function +$_tx_thread_schedule: + .thumb + BX pc @ Switch to 32-bit mode + NOP @ + .arm + STMFD sp!, {lr} @ Save return address + BL _tx_thread_schedule @ Call _tx_thread_schedule function + LDMFD sp!, {lr} @ Recover saved return address + BX lr @ Return to 16-bit caller +@ +@ + .text + .align 2 +@/**************************************************************************/ +@/* */ +@/* FUNCTION RELEASE */ +@/* */ +@/* _tx_thread_schedule Cortex-R52/GNU */ +@/* 6.5.2 */ +@/* AUTHOR */ +@/* */ +@/* Frédéric Desbiens, Eclipse Foundation */ +@/* */ +@/* Derived from the Cortex-R5/GNU port originally written by */ +@/* William E. Lamie, Microsoft Corporation. */ +@/* */ +@/* DESCRIPTION */ +@/* */ +@/* This function waits for a thread control block pointer to appear in */ +@/* the _tx_thread_execute_ptr variable. Once a thread pointer appears */ +@/* in the variable, the corresponding thread is resumed. */ +@/* */ +@/* INPUT */ +@/* */ +@/* None */ +@/* */ +@/* OUTPUT */ +@/* */ +@/* None */ +@/* */ +@/* CALLS */ +@/* */ +@/* None */ +@/* */ +@/* CALLED BY */ +@/* */ +@/* _tx_initialize_kernel_enter ThreadX entry function */ +@/* _tx_thread_system_return Return to system from thread */ +@/* _tx_thread_context_restore Restore thread's context */ +@/* */ +@/**************************************************************************/ +@VOID _tx_thread_schedule(VOID) +@{ + .global _tx_thread_schedule + .type _tx_thread_schedule,function +_tx_thread_schedule: +@ +@ /* Enable interrupts. */ +@ +#ifdef TX_ENABLE_FIQ_SUPPORT + CPSIE if @ Enable IRQ and FIQ interrupts +#else + CPSIE i @ Enable IRQ interrupts +#endif +@ +@ /* Wait for a thread to execute. */ +@ do +@ { + LDR r1, =_tx_thread_execute_ptr @ Address of thread execute ptr +@ +__tx_thread_schedule_loop: +@ + LDR r0, [r1] @ Pickup next thread to execute + CMP r0, #0 @ Is it NULL? + BEQ __tx_thread_schedule_loop @ If so, keep looking for a thread +@ +@ } +@ while(_tx_thread_execute_ptr == TX_NULL); +@ +@ /* Yes! We have a thread to execute. Lockout interrupts and +@ transfer control to it. */ +@ +#ifdef TX_ENABLE_FIQ_SUPPORT + CPSID if @ Disable IRQ and FIQ interrupts +#else + CPSID i @ Disable IRQ interrupts +#endif +@ +@ /* Setup the current thread pointer. */ +@ _tx_thread_current_ptr = _tx_thread_execute_ptr; +@ + LDR r1, =_tx_thread_current_ptr @ Pickup address of current thread + STR r0, [r1] @ Setup current thread pointer +@ +@ /* Increment the run count for this thread. */ +@ _tx_thread_current_ptr -> tx_thread_run_count++; +@ + LDR r2, [r0, #4] @ Pickup run counter + LDR r3, [r0, #24] @ Pickup time-slice for this thread + ADD r2, r2, #1 @ Increment thread run-counter + STR r2, [r0, #4] @ Store the new run counter +@ +@ /* Setup time-slice, if present. */ +@ _tx_timer_time_slice = _tx_thread_current_ptr -> tx_thread_time_slice; +@ + LDR r2, =_tx_timer_time_slice @ Pickup address of time-slice + @ variable + LDR sp, [r0, #8] @ Switch stack pointers + STR r3, [r2] @ Setup time-slice +@ +@ /* Load this thread's MPU regions, if it belongs to a module. */ +@ + LDR r1, [r0, #TXM_THREAD_MODULE_INSTANCE] @ Pickup module instance pointer + CMP r1, #0 @ Is this a module thread? + BEQ __tx_module_regions_off @ No, close the module regions + LDR r2, [r1, #TXM_INSTANCE_DATA_START] @ Pickup module data start + CMP r2, #0 @ Is protection configured? + BEQ __tx_module_regions_off @ No, close the module regions +@ +@ /* Eight regions, written directly rather than through PRSELR. CRm and opc2 +@ are encoded in the instruction, not held in a register, so this cannot be +@ a loop -- and unrolled is what we want anyway: sixteen writes with one +@ barrier pair at the end, where PRSELR would need an ISB per region. +@ +@ Regions 8 and 9 are CRm c12, 10 and 11 are c13, 12 and 13 are c14, 14 and +@ 15 are c15. Even regions use opc2 0 and 1, odd ones 4 and 5. */ +@ + ADD r1, r1, #TXM_INSTANCE_MPU_REGISTERS @ Address of the region table +@ + LDMIA r1!, {r2, r3} @ Region 8: base, limit + MCR p15, 0, r2, c6, c12, 0 @ PRBAR8 + MCR p15, 0, r3, c6, c12, 1 @ PRLAR8 + LDMIA r1!, {r2, r3} @ Region 9 + MCR p15, 0, r2, c6, c12, 4 @ PRBAR9 + MCR p15, 0, r3, c6, c12, 5 @ PRLAR9 + LDMIA r1!, {r2, r3} @ Region 10 + MCR p15, 0, r2, c6, c13, 0 @ PRBAR10 + MCR p15, 0, r3, c6, c13, 1 @ PRLAR10 + LDMIA r1!, {r2, r3} @ Region 11 + MCR p15, 0, r2, c6, c13, 4 @ PRBAR11 + MCR p15, 0, r3, c6, c13, 5 @ PRLAR11 + LDMIA r1!, {r2, r3} @ Region 12 + MCR p15, 0, r2, c6, c14, 0 @ PRBAR12 + MCR p15, 0, r3, c6, c14, 1 @ PRLAR12 + LDMIA r1!, {r2, r3} @ Region 13 + MCR p15, 0, r2, c6, c14, 4 @ PRBAR13 + MCR p15, 0, r3, c6, c14, 5 @ PRLAR13 + LDMIA r1!, {r2, r3} @ Region 14 + MCR p15, 0, r2, c6, c15, 0 @ PRBAR14 + MCR p15, 0, r3, c6, c15, 1 @ PRLAR14 + LDMIA r1!, {r2, r3} @ Region 15 + MCR p15, 0, r2, c6, c15, 4 @ PRBAR15 + MCR p15, 0, r3, c6, c15, 5 @ PRLAR15 +@ + DSB sy @ One barrier pair for the block + ISB + B __tx_module_regions_done +@ +__tx_module_regions_off: +@ +@ /* Not a module thread, or a module without protection. Close all eight +@ regions rather than leaving the previous module's windows open: a thread +@ that owns nothing must reach nothing, and the alternative is that whatever +@ ran last silently lends its memory to whatever runs next. +@ +@ Only PRLAR is written, because clearing its enable bit is what closes the +@ region; the base is irrelevant while it is disabled. */ +@ + MOV r2, #0 @ Region disabled + MCR p15, 0, r2, c6, c12, 1 @ PRLAR8 + MCR p15, 0, r2, c6, c12, 5 @ PRLAR9 + MCR p15, 0, r2, c6, c13, 1 @ PRLAR10 + MCR p15, 0, r2, c6, c13, 5 @ PRLAR11 + MCR p15, 0, r2, c6, c14, 1 @ PRLAR12 + MCR p15, 0, r2, c6, c14, 5 @ PRLAR13 + MCR p15, 0, r2, c6, c15, 1 @ PRLAR14 + MCR p15, 0, r2, c6, c15, 5 @ PRLAR15 + DSB sy + ISB +@ +__tx_module_regions_done: +@ +@ /* Switch to the thread's stack. */ +@ sp = _tx_thread_execute_ptr -> tx_thread_stack_ptr; +@ +#ifdef TX_ENABLE_EXECUTION_CHANGE_NOTIFY +@ +@ /* Call the thread entry function to indicate the thread is executing. */ +@ + BL _tx_execution_thread_enter @ Call the thread execution enter function +#endif +@ +@ /* Determine if an interrupt frame or a synchronous task suspension frame +@ is present. */ +@ + LDMIA sp!, {r4, r5} @ Pickup the stack type and saved CPSR + CMP r4, #0 @ Check for synchronous context switch + BEQ _tx_solicited_return + MSR SPSR_cxsf, r5 @ Setup SPSR for return +#ifdef TX_ENABLE_VFP_SUPPORT + LDR r1, [r0, #144] @ Pickup the VFP enabled flag + CMP r1, #0 @ Is the VFP enabled? + BEQ _tx_skip_interrupt_vfp_restore @ No, skip VFP interrupt restore + VLDMIA sp!, {D0-D15} @ Recover D0-D15 + LDR r4, [sp], #4 @ Pickup FPSCR + VMSR FPSCR, r4 @ Restore FPSCR +_tx_skip_interrupt_vfp_restore: +#endif + LDMIA sp!, {r0-r12, lr, pc}^ @ Return to point of thread interrupt +@ +_tx_solicited_return: +#ifdef TX_ENABLE_VFP_SUPPORT + LDR r1, [r0, #144] @ Pickup the VFP enabled flag + CMP r1, #0 @ Is the VFP enabled? + BEQ _tx_skip_solicited_vfp_restore @ No, skip VFP solicited restore + VLDMIA sp!, {D8-D15} @ Recover D8-D15 + LDR r4, [sp], #4 @ Pickup FPSCR + VMSR FPSCR, r4 @ Restore FPSCR +_tx_skip_solicited_vfp_restore: +#endif + MOV r0, r5 @ Move CPSR to scratch register + LDMIA sp!, {r4-r11, lr} @ Return to thread synchronously + MSR CPSR_cxsf, r0 @ Recover CPSR +@ +#ifdef __THUMB_INTERWORK + BX lr @ Return to caller +#else + MOV pc, lr @ Return to caller +#endif +@ +@} +@ +@ +#ifdef TX_ENABLE_VFP_SUPPORT + .global tx_thread_vfp_enable + .type tx_thread_vfp_enable,function +tx_thread_vfp_enable: + MRS r2, CPSR @ Pickup the CPSR +#ifdef TX_ENABLE_FIQ_SUPPORT + CPSID if @ Disable IRQ and FIQ interrupts +#else + CPSID i @ Disable IRQ interrupts +#endif + LDR r0, =_tx_thread_current_ptr @ Build current thread pointer address + LDR r1, [r0] @ Pickup current thread pointer + CMP r1, #0 @ Check for NULL thread pointer + BEQ __tx_no_thread_to_enable @ If NULL, skip VFP enable + MOV r0, #1 @ Build enable value + STR r0, [r1, #144] @ Set the VFP enable flag (tx_thread_vfp_enable field in TX_THREAD) +__tx_no_thread_to_enable: + MSR CPSR_cxsf, r2 @ Recover CPSR + BX LR @ Return to caller +@ + .global tx_thread_vfp_disable + .type tx_thread_vfp_disable,function +tx_thread_vfp_disable: + MRS r2, CPSR @ Pickup the CPSR +#ifdef TX_ENABLE_FIQ_SUPPORT + CPSID if @ Disable IRQ and FIQ interrupts +#else + CPSID i @ Disable IRQ interrupts +#endif + LDR r0, =_tx_thread_current_ptr @ Build current thread pointer address + LDR r1, [r0] @ Pickup current thread pointer + CMP r1, #0 @ Check for NULL thread pointer + BEQ __tx_no_thread_to_disable @ If NULL, skip VFP disable + MOV r0, #0 @ Build disable value + STR r0, [r1, #144] @ Clear the VFP enable flag (tx_thread_vfp_enable field in TX_THREAD) +__tx_no_thread_to_disable: + MSR CPSR_cxsf, r2 @ Recover CPSR + BX LR @ Return to caller +#endif + diff --git a/ports_module/cortex_r52/gnu/module_manager/src/txm_module_manager_offset_check.c b/ports_module/cortex_r52/gnu/module_manager/src/txm_module_manager_offset_check.c new file mode 100644 index 000000000..caf01e33d --- /dev/null +++ b/ports_module/cortex_r52/gnu/module_manager/src/txm_module_manager_offset_check.c @@ -0,0 +1,99 @@ +/*************************************************************************** + * 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 + **************************************************************************/ + +/**************************************************************************/ +/* */ +/* MODULE MANAGER RELEASE */ +/* */ +/* txm_module_manager_offset_check.c Cortex-R52/GNU */ +/* 6.5.2 */ +/* AUTHOR */ +/* */ +/* Frédéric Desbiens, Eclipse Foundation */ +/* */ +/* DESCRIPTION */ +/* */ +/* Compile-time verification of the structure offsets that this port's */ +/* assembly hard-codes. */ +/* */ +/* tx_thread_schedule.S reaches into TX_THREAD and TXM_MODULE_INSTANCE */ +/* with numeric offsets, because assembly has no other way to do it. */ +/* Nothing in the toolchain connects those numbers to the structures */ +/* they describe, so adding a field, reordering an extension or */ +/* building with a different set of ThreadX options silently moves the */ +/* target and the scheduler reads the wrong word. The failure is a */ +/* corrupted region table or a fault in a thread that did nothing wrong, */ +/* a long way from the change that caused it. */ +/* */ +/* This file exists so that becomes a build error instead. It emits no */ +/* code. */ +/* */ +/* The offsets here are not the same as the Armv8-M module port's, and */ +/* that is the concrete case in point: there the module instance pointer */ +/* sits at 0x90, and here it is 0x94, because the Cortex-R52 port keeps */ +/* tx_thread_vfp_enable ahead of the module fields in */ +/* TX_THREAD_EXTENSION_2. Copying the Armv8-M numbers would have built */ +/* cleanly and misbehaved on the board. */ +/* */ +/* See eclipse-threadx/threadx issue #577, which proposes this check for */ +/* every port rather than only this one. */ +/* */ +/**************************************************************************/ + +#include +#include "tx_api.h" +#include "txm_module.h" + +/* These must match the .equ values at the top of tx_thread_schedule.S. */ + +#define TXM_THREAD_MODULE_INSTANCE 0x94 +#define TXM_INSTANCE_DATA_START 0x2C +#define TXM_INSTANCE_MPU_REGISTERS 0x64 + +_Static_assert(offsetof(TX_THREAD, tx_thread_module_instance_ptr) + == TXM_THREAD_MODULE_INSTANCE, + "tx_thread_schedule.S TXM_THREAD_MODULE_INSTANCE no longer matches " + "TX_THREAD; the scheduler would read the wrong word"); + +_Static_assert(offsetof(TXM_MODULE_INSTANCE, txm_module_instance_data_start) + == TXM_INSTANCE_DATA_START, + "tx_thread_schedule.S TXM_INSTANCE_DATA_START no longer matches " + "TXM_MODULE_INSTANCE; the protection check would test the wrong field"); + +_Static_assert(offsetof(TXM_MODULE_INSTANCE, txm_module_instance_mpu_registers) + == TXM_INSTANCE_MPU_REGISTERS, + "tx_thread_schedule.S TXM_INSTANCE_MPU_REGISTERS no longer matches " + "TXM_MODULE_INSTANCE; the scheduler would load regions from the " + "wrong address"); + +/* The scheduler loads exactly eight regions as sixteen consecutive words, with + LDMIA walking the table two words at a time. If the entry stops being two + words, or the table stops holding eight of them, the unrolled sequence walks + off the end of it. */ + +_Static_assert(sizeof(TXM_MODULE_MPU_INFO) == (2U * sizeof(ULONG)), + "TXM_MODULE_MPU_INFO is no longer two words; the unrolled LDMIA " + "sequence in tx_thread_schedule.S would misread the region table"); + +_Static_assert(TXM_MODULE_MPU_TOTAL_ENTRIES == 8, + "tx_thread_schedule.S has an unrolled sequence for exactly eight " + "regions; change both together"); + +/* The block has to sit inside the directly addressable range. Regions above 15 + are reachable only through PRSELR, which the scheduler does not use. */ + +_Static_assert((TXM_MODULE_MPU_FIRST_REGION + TXM_MODULE_MPU_TOTAL_ENTRIES) <= 16, + "the module region block extends past region 15, which has no " + "direct PRBARn encoding; the scheduler cannot reach it"); From c102b7c13e28fd89405b2e68e81dbecdff196531 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20Desbiens?= Date: Tue, 18 Aug 2026 10:13:47 -0400 Subject: [PATCH 06/33] Added the abort-vector fault capture, and found three bugs writing it Completes the fault path. The C handler was already in place and is architecture-neutral -- it terminates the offending thread and notifies the application -- and this is the part that has to be assembly: the fault registers hold only the most recent fault, so anything that runs first and faults destroys the evidence, and the abort arrives in Abort mode with its own banked sp and lr so C cannot be entered until a stack is known good. Two entry points, because this core reports the two kinds of violation differently. A module writing outside its data region raises a data abort and reports through DFSR and DFAR; a module branching outside its code region raises a prefetch abort and reports through IFSR and IFAR. Both pairs are recorded from either entry, so a reader can tell which one is meaningful. Three bugs in the first version of this, all caught by reading it rather than by its assembling cleanly, which it did throughout: The mode switch used ORR alone to select System mode. That happens to work from Abort mode, whose encoding is 0x17, and would silently select the wrong mode from anywhere else. It clears the field first now. The recovery of the pushed registers built its destination address in the same registers it had just loaded the values into, so the first store wrote the address instead of the module's r0 and the rest were shifted. It also loaded four words where five had been pushed. Copied one at a time now, which is duller and correct. The unexpected-return path restored and returned to the faulting instruction, which would fault again immediately and for ever. It stops instead, where a debugger can see why. The handler is not expected to return at all -- it terminates the thread and the scheduler runs something else. One limitation is documented rather than left to be discovered: the captured sp and lr are read in System mode, which shares User mode's banked copies. That is right for a module fault, which happens in User mode, and wrong for a fault taken in a privileged mode, where System's registers are not that mode's. SPSR records which happened, so a reader can tell when those two fields do not apply. The offsets this file hard-codes are checked against offsetof alongside the scheduler's, including that r4 through r11 are four words past r0 and that r12 is where the block store expects, since the capture writes them as blocks. Assisted-by: Claude Code (Opus 5) --- .../src/txm_module_manager_fault_capture.S | 194 ++++++++++++++++++ .../src/txm_module_manager_offset_check.c | 38 ++++ 2 files changed, 232 insertions(+) create mode 100644 ports_module/cortex_r52/gnu/module_manager/src/txm_module_manager_fault_capture.S diff --git a/ports_module/cortex_r52/gnu/module_manager/src/txm_module_manager_fault_capture.S b/ports_module/cortex_r52/gnu/module_manager/src/txm_module_manager_fault_capture.S new file mode 100644 index 000000000..02cc58fed --- /dev/null +++ b/ports_module/cortex_r52/gnu/module_manager/src/txm_module_manager_fault_capture.S @@ -0,0 +1,194 @@ +@/*************************************************************************** +@ * 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 +@ **************************************************************************/ +@ +@/**************************************************************************/ +@/* */ +@/* MODULE MANAGER RELEASE */ +@/* */ +@/* txm_module_manager_fault_capture.S Cortex-R52/GNU */ +@/* 6.5.2 */ +@/* AUTHOR */ +@/* */ +@/* Frédéric Desbiens, Eclipse Foundation */ +@/* */ +@/* DESCRIPTION */ +@/* */ +@/* Records why a memory protection fault happened, then hands over to */ +@/* the C handler which terminates the offending thread. */ +@/* */ +@/* A board's abort vectors branch here. Two entry points, because this */ +@/* core reports the two kinds of violation in different registers: a */ +@/* module writing outside its data region raises a data abort and */ +@/* reports through DFSR and DFAR, while a module branching outside its */ +@/* code region raises a prefetch abort and reports through IFSR and */ +@/* IFAR. Both are recorded either way, so a reader of the fault info */ +@/* can tell which pair is meaningful, and both entry points converge. */ +@/* */ +@/* This has to be assembly, and it has to run first. The fault */ +@/* registers hold only the most recent fault, so anything that runs */ +@/* before the capture and faults itself destroys the evidence. The */ +@/* abort is also taken in Abort mode, with its own banked sp and lr, so */ +@/* C cannot be entered until a stack is known good. */ +@/* */ +@/**************************************************************************/ + + .arm + .text + .align 2 + + .global _txm_module_manager_data_abort + .global _txm_module_manager_prefetch_abort + .extern _txm_module_manager_memory_fault_info + .extern _txm_module_manager_memory_fault_handler + .extern _tx_thread_current_ptr + +@ Offsets into TXM_MODULE_MANAGER_MEMORY_FAULT_INFO. Checked against offsetof at +@ compile time in txm_module_manager_offset_check.c. + + .equ FAULT_THREAD_PTR, 0x00 + .equ FAULT_CODE_LOCATION, 0x04 + .equ FAULT_DFSR, 0x08 + .equ FAULT_DFAR, 0x0C + .equ FAULT_IFSR, 0x10 + .equ FAULT_IFAR, 0x14 + .equ FAULT_SP, 0x18 + .equ FAULT_R0, 0x1C + .equ FAULT_LR, 0x50 + .equ FAULT_SPSR, 0x54 + + +@/**************************************************************************/ +@/* Data abort: a module wrote or read outside a region it owns. */ +@/**************************************************************************/ + + .type _txm_module_manager_data_abort, %function +_txm_module_manager_data_abort: + +@ lr on data abort entry is the faulting address plus 8. Recorded as the +@ faulting instruction rather than the return address, because the value that +@ helps is where the module went wrong. + + SUB lr, lr, #8 + B _txm_module_manager_fault_common + + +@/**************************************************************************/ +@/* Prefetch abort: a module tried to execute outside its code region. */ +@/**************************************************************************/ + + .type _txm_module_manager_prefetch_abort, %function +_txm_module_manager_prefetch_abort: + +@ lr on prefetch abort entry is the faulting address plus 4. + + SUB lr, lr, #4 + + +@/**************************************************************************/ +@/* Common path. */ +@/**************************************************************************/ + +_txm_module_manager_fault_common: + +@ Save the registers this routine is about to use, on the Abort mode stack, so +@ the values recorded below are the module's and not ours. + + STMDB sp!, {r0-r3, r12} + + LDR r0, =_txm_module_manager_memory_fault_info + +@ Which thread was running, and where it was. + + LDR r1, =_tx_thread_current_ptr + LDR r1, [r1] + STR r1, [r0, #FAULT_THREAD_PTR] + STR lr, [r0, #FAULT_CODE_LOCATION] + +@ Both fault register pairs. Read before anything else can fault over them. + + MRC p15, 0, r1, c5, c0, 0 @ DFSR + STR r1, [r0, #FAULT_DFSR] + MRC p15, 0, r1, c6, c0, 0 @ DFAR + STR r1, [r0, #FAULT_DFAR] + MRC p15, 0, r1, c5, c0, 1 @ IFSR + STR r1, [r0, #FAULT_IFSR] + MRC p15, 0, r1, c6, c0, 2 @ IFAR + STR r1, [r0, #FAULT_IFAR] + +@ The mode the faulting code was in, which is what says whether this was the +@ module in user mode or the kernel itself. + + MRS r1, SPSR + STR r1, [r0, #FAULT_SPSR] + +@ The faulting context's own sp and lr, read from its mode rather than Abort's. +@ Without the mode switch these would be Abort mode's banked copies, which say +@ nothing about the module. +@ +@ System mode is used because it shares User mode's banked sp and lr, and a +@ module faults in User mode. That is exactly right for the case this exists +@ to report and wrong for one it does not: a fault taken in a privileged mode +@ gets System's sp and lr, which are not that mode's. SPSR says which happened, +@ so a reader can tell when these two fields do not apply. + + MRS r2, CPSR + BIC r3, r2, #0x1F @ Clear the mode field first: + ORR r3, r3, #0x1F @ ORR alone would only give + MSR CPSR_c, r3 @ System mode by luck, and + @ only from Abort mode. + @ System shares User's + @ banked sp and lr. + MOV r1, sp + MOV r12, lr + MSR CPSR_c, r2 @ Back to Abort mode + STR r1, [r0, #FAULT_SP] + STR r12, [r0, #FAULT_LR] + +@ The module's r0 through r3 and r12, from where they were pushed above, then +@ r4 through r11 which the fault did not disturb. + +@ Five words were pushed, r0 through r3 and r12, and they are copied one at a +@ time. Loading them as a group and then building the destination address in +@ the same registers destroys the values on the way past, which is what the +@ first version of this did. + + LDR r1, [sp, #0] @ Module r0 + STR r1, [r0, #FAULT_R0] + LDR r1, [sp, #4] @ Module r1 + STR r1, [r0, #(FAULT_R0 + 4)] + LDR r1, [sp, #8] @ Module r2 + STR r1, [r0, #(FAULT_R0 + 8)] + LDR r1, [sp, #12] @ Module r3 + STR r1, [r0, #(FAULT_R0 + 12)] + LDR r1, [sp, #16] @ Module r12 + STR r1, [r0, #(FAULT_R0 + 0x30)] @ r12 sits after r11 + +@ r4 through r11 were never disturbed, so they can go straight out. + + ADD r1, r0, #(FAULT_R0 + 16) + STMIA r1, {r4-r11} + +@ Hand over. The handler terminates the thread and notifies the application; +@ it does not return here. + + BL _txm_module_manager_memory_fault_handler + +@ Not reached. The handler terminates the offending thread and the scheduler +@ runs something else, so it does not come back. If it ever did, returning +@ would resume the instruction that faulted and fault again immediately, for +@ ever -- so stop here instead, where a debugger can see why. + +__txm_module_fault_unexpected_return: + B __txm_module_fault_unexpected_return diff --git a/ports_module/cortex_r52/gnu/module_manager/src/txm_module_manager_offset_check.c b/ports_module/cortex_r52/gnu/module_manager/src/txm_module_manager_offset_check.c index caf01e33d..df6524ca9 100644 --- a/ports_module/cortex_r52/gnu/module_manager/src/txm_module_manager_offset_check.c +++ b/ports_module/cortex_r52/gnu/module_manager/src/txm_module_manager_offset_check.c @@ -78,6 +78,44 @@ _Static_assert(offsetof(TXM_MODULE_INSTANCE, txm_module_instance_mpu_registers) "TXM_MODULE_INSTANCE; the scheduler would load regions from the " "wrong address"); +/* Offsets the fault capture hard-codes, from + txm_module_manager_fault_capture.S. Same hazard as the scheduler's: the + capture runs in Abort mode with a fault in progress, and a wrong offset there + writes the evidence into the wrong field of a structure the application is + about to read. */ + +#define FAULT_THREAD_PTR 0x00 +#define FAULT_CODE_LOCATION 0x04 +#define FAULT_DFSR 0x08 +#define FAULT_DFAR 0x0C +#define FAULT_IFSR 0x10 +#define FAULT_IFAR 0x14 +#define FAULT_SP 0x18 +#define FAULT_R0 0x1C +#define FAULT_LR 0x50 +#define FAULT_SPSR 0x54 + +#define FAULT_OFF(f) offsetof(TXM_MODULE_MANAGER_MEMORY_FAULT_INFO, \ + txm_module_manager_memory_fault_info_##f) + +_Static_assert(FAULT_OFF(thread_ptr) == FAULT_THREAD_PTR, "fault capture: thread_ptr offset moved"); +_Static_assert(FAULT_OFF(code_location) == FAULT_CODE_LOCATION, "fault capture: code_location offset moved"); +_Static_assert(FAULT_OFF(dfsr) == FAULT_DFSR, "fault capture: dfsr offset moved"); +_Static_assert(FAULT_OFF(dfar) == FAULT_DFAR, "fault capture: dfar offset moved"); +_Static_assert(FAULT_OFF(ifsr) == FAULT_IFSR, "fault capture: ifsr offset moved"); +_Static_assert(FAULT_OFF(ifar) == FAULT_IFAR, "fault capture: ifar offset moved"); +_Static_assert(FAULT_OFF(sp) == FAULT_SP, "fault capture: sp offset moved"); +_Static_assert(FAULT_OFF(r0) == FAULT_R0, "fault capture: r0 offset moved"); +_Static_assert(FAULT_OFF(lr) == FAULT_LR, "fault capture: lr offset moved"); +_Static_assert(FAULT_OFF(spsr) == FAULT_SPSR, "fault capture: spsr offset moved"); + +/* The capture writes r4 through r11 as one block starting sixteen bytes past r0, + and r12 thirty-two bytes past that, which assumes the registers are laid out + in order with no padding. */ + +_Static_assert(FAULT_OFF(r4) == (FAULT_R0 + 16), "fault capture: r4 is not four words past r0"); +_Static_assert(FAULT_OFF(r12) == (FAULT_R0 + 0x30), "fault capture: r12 is not where the block store expects"); + /* The scheduler loads exactly eight regions as sixteen consecutive words, with LDMIA walking the table two words at a time. If the entry stops being two words, or the table stops holding eight of them, the unrolled sequence walks From 3e8570daef5bdbc0ba88cc78516ab89d1c891a0f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20Desbiens?= Date: Tue, 18 Aug 2026 10:37:38 -0400 Subject: [PATCH 07/33] Added the module user-mode entry, the only way a module reaches the kernel A module runs in User mode and cannot execute kernel code or touch kernel memory. When it calls a ThreadX service the call arrives here: SVC 1 raises privilege, the dispatch function performs the service, SVC 2 drops back to User mode, and the module continues. Everything the module is allowed to ask for is decided inside _txm_module_manager_kernel_dispatch, in memory the module cannot reach. Derived from the Cortex-R4 module port rather than the Armv8-M one, which is the right parent for this file specifically. Privilege on Armv8-M is a bit in CONTROL and the transition rides an exception return; in AArch32 it is the processor mode, and SVC is how a module asks to change it. The R4 sequence transfers directly even though its MPU does not. One improvement over the R4 version, and it is about isolation rather than memory. That port aligns this function to 4 KB because PMSAv7 regions must be a power of two in size and aligned to their own size, so the smallest region that covers the entry without covering its neighbours is a page -- and whatever else shares that page is then also executable by the module. A base and limit pair has no such constraint. The function is 64-byte aligned, and the measured privileged surface a module can execute is 24 bytes. The kernel entry region's limit is taken from a symbol at the end of the function rather than from its base. One granule covers 24 bytes so the base would work today, but if the entry ever grows past 64 bytes, sizing from the base would leave its tail outside the region and a module would fault on a call it is entitled to make. SVC 2 sits between the dispatch and every return path rather than on one of them. Skipping it would return the module to its own code still privileged, which is the entire protection gone, so it is unconditional. The supervisor call vector that recognises SVC 1 and SVC 2 is the next piece and does not exist yet. Assisted-by: Claude Code (Opus 5) --- .../txm_module_manager_mm_register_setup.c | 14 ++- .../src/txm_module_manager_user_mode_entry.S | 109 ++++++++++++++++++ 2 files changed, 122 insertions(+), 1 deletion(-) create mode 100644 ports_module/cortex_r52/gnu/module_manager/src/txm_module_manager_user_mode_entry.S diff --git a/ports_module/cortex_r52/gnu/module_manager/src/txm_module_manager_mm_register_setup.c b/ports_module/cortex_r52/gnu/module_manager/src/txm_module_manager_mm_register_setup.c index 6e487fd56..2a3fe6791 100644 --- a/ports_module/cortex_r52/gnu/module_manager/src/txm_module_manager_mm_register_setup.c +++ b/ports_module/cortex_r52/gnu/module_manager/src/txm_module_manager_mm_register_setup.c @@ -72,6 +72,11 @@ configuration would need to revisit this, and would need to revisit far more than this. */ +/* Marks the end of the user-mode entry function, so the kernel entry region can + be sized to it. */ + +extern VOID _txm_module_manager_user_mode_entry_end(VOID); + VOID _txm_module_manager_mm_register_setup(TXM_MODULE_INSTANCE *module_instance) { @@ -84,7 +89,14 @@ ULONG callback_stack_size; Mask address to proper range, inner shareable, read only. */ module_instance -> txm_module_instance_mpu_registers[TXM_MODULE_MPU_KERNEL_ENTRY_INDEX].txm_module_mpu_region_base_address = ((ULONG) _txm_module_manager_user_mode_entry & TXM_MODULE_MPU_ADDRESS_MASK) | TXM_MODULE_ATTRIBUTE_NON_SHAREABLE | TXM_MODULE_ATTRIBUTE_READ_ONLY; /* Set the limit address, attribute index, and enable bit. */ - module_instance -> txm_module_instance_mpu_registers[TXM_MODULE_MPU_KERNEL_ENTRY_INDEX].txm_module_mpu_region_limit_address = ((ULONG) _txm_module_manager_user_mode_entry & TXM_MODULE_MPU_ADDRESS_MASK) | TXM_MODULE_ATTRIBUTE_INDEX | TXM_MODULE_ATTRIBUTE_REGION_ENABLE; + /* Limit taken from the end of the function rather than from its base. The + whole privileged surface a module can execute is 24 bytes, so one granule + covers it today and the base would have done -- but if the entry ever grows + past 64 bytes, sizing from the base would silently leave the tail of it + outside the region and the module would fault on a call it is entitled to + make. */ + + module_instance -> txm_module_instance_mpu_registers[TXM_MODULE_MPU_KERNEL_ENTRY_INDEX].txm_module_mpu_region_limit_address = (((ULONG) _txm_module_manager_user_mode_entry_end - 1) & TXM_MODULE_MPU_ADDRESS_MASK) | TXM_MODULE_ATTRIBUTE_INDEX | TXM_MODULE_ATTRIBUTE_REGION_ENABLE; /* End of kernel mode entry setup. */ diff --git a/ports_module/cortex_r52/gnu/module_manager/src/txm_module_manager_user_mode_entry.S b/ports_module/cortex_r52/gnu/module_manager/src/txm_module_manager_user_mode_entry.S new file mode 100644 index 000000000..15ddd4e17 --- /dev/null +++ b/ports_module/cortex_r52/gnu/module_manager/src/txm_module_manager_user_mode_entry.S @@ -0,0 +1,109 @@ +@/*************************************************************************** +@ * 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 +@ **************************************************************************/ +@ +@/**************************************************************************/ +@/* */ +@/* MODULE MANAGER RELEASE */ +@/* */ +@/* txm_module_manager_user_mode_entry.S Cortex-R52/GNU */ +@/* 6.5.2 */ +@/* AUTHOR */ +@/* */ +@/* Frédéric Desbiens, Eclipse Foundation */ +@/* */ +@/* DESCRIPTION */ +@/* */ +@/* The only way a module reaches the kernel. */ +@/* */ +@/* A module runs in User mode and cannot execute kernel code or touch */ +@/* kernel memory. When it calls a ThreadX service, the call arrives */ +@/* here: SVC 1 raises privilege, the dispatch function performs the */ +@/* service, SVC 2 drops back to User mode, and the module continues. */ +@/* */ +@/* This function is the entire privileged surface a module can see. */ +@/* It gets an MPU region of its own -- the one at */ +@/* TXM_MODULE_MPU_KERNEL_ENTRY_INDEX -- because a module must be able */ +@/* to execute these few instructions and nothing else on that side of */ +@/* the boundary. Everything the module is allowed to ask for is */ +@/* decided inside _txm_module_manager_kernel_dispatch, in kernel */ +@/* memory the module cannot reach. */ +@/* */ +@/* SVC 1 and SVC 2 are handled by the port's supervisor call vector. */ +@/* */ +@/* CALLS */ +@/* */ +@/* SVC 1 Leave User mode */ +@/* _txm_module_manager_kernel_dispatch Perform the service */ +@/* SVC 2 Return to User mode */ +@/* */ +@/* CALLED BY */ +@/* */ +@/* Modules in User mode */ +@/* */ +@/**************************************************************************/ + + .arm + .text + +@ 64-byte aligned, which is the PMSAv8-R granule and all that is needed here. +@ +@ The Cortex-R4 module port aligns this to 4 KB, and has to: PMSAv7 regions must +@ be a power of two in size and aligned to their own size, so the smallest +@ region that can cover this function without also covering its neighbours is a +@ page. A base and limit pair has no such constraint, so the kernel entry +@ region can be sized to these instructions and stop. That matters for +@ isolation rather than for memory: on PMSAv7 whatever else shares the page is +@ inside the one region a module is allowed to execute. + + .align 6 + + .global _txm_module_manager_user_mode_entry + .global _txm_module_manager_user_mode_entry_end + .extern _txm_module_manager_kernel_dispatch + + .type _txm_module_manager_user_mode_entry, %function +_txm_module_manager_user_mode_entry: + +_txm_system_mode_enter: + +@ Leave User mode. The supervisor call vector recognises 1 and raises the +@ thread to privileged mode on its kernel stack. + + SVC 1 + +_txm_module_priv: + +@ Privileged now. r3 is pushed alongside lr only to keep the stack eight-byte +@ aligned, which the ABI requires at a public interface. + + PUSH {r3, lr} + BL _txm_module_manager_kernel_dispatch + POP {r3, lr} + +_txm_system_mode_exit: + +@ Back to User mode before returning to the module. If this were skipped the +@ module would resume privileged, which is the whole protection gone -- so it is +@ unconditional and sits between the dispatch and every return path. + + SVC 2 + + BX lr + +@ Marks the end of the region a module is allowed to execute, so the region can +@ be sized to this function rather than rounded up to something larger. + +_txm_module_manager_user_mode_entry_end: + NOP From 140140e7cb197661b01b4d093459476f34b1151d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20Desbiens?= Date: Tue, 18 Aug 2026 10:42:17 -0400 Subject: [PATCH 08/33] Added the supervisor call handler: the privilege boundary itself SVC 1 raises a module thread out of User mode and onto its kernel stack, SVC 2 puts it back. This is the mechanism the rest of the port exists to protect. Two properties make it a boundary rather than a formality, and both are copied in substance from the Cortex-R4 module port because both are load-bearing. Only the two exact instructions inside _txm_module_manager_user_mode_entry may make these calls. lr points one instruction past the call, so the handler compares lr minus four against the known call site and stops if it does not match. Without that check a module could execute SVC 1 anywhere in its own code and come back privileged, which is every protection in this port gone at once. The stacks are swapped, not shared. A module's stack lives in memory the module can write, so the kernel must not run on it -- a module could otherwise corrupt kernel state by scribbling on what it believes is its own stack. SVC 1 switches to a kernel stack outside the module's regions and SVC 2 switches back. The swap happens in System mode because System shares User's banked sp. ThreadX's stack bounds are repointed at whichever stack is in use. Leaving them alone would have a stack check measure the kernel's sp against the module's bounds and report an overflow that has not happened. Unrecognised SVC numbers stop rather than return. Returning would resume the caller as though the call had succeeded, with the privilege state undefined. This core's base port does not use SVC at all -- its vector treats one as a fault -- so there is no third caller to accommodate. The offsets are not the R4 port's. Every field after tx_thread_vfp_enable moves by a word in this port, so the kernel stack fields sit at 0xA8, 0xAC and 0xB0 where R4 has 0xA4, 0xA8 and 0xAC. All twelve are asserted against offsetof, with the two that matter most spelled out in their messages: a wrong user-mode flag offset would leave a thread believing it is privileged when it is not, and a wrong kernel stack offset would put the kernel on memory the module can write. The file declares .syntax unified rather than relying on the default, which rejected the unified conditional-load form. A file that hand-writes exception entry is the last place to leave the assembler's dialect to chance. Assisted-by: Claude Code (Opus 5) --- .../src/txm_module_manager_offset_check.c | 41 +++ .../src/txm_module_manager_svc_handler.S | 233 ++++++++++++++++++ 2 files changed, 274 insertions(+) create mode 100644 ports_module/cortex_r52/gnu/module_manager/src/txm_module_manager_svc_handler.S diff --git a/ports_module/cortex_r52/gnu/module_manager/src/txm_module_manager_offset_check.c b/ports_module/cortex_r52/gnu/module_manager/src/txm_module_manager_offset_check.c index df6524ca9..883d66a83 100644 --- a/ports_module/cortex_r52/gnu/module_manager/src/txm_module_manager_offset_check.c +++ b/ports_module/cortex_r52/gnu/module_manager/src/txm_module_manager_offset_check.c @@ -78,6 +78,47 @@ _Static_assert(offsetof(TXM_MODULE_INSTANCE, txm_module_instance_mpu_registers) "TXM_MODULE_INSTANCE; the scheduler would load regions from the " "wrong address"); +/* Offsets the supervisor call handler hard-codes, from + txm_module_manager_svc_handler.S. This is the privilege boundary, so a wrong + offset here does not merely misbehave: writing the user-mode flag to the wrong + word would leave a thread believing it is privileged when it is not, or the + reverse, and swapping to a stack pointer read from the wrong field would put + the kernel on memory the module can write. */ + +#define THREAD_STACK_PTR 0x08 +#define THREAD_STACK_START 0x0C +#define THREAD_STACK_END 0x10 +#define THREAD_STACK_SIZE 0x14 +#define THREAD_CUR_USER_MODE 0x9C +#define THREAD_KSTACK_START 0xA8 +#define THREAD_KSTACK_END 0xAC +#define THREAD_KSTACK_SIZE 0xB0 +#define THREAD_MSTACK_PTR 0xB4 +#define THREAD_MSTACK_START 0xB8 +#define THREAD_MSTACK_END 0xBC +#define THREAD_MSTACK_SIZE 0xC0 + +#define T_OFF(f) offsetof(TX_THREAD, f) + +_Static_assert(T_OFF(tx_thread_stack_ptr) == THREAD_STACK_PTR, "svc handler: stack_ptr moved"); +_Static_assert(T_OFF(tx_thread_stack_start) == THREAD_STACK_START, "svc handler: stack_start moved"); +_Static_assert(T_OFF(tx_thread_stack_end) == THREAD_STACK_END, "svc handler: stack_end moved"); +_Static_assert(T_OFF(tx_thread_stack_size) == THREAD_STACK_SIZE, "svc handler: stack_size moved"); + +_Static_assert(T_OFF(tx_thread_module_current_user_mode) == THREAD_CUR_USER_MODE, + "svc handler: the user-mode flag moved; a thread could be left " + "believing it is privileged when it is not"); + +_Static_assert(T_OFF(tx_thread_module_kernel_stack_start) == THREAD_KSTACK_START, "svc handler: kernel_stack_start moved"); +_Static_assert(T_OFF(tx_thread_module_kernel_stack_end) == THREAD_KSTACK_END, + "svc handler: kernel_stack_end moved; the kernel would run on a " + "stack read from the wrong field"); +_Static_assert(T_OFF(tx_thread_module_kernel_stack_size) == THREAD_KSTACK_SIZE, "svc handler: kernel_stack_size moved"); +_Static_assert(T_OFF(tx_thread_module_stack_ptr) == THREAD_MSTACK_PTR, "svc handler: module stack_ptr moved"); +_Static_assert(T_OFF(tx_thread_module_stack_start) == THREAD_MSTACK_START, "svc handler: module stack_start moved"); +_Static_assert(T_OFF(tx_thread_module_stack_end) == THREAD_MSTACK_END, "svc handler: module stack_end moved"); +_Static_assert(T_OFF(tx_thread_module_stack_size) == THREAD_MSTACK_SIZE, "svc handler: module stack_size moved"); + /* Offsets the fault capture hard-codes, from txm_module_manager_fault_capture.S. Same hazard as the scheduler's: the capture runs in Abort mode with a fault in progress, and a wrong offset there diff --git a/ports_module/cortex_r52/gnu/module_manager/src/txm_module_manager_svc_handler.S b/ports_module/cortex_r52/gnu/module_manager/src/txm_module_manager_svc_handler.S new file mode 100644 index 000000000..90d553cea --- /dev/null +++ b/ports_module/cortex_r52/gnu/module_manager/src/txm_module_manager_svc_handler.S @@ -0,0 +1,233 @@ +@/*************************************************************************** +@ * 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 +@ **************************************************************************/ +@ +@/**************************************************************************/ +@/* */ +@/* MODULE MANAGER RELEASE */ +@/* */ +@/* txm_module_manager_svc_handler.S Cortex-R52/GNU */ +@/* 6.5.2 */ +@/* AUTHOR */ +@/* */ +@/* Frédéric Desbiens, Eclipse Foundation */ +@/* */ +@/* DESCRIPTION */ +@/* */ +@/* The privilege boundary. A board's supervisor call vector branches */ +@/* here. */ +@/* */ +@/* SVC 1 raises a module thread out of User mode and onto its kernel */ +@/* stack; SVC 2 puts it back. Both are only reachable from the two */ +@/* exact instructions inside _txm_module_manager_user_mode_entry, and */ +@/* that check is what makes the boundary a boundary. Without it a */ +@/* module could execute SVC 1 from anywhere in its own code and come */ +@/* back privileged, which is every protection in this port gone at */ +@/* once. */ +@/* */ +@/* The two stacks are the other half of it. A module's own stack is in */ +@/* memory the module can write, so the kernel must not run on it: a */ +@/* module could otherwise corrupt kernel state by scribbling on what it */ +@/* believes is its own stack. SVC 1 therefore switches to a kernel */ +@/* stack the module cannot reach, and SVC 2 switches back. */ +@/* */ +@/* Any other SVC number stops. This core's base port does not use SVC */ +@/* at all -- its vector treats one as a fault -- so there is no third */ +@/* caller to accommodate and nothing legitimate to fall through to. */ +@/* */ +@/**************************************************************************/ + +@ Unified syntax, declared rather than assumed. In divided syntax the condition +@ precedes the size -- LDRNEH -- and in unified it follows -- LDRHNE. The +@ assembler's default rejected the unified form, and a file that hand-writes +@ exception entry is the last place to leave that to chance. + + .syntax unified + .arm + .text + .align 2 + + .global __tx_module_svc_interrupt + .extern _tx_thread_current_ptr + .extern _txm_system_mode_enter + .extern _txm_system_mode_exit + +@ Processor mode encodings and the CPSR mode field. + + .equ MODE_MASK, 0x1F + .equ USR_MODE, 0x10 + .equ SVC_MODE, 0x13 + .equ SYS_MODE, 0x1F + .equ THUMB_MASK, 0x20 + +@ Offsets into TX_THREAD. Checked against offsetof at compile time in +@ txm_module_manager_offset_check.c. They are not the Cortex-R4 module port's +@ values: tx_thread_vfp_enable sits ahead of the module fields in this port, so +@ everything after it moves by a word. + + .equ THREAD_STACK_PTR, 0x08 + .equ THREAD_STACK_START, 0x0C + .equ THREAD_STACK_END, 0x10 + .equ THREAD_STACK_SIZE, 0x14 + .equ THREAD_CUR_USER_MODE, 0x9C + .equ THREAD_KSTACK_START, 0xA8 + .equ THREAD_KSTACK_END, 0xAC + .equ THREAD_KSTACK_SIZE, 0xB0 + .equ THREAD_MSTACK_PTR, 0xB4 + .equ THREAD_MSTACK_START, 0xB8 + .equ THREAD_MSTACK_END, 0xBC + .equ THREAD_MSTACK_SIZE, 0xC0 + + +@/**************************************************************************/ +@/* Vector entry. */ +@/**************************************************************************/ + + .type __tx_module_svc_interrupt, %function +__tx_module_svc_interrupt: + + STMFD sp!, {r0-r3, r12, lr} @ Preserve the caller's registers + MRS r0, spsr + STMFD sp!, {r0, r3} @ SPSR, plus one more to keep the + @ stack eight-byte aligned + +@ Recover the SVC number from the instruction that caused this. The encoding +@ differs between states, so the saved SPSR decides where to read it from. + + TST r0, #THUMB_MASK + LDRHNE r0, [lr, #-2] @ Thumb: halfword, low 8 bits + BICNE r0, r0, #0xFF00 + LDREQ r0, [lr, #-4] @ ARM: word, low 24 bits + BICEQ r0, r0, #0xFF000000 + + CMP r0, #1 + BEQ _tx_module_svc_enter + CMP r0, #2 + BEQ _tx_module_svc_exit + + +@/**************************************************************************/ +@/* Anything else. */ +@/**************************************************************************/ + + .weak _tx_module_svc_unrecognized +_tx_module_svc_unrecognized: + +@ Stop rather than return. Returning would resume the caller as though the +@ call had succeeded, and for an unrecognised number that means resuming with +@ the privilege state undefined. + +_tx_module_svc_unrecognized_loop: + B _tx_module_svc_unrecognized_loop + + +@/**************************************************************************/ +@/* SVC 1: leave User mode. */ +@/**************************************************************************/ + +_tx_module_svc_enter: + +@ Only the SVC inside the user mode entry function may do this. lr points one +@ instruction past the call, so the call site is lr minus four. A module that +@ executes SVC 1 from its own code fails here and stops. + + LDR r2, =_txm_system_mode_enter + SUB r1, lr, #4 + CMP r1, r2 + BNE _tx_module_svc_unrecognized + + LDR r1, =_tx_thread_current_ptr + LDR r2, [r1] @ The running thread + +@ Record that it is no longer in User mode, so a nested service call and the +@ scheduler both see the truth. + + MOV r1, #0 + STR r1, [r2, #THREAD_CUR_USER_MODE] + +@ Return into System mode rather than User mode. + + LDMFD sp!, {r0, r3} @ SPSR back off the stack + BIC r0, r0, #MODE_MASK + ORR r0, r0, #SYS_MODE + MSR SPSR_cxsf, r0 + +@ Swap stacks. The module's sp is saved so SVC 2 can restore it, and sp is set +@ to the top of the kernel stack, which the module's regions do not cover. +@ System mode shares User mode's banked sp, which is why the swap happens there. + + LDR r1, [r2, #THREAD_KSTACK_END] @ Top of the kernel stack + CPS #SYS_MODE + MOV r3, sp @ The module's own sp + MOV sp, r1 + CPS #SVC_MODE + STR r3, [r2, #THREAD_MSTACK_PTR] + +@ Point ThreadX's stack checking at the kernel stack while it is in use. +@ Without this a stack check would measure the kernel's sp against the module's +@ bounds and report an overflow that has not happened. + + LDR r3, [r2, #THREAD_KSTACK_START] + STR r3, [r2, #THREAD_STACK_START] + LDR r3, [r2, #THREAD_KSTACK_END] + STR r3, [r2, #THREAD_STACK_END] + LDR r3, [r2, #THREAD_KSTACK_SIZE] + STR r3, [r2, #THREAD_STACK_SIZE] + + LDMFD sp!, {r0-r3, r12, pc}^ @ Return, restoring CPSR from SPSR + + +@/**************************************************************************/ +@/* SVC 2: return to User mode. */ +@/**************************************************************************/ + +_tx_module_svc_exit: + +@ Same check, against the other call site. + + LDR r2, =_txm_system_mode_exit + SUB r1, lr, #4 + CMP r1, r2 + BNE _tx_module_svc_unrecognized + + LDR r1, =_tx_thread_current_ptr + LDR r2, [r1] + + MOV r1, #1 + STR r1, [r2, #THREAD_CUR_USER_MODE] + +@ Return into User mode. + + LDMFD sp!, {r0, r3} + BIC r0, r0, #MODE_MASK + ORR r0, r0, #USR_MODE + MSR SPSR_cxsf, r0 + +@ Put the module's own stack back. + + LDR r1, [r2, #THREAD_MSTACK_PTR] + CPS #SYS_MODE + MOV sp, r1 + CPS #SVC_MODE + +@ And its own bounds, for the same reason they were changed on the way in. + + LDR r3, [r2, #THREAD_MSTACK_START] + STR r3, [r2, #THREAD_STACK_START] + LDR r3, [r2, #THREAD_MSTACK_END] + STR r3, [r2, #THREAD_STACK_END] + LDR r3, [r2, #THREAD_MSTACK_SIZE] + STR r3, [r2, #THREAD_STACK_SIZE] + + LDMFD sp!, {r0-r3, r12, pc}^ From fdedbeddac84e2741f9dc7d68d9b46b2e6ec6245 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20Desbiens?= Date: Tue, 18 Aug 2026 10:46:42 -0400 Subject: [PATCH 09/33] Added the module thread stack build, which decides the mode a module starts in A kernel thread starts in SVC mode. A module thread starts in User mode when it asked for protection and System mode when it did not, and that choice is made here, in the initial CPSR of its first stack frame. Everything else about the frame is identical to a kernel thread's. Derived from this port's own tx_thread_stack_build rather than from another architecture's module port, because everything except the mode selection is already correct here and the frame layout is the port's own. The kernel stack build is copied across unchanged, since the module port needs its own copy of the port assembly. The user-mode flag is read from tx_thread_module_user_mode at 0xA0. The Cortex-R4 module port reads 0x9C, which in this port is tx_thread_module_current_user_mode -- a different field, holding whether the thread is privileged right now rather than whether it should start unprivileged. Copying that offset would have built cleanly and started module threads in the wrong mode, which is protection absent from the first instruction while looking present everywhere else. Two assertions guard it. One pins the offset. The other checks that the requested and current user-mode fields have not collapsed onto the same offset, because if they ever did, the stack build and the SVC handler would silently be reading each other's field and the failure would look like intermittent privilege rather than a layout change. The Thumb bit is taken from the requested entry point, so a module built for Thumb starts in Thumb state rather than executing its first instruction as ARM. Assisted-by: Claude Code (Opus 5) --- .../src/tx_thread_stack_build.S | 170 +++++++++++++++ .../src/txm_module_manager_offset_check.c | 17 ++ .../txm_module_manager_thread_stack_build.S | 201 ++++++++++++++++++ 3 files changed, 388 insertions(+) create mode 100644 ports_module/cortex_r52/gnu/module_manager/src/tx_thread_stack_build.S create mode 100644 ports_module/cortex_r52/gnu/module_manager/src/txm_module_manager_thread_stack_build.S diff --git a/ports_module/cortex_r52/gnu/module_manager/src/tx_thread_stack_build.S b/ports_module/cortex_r52/gnu/module_manager/src/tx_thread_stack_build.S new file mode 100644 index 000000000..dc9238c43 --- /dev/null +++ b/ports_module/cortex_r52/gnu/module_manager/src/tx_thread_stack_build.S @@ -0,0 +1,170 @@ +@/*************************************************************************** +@ * Copyright (c) 2024 Microsoft Corporation +@ * 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. +@ * +@ * SPDX-License-Identifier: MIT +@ **************************************************************************/ +@ Some portions generated by Claude Code (Opus 5). +@ +@ +@/**************************************************************************/ +@/**************************************************************************/ +@/** */ +@/** ThreadX Component */ +@/** */ +@/** Thread */ +@/** */ +@/**************************************************************************/ +@/**************************************************************************/ +#ifdef TX_INCLUDE_USER_DEFINE_FILE +#include "tx_user.h" +#endif + + .arm + +SVC_MODE = 0x13 @ SVC mode +#ifdef TX_ENABLE_FIQ_SUPPORT +CPSR_MASK = 0xDF @ Mask initial CPSR, IRQ & FIQ interrupts enabled +#else +CPSR_MASK = 0x9F @ Mask initial CPSR, IRQ interrupts enabled +#endif +@ +@ +@/* Define the 16-bit Thumb mode veneer for _tx_thread_stack_build for +@ applications calling this function from to 16-bit Thumb mode. */ +@ + .text + .align 2 + .thumb + .global $_tx_thread_stack_build + .type $_tx_thread_stack_build,function +$_tx_thread_stack_build: + BX pc @ Switch to 32-bit mode + NOP @ + .arm + STMFD sp!, {lr} @ Save return address + BL _tx_thread_stack_build @ Call _tx_thread_stack_build function + LDMFD sp!, {lr} @ Recover saved return address + BX lr @ Return to 16-bit caller +@ +@ + .text + .align 2 +@/**************************************************************************/ +@/* */ +@/* FUNCTION RELEASE */ +@/* */ +@/* _tx_thread_stack_build Cortex-R52/GNU */ +@/* 6.5.2 */ +@/* AUTHOR */ +@/* */ +@/* Frédéric Desbiens, Eclipse Foundation */ +@/* */ +@/* Derived from the Cortex-R5/GNU port originally written by */ +@/* William E. Lamie, Microsoft Corporation. */ +@/* */ +@/* DESCRIPTION */ +@/* */ +@/* This function builds a stack frame on the supplied thread's stack. */ +@/* The stack frame results in a fake interrupt return to the supplied */ +@/* function pointer. */ +@/* */ +@/* INPUT */ +@/* */ +@/* thread_ptr Pointer to thread control blk */ +@/* function_ptr Pointer to return function */ +@/* */ +@/* OUTPUT */ +@/* */ +@/* None */ +@/* */ +@/* CALLS */ +@/* */ +@/* None */ +@/* */ +@/* CALLED BY */ +@/* */ +@/* _tx_thread_create Create thread service */ +@/* */ +@/**************************************************************************/ +@VOID _tx_thread_stack_build(TX_THREAD *thread_ptr, VOID (*function_ptr)(VOID)) +@{ + .global _tx_thread_stack_build + .type _tx_thread_stack_build,function +_tx_thread_stack_build: +@ +@ +@ /* Build a fake interrupt frame. The form of the fake interrupt stack +@ on the ARM9 should look like the following after it is built: +@ +@ Stack Top: 1 Interrupt stack frame type +@ CPSR Initial value for CPSR +@ a1 (r0) Initial value for a1 +@ a2 (r1) Initial value for a2 +@ a3 (r2) Initial value for a3 +@ a4 (r3) Initial value for a4 +@ v1 (r4) Initial value for v1 +@ v2 (r5) Initial value for v2 +@ v3 (r6) Initial value for v3 +@ v4 (r7) Initial value for v4 +@ v5 (r8) Initial value for v5 +@ sb (r9) Initial value for sb +@ sl (r10) Initial value for sl +@ fp (r11) Initial value for fp +@ ip (r12) Initial value for ip +@ lr (r14) Initial value for lr +@ pc (r15) Initial value for pc +@ 0 For stack backtracing +@ +@ Stack Bottom: (higher memory address) */ +@ + LDR r2, [r0, #16] @ Pickup end of stack area + BIC r2, r2, #7 @ Ensure 8-byte alignment + SUB r2, r2, #76 @ Allocate space for the stack frame +@ +@ /* Actually build the stack frame. */ +@ + MOV r3, #1 @ Build interrupt stack type + STR r3, [r2, #0] @ Store stack type + MOV r3, #0 @ Build initial register value + STR r3, [r2, #8] @ Store initial r0 + STR r3, [r2, #12] @ Store initial r1 + STR r3, [r2, #16] @ Store initial r2 + STR r3, [r2, #20] @ Store initial r3 + STR r3, [r2, #24] @ Store initial r4 + STR r3, [r2, #28] @ Store initial r5 + STR r3, [r2, #32] @ Store initial r6 + STR r3, [r2, #36] @ Store initial r7 + STR r3, [r2, #40] @ Store initial r8 + STR r3, [r2, #44] @ Store initial r9 + LDR r3, [r0, #12] @ Pickup stack starting address + STR r3, [r2, #48] @ Store initial r10 (sl) + LDR r3,=_tx_thread_schedule @ Pickup address of _tx_thread_schedule for GDB backtrace + STR r3, [r2, #60] @ Store initial r14 (lr) + MOV r3, #0 @ Build initial register value + STR r3, [r2, #52] @ Store initial r11 + STR r3, [r2, #56] @ Store initial r12 + STR r1, [r2, #64] @ Store initial pc + STR r3, [r2, #68] @ 0 for back-trace + MRS r1, CPSR @ Pickup CPSR + BIC r1, r1, #CPSR_MASK @ Mask mode bits of CPSR + ORR r3, r1, #SVC_MODE @ Build CPSR, SVC mode, interrupts enabled + STR r3, [r2, #4] @ Store initial CPSR +@ +@ /* Setup stack pointer. */ +@ thread_ptr -> tx_thread_stack_ptr = r2; +@ + STR r2, [r0, #8] @ Save stack pointer in thread's + @ control block +#ifdef __THUMB_INTERWORK + BX lr @ Return to caller +#else + MOV pc, lr @ Return to caller +#endif +@} + + diff --git a/ports_module/cortex_r52/gnu/module_manager/src/txm_module_manager_offset_check.c b/ports_module/cortex_r52/gnu/module_manager/src/txm_module_manager_offset_check.c index 883d66a83..276b919cd 100644 --- a/ports_module/cortex_r52/gnu/module_manager/src/txm_module_manager_offset_check.c +++ b/ports_module/cortex_r52/gnu/module_manager/src/txm_module_manager_offset_check.c @@ -78,6 +78,23 @@ _Static_assert(offsetof(TXM_MODULE_INSTANCE, txm_module_instance_mpu_registers) "TXM_MODULE_INSTANCE; the scheduler would load regions from the " "wrong address"); +/* Offset the module thread stack build hard-codes. This one decides the mode a + module thread starts in, so a wrong value starts it privileged when it asked + not to be -- protection that is absent from the first instruction and looks + present everywhere else. The Cortex-R4 module port reads 0x9C here, which in + this port is a different field. */ + +_Static_assert(offsetof(TX_THREAD, tx_thread_module_user_mode) == 0xA0, + "txm_module_manager_thread_stack_build.S THREAD_MODULE_USER_MODE " + "no longer matches TX_THREAD; module threads could start in the " + "wrong privilege mode"); + +_Static_assert(offsetof(TX_THREAD, tx_thread_module_user_mode) + != offsetof(TX_THREAD, tx_thread_module_current_user_mode), + "the requested and current user-mode fields have collapsed onto " + "one offset; the stack build and the SVC handler would be reading " + "each other's field"); + /* Offsets the supervisor call handler hard-codes, from txm_module_manager_svc_handler.S. This is the privilege boundary, so a wrong offset here does not merely misbehave: writing the user-mode flag to the wrong diff --git a/ports_module/cortex_r52/gnu/module_manager/src/txm_module_manager_thread_stack_build.S b/ports_module/cortex_r52/gnu/module_manager/src/txm_module_manager_thread_stack_build.S new file mode 100644 index 000000000..e65c98d2a --- /dev/null +++ b/ports_module/cortex_r52/gnu/module_manager/src/txm_module_manager_thread_stack_build.S @@ -0,0 +1,201 @@ +@/*************************************************************************** +@ * Copyright (c) 2024 Microsoft Corporation +@ * 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. +@ * +@ * SPDX-License-Identifier: MIT +@ **************************************************************************/ +@ Some portions generated by Claude Code (Opus 5). +@ +@ +@/**************************************************************************/ +@/**************************************************************************/ +@/** */ +@/** ThreadX Component */ +@/** */ +@/** Thread */ +@/** */ +@/**************************************************************************/ +@/**************************************************************************/ +#ifdef TX_INCLUDE_USER_DEFINE_FILE +#include "tx_user.h" +#endif + + .syntax unified + .arm + +SVC_MODE = 0x13 @ SVC mode +USR_MODE = 0x10 @ User mode, unprivileged +SYS_MODE = 0x1F @ System mode, privileged +THUMB_MASK = 0x20 @ CPSR Thumb bit + +@ Offset of tx_thread_module_user_mode, checked against offsetof at compile time +@ in txm_module_manager_offset_check.c. + +THREAD_MODULE_USER_MODE = 0xA0 +#ifdef TX_ENABLE_FIQ_SUPPORT +CPSR_MASK = 0xDF @ Mask initial CPSR, IRQ & FIQ interrupts enabled +#else +CPSR_MASK = 0x9F @ Mask initial CPSR, IRQ interrupts enabled +#endif +@ +@ +@/* Define the 16-bit Thumb mode veneer for _txm_module_manager_thread_stack_build for +@ applications calling this function from to 16-bit Thumb mode. */ +@ + .text + .align 2 + .thumb + .global $_txm_module_manager_thread_stack_build + .type $_txm_module_manager_thread_stack_build,function +$_txm_module_manager_thread_stack_build: + BX pc @ Switch to 32-bit mode + NOP @ + .arm + STMFD sp!, {lr} @ Save return address + BL _txm_module_manager_thread_stack_build @ Call _txm_module_manager_thread_stack_build function + LDMFD sp!, {lr} @ Recover saved return address + BX lr @ Return to 16-bit caller +@ +@ + .text + .align 2 +@/**************************************************************************/ +@/* */ +@/* FUNCTION RELEASE */ +@/* */ +@/* _txm_module_manager_thread_stack_build Cortex-R52/GNU */ +@/* 6.5.2 */ +@/* AUTHOR */ +@/* */ +@/* Frédéric Desbiens, Eclipse Foundation */ +@/* */ +@/* Derived from the Cortex-R5/GNU port originally written by */ +@/* William E. Lamie, Microsoft Corporation. */ +@/* */ +@/* DESCRIPTION */ +@/* */ +@/* This function builds a stack frame on the supplied thread's stack. */ +@/* The stack frame results in a fake interrupt return to the supplied */ +@/* function pointer. */ +@/* */ +@/* INPUT */ +@/* */ +@/* thread_ptr Pointer to thread control blk */ +@/* function_ptr Pointer to return function */ +@/* */ +@/* OUTPUT */ +@/* */ +@/* None */ +@/* */ +@/* CALLS */ +@/* */ +@/* None */ +@/* */ +@/* CALLED BY */ +@/* */ +@/* _tx_thread_create Create thread service */ +@/* */ +@/**************************************************************************/ +@VOID _txm_module_manager_thread_stack_build(TX_THREAD *thread_ptr, VOID (*function_ptr)(VOID)) +@{ + .global _txm_module_manager_thread_stack_build + .type _txm_module_manager_thread_stack_build,function +_txm_module_manager_thread_stack_build: +@ +@ +@ /* Build a fake interrupt frame. The form of the fake interrupt stack +@ on the ARM9 should look like the following after it is built: +@ +@ Stack Top: 1 Interrupt stack frame type +@ CPSR Initial value for CPSR +@ a1 (r0) Initial value for a1 +@ a2 (r1) Initial value for a2 +@ a3 (r2) Initial value for a3 +@ a4 (r3) Initial value for a4 +@ v1 (r4) Initial value for v1 +@ v2 (r5) Initial value for v2 +@ v3 (r6) Initial value for v3 +@ v4 (r7) Initial value for v4 +@ v5 (r8) Initial value for v5 +@ sb (r9) Initial value for sb +@ sl (r10) Initial value for sl +@ fp (r11) Initial value for fp +@ ip (r12) Initial value for ip +@ lr (r14) Initial value for lr +@ pc (r15) Initial value for pc +@ 0 For stack backtracing +@ +@ Stack Bottom: (higher memory address) */ +@ + LDR r2, [r0, #16] @ Pickup end of stack area + BIC r2, r2, #7 @ Ensure 8-byte alignment + SUB r2, r2, #76 @ Allocate space for the stack frame +@ +@ /* Actually build the stack frame. */ +@ + MOV r3, #1 @ Build interrupt stack type + STR r3, [r2, #0] @ Store stack type + MOV r3, #0 @ Build initial register value + STR r3, [r2, #8] @ Store initial r0 + STR r3, [r2, #12] @ Store initial r1 + STR r3, [r2, #16] @ Store initial r2 + STR r3, [r2, #20] @ Store initial r3 + STR r3, [r2, #24] @ Store initial r4 + STR r3, [r2, #28] @ Store initial r5 + STR r3, [r2, #32] @ Store initial r6 + STR r3, [r2, #36] @ Store initial r7 + STR r3, [r2, #40] @ Store initial r8 + STR r3, [r2, #44] @ Store initial r9 + LDR r3, [r0, #12] @ Pickup stack starting address + STR r3, [r2, #48] @ Store initial r10 (sl) + LDR r3,=_tx_thread_schedule @ Pickup address of _tx_thread_schedule for GDB backtrace + STR r3, [r2, #60] @ Store initial r14 (lr) + MOV r3, #0 @ Build initial register value + STR r3, [r2, #52] @ Store initial r11 + STR r3, [r2, #56] @ Store initial r12 + STR r1, [r2, #64] @ Store initial pc + STR r3, [r2, #68] @ 0 for back-trace +@ +@ /* The mode a module thread starts in is the whole point of this function +@ existing separately. A kernel thread starts in SVC mode; a module thread +@ starts in User mode when it asked for protection, and System mode when it +@ did not. Everything else about the frame is identical, which is why this +@ is derived from the port's own stack build rather than from another +@ architecture's module port. +@ +@ The flag is read from tx_thread_module_user_mode at 0xA0. The Cortex-R4 +@ module port reads it from 0x9C, which in this port is +@ tx_thread_module_current_user_mode -- a different field, holding whether +@ the thread is privileged right now rather than whether it should start +@ unprivileged. Copying that offset would have built cleanly and started +@ module threads in the wrong mode. */ +@ + MRS r1, CPSR @ Pickup CPSR + BIC r1, r1, #CPSR_MASK @ Mask mode bits of CPSR +@ + TST r0, #1 @ Was a Thumb entry point requested? + ORRNE r1, r1, #THUMB_MASK @ Yes, start in Thumb state +@ + LDR r3, [r0, #THREAD_MODULE_USER_MODE] @ Pickup the requested user mode + TST r3, #1 + ORREQ r3, r1, #SYS_MODE @ Clear: privileged, System mode + ORRNE r3, r1, #USR_MODE @ Set: unprivileged, User mode + STR r3, [r2, #4] @ Store initial CPSR +@ +@ /* Setup stack pointer. */ +@ thread_ptr -> tx_thread_stack_ptr = r2; +@ + STR r2, [r0, #8] @ Save stack pointer in thread's + @ control block +#ifdef __THUMB_INTERWORK + BX lr @ Return to caller +#else + MOV pc, lr @ Return to caller +#endif +@} + + From 451cb11cd57020c54aa5282a0157294d6c0c9abe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20Desbiens?= Date: Tue, 18 Aug 2026 10:58:44 -0400 Subject: [PATCH 10/33] Moved module-port threads to SYS mode, which the context paths depend on In a module port no thread runs in SVC mode. SVC is reserved for the supervisor call handler, which is how a module reaches the kernel, and a thread sitting in SVC mode would be using that handler's banked stack pointer as its own. So kernel threads start in SYS mode and module threads in User mode, and the two share a banked sp -- which is what lets the interrupt paths reach a thread's stack without caring whose thread it is. Three files, and the first was a bug I had already committed. tx_thread_stack_build was copied from the base port unchanged, which left kernel threads starting in SVC mode. The restore path would then have looked for their context on the SYS stack while it had been pushed onto the SVC one. One line, and it is now SYS with the reasoning recorded next to it. Found by checking what the Cortex-R4 module port does differently from its own base port rather than by assuming the file needed no changes. tx_thread_context_restore now enters SYS mode wherever it was entering SVC mode to reach the interrupted thread's stack. Entering SVC there would push the interrupted context onto the supervisor call handler's stack and the thread would resume on a corrupted frame. Three switches changed; the IRQ-mode switch in the middle is unrelated and stays. tx_thread_context_save and tx_thread_system_return are copied unchanged, having no mode assumptions at all. That was checked rather than assumed, and it matches what the R4 module port does with the same two files. One thing deliberately not copied. R4's module context restore replaces the branch to the scheduler with an exception return that sets SPSR to SYS mode and clears the Thumb bit. This port reaches the scheduler already in SYS mode, privileged and on the thread's stack, so a plain branch appears sufficient and is what the base port does. R4 may be solving something specific to its own structure. Copying a sequence I cannot yet explain seemed worse than the version I can reason about, and this note is here so a reviewer can disagree -- none of it has run on hardware yet, and the interrupt paths are where a mistake will show up first. Assisted-by: Claude Code (Opus 5) --- .../src/tx_thread_context_restore.S | 259 ++++++++++++++++++ .../src/tx_thread_context_save.S | 193 +++++++++++++ .../src/tx_thread_stack_build.S | 16 +- .../src/tx_thread_system_return.S | 169 ++++++++++++ 4 files changed, 636 insertions(+), 1 deletion(-) create mode 100644 ports_module/cortex_r52/gnu/module_manager/src/tx_thread_context_restore.S create mode 100644 ports_module/cortex_r52/gnu/module_manager/src/tx_thread_context_save.S create mode 100644 ports_module/cortex_r52/gnu/module_manager/src/tx_thread_system_return.S diff --git a/ports_module/cortex_r52/gnu/module_manager/src/tx_thread_context_restore.S b/ports_module/cortex_r52/gnu/module_manager/src/tx_thread_context_restore.S new file mode 100644 index 000000000..4069b440e --- /dev/null +++ b/ports_module/cortex_r52/gnu/module_manager/src/tx_thread_context_restore.S @@ -0,0 +1,259 @@ +@/*************************************************************************** +@ * Copyright (c) 2024 Microsoft Corporation +@ * 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. +@ * +@ * SPDX-License-Identifier: MIT +@ **************************************************************************/ +@ Some portions generated by Claude Code (Opus 5). +@ +@ +@/**************************************************************************/ +@/**************************************************************************/ +@/** */ +@/** ThreadX Component */ +@/** */ +@/** Thread */ +@/** */ +@/**************************************************************************/ +@/**************************************************************************/ +#ifdef TX_INCLUDE_USER_DEFINE_FILE +#include "tx_user.h" +#endif + + .arm + +#ifdef TX_ENABLE_FIQ_SUPPORT +SVC_MODE = 0xD3 @ Disable IRQ/FIQ, SVC mode +SYS_MODE = 0xDF @ Disable IRQ/FIQ, SYS mode +IRQ_MODE = 0xD2 @ Disable IRQ/FIQ, IRQ mode +#else +SVC_MODE = 0x93 @ Disable IRQ, SVC mode +SYS_MODE = 0x9F @ Disable IRQ, SYS mode +IRQ_MODE = 0x92 @ Disable IRQ, IRQ mode +#endif +@ + .global _tx_thread_system_state + .global _tx_thread_current_ptr + .global _tx_thread_execute_ptr + .global _tx_timer_time_slice + .global _tx_thread_schedule + .global _tx_thread_preempt_disable + .global _tx_execution_isr_exit +@ +@ +@/* No 16-bit Thumb mode veneer code is needed for _tx_thread_context_restore +@ since it will never be called 16-bit mode. */ +@ + .arm + .text + .align 2 +@/**************************************************************************/ +@/* */ +@/* FUNCTION RELEASE */ +@/* */ +@/* _tx_thread_context_restore Cortex-R52/GNU */ +@/* 6.5.2 */ +@/* AUTHOR */ +@/* */ +@/* Frédéric Desbiens, Eclipse Foundation */ +@/* */ +@/* Derived from the Cortex-R5/GNU port originally written by */ +@/* William E. Lamie, Microsoft Corporation. */ +@/* */ +@/* DESCRIPTION */ +@/* */ +@/* This function restores the interrupt context if it is processing a */ +@/* nested interrupt. If not, it returns to the interrupt thread if no */ +@/* preemption is necessary. Otherwise, if preemption is necessary or */ +@/* if no thread was running, the function returns to the scheduler. */ +@/* */ +@/* INPUT */ +@/* */ +@/* None */ +@/* */ +@/* OUTPUT */ +@/* */ +@/* None */ +@/* */ +@/* CALLS */ +@/* */ +@/* _tx_thread_schedule Thread scheduling routine */ +@/* */ +@/* CALLED BY */ +@/* */ +@/* ISRs Interrupt Service Routines */ +@/* */ +@/**************************************************************************/ +@VOID _tx_thread_context_restore(VOID) +@{ + .global _tx_thread_context_restore + .type _tx_thread_context_restore,function +_tx_thread_context_restore: +@ +@ /* Lockout interrupts. */ +@ +#ifdef TX_ENABLE_FIQ_SUPPORT + CPSID if @ Disable IRQ and FIQ interrupts +#else + CPSID i @ Disable IRQ interrupts +#endif + +#ifdef TX_ENABLE_EXECUTION_CHANGE_NOTIFY +@ +@ /* Call the ISR exit function to indicate an ISR is complete. */ +@ + BL _tx_execution_isr_exit @ Call the ISR exit function +#endif +@ +@ /* Determine if interrupts are nested. */ +@ if (--_tx_thread_system_state) +@ { +@ + LDR r3, =_tx_thread_system_state @ Pickup address of system state variable + LDR r2, [r3] @ Pickup system state + SUB r2, r2, #1 @ Decrement the counter + STR r2, [r3] @ Store the counter + CMP r2, #0 @ Was this the first interrupt? + BEQ __tx_thread_not_nested_restore @ If so, not a nested restore +@ +@ /* Interrupts are nested. */ +@ +@ /* Just recover the saved registers and return to the point of +@ interrupt. */ +@ + LDMIA sp!, {r0, r10, r12, lr} @ Recover SPSR, POI, and scratch regs + MSR SPSR_cxsf, r0 @ Put SPSR back + LDMIA sp!, {r0-r3} @ Recover r0-r3 + MOVS pc, lr @ Return to point of interrupt +@ +@ } +__tx_thread_not_nested_restore: +@ +@ /* Determine if a thread was interrupted and no preemption is required. */ +@ else if (((_tx_thread_current_ptr) && (_tx_thread_current_ptr == _tx_thread_execute_ptr)) +@ || (_tx_thread_preempt_disable)) +@ { +@ + LDR r1, =_tx_thread_current_ptr @ Pickup address of current thread ptr + LDR r0, [r1] @ Pickup actual current thread pointer + CMP r0, #0 @ Is it NULL? + BEQ __tx_thread_idle_system_restore @ Yes, idle system was interrupted +@ + LDR r3, =_tx_thread_preempt_disable @ Pickup preempt disable address + LDR r2, [r3] @ Pickup actual preempt disable flag + CMP r2, #0 @ Is it set? + BNE __tx_thread_no_preempt_restore @ Yes, don't preempt this thread + LDR r3, =_tx_thread_execute_ptr @ Pickup address of execute thread ptr + LDR r2, [r3] @ Pickup actual execute thread pointer + CMP r0, r2 @ Is the same thread highest priority? + BNE __tx_thread_preempt_restore @ No, preemption needs to happen +@ +@ +__tx_thread_no_preempt_restore: +@ +@ /* Restore interrupted thread or ISR. */ +@ +@ /* Pickup the saved stack pointer. */ +@ tmp_ptr = _tx_thread_current_ptr -> tx_thread_stack_ptr; +@ +@ /* Recover the saved context and return to the point of interrupt. */ +@ + LDMIA sp!, {r0, r10, r12, lr} @ Recover SPSR, POI, and scratch regs + MSR SPSR_cxsf, r0 @ Put SPSR back + LDMIA sp!, {r0-r3} @ Recover r0-r3 + MOVS pc, lr @ Return to point of interrupt +@ +@ } +@ else +@ { +__tx_thread_preempt_restore: +@ + LDMIA sp!, {r3, r10, r12, lr} @ Recover temporarily saved registers + MOV r1, lr @ Save lr (point of interrupt) +@ +@ /* SYS mode rather than SVC, and this is the substantive difference between +@ this file and the base port's. The interrupted thread's stack is the +@ banked sp that User and System mode share, whoever the thread belongs to: +@ a module thread was in User mode, and a kernel thread in this port starts +@ in System mode for exactly this reason. Entering SVC mode here would +@ push the interrupted context onto the supervisor call handler's stack +@ instead of the thread's, and the thread would resume with a corrupted +@ frame. */ +@ + MOV r2, #SYS_MODE @ Build SYS mode CPSR + MSR CPSR_c, r2 @ Enter SYS mode + STR r1, [sp, #-4]! @ Save point of interrupt + STMDB sp!, {r4-r12, lr} @ Save upper half of registers + MOV r4, r3 @ Save SPSR in r4 + MOV r2, #IRQ_MODE @ Build IRQ mode CPSR + MSR CPSR_c, r2 @ Enter IRQ mode + LDMIA sp!, {r0-r3} @ Recover r0-r3 + MOV r5, #SYS_MODE @ Build SYS mode CPSR + MSR CPSR_c, r5 @ Enter SYS mode + STMDB sp!, {r0-r3} @ Save r0-r3 on thread's stack +@ + LDR r1, =_tx_thread_current_ptr @ Pickup address of current thread ptr + LDR r0, [r1] @ Pickup current thread pointer +@ +#ifdef TX_ENABLE_VFP_SUPPORT + LDR r2, [r0, #144] @ Pickup the VFP enabled flag + CMP r2, #0 @ Is the VFP enabled? + BEQ _tx_skip_irq_vfp_save @ No, skip VFP IRQ save + VMRS r2, FPSCR @ Pickup the FPSCR + STR r2, [sp, #-4]! @ Save FPSCR + VSTMDB sp!, {D0-D15} @ Save D0-D15 +_tx_skip_irq_vfp_save: +#endif +@ + MOV r3, #1 @ Build interrupt stack type + STMDB sp!, {r3, r4} @ Save interrupt stack type and SPSR + STR sp, [r0, #8] @ Save stack pointer in thread control + @ block +@ +@ /* Save the remaining time-slice and disable it. */ +@ if (_tx_timer_time_slice) +@ { +@ + LDR r3, =_tx_timer_time_slice @ Pickup time-slice variable address + LDR r2, [r3] @ Pickup time-slice + CMP r2, #0 @ Is it active? + BEQ __tx_thread_dont_save_ts @ No, don't save it +@ +@ _tx_thread_current_ptr -> tx_thread_time_slice = _tx_timer_time_slice; +@ _tx_timer_time_slice = 0; +@ + STR r2, [r0, #24] @ Save thread's time-slice + MOV r2, #0 @ Clear value + STR r2, [r3] @ Disable global time-slice flag +@ +@ } +__tx_thread_dont_save_ts: +@ +@ +@ /* Clear the current task pointer. */ +@ _tx_thread_current_ptr = TX_NULL; +@ + MOV r0, #0 @ NULL value + STR r0, [r1] @ Clear current thread pointer +@ +@ /* Return to the scheduler. */ +@ _tx_thread_schedule(); +@ + B _tx_thread_schedule @ Return to scheduler +@ } +@ +__tx_thread_idle_system_restore: +@ +@ /* Just return back to the scheduler! */ +@ + MOV r0, #SYS_MODE @ Build SYS mode CPSR + MSR CPSR_c, r0 @ Enter SYS mode + B _tx_thread_schedule @ Return to scheduler +@} + + + diff --git a/ports_module/cortex_r52/gnu/module_manager/src/tx_thread_context_save.S b/ports_module/cortex_r52/gnu/module_manager/src/tx_thread_context_save.S new file mode 100644 index 000000000..9b61077e4 --- /dev/null +++ b/ports_module/cortex_r52/gnu/module_manager/src/tx_thread_context_save.S @@ -0,0 +1,193 @@ +@/*************************************************************************** +@ * Copyright (c) 2024 Microsoft Corporation +@ * 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. +@ * +@ * SPDX-License-Identifier: MIT +@ **************************************************************************/ +@ Some portions generated by Claude Code (Opus 5). +@ +@ +@/**************************************************************************/ +@/**************************************************************************/ +@/** */ +@/** ThreadX Component */ +@/** */ +@/** Thread */ +@/** */ +@/**************************************************************************/ +@/**************************************************************************/ +#ifdef TX_INCLUDE_USER_DEFINE_FILE +#include "tx_user.h" +#endif + + .global _tx_thread_system_state + .global _tx_thread_current_ptr + .global _tx_irq_processing_return + .global _tx_execution_isr_enter +@ +@ +@/* No 16-bit Thumb mode veneer code is needed for _tx_thread_context_save +@ since it will never be called 16-bit mode. */ +@ + .arm + .text + .align 2 +@/**************************************************************************/ +@/* */ +@/* FUNCTION RELEASE */ +@/* */ +@/* _tx_thread_context_save Cortex-R52/GNU */ +@/* 6.5.2 */ +@/* AUTHOR */ +@/* */ +@/* Frédéric Desbiens, Eclipse Foundation */ +@/* */ +@/* Derived from the Cortex-R5/GNU port originally written by */ +@/* William E. Lamie, Microsoft Corporation. */ +@/* */ +@/* DESCRIPTION */ +@/* */ +@/* This function saves the context of an executing thread in the */ +@/* beginning of interrupt processing. The function also ensures that */ +@/* the system stack is used upon return to the calling ISR. */ +@/* */ +@/* INPUT */ +@/* */ +@/* None */ +@/* */ +@/* OUTPUT */ +@/* */ +@/* None */ +@/* */ +@/* CALLS */ +@/* */ +@/* None */ +@/* */ +@/* CALLED BY */ +@/* */ +@/* ISRs */ +@/* */ +@/**************************************************************************/ +@VOID _tx_thread_context_save(VOID) +@{ + .global _tx_thread_context_save + .type _tx_thread_context_save,function +_tx_thread_context_save: +@ +@ /* Upon entry to this routine, it is assumed that IRQ interrupts are locked +@ out, we are in IRQ mode, and all registers are intact. */ +@ +@ /* Check for a nested interrupt condition. */ +@ if (_tx_thread_system_state++) +@ { +@ + STMDB sp!, {r0-r3} @ Save some working registers +#ifdef TX_ENABLE_FIQ_SUPPORT + CPSID if @ Disable FIQ interrupts +#endif + LDR r3, =_tx_thread_system_state @ Pickup address of system state variable + LDR r2, [r3] @ Pickup system state + CMP r2, #0 @ Is this the first interrupt? + BEQ __tx_thread_not_nested_save @ Yes, not a nested context save +@ +@ /* Nested interrupt condition. */ +@ + ADD r2, r2, #1 @ Increment the interrupt counter + STR r2, [r3] @ Store it back in the variable +@ +@ /* Save the rest of the scratch registers on the stack and return to the +@ calling ISR. */ +@ + MRS r0, SPSR @ Pickup saved SPSR + SUB lr, lr, #4 @ Adjust point of interrupt + STMDB sp!, {r0, r10, r12, lr} @ Store other registers +@ +@ /* Return to the ISR. */ +@ + MOV r10, #0 @ Clear stack limit + +#ifdef TX_ENABLE_EXECUTION_CHANGE_NOTIFY +@ +@ /* Call the ISR enter function to indicate an ISR is executing. */ +@ + PUSH {lr} @ Save ISR lr + BL _tx_execution_isr_enter @ Call the ISR enter function + POP {lr} @ Recover ISR lr +#endif + + B __tx_irq_processing_return @ Continue IRQ processing +@ +__tx_thread_not_nested_save: +@ } +@ +@ /* Otherwise, not nested, check to see if a thread was running. */ +@ else if (_tx_thread_current_ptr) +@ { +@ + ADD r2, r2, #1 @ Increment the interrupt counter + STR r2, [r3] @ Store it back in the variable + LDR r1, =_tx_thread_current_ptr @ Pickup address of current thread ptr + LDR r0, [r1] @ Pickup current thread pointer + CMP r0, #0 @ Is it NULL? + BEQ __tx_thread_idle_system_save @ If so, interrupt occurred in + @ scheduling loop - nothing needs saving! +@ +@ /* Save minimal context of interrupted thread. */ +@ + MRS r2, SPSR @ Pickup saved SPSR + SUB lr, lr, #4 @ Adjust point of interrupt + STMDB sp!, {r2, r10, r12, lr} @ Store other registers +@ +@ /* Save the current stack pointer in the thread's control block. */ +@ _tx_thread_current_ptr -> tx_thread_stack_ptr = sp; +@ +@ /* Switch to the system stack. */ +@ sp = _tx_thread_system_stack_ptr@ +@ + MOV r10, #0 @ Clear stack limit + +#ifdef TX_ENABLE_EXECUTION_CHANGE_NOTIFY +@ +@ /* Call the ISR enter function to indicate an ISR is executing. */ +@ + PUSH {lr} @ Save ISR lr + BL _tx_execution_isr_enter @ Call the ISR enter function + POP {lr} @ Recover ISR lr +#endif + + B __tx_irq_processing_return @ Continue IRQ processing +@ +@ } +@ else +@ { +@ +__tx_thread_idle_system_save: +@ +@ /* Interrupt occurred in the scheduling loop. */ +@ +@ /* Not much to do here, just adjust the stack pointer, and return to IRQ +@ processing. */ +@ + MOV r10, #0 @ Clear stack limit + +#ifdef TX_ENABLE_EXECUTION_CHANGE_NOTIFY +@ +@ /* Call the ISR enter function to indicate an ISR is executing. */ +@ + PUSH {lr} @ Save ISR lr + BL _tx_execution_isr_enter @ Call the ISR enter function + POP {lr} @ Recover ISR lr +#endif + + ADD sp, sp, #16 @ Recover saved registers + B __tx_irq_processing_return @ Continue IRQ processing +@ +@ } +@} + + + diff --git a/ports_module/cortex_r52/gnu/module_manager/src/tx_thread_stack_build.S b/ports_module/cortex_r52/gnu/module_manager/src/tx_thread_stack_build.S index dc9238c43..88293e290 100644 --- a/ports_module/cortex_r52/gnu/module_manager/src/tx_thread_stack_build.S +++ b/ports_module/cortex_r52/gnu/module_manager/src/tx_thread_stack_build.S @@ -27,6 +27,7 @@ .arm SVC_MODE = 0x13 @ SVC mode +SYS_MODE = 0x1F @ SYS mode, privileged #ifdef TX_ENABLE_FIQ_SUPPORT CPSR_MASK = 0xDF @ Mask initial CPSR, IRQ & FIQ interrupts enabled #else @@ -152,7 +153,20 @@ _tx_thread_stack_build: STR r3, [r2, #68] @ 0 for back-trace MRS r1, CPSR @ Pickup CPSR BIC r1, r1, #CPSR_MASK @ Mask mode bits of CPSR - ORR r3, r1, #SVC_MODE @ Build CPSR, SVC mode, interrupts enabled +@ +@ /* SYS mode, not SVC. In a module port no thread runs in SVC mode: SVC is +@ reserved for the supervisor call handler, which is how a module reaches +@ the kernel, and a thread sitting in SVC mode would be using the handler's +@ banked stack pointer as its own. Kernel threads therefore start in SYS +@ mode, which is privileged and shares User mode's banked sp -- the same sp +@ a module thread uses -- so the context save and restore paths can reach a +@ thread's stack the same way whoever it belongs to. +@ +@ This is the one line that differs from the base port's stack build, and +@ leaving it as SVC would have put kernel threads on the SVC stack while the +@ restore path looked for them on the SYS one. */ +@ + ORR r3, r1, #SYS_MODE @ Build CPSR, SYS mode, interrupts enabled STR r3, [r2, #4] @ Store initial CPSR @ @ /* Setup stack pointer. */ diff --git a/ports_module/cortex_r52/gnu/module_manager/src/tx_thread_system_return.S b/ports_module/cortex_r52/gnu/module_manager/src/tx_thread_system_return.S new file mode 100644 index 000000000..c0ab615b4 --- /dev/null +++ b/ports_module/cortex_r52/gnu/module_manager/src/tx_thread_system_return.S @@ -0,0 +1,169 @@ +@/*************************************************************************** +@ * Copyright (c) 2024 Microsoft Corporation +@ * 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. +@ * +@ * SPDX-License-Identifier: MIT +@ **************************************************************************/ +@ Some portions generated by Claude Code (Opus 5). +@ +@ +@/**************************************************************************/ +@/**************************************************************************/ +@/** */ +@/** ThreadX Component */ +@/** */ +@/** Thread */ +@/** */ +@/**************************************************************************/ +@/**************************************************************************/ +#ifdef TX_INCLUDE_USER_DEFINE_FILE +#include "tx_user.h" +#endif + + .arm +@ +@ + .global _tx_thread_current_ptr + .global _tx_timer_time_slice + .global _tx_thread_schedule + .global _tx_execution_thread_exit +@ +@ +@ +@/* Define the 16-bit Thumb mode veneer for _tx_thread_system_return for +@ applications calling this function from to 16-bit Thumb mode. */ +@ + .text + .align 2 + .global $_tx_thread_system_return + .type $_tx_thread_system_return,function +$_tx_thread_system_return: + .thumb + BX pc @ Switch to 32-bit mode + NOP @ + .arm + STMFD sp!, {lr} @ Save return address + BL _tx_thread_system_return @ Call _tx_thread_system_return function + LDMFD sp!, {lr} @ Recover saved return address + BX lr @ Return to 16-bit caller +@ +@ + .text + .align 2 +@/**************************************************************************/ +@/* */ +@/* FUNCTION RELEASE */ +@/* */ +@/* _tx_thread_system_return Cortex-R52/GNU */ +@/* 6.5.2 */ +@/* AUTHOR */ +@/* */ +@/* Frédéric Desbiens, Eclipse Foundation */ +@/* */ +@/* Derived from the Cortex-R5/GNU port originally written by */ +@/* William E. Lamie, Microsoft Corporation. */ +@/* */ +@/* DESCRIPTION */ +@/* */ +@/* This function is target processor specific. It is used to transfer */ +@/* control from a thread back to the ThreadX system. Only a */ +@/* minimal context is saved since the compiler assumes temp registers */ +@/* are going to get slicked by a function call anyway. */ +@/* */ +@/* INPUT */ +@/* */ +@/* None */ +@/* */ +@/* OUTPUT */ +@/* */ +@/* None */ +@/* */ +@/* CALLS */ +@/* */ +@/* _tx_thread_schedule Thread scheduling loop */ +@/* */ +@/* CALLED BY */ +@/* */ +@/* ThreadX components */ +@/* */ +@/**************************************************************************/ +@VOID _tx_thread_system_return(VOID) +@{ + .global _tx_thread_system_return + .type _tx_thread_system_return,function +_tx_thread_system_return: +@ +@ /* Lockout interrupts. */ +@ + MRS r1, CPSR @ Pickup the CPSR +#ifdef TX_ENABLE_FIQ_SUPPORT + CPSID if @ Disable IRQ and FIQ interrupts +#else + CPSID i @ Disable IRQ interrupts +#endif +@ /* Save minimal context on the stack. */ +@ + STMDB sp!, {r4-r11, lr} @ Save minimal context + LDR r5, =_tx_thread_current_ptr @ Pickup address of current ptr + LDR r6, [r5, #0] @ Pickup current thread pointer +@ +#ifdef TX_ENABLE_VFP_SUPPORT + LDR r0, [r6, #144] @ Pickup the VFP enabled flag + CMP r0, #0 @ Is the VFP enabled? + BEQ _tx_skip_solicited_vfp_save @ No, skip VFP solicited save + VMRS r4, FPSCR @ Pickup the FPSCR + STR r4, [sp, #-4]! @ Save FPSCR + VSTMDB sp!, {D8-D15} @ Save D8-D15 +_tx_skip_solicited_vfp_save: +#endif +@ + MOV r0, #0 @ Build a solicited stack type + STMDB sp!, {r0-r1} @ Save type and CPSR +@ +@ +#ifdef TX_ENABLE_EXECUTION_CHANGE_NOTIFY +@ +@ /* Call the thread exit function to indicate the thread is no longer executing. */ +@ + BL _tx_execution_thread_exit @ Call the thread exit function +#endif +@ + LDR r2, =_tx_timer_time_slice @ Pickup address of time slice + LDR r1, [r2, #0] @ Pickup current time slice +@ +@ /* Save current stack and switch to system stack. */ +@ _tx_thread_current_ptr -> tx_thread_stack_ptr = sp; +@ sp = _tx_thread_system_stack_ptr; +@ + STR sp, [r6, #8] @ Save thread stack pointer +@ +@ /* Determine if the time-slice is active. */ +@ if (_tx_timer_time_slice) +@ { +@ + MOV r4, #0 @ Build clear value + CMP r1, #0 @ Is a time-slice active? + BEQ __tx_thread_dont_save_ts @ No, don't save the time-slice +@ +@ /* Save time-slice for the thread and clear the current time-slice. */ +@ _tx_thread_current_ptr -> tx_thread_time_slice = _tx_timer_time_slice; +@ _tx_timer_time_slice = 0; +@ + STR r4, [r2, #0] @ Clear time-slice + STR r1, [r6, #24] @ Save current time-slice +@ +@ } +__tx_thread_dont_save_ts: +@ +@ /* Clear the current thread pointer. */ +@ _tx_thread_current_ptr = TX_NULL; +@ + STR r4, [r5, #0] @ Clear current thread pointer + B _tx_thread_schedule @ Jump to scheduler! +@ +@} + From d5194a4deb7debb34e36783547ac306e394f090a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20Desbiens?= Date: Tue, 18 Aug 2026 11:04:22 -0400 Subject: [PATCH 11/33] Disabled the port dispatch, and added the module-side library Two small pieces, both decided by finding out what this port does not need rather than by adding something. The port dispatch is now undefined. It exists so a port can service kernel requests specific to it, and the only ones the Armv8-M port dispatches are its secure-stack allocate and free, which belong to the security extension this core does not have. The header inherited the define from that port, which would have had the module manager call a _txm_module_manager_port_dispatch that does not exist. The Cortex-R4 module port leaves it undefined for the same reason, and the comment says so where the define used to be. module_lib gets the thread shell entry and nothing else. The Cortex-R4 module library also carries txm_module_initialize.S, and that file is not portable here: it references __scatterload and __rt_entry, which are ARM Compiler runtime symbols. The GNU Armv8-M module library has no initialize file at all, which is the better guide for a GNU port -- so the shell entry is taken from there, and it turned out architecture-neutral apart from its identity line. That is the pattern for most of this port so far: the Armv8-M port is the right parent for anything touching the MPU or the toolchain, and the Cortex-R4 port for anything touching processor modes and privilege. Taking either one wholesale would have been wrong. Assisted-by: Claude Code (Opus 5) --- .../cortex_r52/gnu/inc/txm_module_port.h | 9 +- .../src/txm_module_thread_shell_entry.c | 168 ++++++++++++++++++ 2 files changed, 176 insertions(+), 1 deletion(-) create mode 100644 ports_module/cortex_r52/gnu/module_lib/src/txm_module_thread_shell_entry.c diff --git a/ports_module/cortex_r52/gnu/inc/txm_module_port.h b/ports_module/cortex_r52/gnu/inc/txm_module_port.h index 5fd3aad5f..3ccc5bae0 100644 --- a/ports_module/cortex_r52/gnu/inc/txm_module_port.h +++ b/ports_module/cortex_r52/gnu/inc/txm_module_port.h @@ -100,7 +100,14 @@ The following extensions must also be defined in tx_port.h: /* Define the properties for this particular module port. */ -#define TXM_MODULE_PORT_DISPATCH +/* No port dispatch. It exists so a port can service kernel requests specific to + it, and this one has none: the only calls the Armv8-M port dispatches are its + secure-stack allocate and free, which belong to the security extension this core + does not have. Defining it would have the module manager call a + _txm_module_manager_port_dispatch that does not exist. The Cortex-R4 module + port leaves it undefined for the same reason. */ + +/* #define TXM_MODULE_PORT_DISPATCH */ #define TXM_MODULE_MEMORY_PROTECTION_ENABLED diff --git a/ports_module/cortex_r52/gnu/module_lib/src/txm_module_thread_shell_entry.c b/ports_module/cortex_r52/gnu/module_lib/src/txm_module_thread_shell_entry.c new file mode 100644 index 000000000..4f01b7a8c --- /dev/null +++ b/ports_module/cortex_r52/gnu/module_lib/src/txm_module_thread_shell_entry.c @@ -0,0 +1,168 @@ +/*************************************************************************** + * Copyright (c) 2024 Microsoft Corporation + * Copyright (c) 2026-present 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. + * + * SPDX-License-Identifier: MIT + **************************************************************************/ + + +/**************************************************************************/ +/**************************************************************************/ +/** */ +/** ThreadX Component */ +/** */ +/** Module */ +/** */ +/**************************************************************************/ +/**************************************************************************/ + +#ifndef TXM_MODULE +#define TXM_MODULE +#endif + +#ifndef TX_SOURCE_CODE +#define TX_SOURCE_CODE +#endif + + +/* Include necessary system files. */ + +#include "txm_module.h" +#include "tx_thread.h" + +/* Define the global module entry pointer from the start thread of the module. */ + +TXM_MODULE_THREAD_ENTRY_INFO *_txm_module_entry_info; + + +/* Define the dispatch function pointer used in the module implementation. */ + +ULONG (*_txm_module_kernel_call_dispatcher)(ULONG kernel_request, ULONG param_1, ULONG param_2, ULONG param3); + + +/* Define the GCC startup code that clears the uninitialized global data and sets up the + preset global variables. */ + +extern VOID _gcc_setup(TXM_MODULE_INSTANCE *); + + +/**************************************************************************/ +/* */ +/* FUNCTION RELEASE */ +/* */ +/* _txm_module_thread_shell_entry Cortex-R52/GNU */ +/* 6.1.5 */ +/* AUTHOR */ +/* */ +/* Scott Larson, Microsoft Corporation */ +/* */ +/* DESCRIPTION */ +/* */ +/* This function calls the specified entry function of the thread. It */ +/* also provides a place for the thread's entry function to return. */ +/* If the thread returns, this function places the thread in a */ +/* "COMPLETED" state. */ +/* */ +/* INPUT */ +/* */ +/* thread_ptr Pointer to current thread */ +/* thread_info Pointer to thread entry info */ +/* */ +/* OUTPUT */ +/* */ +/* None */ +/* */ +/* CALLS */ +/* */ +/* _gcc_setup GNU global init function */ +/* thread_entry Thread's entry function */ +/* tx_thread_resume Resume the module callback thread */ +/* _txm_module_thread_system_suspend Module thread suspension routine */ +/* */ +/* CALLED BY */ +/* */ +/* Initial thread stack frame */ +/* */ +/**************************************************************************/ +VOID _txm_module_thread_shell_entry(TX_THREAD *thread_ptr, TXM_MODULE_THREAD_ENTRY_INFO *thread_info) +{ + +#ifndef TX_DISABLE_NOTIFY_CALLBACKS + VOID (*entry_exit_notify)(TX_THREAD *, UINT); +#endif + + + /* Determine if this is the start thread. If so, we must prepare the module for + execution. If not, simply skip the C startup code. */ + if (thread_info -> txm_module_thread_entry_info_start_thread) + { + /* Initialize the GNU C environment. */ + _gcc_setup(thread_info -> txm_module_thread_entry_info_code_base_address); + + /* Save the entry info pointer, for later use. */ + _txm_module_entry_info = thread_info; + + /* Save the kernel function dispatch address. This is used to make all resident calls from + the module. */ + _txm_module_kernel_call_dispatcher = thread_info -> txm_module_thread_entry_info_kernel_call_dispatcher; + + /* Ensure that we have a valid pointer. */ + while (!_txm_module_kernel_call_dispatcher) + { + /* Loop here, if an error is present getting the dispatch function pointer! + An error here typically indicates the resident portion of _tx_thread_schedule + is not supporting the trap to obtain the function pointer. */ + } + + /* Resume the module's callback thread, already created in the manager. */ + _txe_thread_resume(thread_info -> txm_module_thread_entry_info_callback_request_thread); + } + +#ifndef TX_DISABLE_NOTIFY_CALLBACKS + + /* Pickup the entry/exit application callback routine. */ + entry_exit_notify = thread_info -> txm_module_thread_entry_info_exit_notify; + + /* Determine if an application callback routine is specified. */ + if (entry_exit_notify != TX_NULL) + { + + /* Yes, notify application that this thread has been entered! */ + (entry_exit_notify)(thread_ptr, TX_THREAD_ENTRY); + } +#endif + + /* Call current thread's entry function. */ + (thread_info -> txm_module_thread_entry_info_entry) (thread_info -> txm_module_thread_entry_info_parameter); + + /* Suspend thread with a "completed" state. */ + + +#ifndef TX_DISABLE_NOTIFY_CALLBACKS + + /* Pickup the entry/exit application callback routine again. */ + entry_exit_notify = thread_info -> txm_module_thread_entry_info_exit_notify; + + /* Determine if an application callback routine is specified. */ + if (entry_exit_notify != TX_NULL) + { + + /* Yes, notify application that this thread has exited! */ + (entry_exit_notify)(thread_ptr, TX_THREAD_EXIT); + } +#endif + + /* Call actual thread suspension routine. */ + _txm_module_thread_system_suspend(thread_ptr); + +#ifdef TX_SAFETY_CRITICAL + + /* If we ever get here, raise safety critical exception. */ + TX_SAFETY_CRITICAL_EXCEPTION(__FILE__, __LINE__, 0); +#endif +} + From d02d9f4aa0ab68c7dbb4ca51ccdc1c8d90d01a48 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20Desbiens?= Date: Tue, 18 Aug 2026 11:12:29 -0400 Subject: [PATCH 12/33] Added the module preamble, which checks its own declared size The header the module manager reads before it will load a module: identity, version, properties, and the entry points it needs, all as offsets from the start of the preamble rather than addresses. A module is position independent because the manager decides where it lands, so it cannot know its own addresses at link time. Properties are 0x02000003 -- the GNU compiler field, plus user mode and memory protection. Those last two together are the point of this port; a module built without them would load and run privileged, which the manager permits and this example does not want. Two departures from the Cortex-R4 preamble this is modelled on. Code and data sizes come from the linker script rather than being written in as literals. R4's preamble carries the numbers 9000 and 11000, and a module that grows past them gets less memory mapped than it uses -- a fault in a module that did nothing wrong, whose cause is a constant in a file nobody thought to change. The comment says so, along with the fact that the right response to such a fault is not to inflate the numbers. The preamble declares its own length in its fourth word and the manager believes it, so the assembler now checks that the declaration matches the file. Tested both ways: it passes as written, and with one extra word added it fails the build naming the problem. A size field that agrees with reality only because someone counted once is a latent bug. Assisted-by: Claude Code (Opus 5) --- .../s32z280_evb/txm_module_preamble.S | 123 ++++++++++++++++++ 1 file changed, 123 insertions(+) create mode 100644 ports_module/cortex_r52/gnu/example_build/s32z280_evb/txm_module_preamble.S diff --git a/ports_module/cortex_r52/gnu/example_build/s32z280_evb/txm_module_preamble.S b/ports_module/cortex_r52/gnu/example_build/s32z280_evb/txm_module_preamble.S new file mode 100644 index 000000000..f1c4b9f5a --- /dev/null +++ b/ports_module/cortex_r52/gnu/example_build/s32z280_evb/txm_module_preamble.S @@ -0,0 +1,123 @@ +@/*************************************************************************** +@ * 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 +@ **************************************************************************/ +@ +@/**************************************************************************/ +@/* */ +@/* MODULE PREAMBLE RELEASE */ +@/* */ +@/* txm_module_preamble.S Cortex-R52/GNU */ +@/* 6.5.2 */ +@/* AUTHOR */ +@/* */ +@/* Frédéric Desbiens, Eclipse Foundation */ +@/* */ +@/* DESCRIPTION */ +@/* */ +@/* The header the module manager reads before it will load a module. */ +@/* */ +@/* It must be the first thing in the module image, which is what the */ +@/* linker script arranges, and every entry point in it is an offset */ +@/* from the start of the preamble rather than an address. A module is */ +@/* position independent: the manager decides where it lands, so the */ +@/* module cannot know its own addresses at link time. */ +@/* */ +@/* Code and data sizes come from the linker script rather than being */ +@/* written in by hand. The Cortex-R4 preamble carries literal numbers */ +@/* for them, which is a standing invitation to grow a module past its */ +@/* declared size and have the manager map less memory than it uses -- */ +@/* a fault in a module that did nothing wrong, whose cause is a */ +@/* constant in a file nobody thought to change. */ +@/* */ +@/**************************************************************************/ + + .syntax unified + .arm + + .global __txm_module_preamble + + .extern _txm_module_thread_shell_entry + .extern _txm_module_callback_request_thread_entry + .extern demo_module_start + +@ Supplied by the module's linker script. + + .extern __txm_module_code_size + .extern __txm_module_data_size + +@ Properties. The compiler field tells the manager which set of entry-point +@ adjustments to apply, and the option bits say what the module is asking for: +@ user mode and memory protection, which together are the point of this port. +@ +@ 0x02000000 TXM_MODULE_GNU_COMPILER +@ 0x00000001 TXM_MODULE_USER_MODE +@ 0x00000002 TXM_MODULE_MEMORY_PROTECTION + + .equ MODULE_PROPERTIES, 0x02000003 + + .section .txm_module_preamble, "a" + .align 6 + +__txm_module_preamble: + + .word 0x4D4F4455 @ Module ID, "MODU" + .word 0x6 @ Major version + .word 0x1 @ Minor version + .word 32 @ Preamble size, 32-bit words + .word 0x52520001 @ Application-defined ID + .word MODULE_PROPERTIES @ Properties, see above + +@ Entry points, as offsets from the preamble. + + .word _txm_module_thread_shell_entry - __txm_module_preamble + .word demo_module_start - __txm_module_preamble + .word 0 @ No stop thread + .word 1 @ Start/stop thread priority + .word 1024 @ Start/stop thread stack size + .word _txm_module_callback_request_thread_entry - __txm_module_preamble + .word 1 @ Callback thread priority + .word 1024 @ Callback thread stack size + +@ Sizes, from the linker script. The manager rounds both up to the 64-byte MPU +@ granule and maps exactly this much; anything the module touches beyond it +@ faults, which is the intended behaviour and not a bug to work around by +@ inflating these numbers. + + .word __txm_module_code_size + .word __txm_module_data_size + + .word 0 @ Reserved 0 + .word 0 @ Reserved 1 + .word 0 @ Reserved 2 + .word 0 @ Reserved 3 + .word 0 @ Reserved 4 + .word 0 @ Reserved 5 + .word 0 @ Reserved 6 + .word 0 @ Reserved 7 + .word 0 @ Reserved 8 + .word 0 @ Reserved 9 + .word 0 @ Reserved 10 + .word 0 @ Reserved 11 + .word 0 @ Reserved 12 + .word 0 @ Reserved 13 + .word 0 @ Reserved 14 + .word 0 @ Reserved 15 + +@ The preamble declares its own length in its fourth word, and the manager +@ believes it. If the two ever disagree the manager reads entry points from the +@ wrong offsets, so the assembler checks rather than the reader. + + .if (. - __txm_module_preamble) != (32 * 4) + .error "txm_module_preamble is not the 32 words its size field declares" + .endif From 523ccef04b328896baa0aa87d5c5b24bf3792e4b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20Desbiens?= Date: Tue, 18 Aug 2026 11:24:58 -0400 Subject: [PATCH 13/33] Added a sample module that tests the boundary rather than the API The Armv8-M sample module creates eight threads, a queue, a semaphore, a mutex, event flags and two pools. That demonstrates the API works. It does not demonstrate the boundary works, and the boundary is the only thing this port adds -- that module would pass identically with the MPU switched off. This one does three things and is killed by the third: 1. Writes and reads its own data, which must succeed. 2. Calls tx_thread_sleep, which must succeed. A sleep rather than something cheaper because it yields: the scheduler then runs a module thread and reloads its regions on the way back, so the call exercises the region switch as well as the supervisor call boundary. 3. Reads an address the manager gives it, which must fault and must terminate the thread. Step 3 is the whole point. Steps 1 and 2 passing without step 3 faulting means the module is running unprotected, and there is a fourth flag set only on that path so the manager can report it as a failure rather than reading a missing flag as success. The forbidden address is passed in by the manager rather than hard-coded, so the module carries no assumption about where the manager lives. The module cannot print. The console belongs to the board support package, outside every region a module owns, so reaching for it would fault exactly as step 3 does. Progress is recorded in the module's own data for the manager to read back afterwards, which is the only reporting channel a protected module has. Assisted-by: Claude Code (Opus 5) --- .../s32z280_evb/sample_threadx_module.c | 133 ++++++++++++++++++ 1 file changed, 133 insertions(+) create mode 100644 ports_module/cortex_r52/gnu/example_build/s32z280_evb/sample_threadx_module.c diff --git a/ports_module/cortex_r52/gnu/example_build/s32z280_evb/sample_threadx_module.c b/ports_module/cortex_r52/gnu/example_build/s32z280_evb/sample_threadx_module.c new file mode 100644 index 000000000..9d4162e90 --- /dev/null +++ b/ports_module/cortex_r52/gnu/example_build/s32z280_evb/sample_threadx_module.c @@ -0,0 +1,133 @@ +/*************************************************************************** + * 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 + **************************************************************************/ + +/**************************************************************************/ +/* */ +/* SAMPLE MODULE RELEASE */ +/* */ +/* sample_threadx_module.c Cortex-R52/GNU */ +/* 6.5.2 */ +/* AUTHOR */ +/* */ +/* Frédéric Desbiens, Eclipse Foundation */ +/* */ +/* DESCRIPTION */ +/* */ +/* A module that exercises the protection boundary rather than */ +/* demonstrating features. */ +/* */ +/* The Armv8-M sample module creates eight threads, a queue, a */ +/* semaphore, a mutex, event flags and two pools. That shows the API */ +/* works; it does not show that the boundary works, and the boundary is */ +/* the only thing this port adds. So this module does four things and */ +/* stops: */ +/* */ +/* 1. Writes and reads its own data, which must succeed. */ +/* 2. Makes a kernel call, which must succeed -- proving a module in */ +/* User mode can reach the kernel through the supervisor call */ +/* boundary and come back. */ +/* 3. Reads the manager's memory, which must fault. */ +/* 4. Never reaches step 4, because step 3 terminates it. */ +/* */ +/* Steps 1 and 2 passing without step 3 faulting would mean the module */ +/* is running unprotected, which is the failure this example exists to */ +/* detect. A module that only ever touched its own memory would pass */ +/* identically with the MPU switched off. */ +/* */ +/* The module cannot print. The console belongs to the board support */ +/* package, outside every region a module owns, so reaching it would */ +/* fault as surely as step 3 does. Progress is recorded in the */ +/* module's own data and read back by the manager, which can print. */ +/* */ +/**************************************************************************/ + +#include "txm_module.h" + +/* Progress, in the module's own data. The manager reads this after the module + faults, which is how a module with no console reports what it managed to do. */ + +#define MODULE_PROGRESS_OWN_DATA 0x00000001UL +#define MODULE_PROGRESS_KERNEL_CALL 0x00000002UL +#define MODULE_PROGRESS_ATTEMPTED_STEAL 0x00000004UL +#define MODULE_PROGRESS_SURVIVED_STEAL 0x00000008UL + +volatile ULONG module_progress; +volatile ULONG module_scratch[16]; + +/* Filled in by the manager before the module starts: an address the module has no + business touching. Passing it in rather than hard-coding one keeps the module + free of assumptions about where the manager lives. */ + +volatile ULONG module_forbidden_address; + + +void demo_module_start(ULONG id) +{ + ULONG i; + ULONG sum = 0UL; + + (void) id; + + /* 1. The module's own data. If this faults, the data region is wrong and + nothing else in this file will be reached. */ + + for (i = 0UL; i < 16UL; i++) + { + module_scratch[i] = i * 3UL; + } + + for (i = 0UL; i < 16UL; i++) + { + sum += module_scratch[i]; + } + + if (sum == 360UL) /* 3 * (0 + 1 + ... + 15) */ + { + module_progress |= MODULE_PROGRESS_OWN_DATA; + } + + /* 2. A kernel call, which leaves User mode through the supervisor call + boundary and must come back. A sleep is used because it is the + simplest service with an observable effect and it yields, so the + scheduler runs a module thread and reloads its regions on the way + back -- exercising the region switch as well as the call. */ + + if (tx_thread_sleep(2UL) == TX_SUCCESS) + { + module_progress |= MODULE_PROGRESS_KERNEL_CALL; + } + + /* 3. Somebody else's memory. This must fault, and the fault must terminate + this thread. Marked before the access rather than after, because after + is not reached if the port is working. */ + + module_progress |= MODULE_PROGRESS_ATTEMPTED_STEAL; + + sum += *((volatile ULONG *) module_forbidden_address); + + /* 4. Only reached if the access above was permitted, which means the module + is running unprotected. The manager treats this flag as a failure. */ + + module_progress |= MODULE_PROGRESS_SURVIVED_STEAL; + + /* Keep the compiler from discarding the read above. */ + + module_scratch[0] = sum; + + while (1) + { + (void) tx_thread_sleep(100UL); + } +} From dabb02fae82a9e59206b1ee9b80746197f85d847 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20Desbiens?= Date: Tue, 18 Aug 2026 11:29:48 -0400 Subject: [PATCH 14/33] Added the module manager sample, whose expected result is a fault Loads the sample module, lets it reach outside its own memory, and reports what the hardware did about it. The fault is the result, not an error path. A module that starts and runs proves the loader works; a module stopped by the memory protection unit when it reaches outside its own memory proves the port works. So the absence of a fault is the failure, and the reporting says so in those words rather than printing a count and leaving the reader to judge. The report checks more than that a fault happened. SPSR's mode field says who faulted, and only User mode counts: a fault taken from privileged code would mean something else went wrong and the test has not passed. There are therefore two distinct failure messages, one for no fault at all and one for a fault from the wrong mode, because those have completely different causes. The fault notification records and does not print. It runs in the fault path, and this board's console is a polled driver that spins waiting for each byte to go out -- which is a poor thing to do with a fault in progress and a thread mid termination. A separate low-priority thread prints afterwards, and waits a bounded time for the fault rather than for ever, since a fault that never arrives is the result to report. The fault information structure needs an extern declaration in the application. The module manager expands it into the port's fault handler through a macro rather than declaring it in a header, so anything that wants to read it has to say so itself. Worth a comment, because the error a reader gets otherwise names a similar-looking function instead. Loading is in place, from where the linker puts the image, so nothing is copied and no filesystem or download path is needed. That changes where the module's code lives, not whether it is protected: the manager still maps it with its own MPU regions, which is the part under test. Assisted-by: Claude Code (Opus 5) --- .../s32z280_evb/sample_threadx_module.c | 16 +- .../sample_threadx_module_manager.c | 225 ++++++++++++++++++ 2 files changed, 237 insertions(+), 4 deletions(-) create mode 100644 ports_module/cortex_r52/gnu/example_build/s32z280_evb/sample_threadx_module_manager.c diff --git a/ports_module/cortex_r52/gnu/example_build/s32z280_evb/sample_threadx_module.c b/ports_module/cortex_r52/gnu/example_build/s32z280_evb/sample_threadx_module.c index 9d4162e90..470071f7d 100644 --- a/ports_module/cortex_r52/gnu/example_build/s32z280_evb/sample_threadx_module.c +++ b/ports_module/cortex_r52/gnu/example_build/s32z280_evb/sample_threadx_module.c @@ -66,11 +66,19 @@ volatile ULONG module_progress; volatile ULONG module_scratch[16]; -/* Filled in by the manager before the module starts: an address the module has no - business touching. Passing it in rather than hard-coding one keeps the module - free of assumptions about where the manager lives. */ +/* An address the module has no business touching, used by step 3. -volatile ULONG module_forbidden_address; + It defaults to the base of DRAM0, which is where the manager's own data and + stacks live on this board and is covered by a region the module does not own. + The manager can overwrite this before starting the module if it wants to aim + the test somewhere else, but it does not have to: locating a symbol inside a + loaded module means walking its image, and an example should not need that to + make its point. + + Whatever it holds must be outside every region the module owns, or step 3 + succeeds and the test silently proves nothing. */ + +volatile ULONG module_forbidden_address = 0x31780000UL; void demo_module_start(ULONG id) diff --git a/ports_module/cortex_r52/gnu/example_build/s32z280_evb/sample_threadx_module_manager.c b/ports_module/cortex_r52/gnu/example_build/s32z280_evb/sample_threadx_module_manager.c new file mode 100644 index 000000000..fb9517509 --- /dev/null +++ b/ports_module/cortex_r52/gnu/example_build/s32z280_evb/sample_threadx_module_manager.c @@ -0,0 +1,225 @@ +/*************************************************************************** + * 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 + **************************************************************************/ + +/**************************************************************************/ +/* */ +/* MODULE MANAGER SAMPLE RELEASE */ +/* */ +/* sample_threadx_module_manager.c Cortex-R52/GNU */ +/* 6.5.2 */ +/* AUTHOR */ +/* */ +/* Frédéric Desbiens, Eclipse Foundation */ +/* */ +/* DESCRIPTION */ +/* */ +/* Loads the sample module, lets it misbehave, and reports what the */ +/* hardware did about it. */ +/* */ +/* The result this example exists to produce is the fault. A module */ +/* that starts and runs proves the loader works; a module that is */ +/* stopped by the memory protection unit when it reaches outside its */ +/* own memory proves the port works. So the fault notification is not */ +/* an error path here, it is the expected outcome, and its absence is */ +/* the failure. */ +/* */ +/* What the module image is and where it comes from: it is linked into */ +/* this application as a separate section and loaded in place, so */ +/* nothing is copied and no filesystem or download path is needed. */ +/* The manager still maps it with its own MPU regions, which is what */ +/* matters -- loading in place changes where the code lives, not */ +/* whether it is protected. */ +/* */ +/**************************************************************************/ + +#include "tx_api.h" +#include "txm_module.h" +#include "linflexd.h" +#include "platform.h" +#include "thread_mpu.h" + +/* The fault information the abort vector captured. Declared here because the + module manager expands it into the port's fault handler through the + TXM_MODULE_MANAGER_FAULT_INFO macro rather than declaring it in a header, so an + application that wants to read it has to say so itself. */ + +extern TXM_MODULE_MANAGER_MEMORY_FAULT_INFO _txm_module_manager_memory_fault_info; + +/* Where the module image sits. Provided by the linker script, which places the + module's preamble first so this address is also the preamble address. */ + +extern unsigned char __module_image_start__; + +/* Memory the manager hands out to modules: object memory, and the data region a + module's own variables live in. It has to be outside every region a module is + given, or a module could reach another module's data. */ + +#ifndef MODULE_POOL_SIZE +#define MODULE_POOL_SIZE (16U * 1024U) +#endif + +static unsigned char module_pool[MODULE_POOL_SIZE] __attribute__((aligned(64))); + +static TXM_MODULE_INSTANCE demo_module; + +/* What the fault handler saw. Read by the reporting thread after the module has + been terminated. */ + +static volatile ULONG fault_count; +static volatile ULONG fault_dfsr; +static volatile ULONG fault_dfar; +static volatile ULONG fault_spsr; +static volatile ULONG fault_code_location; + +static TX_THREAD report_thread; +static unsigned char report_stack[2048] __attribute__((aligned(8))); + + +/**************************************************************************/ +/* Fault notification. */ +/* */ +/* Called by the module manager after it has terminated the offending */ +/* thread. Records rather than prints: this runs in the fault path, and */ +/* the console is a polled driver that spins waiting for a transmit to */ +/* complete. */ +/**************************************************************************/ + +static void module_fault_notify(TX_THREAD *thread_ptr, TXM_MODULE_INSTANCE *module_instance) +{ + (void) thread_ptr; + (void) module_instance; + + fault_count++; + fault_dfsr = _txm_module_manager_memory_fault_info.txm_module_manager_memory_fault_info_dfsr; + fault_dfar = _txm_module_manager_memory_fault_info.txm_module_manager_memory_fault_info_dfar; + fault_spsr = _txm_module_manager_memory_fault_info.txm_module_manager_memory_fault_info_spsr; + fault_code_location = (ULONG) _txm_module_manager_memory_fault_info.txm_module_manager_memory_fault_info_code_location; +} + + +static void put_hex(unsigned long value) +{ + linflexd_put_hex32((unsigned int) value); +} + + +/**************************************************************************/ +/* Reporting. */ +/**************************************************************************/ + +static void report_entry(ULONG input) +{ + (void) input; + + /* Give the module time to run its three steps and be terminated by the + third. Bounded: if the fault never arrives, that is the result to + report, not a reason to wait for ever. */ + + { + unsigned int waited = 0U; + + while ((fault_count == 0UL) && (waited < 50U)) + { + tx_thread_sleep(2UL); + waited++; + } + } + + linflexd_puts("\n=== ThreadX modules on S32Z280: protection boundary ===\n"); + + linflexd_puts("module faults = "); + put_hex(fault_count); + linflexd_puts("\n"); + + if (fault_count == 0UL) + { + linflexd_puts("FAIL the module reached outside its memory and was not stopped\n"); + } + else + { + linflexd_puts("DFSR = "); + put_hex(fault_dfsr); + linflexd_puts("\nDFAR = "); + put_hex(fault_dfar); + linflexd_puts("\nSPSR = "); + put_hex(fault_spsr); + linflexd_puts("\nfaulting pc = "); + put_hex(fault_code_location); + linflexd_puts("\n"); + + /* SPSR's mode field says who faulted. 0x10 is User mode, which is where + a protected module runs; anything else means the fault came from + privileged code and this is not the test passing. */ + + if ((fault_spsr & 0x1FUL) == 0x10UL) + { + linflexd_puts("PASS the module faulted in User mode and was terminated\n"); + } + else + { + linflexd_puts("FAIL a fault was taken, but not from User mode\n"); + } + } + + linflexd_puts("=== end ===\n"); + + while (1) + { + tx_thread_sleep(100UL); + } +} + + +/**************************************************************************/ +/* Application definition. */ +/**************************************************************************/ + +void tx_application_define(void *first_unused_memory) +{ + UINT status; + + (void) first_unused_memory; + + linflexd_puts("\n=== module manager starting ===\n"); + + /* Bring the manager up on its pool, then register the fault handler before + anything is loaded. Registering afterwards would leave a window in which + a module could fault with nobody listening. */ + + txm_module_manager_initialize((VOID *) module_pool, MODULE_POOL_SIZE); + txm_module_manager_memory_fault_notify(module_fault_notify); + + /* Load in place, from where the linker put the image. */ + + status = txm_module_manager_in_place_load(&demo_module, "demo module", + (VOID *) &__module_image_start__); + + linflexd_puts("in_place_load = "); + put_hex(status); + linflexd_puts("\n"); + + if (status == TX_SUCCESS) + { + status = txm_module_manager_start(&demo_module); + + linflexd_puts("module start = "); + put_hex(status); + linflexd_puts("\n"); + } + + (void) tx_thread_create(&report_thread, "report", report_entry, 0UL, + report_stack, sizeof(report_stack), + 20U, 20U, TX_NO_TIME_SLICE, TX_AUTO_START); +} From 23c653ae2b3b7056bfb12173d0e05c3a0b5a7f0a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20Desbiens?= Date: Tue, 18 Aug 2026 11:33:28 -0400 Subject: [PATCH 15/33] Routed the board's SVC and abort vectors to the module port, when built for it Six example images share this entry.S, so the wiring is behind TXM_MODULE_MANAGER and the other five see no change at all. SVC becomes the module boundary. It was a fault, because nothing in this port used it; under the guard it branches to __tx_module_svc_interrupt instead. The abort vectors are the interesting half, because they cannot simply be redirected. The boot probes provoke deliberate faults and rely on the existing recoverable path -- that is what X2 and X4 test -- while a module's violation has to reach the module manager. So both handlers now test who faulted first: SPSR's mode field being User means a module reaching outside its regions, and anything else continues down the path that was already there. r0 is pushed and popped around that test rather than simply used, and the reason is worth recording. The existing handler clobbers r0 through r2 immediately without saving them, which is tolerable for a probe that provokes its own fault and does not care what it resumes with. The module fault capture, though, records the faulting registers so an application can see what the module was doing, and it would have recorded ours. LDM does not affect the flags, so the comparison still holds after the pop. That existing clobber is a fragility this commit does not fix but should not leave unmentioned: any code recovering from a deliberate fault on this board resumes with r0, r1 and r2 destroyed. The boot probes survive it because their next actions do not use those registers, and the per-thread isolation test in #637 survives it for the same reason rather than by design. Verified both ways. All three existing targets build clean, entry.S assembles with the guard on, and the boot image still passes six of six probes on the board -- including X2 and X4, which are the two that exercise the abort handlers this commit touches. Assisted-by: Claude Code (Opus 5) --- .../gnu/example_build/s32z280_evb/entry.S | 50 +++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/ports/cortex_r52/gnu/example_build/s32z280_evb/entry.S b/ports/cortex_r52/gnu/example_build/s32z280_evb/entry.S index 6813251ae..83818d4d5 100644 --- a/ports/cortex_r52/gnu/example_build/s32z280_evb/entry.S +++ b/ports/cortex_r52/gnu/example_build/s32z280_evb/entry.S @@ -136,6 +136,12 @@ cntfrq_at_el2: .word 0 hmpuir_at_el2: .word 0 .global probe_stage probe_stage: .word 0 +#ifdef TXM_MODULE_MANAGER + .extern __tx_module_svc_interrupt + .extern _txm_module_manager_data_abort + .extern _txm_module_manager_prefetch_abort +#endif + .global fault_expected fault_expected: .word 0 .global fault_taken @@ -184,7 +190,11 @@ el2_vectors: el1_vectors: b fault_el1_reset /* 0x00 */ b fault_el1_undef /* 0x04 */ +#ifdef TXM_MODULE_MANAGER + b __tx_module_svc_interrupt /* 0x08 module kernel boundary */ +#else b fault_el1_svc /* 0x08 */ +#endif b fault_el1_pabt /* 0x0C */ b fault_el1_dabt /* 0x10 */ b fault_el1_reserved /* 0x14 */ @@ -794,6 +804,26 @@ fault_el1_svc: mov r3, lr FAULT_TAIL 0x48 fault_el1_pabt: +#ifdef TXM_MODULE_MANAGER + /* A fault from User mode is a module reaching outside its regions, and it + belongs to the module manager, which will record it and terminate the + thread. A fault from a privileged mode continues on the recoverable path + below, which the boot probes depend on. + + r0 is pushed and popped around the test rather than simply used. The code + below clobbers r0 through r2 without saving them, which is tolerable for a + probe that provokes its own fault, but the module capture records the + faulting registers and would record ours instead. LDM does not affect the + flags, so the comparison still holds after the pop. */ + + stmdb sp!, {r0} + mrs r0, spsr + and r0, r0, #0x1F + cmp r0, #0x10 /* User mode? */ + ldmia sp!, {r0} + beq _txm_module_manager_prefetch_abort +#endif + /* Recoverable when a test has armed fault_expected_pabt. * * Recovery differs from the data-abort case in kind, not degree. There, @@ -835,6 +865,26 @@ fault_el1_pabt_fatal: mrc p15, 0, r3, c6, c0, 2 /* IFAR */ FAULT_TAIL 0x4C fault_el1_dabt: +#ifdef TXM_MODULE_MANAGER + /* A fault from User mode is a module reaching outside its regions, and it + belongs to the module manager, which will record it and terminate the + thread. A fault from a privileged mode continues on the recoverable path + below, which the boot probes depend on. + + r0 is pushed and popped around the test rather than simply used. The code + below clobbers r0 through r2 without saving them, which is tolerable for a + probe that provokes its own fault, but the module capture records the + faulting registers and would record ours instead. LDM does not affect the + flags, so the comparison still holds after the pop. */ + + stmdb sp!, {r0} + mrs r0, spsr + and r0, r0, #0x1F + cmp r0, #0x10 /* User mode? */ + ldmia sp!, {r0} + beq _txm_module_manager_data_abort +#endif + /* Recoverable when a test has armed fault_expected. The protection map is only meaningful if a violation actually faults, so the test provokes one deliberately and this path lets it continue: record the syndrome and From 970542ebb1964b1aba82aceb52ecbe48ac84660a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20Desbiens?= Date: Tue, 18 Aug 2026 13:57:52 -0400 Subject: [PATCH 16/33] Built the module manager example, and found five ways it can look right and be wrong The module manager image now builds: kernel, manager, and a sample module placed in a 64 KB carve-out at the top of DRAM1 that no kernel MPU region covers. The layout is the one we chose deliberately. DRAM1 rather than DRAM2 because it runs at full core speed, and because code and data can be contiguous -- putting module code in the code RAM region instead looks obvious and does not work, since that region is read-only and a module's data would have to live somewhere else. Region 1 stops at 0x317EFFFF and the linker's DATA region shrinks to 448 KB to match, of which the kernel uses about 34 KB. Five bugs on the way here, and every one of them produced something that built: The module port cannot share the base port's ThreadX library. Its tx_port.h adds the owning module instance to four object types and the module fields to TX_THREAD. Measured: TX_QUEUE 68 bytes against 60, TX_SEMAPHORE 40 against 32, TX_THREAD 236 against 184. A kernel compiled one way and a manager the other disagree about every object and link without a word, because C linking does not compare layouts. The failure would have been memory corruption at run time, and the first hardware runs would have been spent hunting it in the port. There is now a threadx_module library built from the same sources with the module headers. The object extensions were missing from the module tx_port.h, because it was derived from the base port rather than from another module port. That is what surfaced the library problem. Linker script object patterns must carry the source suffix and match the object extension: CMake compiles foo.c to foo.c.obj here. Getting it wrong produces an empty section at whatever address the location counter held -- an image that builds, links, reports a plausible size and contains no module. ALIGN belongs after the colon, not in the address position. In the address position it places the section at the aligned current counter and then the region assignment fails, which at least says so. Section placement is first match, and the kernel's .text claims *(.text*). With the module block after .text, the module's code was absorbed into kernel .text while only its data landed in the carve-out. The image built, linked, and reported 320 bytes of module -- outside the memory the manager would protect. The block now sits before .text and the comment says why. _gcc_setup is a stub. On the Armv8-M module port it copies and rewrites the global offset table so a position-independent module's data references reach the memory the manager allocated. This port links modules absolutely for now, so there is nothing to relocate, and the stub is where that work lands when modules become properly relocatable. That is the next step and the reason this arrangement is a stepping stone rather than the destination. Nothing here has run yet. Assisted-by: Claude Code (Opus 5) --- .../example_build/s32z280_evb/CMakeLists.txt | 151 ++++++++++ .../gnu/example_build/s32z280_evb/link.lds | 5 +- .../gnu/example_build/s32z280_evb/mpu.c | 6 +- .../gnu/example_build/s32z280_evb/platform.h | 26 ++ .../example_build/s32z280_evb/link_module.lds | 272 ++++++++++++++++++ .../sample_threadx_module_manager.c | 28 ++ ports_module/cortex_r52/gnu/inc/tx_port.h | 27 +- .../gnu/module_lib/src/txm_module_gcc_setup.S | 69 +++++ .../src/txm_module_manager_user_mode_entry.S | 8 + 9 files changed, 586 insertions(+), 6 deletions(-) create mode 100644 ports_module/cortex_r52/gnu/example_build/s32z280_evb/link_module.lds create mode 100644 ports_module/cortex_r52/gnu/module_lib/src/txm_module_gcc_setup.S 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 7f216700c..21413f614 100644 --- a/ports/cortex_r52/gnu/example_build/s32z280_evb/CMakeLists.txt +++ b/ports/cortex_r52/gnu/example_build/s32z280_evb/CMakeLists.txt @@ -244,3 +244,154 @@ if(TX_R52_ENABLE_FIQ_NESTING) ${EVB_LINK_QUIET_RWX} ) endif() + +# --------------------------------------------------------------------------- +# ThreadX modules: the module manager, with a sample module linked into the +# module area and loaded in place from there. +# +# This target deliberately does NOT link the threadx library's port assembly. +# The module port carries its own copies of the scheduler, context restore and +# stack build, because the region switch happens in the scheduler and a module +# thread starts in a different processor mode. Linking both would give two +# definitions of every one of them. +# +# Guarded by an option, off by default, because it is the only target that needs +# TXM_MODULE_MANAGER in entry.S and the only one that links common_modules. +# --------------------------------------------------------------------------- + +option(TX_R52_BUILD_S32Z280_MODULE_EXAMPLE + "Build the NXP S32Z280-594EVB ThreadX module manager example" OFF) + +if(TX_R52_BUILD_S32Z280_MODULE_EXAMPLE) + + set(MOD_DIR ${CMAKE_SOURCE_DIR}/ports_module/cortex_r52/gnu) + set(MOD_EVB ${MOD_DIR}/example_build/s32z280_evb) + + # --------------------------------------------------------------------- + # A second ThreadX library, built with the module port's headers. + # + # This is not optional and not a convenience. The module port's tx_port.h + # adds the owning module instance to TX_QUEUE, TX_SEMAPHORE, + # TX_EVENT_FLAGS_GROUP and TX_TIMER, and the module fields to TX_THREAD. + # Measured: TX_QUEUE is 60 bytes against the base port's, TX_SEMAPHORE 40 + # against 32, TX_THREAD 236 against 184. A kernel compiled against the base + # port and a manager compiled against this one disagree about every object, + # and they link without complaint, because C linking does not compare struct + # layouts. The result would be memory corruption at run time with nothing in + # the build to hint at it. + # + # The base port's assembly is reused except for the five files the module port + # replaces, and tx_port_offset_check.c is included deliberately: if the module + # headers moved any offset that assembly depends on, that check is what says + # so, and it is better to find out here than on the board. + # --------------------------------------------------------------------- + + file(GLOB TX_COMMON_SRC ${CMAKE_SOURCE_DIR}/common/src/*.c) + + set(TX_R52_PORT_SRC ${CMAKE_SOURCE_DIR}/ports/cortex_r52/gnu/src) + + add_library(threadx_module STATIC EXCLUDE_FROM_ALL + ${TX_COMMON_SRC} + ${TX_R52_PORT_SRC}/tx_port_offset_check.c + ${TX_R52_PORT_SRC}/tx_thread_fiq_context_restore.S + ${TX_R52_PORT_SRC}/tx_thread_fiq_context_save.S + ${TX_R52_PORT_SRC}/tx_thread_fiq_nesting_end.S + ${TX_R52_PORT_SRC}/tx_thread_fiq_nesting_start.S + ${TX_R52_PORT_SRC}/tx_thread_interrupt_control.S + ${TX_R52_PORT_SRC}/tx_thread_interrupt_disable.S + ${TX_R52_PORT_SRC}/tx_thread_interrupt_restore.S + ${TX_R52_PORT_SRC}/tx_thread_irq_nesting_end.S + ${TX_R52_PORT_SRC}/tx_thread_irq_nesting_start.S + ${TX_R52_PORT_SRC}/tx_thread_vectored_context_save.S + ${TX_R52_PORT_SRC}/tx_timer_interrupt.S + ) + + target_include_directories(threadx_module PUBLIC + ${MOD_DIR}/inc + ${CMAKE_SOURCE_DIR}/common/inc + ${CMAKE_SOURCE_DIR}/common_modules/module_manager/inc + ${CMAKE_SOURCE_DIR}/common_modules/module_lib/inc + ${CMAKE_SOURCE_DIR}/common_modules/inc + ) + + target_compile_definitions(threadx_module PUBLIC TXM_MODULE_MANAGER) + + # The portable half of the module manager. Globbed rather than listed: it is + # a whole upstream component, not a selection from one, and a file added to it + # upstream should not need a change here to be compiled. + + file(GLOB TXM_MANAGER_SRC ${CMAKE_SOURCE_DIR}/common_modules/module_manager/src/*.c) + + # The module-side library: the API shims that run inside the module. + file(GLOB TXM_MODULE_LIB_SRC ${CMAKE_SOURCE_DIR}/common_modules/module_lib/src/*.c) + + add_executable(s32z280_module.elf EXCLUDE_FROM_ALL + # Board support, shared with the other examples + ${EVB_DIR}/entry.S + ${EVB_DIR}/linflexd.c + ${EVB_DIR}/timer.c + ${EVB_DIR}/mpu.c + ${EVB_DIR}/gicv3.c + ${EVB_DIR}/irq_dispatch.c + ${EVB_DIR}/gic_probe.c + ${EVB_DIR}/cache.c + ${EVB_DIR}/tcm.c + ${EVB_DIR}/thread_mpu.c + ${EVB_DIR}/tx_initialize_low_level.S + + # Module manager port + ${MOD_DIR}/module_manager/src/tx_thread_schedule.S + ${MOD_DIR}/module_manager/src/tx_thread_context_save.S + ${MOD_DIR}/module_manager/src/tx_thread_context_restore.S + ${MOD_DIR}/module_manager/src/tx_thread_stack_build.S + ${MOD_DIR}/module_manager/src/tx_thread_system_return.S + ${MOD_DIR}/module_manager/src/txm_module_manager_svc_handler.S + ${MOD_DIR}/module_manager/src/txm_module_manager_fault_capture.S + ${MOD_DIR}/module_manager/src/txm_module_manager_user_mode_entry.S + ${MOD_DIR}/module_manager/src/txm_module_manager_thread_stack_build.S + ${MOD_DIR}/module_manager/src/txm_module_manager_mm_register_setup.c + ${MOD_DIR}/module_manager/src/txm_module_manager_memory_fault_handler.c + ${MOD_DIR}/module_manager/src/txm_module_manager_memory_fault_notify.c + ${MOD_DIR}/module_manager/src/txm_module_manager_alignment_adjust.c + ${MOD_DIR}/module_manager/src/txm_module_manager_external_memory_enable.c + ${MOD_DIR}/module_manager/src/txm_module_manager_offset_check.c + + # The module itself, placed in the module area by link_module.lds + ${MOD_EVB}/txm_module_preamble.S + ${MOD_EVB}/sample_threadx_module.c + ${MOD_DIR}/module_lib/src/txm_module_thread_shell_entry.c + ${MOD_DIR}/module_lib/src/txm_module_gcc_setup.S + ${TXM_MODULE_LIB_SRC} + + # The application + ${MOD_EVB}/sample_threadx_module_manager.c + + # The portable module manager + ${TXM_MANAGER_SRC} + ) + + target_include_directories(s32z280_module.elf PRIVATE + ${EVB_DIR} + ${MOD_DIR}/inc + ${CMAKE_SOURCE_DIR}/common_modules/module_manager/inc + ${CMAKE_SOURCE_DIR}/common_modules/module_lib/inc + ${CMAKE_SOURCE_DIR}/common_modules/inc + ) + + target_compile_definitions(s32z280_module.elf PRIVATE + TXM_MODULE_MANAGER + TX_R52_USE_THREADX_IRQ + ) + + target_compile_options(s32z280_module.elf PRIVATE -g) + + target_link_options(s32z280_module.elf PRIVATE + -T${MOD_EVB}/link_module.lds + -nostartfiles + -Wl,-Map=s32z280_module.map + ${EVB_LINK_QUIET_RWX} + ) + + target_link_libraries(s32z280_module.elf PRIVATE threadx_module) + +endif() diff --git a/ports/cortex_r52/gnu/example_build/s32z280_evb/link.lds b/ports/cortex_r52/gnu/example_build/s32z280_evb/link.lds index cb73bb35a..796cb88d7 100644 --- a/ports/cortex_r52/gnu/example_build/s32z280_evb/link.lds +++ b/ports/cortex_r52/gnu/example_build/s32z280_evb/link.lds @@ -54,7 +54,10 @@ MEMORY Used for data that wants deterministic access without depending on a cache -- an enabled TCM is never cached. */ BTCM (rw) : ORIGIN = 0x30100000, LENGTH = 0x00004000 /* 16 KB */ - DATA (rwx) : ORIGIN = 0x31780000, LENGTH = 0x00080000 /* 512 KB: DRAM0 + DRAM1, both full core speed */ + DATA (rwx) : ORIGIN = 0x31780000, LENGTH = 0x00070000 /* 448 KB: DRAM0 + DRAM1 below the module area */ + /* Module memory, uncovered by any kernel MPU region on purpose. See + S32Z_MODULE_AREA_BASE in platform.h. */ + MODULE (rwx) : ORIGIN = 0x317F0000, LENGTH = 0x00010000 /* 64 KB */ } ENTRY(_start) 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 2073d0db5..f1e6271ae 100644 --- a/ports/cortex_r52/gnu/example_build/s32z280_evb/mpu.c +++ b/ports/cortex_r52/gnu/example_build/s32z280_evb/mpu.c @@ -302,8 +302,12 @@ static void build_table(void) Reference Manual and confirmed on the board. */ mpu_regions[1].mpu_region_base = S32Z_DATA_SRAM_BASE; + /* Stops before the module area at the top of DRAM1. A module's memory must + not be covered by a kernel region, or every thread could reach it and the + manager's own regions would be decoration. */ + mpu_regions[1].mpu_region_limit = S32Z_DATA_SRAM_BASE - + S32Z_DATA_SRAM_SIZE - 1UL; + + S32Z_DATA_SRAM_SHARED_SIZE - 1UL; mpu_regions[1].mpu_region_ap = MPU_AP_RW_EL1; mpu_regions[1].mpu_region_execute_never = 1U; mpu_regions[1].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 4adbe55ec..0ad91cecb 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,32 @@ #define S32Z_DRAM0_SIZE 0x00040000UL #define S32Z_DRAM1_BASE 0x317C0000UL #define S32Z_DRAM1_SIZE 0x00040000UL +/* Memory for loadable modules: the top 64 KB of DRAM1, deliberately left out of + the broad data region in mpu.c. + + It has to be outside every region the board support package programs. The + manager gives a module its own regions, and if the kernel's map already covered + that memory then every thread could reach the module and back -- the isolation + would be nominal. Carving a hole is the only way to make it real. + + DRAM1 rather than DRAM2 because it runs at full core speed (S32Z2 RM 6.3.6), + and because code and data can be contiguous here. Putting module code in the + code RAM region instead would have been the obvious choice and does not work: + that region is read-only, so a module's data would have to live somewhere else + and the module would straddle two carve-outs. + + 64 KB is arbitrary but not accidental: it leaves 448 KB of full-speed RAM for + the kernel's data, stacks and bss, which currently use about 14 KB. */ + +#define S32Z_MODULE_AREA_BASE 0x317F0000UL +#define S32Z_MODULE_AREA_SIZE 0x00010000UL /* 64 KB */ + +/* What is left of the full-speed pair for the kernel itself. link.lds sizes its + DATA region from this, so the linker cannot place kernel data in the module + area by accident. */ + +#define S32Z_DATA_SRAM_SHARED_SIZE (S32Z_MODULE_AREA_BASE - S32Z_DRAM0_BASE) + /* 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 diff --git a/ports_module/cortex_r52/gnu/example_build/s32z280_evb/link_module.lds b/ports_module/cortex_r52/gnu/example_build/s32z280_evb/link_module.lds new file mode 100644 index 000000000..d56b08edc --- /dev/null +++ b/ports_module/cortex_r52/gnu/example_build/s32z280_evb/link_module.lds @@ -0,0 +1,272 @@ +/*************************************************************************** + * 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 + **************************************************************************/ + +/* Link map for the NXP S32Z280-594EVB, RTU0 core 0. + * + * Code goes to the RTU code SRAM at its instruction-fetch address, 0x79900000, + * which is also where the core resets: MC_ME_PRTN0_CORE0_ADDR reads 0x79900000 + * on this board, so a debugger-loaded image needs no relocation to be reached + * by the reset vector. That window is writable over the debug AXI port even + * though NXP's debugger memory map declares it read-only, so plain `load` + * works and no LMA alias is required. A flash-booted image is different: it + * would have to be written through the data alias at 0x32100000. + * + * Data, bss and the per-mode stacks go to the RTU-local data SRAM at + * 0x31780000. Keeping them out of the code SRAM avoids the boundary problem + * the FVP build hit, where an aligned .data start straddled the code/data + * split, and RTU-local traffic does not leave the RTU. + * + * The TCMs are deliberately unused: they are not accessible at reset until + * their region registers are programmed, so nothing needed for early boot can + * live there. + */ + +__hyp_stack_size__ = 0x0800; +__svc_stack_size__ = 0x1000; +__irq_stack_size__ = 0x0800; +__fiq_stack_size__ = 0x0400; +__abt_stack_size__ = 0x0400; +__und_stack_size__ = 0x0400; +__sys_stack_size__ = 0x0800; + +MEMORY +{ + CODE (rx) : ORIGIN = 0x79900000, LENGTH = 0x00700000 /* 7 MB */ + /* ATCM. entry.S programmes IMP_ATCMREGIONR to this base and enables the + bank before anything runs from it, and mpu.c maps it executable. Code + placed here runs at full core speed with one wait state, where CODE is + RTU code RAM and runs at half the core frequency (S32Z2 RM 6.3.6). */ + ATCM (rwx) : ORIGIN = 0x30000000, LENGTH = 0x00010000 /* 64 KB */ + /* BTCM. 16 KB at zero wait states, enabled by entry.S, and preloaded there + as well because ECC means a location must be written before it is read. + Used for data that wants deterministic access without depending on a + cache -- an enabled TCM is never cached. */ + BTCM (rw) : ORIGIN = 0x30100000, LENGTH = 0x00004000 /* 16 KB */ + DATA (rwx) : ORIGIN = 0x31780000, LENGTH = 0x00070000 /* 448 KB: DRAM0 + DRAM1 below the module area */ + /* Module memory, uncovered by any kernel MPU region on purpose. See + S32Z_MODULE_AREA_BASE in platform.h. */ + MODULE (rwx) : ORIGIN = 0x317F0000, LENGTH = 0x00010000 /* 64 KB */ +} + +ENTRY(_start) + +SECTIONS +{ + /* .text.boot must come FIRST: MC_ME_PRTN0_CORE0_ADDR is 0x79900000 on + * this board, so whatever lands at the start of CODE is what the core + * executes out of reset, and that has to be _start. It must also be a + * T32 instruction, since the core resets in Thumb state. The vector + * tables need alignment but not a fixed address, because HVBAR and VBAR + * are programmed at run time -- so they follow rather than lead. + */ + /* No ALIGN() address expression on any output section below. Written as + * " ALIGN(n) : { }" the ALIGN is the section's *address*, evaluated + * from a location counter that starts at zero, and it silently overrides + * "> CODE" -- every section then lands near address 0 and the link fails + * with "not within region". The FVP script uses that form harmlessly only + * because its region starts at 0x00000000. Alignment comes from the input + * sections (the vector tables carry .align 5) or from an explicit + * ". = ALIGN(n);" inside the braces. + */ + .boot : + { + KEEP(*(.text.boot)) + } > CODE + + .vectors_el2 : + { + KEEP(*(.vectors_el2)) + } > CODE + + .vectors_el1 : + { + KEEP(*(.vectors_el1)) + } > CODE + + /* The module image, at a fixed address in the module area. + + This is the stepping-stone arrangement: the module is linked absolutely, + here, and loaded in place from here. The common module manager does no + relocation processing -- there is no .rel.dyn handling anywhere in + common_modules -- so a position-independent module would have nothing to + fix up its references, and absolute linking is what actually works today. + The next step is to add that processing and build the module properly + position independent; the memory layout below does not change when that + happens, only how the image gets here. + + The object names carry the source suffix and a trailing wildcard: CMake + compiles foo.c to foo.c.obj here, because the toolchain file sets the object + extension, and to foo.c.o elsewhere. The wildcard matches either. + + Getting this wrong does not fail the link. It produces an empty section at + whatever address the location counter happened to hold, and an image that + builds, links and has no module in it -- which is what the first two + versions of this did, once with the suffix missing entirely and once with + .o where this build produces .obj. + + This block sits before .text deliberately. Placement is first match, and + the kernel's .text claims *(.text*) -- so with this block after it, the + module's code was silently absorbed into kernel .text while only its data + landed here. The image built, linked, reported a plausible size, and the + module's code was outside the region the manager would protect. + + Module objects are named individually rather than matched by wildcard. + A wildcard over the module's sources would quietly absorb any new file + added to the manager side too, and a kernel object landing in the module + region would be handed to a module as its own memory. + + Code and data are separated by __txm_module_code_end__ so the preamble can + declare the two sizes the manager needs. Both are rounded to the 64-byte + MPU granule, because that is what the manager can express as a region. */ + + .module_image : ALIGN(64) + { + __module_image_start__ = .; + + KEEP(*txm_module_preamble.S.o*(.txm_module_preamble)) + + *sample_threadx_module.c.o*(.text .text.*) + *txm_module_thread_shell_entry.c.o*(.text .text.*) + *txm_module_gcc_setup.S.o*(.text .text.*) + + /* The whole module-side library, matched by path. These are the module's + own API shims -- they run in User mode as part of the module, and if + they landed in kernel .text the module would fault on its first + service call. Matched by directory rather than named one by one + because there are ninety-eight of them. */ + + *common_modules/module_lib/src/*(.text .text.*) + *sample_threadx_module.c.o*(.rodata .rodata.*) + *txm_module_thread_shell_entry.c.o*(.rodata .rodata.*) + *common_modules/module_lib/src/*(.rodata .rodata.*) + + . = ALIGN(64); + __txm_module_code_end__ = .; + + *sample_threadx_module.c.o*(.data .data.*) + *txm_module_thread_shell_entry.c.o*(.data .data.*) + *common_modules/module_lib/src/*(.data .data.*) + *sample_threadx_module.c.o*(.bss .bss.* COMMON) + *txm_module_thread_shell_entry.c.o*(.bss .bss.* COMMON) + *common_modules/module_lib/src/*(.bss .bss.* COMMON) + + . = ALIGN(64); + __module_image_end__ = .; + } > MODULE + + __txm_module_code_size = __txm_module_code_end__ - __module_image_start__; + __txm_module_data_size = __module_image_end__ - __txm_module_code_end__; + + .text : + { + *(.text*) + *(.glue_7) + *(.glue_7t) + } > CODE + + /* Code that runs from tightly-coupled memory. Loaded into CODE and copied + to ATCM by atcm_copy_text() before it is called, because ECC is enabled + on this part and a TCM location must be written before it is read + (Cortex-R52 TRM 6.2.2). The copy uses 64-bit stores for the same + reason: ATCM requires them where BTCM and CTCM accept 32-bit. + + ALIGN(8) at both ends so the copy can move whole 64-bit units without a + tail case, which is what keeps the ECC requirement satisfied for every + location written. */ + + .atcm_text : ALIGN(8) + { + __atcm_text_start__ = .; + *(.atcm_text*) + . = ALIGN(8); + __atcm_text_end__ = .; + } > ATCM AT> CODE + + __atcm_text_load__ = LOADADDR(.atcm_text); + + .rodata : + { + *(.rodata*) + . = ALIGN(4); + __code_end__ = .; + } > CODE + + .data : + { + . = ALIGN(8); + __data_start__ = .; + *(.data*) + . = ALIGN(8); + __data_end__ = .; + } > DATA + + /* Data placed in BTCM. NOLOAD: nothing is copied in, and entry.S has + already written every location in the bank to establish ECC check bits, + so anything here is safe to read before it is written by the program. */ + + .btcm_bss (NOLOAD) : ALIGN(8) + { + __btcm_bss_start__ = .; + *(.btcm_bss*) + . = ALIGN(8); + __btcm_bss_end__ = .; + } > BTCM + + .bss (NOLOAD) : + { + . = ALIGN(8); + __bss_start__ = .; + *(.bss*) + *(COMMON) + . = ALIGN(8); + __bss_end__ = .; + } > DATA + + /* Per-mode stacks. Each mode needs its own on an R-profile core: an + * exception taken while a handler is running would otherwise reuse the + * interrupted mode's stack pointer. + */ + .stacks (NOLOAD) : + { + . = ALIGN(8); + . = . + __hyp_stack_size__; + __hyp_stack_top = .; + . = ALIGN(8); + . = . + __svc_stack_size__; + __svc_stack_top = .; + . = ALIGN(8); + . = . + __irq_stack_size__; + __irq_stack_top = .; + . = ALIGN(8); + . = . + __fiq_stack_size__; + __fiq_stack_top = .; + . = ALIGN(8); + . = . + __abt_stack_size__; + __abt_stack_top = .; + . = ALIGN(8); + . = . + __und_stack_size__; + __und_stack_top = .; + . = ALIGN(8); + . = . + __sys_stack_size__; + __sys_stack_top = .; + } > DATA + + /* Both spellings: _tx_initialize_low_level publishes the first free + address to tx_application_define from _end, while C library conventions + use end. */ + + PROVIDE(end = .); + PROVIDE(_end = .); +} diff --git a/ports_module/cortex_r52/gnu/example_build/s32z280_evb/sample_threadx_module_manager.c b/ports_module/cortex_r52/gnu/example_build/s32z280_evb/sample_threadx_module_manager.c index fb9517509..011cb4647 100644 --- a/ports_module/cortex_r52/gnu/example_build/s32z280_evb/sample_threadx_module_manager.c +++ b/ports_module/cortex_r52/gnu/example_build/s32z280_evb/sample_threadx_module_manager.c @@ -186,6 +186,34 @@ static void report_entry(ULONG input) /* Application definition. */ /**************************************************************************/ +/**************************************************************************/ +/* Board entry. */ +/* */ +/* entry.S calls this once the core is at EL1 with the MPU and caches */ +/* configured. Same shape as the other examples on this board: bring the */ +/* console up, say so, and enter the kernel. */ +/**************************************************************************/ + +void bsp_main(void) +{ + (void) linflexd_init(); + + linflexd_puts("\n=== ThreadX modules :: S32Z280-594EVB ===\n"); + linflexd_puts("entering kernel\n"); + + tx_kernel_enter(); + + /* Not reached. */ + + linflexd_puts("FAIL tx_kernel_enter returned\n"); + + for (;;) + { + __asm__ volatile("nop"); + } +} + + void tx_application_define(void *first_unused_memory) { UINT status; diff --git a/ports_module/cortex_r52/gnu/inc/tx_port.h b/ports_module/cortex_r52/gnu/inc/tx_port.h index bac4428cc..fdc5a7dc0 100644 --- a/ports_module/cortex_r52/gnu/inc/tx_port.h +++ b/ports_module/cortex_r52/gnu/inc/tx_port.h @@ -238,11 +238,30 @@ typedef unsigned short USHORT; #define TX_BLOCK_POOL_EXTENSION #define TX_BYTE_POOL_EXTENSION -#define TX_EVENT_FLAGS_GROUP_EXTENSION +/* The four object extensions below each carry the module instance that owns the + object, and the module manager needs them to answer a question it must answer + on every service call: is the object this module is asking about one it owns? + Without them a module could pass any address it liked as a queue or a semaphore + and the kernel would act on whatever was there. + + They are also why a module port cannot share the base port's ThreadX library. + They change the layout of TX_QUEUE, TX_SEMAPHORE, TX_EVENT_FLAGS_GROUP and + TX_TIMER, so a library compiled against the base port's tx_port.h and a manager + compiled against this one disagree about every kernel object -- and both + compile, so nothing says so. + + Block pool, byte pool and mutex stay empty, as they are in the Armv8-M module + port: the manager tracks ownership of those differently. */ + +#define TX_EVENT_FLAGS_GROUP_EXTENSION VOID *tx_event_flags_group_module_instance; \ + VOID (*tx_event_flags_group_set_module_notify)(struct TX_EVENT_FLAGS_GROUP_STRUCT *group_ptr); #define TX_MUTEX_EXTENSION -#define TX_QUEUE_EXTENSION -#define TX_SEMAPHORE_EXTENSION -#define TX_TIMER_EXTENSION +#define TX_QUEUE_EXTENSION VOID *tx_queue_module_instance; \ + VOID (*tx_queue_send_module_notify)(struct TX_QUEUE_STRUCT *queue_ptr); +#define TX_SEMAPHORE_EXTENSION VOID *tx_semaphore_module_instance; \ + VOID (*tx_semaphore_put_module_notify)(struct TX_SEMAPHORE_STRUCT *semaphore_ptr); +#define TX_TIMER_EXTENSION VOID *tx_timer_module_instance; \ + VOID (*tx_timer_module_expiration_function)(ULONG id); /* Define the user extension field of the thread control block. Nothing diff --git a/ports_module/cortex_r52/gnu/module_lib/src/txm_module_gcc_setup.S b/ports_module/cortex_r52/gnu/module_lib/src/txm_module_gcc_setup.S new file mode 100644 index 000000000..ef6c39355 --- /dev/null +++ b/ports_module/cortex_r52/gnu/module_lib/src/txm_module_gcc_setup.S @@ -0,0 +1,69 @@ +@/*************************************************************************** +@ * 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 +@ **************************************************************************/ +@ +@/**************************************************************************/ +@/* */ +@/* MODULE LIBRARY RELEASE */ +@/* */ +@/* txm_module_gcc_setup.S Cortex-R52/GNU */ +@/* 6.5.2 */ +@/* AUTHOR */ +@/* */ +@/* Frédéric Desbiens, Eclipse Foundation */ +@/* */ +@/* DESCRIPTION */ +@/* */ +@/* Where a position-independent module fixes up its own data */ +@/* references, and currently a no-op. */ +@/* */ +@/* The module shell entry calls this before running the module, passing */ +@/* the address the module's code was loaded at. On the Armv8-M module */ +@/* port the equivalent copies the global offset table and rewrites every */ +@/* entry so the module's data references point at the memory the manager */ +@/* allocated, rather than wherever the module was linked. */ +@/* */ +@/* This port does not need that yet, because the module is linked */ +@/* absolutely at the address it runs from and loaded in place there. */ +@/* Nothing needs relocating, so there is nothing to do. */ +@/* */ +@/* That is a stepping stone and not the destination. Absolute linking */ +@/* means one instance of a module, at one fixed address, decided at */ +@/* build time -- which is not how modules are used. Making them */ +@/* genuinely relocatable means building position independent, having the */ +@/* manager process the relocations, and implementing this function for */ +@/* real. The hook is here so that work has somewhere to land and so */ +@/* the shell entry does not have to change when it does. */ +@/* */ +@/* INPUT */ +@/* */ +@/* r0 Address the module's code was loaded at */ +@/* */ +@/**************************************************************************/ + + .syntax unified + .arm + .text + .align 2 + + .global _gcc_setup + .type _gcc_setup, %function + +_gcc_setup: + +@ Nothing to relocate while modules are linked absolutely. r0 holds the load +@ address and is deliberately ignored rather than removed from the interface, +@ because the real implementation needs it. + + BX lr diff --git a/ports_module/cortex_r52/gnu/module_manager/src/txm_module_manager_user_mode_entry.S b/ports_module/cortex_r52/gnu/module_manager/src/txm_module_manager_user_mode_entry.S index 15ddd4e17..6a338820a 100644 --- a/ports_module/cortex_r52/gnu/module_manager/src/txm_module_manager_user_mode_entry.S +++ b/ports_module/cortex_r52/gnu/module_manager/src/txm_module_manager_user_mode_entry.S @@ -71,6 +71,14 @@ .global _txm_module_manager_user_mode_entry .global _txm_module_manager_user_mode_entry_end + +@ The supervisor call handler compares the faulting lr against these two, to +@ refuse an SVC 1 or SVC 2 raised from anywhere else. They have to be visible +@ outside this file for that check to link, which also means they are the two +@ addresses the whole privilege boundary rests on. + + .global _txm_system_mode_enter + .global _txm_system_mode_exit .extern _txm_module_manager_kernel_dispatch .type _txm_module_manager_user_mode_entry, %function From 87536af07a7032d8f465526eb6b35cf07a961259 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20Desbiens?= Date: Wed, 19 Aug 2026 08:35:04 -0400 Subject: [PATCH 17/33] Built the module as its own image, because one link cannot hold both The module's sources were compiled into the manager's image and gathered into the module area by object name. That cannot work, and the reason is not obvious: the module-side library defines shims named after the same ThreadX entry points the kernel defines, and in a single link the shims win, because they arrive as objects and the kernel arrives as a static library. Nothing warns. The manager's own tx_thread_create resolved to the module's copy at 0x317F1700, inside the module area that no kernel MPU region covers, so tx_application_define called into unmapped memory and the core took a silent prefetch abort before the first thread ran. The module is now a separate link unit -- link_demo_module.lds and the s32z280_demo_module.elf target -- objcopied to a raw binary and carried into the manager image as bytes by module_blob.S. None of the module's symbols enter the manager's link, so the collision cannot recur rather than being avoided by care. Keeping the module out of the link also retires a class of quiet failure that gathering by object name had. Matching input sections by object file name fails open: CMake compiles foo.c to foo.c.obj here and foo.c.o elsewhere, and a pattern that matches nothing yields an empty section at whatever address the location counter held -- an image that builds, links, reports a plausible size and contains no module. That happened twice, once with the suffix missing and once with .o where this build produces .obj. .incbin searches the assembler's include path rather than the source tree, and demo_module.bin is generated, hence the -Wa,-I on module_blob.S pointing at the build directory. Also ignored build_mod/ and build_fvp/, which are the directories the documented build commands create. Assisted-by: Claude Code (Opus 5) --- .gitignore | 2 + .../example_build/s32z280_evb/CMakeLists.txt | 78 ++++++++++++-- .../s32z280_evb/link_demo_module.lds | 102 ++++++++++++++++++ .../example_build/s32z280_evb/link_module.lds | 97 ++++++----------- .../example_build/s32z280_evb/module_blob.S | 65 +++++++++++ 5 files changed, 272 insertions(+), 72 deletions(-) create mode 100644 ports_module/cortex_r52/gnu/example_build/s32z280_evb/link_demo_module.lds create mode 100644 ports_module/cortex_r52/gnu/example_build/s32z280_evb/module_blob.S diff --git a/.gitignore b/.gitignore index cadb24438..1c68bd826 100644 --- a/.gitignore +++ b/.gitignore @@ -34,6 +34,8 @@ build_r52_vfp/ build_r52_uart/ build_r52_mpu/ build_r52_all/ +build_mod/ +build_fvp/ # Keil uVision per-user state, regenerated when a project is opened. # The .uvprojx / .uvproj project files are the ones worth tracking. 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 21413f614..ab2bac6fd 100644 --- a/ports/cortex_r52/gnu/example_build/s32z280_evb/CMakeLists.txt +++ b/ports/cortex_r52/gnu/example_build/s32z280_evb/CMakeLists.txt @@ -322,9 +322,64 @@ if(TX_R52_BUILD_S32Z280_MODULE_EXAMPLE) file(GLOB TXM_MANAGER_SRC ${CMAKE_SOURCE_DIR}/common_modules/module_manager/src/*.c) - # The module-side library: the API shims that run inside the module. + # The module-side library: the API shims that run inside the module. These + # belong to the module's image and must not reach the manager's link -- they + # define the same names as the kernel's own entry points, and as objects they + # beat the kernel's static library, so in one link every service call the + # manager makes is redirected into the module. See link_demo_module.lds. + file(GLOB TXM_MODULE_LIB_SRC ${CMAKE_SOURCE_DIR}/common_modules/module_lib/src/*.c) + # ------------------------------------------------------------------------ + # The module, built as its own image and reduced to a raw binary. + # ------------------------------------------------------------------------ + + add_executable(s32z280_demo_module.elf EXCLUDE_FROM_ALL + ${MOD_EVB}/txm_module_preamble.S + ${MOD_EVB}/sample_threadx_module.c + ${MOD_DIR}/module_lib/src/txm_module_thread_shell_entry.c + ${MOD_DIR}/module_lib/src/txm_module_gcc_setup.S + ${TXM_MODULE_LIB_SRC} + ) + + # No TXM_MODULE_MANAGER here: that define selects the kernel side of the + # shared headers, and this is the other side. + + target_include_directories(s32z280_demo_module.elf PRIVATE + ${EVB_DIR} + ${MOD_DIR}/inc + ${CMAKE_SOURCE_DIR}/common_modules/module_lib/inc + ${CMAKE_SOURCE_DIR}/common_modules/inc + ${CMAKE_SOURCE_DIR}/common/inc + ) + + target_compile_options(s32z280_demo_module.elf PRIVATE -g) + + target_link_options(s32z280_demo_module.elf PRIVATE + -T${MOD_EVB}/link_demo_module.lds + -nostartfiles + -nostdlib + -Wl,-Map=s32z280_demo_module.map + ${EVB_LINK_QUIET_RWX} + ) + + # The raw image the manager embeds. Written into the binary directory + # because that is the include path module_blob.S searches. + + add_custom_command( + OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/demo_module.bin + COMMAND ${CMAKE_OBJCOPY} -O binary + $ + ${CMAKE_CURRENT_BINARY_DIR}/demo_module.bin + DEPENDS s32z280_demo_module.elf + COMMENT "Converting the demonstration module to a raw image" + VERBATIM + ) + + add_custom_target(s32z280_demo_module_bin + DEPENDS ${CMAKE_CURRENT_BINARY_DIR}/demo_module.bin + ) + add_executable(s32z280_module.elf EXCLUDE_FROM_ALL # Board support, shared with the other examples ${EVB_DIR}/entry.S @@ -356,12 +411,10 @@ if(TX_R52_BUILD_S32Z280_MODULE_EXAMPLE) ${MOD_DIR}/module_manager/src/txm_module_manager_external_memory_enable.c ${MOD_DIR}/module_manager/src/txm_module_manager_offset_check.c - # The module itself, placed in the module area by link_module.lds - ${MOD_EVB}/txm_module_preamble.S - ${MOD_EVB}/sample_threadx_module.c - ${MOD_DIR}/module_lib/src/txm_module_thread_shell_entry.c - ${MOD_DIR}/module_lib/src/txm_module_gcc_setup.S - ${TXM_MODULE_LIB_SRC} + # The module, as bytes rather than as objects. module_blob.S includes the + # raw image built by s32z280_demo_module.elf, so none of the module's + # symbols enter this link. + ${MOD_EVB}/module_blob.S # The application ${MOD_EVB}/sample_threadx_module_manager.c @@ -385,6 +438,17 @@ if(TX_R52_BUILD_S32Z280_MODULE_EXAMPLE) target_compile_options(s32z280_module.elf PRIVATE -g) + # .incbin searches the assembler's include paths, not the source tree, and + # demo_module.bin is generated -- so the build directory has to be on that + # path or module_blob.S cannot find the image. + + set_source_files_properties(${MOD_EVB}/module_blob.S PROPERTIES + OBJECT_DEPENDS ${CMAKE_CURRENT_BINARY_DIR}/demo_module.bin + COMPILE_OPTIONS "-Wa,-I${CMAKE_CURRENT_BINARY_DIR}" + ) + + add_dependencies(s32z280_module.elf s32z280_demo_module_bin) + target_link_options(s32z280_module.elf PRIVATE -T${MOD_EVB}/link_module.lds -nostartfiles diff --git a/ports_module/cortex_r52/gnu/example_build/s32z280_evb/link_demo_module.lds b/ports_module/cortex_r52/gnu/example_build/s32z280_evb/link_demo_module.lds new file mode 100644 index 000000000..d19a4c45a --- /dev/null +++ b/ports_module/cortex_r52/gnu/example_build/s32z280_evb/link_demo_module.lds @@ -0,0 +1,102 @@ +/*************************************************************************** + * 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 + **************************************************************************/ + +/* Link map for the demonstration module alone, for the S32Z280-594EVB. + * + * The module is a separate link unit from the manager, and it has to be. Both + * sides define the ThreadX API: the kernel defines the real _txe_* entry points + * and the module library defines shims of the same names that trap into the + * kernel instead. Linking them together does not produce a duplicate-symbol + * error, because the kernel arrives as a static library and the module library + * as ordinary objects, and an object always beats an archive member. The module + * shims therefore win for the whole image, and the manager's own calls to + * tx_thread_create and friends are quietly redirected into the module. + * + * That was not a theory. It put _txe_thread_create at 0x317F1700, inside the + * module area, which no kernel MPU region covers -- so tx_application_define + * called into unmapped memory and the core took a prefetch abort with nothing on + * the console to say so. + * + * The manager embeds the binary produced from this link at the same address it + * is linked for, so the two agree on where the module lives without either one + * needing the other's symbols. + * + * Absolute, not position independent. This is the stepping-stone arrangement: + * the common module manager does no relocation processing -- there is no + * .rel.dyn handling anywhere in common_modules -- so a position-independent + * module would have nothing to fix up its references. Making the module + * genuinely relocatable is the next step, and it changes how the image gets + * here, not where it ends up. + */ + +MEMORY +{ + /* Must match S32Z_MODULE_AREA_BASE and S32Z_MODULE_AREA_SIZE in platform.h, + and the MODULE region in the manager's link_module.lds. */ + MODULE (rwx) : ORIGIN = 0x317F0000, LENGTH = 0x00010000 /* 64 KB */ +} + +SECTIONS +{ + .module_image : + { + __module_image_start__ = .; + + /* The preamble is first because that is where the manager looks for it: + it reads the properties, the entry points and the two sizes from the + first words of the image. KEEP because nothing references it. */ + + KEEP(*(.txm_module_preamble)) + + *(.text .text.*) + *(.glue_7) + *(.glue_7t) + *(.rodata .rodata.*) + + /* Code and data are separated here so the preamble can declare the two + sizes the manager needs, each rounded to the 64-byte PMSAv8-R granule + because that is the smallest region the manager can express. */ + + . = ALIGN(64); + __txm_module_code_end__ = .; + + *(.data .data.*) + + /* .bss last, and deliberately inside the image rather than discarded. + An in-place load does not zero it -- the manager copies nothing and + runs the module where it lies -- so a module that relies on .bss being + zero would read whatever the SRAM held. The sample initialises what + it reads for that reason. */ + + *(.bss .bss.* COMMON) + + . = ALIGN(64); + __module_image_end__ = .; + } > MODULE + + __txm_module_code_size = __txm_module_code_end__ - __module_image_start__; + __txm_module_data_size = __module_image_end__ - __txm_module_code_end__; + + /* Unwind tables and toolchain notes would otherwise land at address zero and + be carried into the raw binary. A module has no unwinder. */ + + /DISCARD/ : + { + *(.ARM.exidx*) + *(.ARM.extab*) + *(.comment) + *(.note.*) + } +} diff --git a/ports_module/cortex_r52/gnu/example_build/s32z280_evb/link_module.lds b/ports_module/cortex_r52/gnu/example_build/s32z280_evb/link_module.lds index d56b08edc..ca910a6cc 100644 --- a/ports_module/cortex_r52/gnu/example_build/s32z280_evb/link_module.lds +++ b/ports_module/cortex_r52/gnu/example_build/s32z280_evb/link_module.lds @@ -96,78 +96,45 @@ SECTIONS } > CODE /* The module image, at a fixed address in the module area. - - This is the stepping-stone arrangement: the module is linked absolutely, - here, and loaded in place from here. The common module manager does no - relocation processing -- there is no .rel.dyn handling anywhere in - common_modules -- so a position-independent module would have nothing to - fix up its references, and absolute linking is what actually works today. - The next step is to add that processing and build the module properly - position independent; the memory layout below does not change when that - happens, only how the image gets here. - The object names carry the source suffix and a trailing wildcard: CMake - compiles foo.c to foo.c.obj here, because the toolchain file sets the object - extension, and to foo.c.o elsewhere. The wildcard matches either. - - Getting this wrong does not fail the link. It produces an empty section at - whatever address the location counter happened to hold, and an image that - builds, links and has no module in it -- which is what the first two - versions of this did, once with the suffix missing entirely and once with - .o where this build produces .obj. - - This block sits before .text deliberately. Placement is first match, and - the kernel's .text claims *(.text*) -- so with this block after it, the - module's code was silently absorbed into kernel .text while only its data - landed here. The image built, linked, reported a plausible size, and the - module's code was outside the region the manager would protect. - - Module objects are named individually rather than matched by wildcard. - A wildcard over the module's sources would quietly absorb any new file - added to the manager side too, and a kernel object landing in the module - region would be handed to a module as its own memory. - - Code and data are separated by __txm_module_code_end__ so the preamble can - declare the two sizes the manager needs. Both are rounded to the 64-byte - MPU granule, because that is what the manager can express as a region. */ - - .module_image : ALIGN(64) + The module is not linked here. It is built as its own image by the + s32z280_demo_module.elf target, objcopied to a raw binary and included as + bytes by module_blob.S, and this section only decides where those bytes + land. The address has to agree with what the module was linked for -- + both come from S32Z_MODULE_AREA_BASE -- because an in-place load runs the + module where it lies. + + The earlier arrangement compiled the module's sources into this image and + gathered them here by object name. That cannot work. The module library + defines shims named after the same ThreadX entry points the kernel + defines, and in a single link the shims win, because they arrive as + objects and the kernel arrives as a static library. The manager's own + tx_thread_create resolved to the module's copy at 0x317F1700, called into + the module area that no kernel region covers, and took a silent prefetch + abort before the first thread ever ran. + + Keeping the module out of the link also removes a whole class of quiet + failure that arrangement had: matching input sections by object-file name + fails open. A missing suffix or a wildcard that does not match produces an + empty section, a link that succeeds and an image with no module in it, + twice over here -- and the block had to sit before .text, because + placement is first match and the kernel's *(.text*) would otherwise + swallow the module's code while leaving its data behind. + + This block still sits before .text: harmless now, and it keeps the module + at the head of the map where it is easy to find. */ + + .module_image : { __module_image_start__ = .; - - KEEP(*txm_module_preamble.S.o*(.txm_module_preamble)) - - *sample_threadx_module.c.o*(.text .text.*) - *txm_module_thread_shell_entry.c.o*(.text .text.*) - *txm_module_gcc_setup.S.o*(.text .text.*) - - /* The whole module-side library, matched by path. These are the module's - own API shims -- they run in User mode as part of the module, and if - they landed in kernel .text the module would fault on its first - service call. Matched by directory rather than named one by one - because there are ninety-eight of them. */ - - *common_modules/module_lib/src/*(.text .text.*) - *sample_threadx_module.c.o*(.rodata .rodata.*) - *txm_module_thread_shell_entry.c.o*(.rodata .rodata.*) - *common_modules/module_lib/src/*(.rodata .rodata.*) - - . = ALIGN(64); - __txm_module_code_end__ = .; - - *sample_threadx_module.c.o*(.data .data.*) - *txm_module_thread_shell_entry.c.o*(.data .data.*) - *common_modules/module_lib/src/*(.data .data.*) - *sample_threadx_module.c.o*(.bss .bss.* COMMON) - *txm_module_thread_shell_entry.c.o*(.bss .bss.* COMMON) - *common_modules/module_lib/src/*(.bss .bss.* COMMON) - + KEEP(*(.module_blob)) . = ALIGN(64); __module_image_end__ = .; } > MODULE - __txm_module_code_size = __txm_module_code_end__ - __module_image_start__; - __txm_module_data_size = __module_image_end__ - __txm_module_code_end__; + /* The code and data sizes are not computed here any more. They belong to + the module's own link, which is where its preamble is assembled and where + the boundary between its code and its data is actually known. */ .text : { diff --git a/ports_module/cortex_r52/gnu/example_build/s32z280_evb/module_blob.S b/ports_module/cortex_r52/gnu/example_build/s32z280_evb/module_blob.S new file mode 100644 index 000000000..bd4045422 --- /dev/null +++ b/ports_module/cortex_r52/gnu/example_build/s32z280_evb/module_blob.S @@ -0,0 +1,65 @@ +@/*************************************************************************** +@ * 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 +@ **************************************************************************/ +@ +@/**************************************************************************/ +@/* */ +@/* MODULE MANAGER RELEASE */ +@/* */ +@/* module_blob.S Cortex-R52/GNU */ +@/* 6.5.2 */ +@/* AUTHOR */ +@/* */ +@/* Frédéric Desbiens, Eclipse Foundation */ +@/* */ +@/* DESCRIPTION */ +@/* */ +@/* Carries the demonstration module into the manager image as data. */ +@/* */ +@/* The module is built as its own link unit and objcopied to a raw */ +@/* binary, which is included here verbatim. It has to be a separate */ +@/* link: the module library defines shims named after the ThreadX API */ +@/* entry points the kernel also defines, and in one link the shims win */ +@/* -- objects beat archive members -- so the manager's own service calls */ +@/* end up trapping into the module. */ +@/* */ +@/* Included as bytes rather than linked as objects, so the module's */ +@/* symbols never enter the manager's link at all. Nothing here is */ +@/* called: the manager finds the preamble at the start of the image and */ +@/* reaches everything else through that. */ +@/* */ +@/**************************************************************************/ + + .section .module_blob, "a" + +@ 64-byte aligned, the PMSAv8-R granule, so the image starts where a region +@ can start. The linker script places this section at the module area base, +@ which is the address the module itself was linked for. + + .align 6 + + .global __demo_module_image + .global __demo_module_image_end + +__demo_module_image: + +@ demo_module.bin is produced by the s32z280_demo_module.elf target. The +@ assembler finds it through an include path pointing at the build directory, +@ set in CMakeLists.txt -- .incbin searches the -I paths, not the source tree. + + .incbin "demo_module.bin" + +__demo_module_image_end: + + .align 6 From a07d21f3db4d65f793ceb4a573e796ababa7e390 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20Desbiens?= Date: Wed, 19 Aug 2026 08:35:40 -0400 Subject: [PATCH 18/33] Gave the kernel a window onto the module area, owned by the scheduler No kernel MPU region covers the module area, and that absence is what stops every thread from reaching a module's memory. But the manager has to read a module's preamble and write its data to load it at all, so privileged code needs some way in. Without one the load faulted on its first read of the image, and because a privileged data abort ends in a handler that only spins, that presented as the load hanging. The way in is region 16, above both the kernel's boot regions 0-7 and the eight the manager hands to a module, so neither the boot table nor the scheduler's per-thread region load can disturb it. MPUIR reports 20 EL1 regions on this part. EL1 read/write with no EL0 access, so the manager can load through it and a module cannot use it to reach anything. It cannot simply be left enabled. PMSAv8-R has no region priority: two enabled regions matching one address is CONSTRAINED UNPREDICTABLE and on this part it aborts, and the window covers exactly the memory a module's own regions cover. So the scheduler owns it -- enabling it for every thread that owns no module and disabling it for every thread that does. The two are mutually exclusive by ownership rather than by anyone remembering to bracket a call, which is the property worth having. The register words are computed once by mpu_module_window_init and read from memory by the scheduler, so no register layout lives in assembly. Field positions confirmed against the Cortex-R52 TRM Table 3-124: BASE[31:6], RES0[5], SH[4:3], AP[2:1], XN[0], and AP 0b00 is EL1 read/write with no EL0 access. Region 16 is past the direct PRBARn encodings, so it is reached through PRSELR with an ISB, which is why the window is one region and not the whole block. The module pool moves with it, out of .bss and into the module area. The manager carves a module's data from that pool and hands it over as an MPU region; in .bss it would sit inside the kernel's data region and the two would overlap, which is the same rule again. The region number is spelled twice, in mpu.h and in the scheduler's MCR, because the assembler cannot include the board header. Two static asserts hold them together: one that the numbers agree, and one that the window sits clear of the module block, since if they ever named the same region the scheduler would enable one believing it was the other. Assisted-by: Claude Code (Opus 5) --- .../gnu/example_build/s32z280_evb/mpu.c | 76 +++++++++++++++++++ .../gnu/example_build/s32z280_evb/mpu.h | 24 ++++++ .../example_build/s32z280_evb/link_module.lds | 18 +++++ .../sample_threadx_module_manager.c | 9 ++- .../module_manager/src/tx_thread_schedule.S | 48 ++++++++++++ .../src/txm_module_manager_offset_check.c | 18 +++++ 6 files changed, 192 insertions(+), 1 deletion(-) 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 f1e6271ae..2b9e08d56 100644 --- a/ports/cortex_r52/gnu/example_build/s32z280_evb/mpu.c +++ b/ports/cortex_r52/gnu/example_build/s32z280_evb/mpu.c @@ -457,6 +457,72 @@ static void program_region(unsigned int index, const MPU_REGION *region_ptr) } +/**************************************************************************/ +/* mpu_module_load_window_open / mpu_module_load_window_close */ +/* */ +/* A window over the module area, for the manager to load through. */ +/* */ +/* No kernel region covers the module area, which is what stops every */ +/* thread from reaching a module's memory -- but the manager has to read */ +/* the preamble and write the module's data to load it at all. Without */ +/* this the load faulted on its first read of the image, and because a */ +/* privileged data abort ends in a handler that only spins, that looked */ +/* exactly like the load hanging. */ +/* */ +/* Opened around the load and closed straight after, rather than left in */ +/* place, because PMSAv8-R has no region priority: if this region were */ +/* still enabled when a module thread ran it would overlap the module's */ +/* own regions, and overlapping regions are CONSTRAINED UNPREDICTABLE. */ +/* Closing it before any module thread starts is what keeps the two from */ +/* ever being enabled together. */ +/* */ +/* Region 16, above both the kernel's 0-7 and the eight the manager */ +/* hands to a module, so neither the scheduler's per-thread region load */ +/* nor the boot table can disturb it. MPUIR reports 20 EL1 regions on */ +/* this part. EL1 read/write with no EL0 access: the manager can load */ +/* through it and a module cannot use it to reach anything. */ +/**************************************************************************/ + +unsigned long mpu_module_window_prbar; +unsigned long mpu_module_window_prlar; + + +void mpu_module_window_init(void) +{ + MPU_REGION window; + + window.mpu_region_base = S32Z_MODULE_AREA_BASE; + window.mpu_region_limit = S32Z_MODULE_AREA_BASE + + S32Z_MODULE_AREA_SIZE - 1UL; + window.mpu_region_ap = MPU_AP_RW_EL1; + window.mpu_region_execute_never = 1U; + window.mpu_region_shareability = MPU_SH_NON; + window.mpu_region_attr_index = MPU_ATTR_NORMAL_WB; + window.mpu_region_name = "module window RW NX EL1"; + + /* Published for the scheduler, which turns this region on and off on every + dispatch and has no business computing register layouts in assembly. */ + + mpu_module_window_prbar = (window.mpu_region_base & 0xFFFFFFC0UL) + | (((unsigned long) window.mpu_region_shareability & 0x3UL) << 3) + | (((unsigned long) window.mpu_region_ap & 0x3UL) << 1) + | ((unsigned long) window.mpu_region_execute_never & 0x1UL); + + mpu_module_window_prlar = (window.mpu_region_limit & 0xFFFFFFC0UL) + | (((unsigned long) window.mpu_region_attr_index & 0x7UL) << 1) + | 1UL; + + /* Enabled now, because everything running before the first module thread is + privileged code that may need to reach module memory -- the manager loads + a module from a kernel thread. */ + + program_region(MPU_MODULE_LOAD_REGION, &window); + + data_sync_barrier(); + instruction_barrier(); +} + + /**************************************************************************/ /* mpu_init */ /**************************************************************************/ @@ -503,6 +569,16 @@ unsigned int mpu_init(void) write_prlar(0UL); } +#ifdef TXM_MODULE_MANAGER + + /* After the loop above, which would otherwise disable it again: the module + window lives above the regions this table uses, so it counts as unused + here. */ + + mpu_module_window_init(); + +#endif + MARK(0x40); data_sync_barrier(); diff --git a/ports/cortex_r52/gnu/example_build/s32z280_evb/mpu.h b/ports/cortex_r52/gnu/example_build/s32z280_evb/mpu.h index a9911e92e..19d183cce 100644 --- a/ports/cortex_r52/gnu/example_build/s32z280_evb/mpu.h +++ b/ports/cortex_r52/gnu/example_build/s32z280_evb/mpu.h @@ -115,4 +115,28 @@ const MPU_REGION *mpu_region_table(unsigned int *count_ptr); void mpu_read_region(unsigned int index, unsigned long *prbar_ptr, unsigned long *prlar_ptr); +/* Region index for the manager's load window over the module area. Above the + kernel's 0-7 and above the eight the manager hands to a module, so neither + the boot table nor the scheduler's per-thread region load can disturb it. */ + +#define MPU_MODULE_LOAD_REGION 16U + +/* The module area window, which is what lets privileged code reach module + memory at all -- no other kernel region covers it. + + PMSAv8-R has no region priority, so this must never be enabled at the same + time as the regions a module is given, which cover the same memory. That is + guaranteed by who owns it rather than by careful calling: the scheduler turns + this region on for every thread that is not a module thread and off for every + thread that is, so the window is enabled exactly when no module regions are + loaded. The two register words are published for it below. + + Enabled here at boot, because everything before the first module thread is + privileged code that may need to reach module memory. */ + +void mpu_module_window_init(void); + +extern unsigned long mpu_module_window_prbar; +extern unsigned long mpu_module_window_prlar; + #endif /* MPU_H */ diff --git a/ports_module/cortex_r52/gnu/example_build/s32z280_evb/link_module.lds b/ports_module/cortex_r52/gnu/example_build/s32z280_evb/link_module.lds index ca910a6cc..272990bd6 100644 --- a/ports_module/cortex_r52/gnu/example_build/s32z280_evb/link_module.lds +++ b/ports_module/cortex_r52/gnu/example_build/s32z280_evb/link_module.lds @@ -132,6 +132,24 @@ SECTIONS __module_image_end__ = .; } > MODULE + /* The pool the manager allocates module data from, in the module area for + the same reason the image is: whatever the manager hands to a module + becomes an MPU region, and PMSAv8-R will not allow that region to overlap + a kernel one. Anything a module can be given has to live outside every + region in the boot table, and this area is the only such memory. + + NOLOAD because nothing is copied in -- the byte pool writes its own + headers when it is created. */ + + .module_pool (NOLOAD) : + { + . = ALIGN(64); + __module_pool_start__ = .; + *(.module_pool) + . = ALIGN(64); + __module_pool_end__ = .; + } > MODULE + /* The code and data sizes are not computed here any more. They belong to the module's own link, which is where its preamble is assembled and where the boundary between its code and its data is actually known. */ diff --git a/ports_module/cortex_r52/gnu/example_build/s32z280_evb/sample_threadx_module_manager.c b/ports_module/cortex_r52/gnu/example_build/s32z280_evb/sample_threadx_module_manager.c index 011cb4647..3c5371c9c 100644 --- a/ports_module/cortex_r52/gnu/example_build/s32z280_evb/sample_threadx_module_manager.c +++ b/ports_module/cortex_r52/gnu/example_build/s32z280_evb/sample_threadx_module_manager.c @@ -70,7 +70,14 @@ extern unsigned char __module_image_start__; #define MODULE_POOL_SIZE (16U * 1024U) #endif -static unsigned char module_pool[MODULE_POOL_SIZE] __attribute__((aligned(64))); +/* In the module area, not in .bss. The manager carves a module's data out of + this pool, and that data is handed to the module as an MPU region -- so if the + pool lived in .bss it would sit inside the kernel's data region and the two + would overlap, which PMSAv8-R does not allow. Keeping the pool in the module + area is what makes a module's data disjoint from every kernel region. */ + +static unsigned char module_pool[MODULE_POOL_SIZE] + __attribute__((aligned(64), section(".module_pool"))); static TXM_MODULE_INSTANCE demo_module; diff --git a/ports_module/cortex_r52/gnu/module_manager/src/tx_thread_schedule.S b/ports_module/cortex_r52/gnu/module_manager/src/tx_thread_schedule.S index 24d6ab27c..3608a5ac3 100644 --- a/ports_module/cortex_r52/gnu/module_manager/src/tx_thread_schedule.S +++ b/ports_module/cortex_r52/gnu/module_manager/src/tx_thread_schedule.S @@ -37,6 +37,14 @@ .equ TXM_INSTANCE_DATA_START, 0x2C .equ TXM_INSTANCE_MPU_REGISTERS, 0x64 +@ The kernel's window over the module area. Must match MPU_MODULE_LOAD_REGION +@ in mpu.h; the offset check asserts the two agree. + + .equ MPU_MODULE_WINDOW_REGION, 16 + + .global mpu_module_window_prbar + .global mpu_module_window_prlar + .global _tx_thread_execute_ptr .global _tx_thread_current_ptr .global _tx_timer_time_slice @@ -203,6 +211,22 @@ __tx_thread_schedule_loop: LDMIA r1!, {r2, r3} @ Region 15 MCR p15, 0, r2, c6, c15, 4 @ PRBAR15 MCR p15, 0, r3, c6, c15, 5 @ PRLAR15 +@ +@ +@ /* Close the kernel's window over the module area. It covers the same +@ memory the regions just loaded do, and PMSAv8-R has no region priority: +@ two enabled regions matching one address is CONSTRAINED UNPREDICTABLE, +@ and on this part it aborts. A module thread reaches its own memory +@ through the regions above and needs nothing else. +@ +@ Region 16 is above the direct-access encodings, so it goes through +@ PRSELR. Clearing PRLAR is what disables it. */ +@ + MOV r2, #MPU_MODULE_WINDOW_REGION + MCR p15, 0, r2, c6, c2, 1 @ PRSELR + ISB + MOV r2, #0 + MCR p15, 0, r2, c6, c3, 1 @ PRLAR, region disabled @ DSB sy @ One barrier pair for the block ISB @@ -227,6 +251,30 @@ __tx_module_regions_off: MCR p15, 0, r2, c6, c14, 5 @ PRLAR13 MCR p15, 0, r2, c6, c15, 1 @ PRLAR14 MCR p15, 0, r2, c6, c15, 5 @ PRLAR15 +@ +@ /* Open the kernel's window over the module area, now that no module region +@ is enabled to overlap it. This is the only thing that lets privileged +@ code reach module memory: no boot region covers it, so without this the +@ manager cannot read a module's preamble to load it. +@ +@ Turning it on here, rather than leaving it on permanently, is what keeps +@ it and the module's own regions mutually exclusive without anyone having +@ to remember to bracket a call. A thread that owns no module gets the +@ window; a thread that owns one gets its regions instead. +@ +@ The two register words are computed once by mpu_module_window_init and +@ read from memory here, so this code holds no register layout. */ +@ + MOV r2, #MPU_MODULE_WINDOW_REGION + MCR p15, 0, r2, c6, c2, 1 @ PRSELR + ISB + LDR r2, =mpu_module_window_prbar + LDR r3, [r2] + MCR p15, 0, r3, c6, c3, 0 @ PRBAR + LDR r2, =mpu_module_window_prlar + LDR r3, [r2] + MCR p15, 0, r3, c6, c3, 1 @ PRLAR, region enabled +@ DSB sy ISB @ diff --git a/ports_module/cortex_r52/gnu/module_manager/src/txm_module_manager_offset_check.c b/ports_module/cortex_r52/gnu/module_manager/src/txm_module_manager_offset_check.c index 276b919cd..ae4b1ef0f 100644 --- a/ports_module/cortex_r52/gnu/module_manager/src/txm_module_manager_offset_check.c +++ b/ports_module/cortex_r52/gnu/module_manager/src/txm_module_manager_offset_check.c @@ -55,6 +55,7 @@ #include #include "tx_api.h" #include "txm_module.h" +#include "mpu.h" /* These must match the .equ values at the top of tx_thread_schedule.S. */ @@ -193,3 +194,20 @@ _Static_assert(TXM_MODULE_MPU_TOTAL_ENTRIES == 8, _Static_assert((TXM_MODULE_MPU_FIRST_REGION + TXM_MODULE_MPU_TOTAL_ENTRIES) <= 16, "the module region block extends past region 15, which has no " "direct PRBARn encoding; the scheduler cannot reach it"); + +/* The kernel's window over the module area. tx_thread_schedule.S hard-codes the + region number in an MCR to PRSELR, because the assembler cannot include this + board header, so the two spellings are checked against each other here. + + It also has to sit clear of the module's own block: the scheduler enables the + window for a thread that owns no module and the block for a thread that does, + and if the two ever named the same region one would silently be the other. */ + +_Static_assert(MPU_MODULE_LOAD_REGION == 16, + "tx_thread_schedule.S writes region 16 as the module window; " + "change MPU_MODULE_WINDOW_REGION there to match"); + +_Static_assert(MPU_MODULE_LOAD_REGION >= (TXM_MODULE_MPU_FIRST_REGION + + TXM_MODULE_MPU_TOTAL_ENTRIES), + "the module window overlaps the regions handed to a module; the " + "scheduler would enable one believing it was the other"); From d8f898e490a9be439c71c7496ba132722b10156b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20Desbiens?= Date: Wed, 19 Aug 2026 08:37:10 -0400 Subject: [PATCH 19/33] Moved the scheduler into System mode, and borrowed SVC for the return Every thread in this port runs on the User-banked stack pointer: kernel threads in System mode, module threads in User mode, and those two modes share SP_usr and LR_usr. That shared bank is what lets one save and restore path reach any thread's stack without first asking who owns it, and it is the reason this port can dispatch a module thread and a kernel thread through the same code. It also means the scheduler must be in System mode before it writes sp. It already is when reached from the context restore, the system return and the idle path -- but it is in SVC on the very first dispatch out of tx_kernel_enter. Writing sp there sets SP_svc, the exception return then switches to System mode, and the thread starts life on whatever SP_usr happened to hold. Nothing faults at the point of the mistake. So the mode is now forced rather than assumed, before any write to sp and with nothing in between that touches the stack, since sp is undefined for the mode until it is set. The return needs the opposite treatment. System mode has no SPSR, so the old single LDMIA with the ^ suffix cannot work there: the thread's CPSR has nowhere to sit. The frame is now popped in System mode, where sp and lr belong to the thread, and only the final exception return borrows SVC to hold the SPSR and the resume address. SVC banks its own sp and lr, so switching back does not disturb what was just restored. The frame position is captured into r1 before the mode change rather than recomputed, because sp lands on the saved r0 whether or not VFP state was in the way, and r0-r7 are common to every mode so r1 survives the switch. Assisted-by: Claude Code (Opus 5) --- .../module_manager/src/tx_thread_schedule.S | 66 ++++++++++++++++++- 1 file changed, 64 insertions(+), 2 deletions(-) diff --git a/ports_module/cortex_r52/gnu/module_manager/src/tx_thread_schedule.S b/ports_module/cortex_r52/gnu/module_manager/src/tx_thread_schedule.S index 3608a5ac3..b97309a15 100644 --- a/ports_module/cortex_r52/gnu/module_manager/src/tx_thread_schedule.S +++ b/ports_module/cortex_r52/gnu/module_manager/src/tx_thread_schedule.S @@ -37,6 +37,32 @@ .equ TXM_INSTANCE_DATA_START, 0x2C .equ TXM_INSTANCE_MPU_REGISTERS, 0x64 +@ Every thread in this port runs on the User-banked stack pointer: kernel threads +@ in System mode, module threads in User mode, and those two modes share SP_usr +@ and LR_usr. That shared bank is what lets one save and restore path reach any +@ thread's stack without first asking who owns it. +@ +@ So the scheduler has to run in System mode rather than SVC. It is already in +@ System mode when it is reached from the context restore, from the system return +@ and from the idle path, but it is in SVC mode on the very first dispatch out of +@ the kernel enter, so the mode is forced below instead of assumed. Getting this +@ wrong is silent rather than loud: writing sp in SVC mode sets SP_svc, the +@ exception return then switches to System mode, and the thread starts life on +@ whatever SP_usr happened to hold. +@ +@ The return itself needs a mode that has an SPSR, which System mode has not got, +@ so the frame is popped in System mode and only the final exception return +@ borrows SVC. + + .equ SVC_MODE_BITS, 0x13 + .equ SYS_MODE_BITS, 0x1F + +@ Offsets from the saved r0 word of an interrupt frame, which is where sp points +@ once the stack type, the saved CPSR and any VFP state have been popped. The +@ frame itself is built by tx_thread_stack_build.S and the context restore. + + .equ FRAME_PC_FROM_R0, 56 + @ The kernel's window over the module area. Must match MPU_MODULE_LOAD_REGION @ in mpu.h; the offset check asserts the two agree. @@ -165,6 +191,14 @@ __tx_thread_schedule_loop: @ LDR r2, =_tx_timer_time_slice @ Pickup address of time-slice @ variable +@ +@ /* Enter System mode before touching sp, so that the stack pointer written +@ below is the User-banked one every thread in this port actually runs on. +@ Nothing between here and the write uses the stack, which matters because +@ sp is undefined for this mode until it is set. */ +@ + CPS #SYS_MODE_BITS @ Threads run on SP_usr +@ LDR sp, [r0, #8] @ Switch stack pointers STR r3, [r2] @ Setup time-slice @ @@ -296,7 +330,10 @@ __tx_module_regions_done: LDMIA sp!, {r4, r5} @ Pickup the stack type and saved CPSR CMP r4, #0 @ Check for synchronous context switch BEQ _tx_solicited_return - MSR SPSR_cxsf, r5 @ Setup SPSR for return +@ +@ /* r5 holds the thread's CPSR. It cannot go into SPSR yet: this is System +@ mode, which has no SPSR, so it is carried to the SVC borrow below. */ +@ #ifdef TX_ENABLE_VFP_SUPPORT LDR r1, [r0, #144] @ Pickup the VFP enabled flag CMP r1, #0 @ Is the VFP enabled? @@ -306,7 +343,32 @@ __tx_module_regions_done: VMSR FPSCR, r4 @ Restore FPSCR _tx_skip_interrupt_vfp_restore: #endif - LDMIA sp!, {r0-r12, lr, pc}^ @ Return to point of thread interrupt +@ +@ /* sp now points at the saved r0, whether or not VFP state was in the way, +@ which is why the frame position is captured here rather than computed from +@ the thread's stack pointer. r1 survives the mode change: r0-r7 are common +@ to every mode. +@ +@ The two halves of the return have to be prepared in different modes. SVC +@ holds the SPSR and the return address; System mode holds the sp and lr the +@ thread itself will use. Preparing SVC first means the frame pop can end in +@ System mode with nothing left to do but the return. */ +@ + MOV r1, sp @ Frame position, at the saved r0 + CPS #SVC_MODE_BITS @ Borrow a mode that has an SPSR + MSR SPSR_cxsf, r5 @ Thread's CPSR, applied by the return + LDR lr, [r1, #FRAME_PC_FROM_R0] @ Thread's resume address + CPS #SYS_MODE_BITS @ Back to the thread's own bank +@ + LDMIA sp!, {r0-r12, lr} @ Restore, LR_usr included + ADD sp, sp, #4 @ Step over the saved pc +@ +@ /* SP_usr and LR_usr are now the thread's. Only the exception return is left, +@ and it must happen in the mode holding the SPSR -- switching back does not +@ disturb either, because SVC banks its own sp and lr. */ +@ + CPS #SVC_MODE_BITS @ Mode holding SPSR and the address + MOVS pc, lr @ Return to point of thread interrupt @ _tx_solicited_return: #ifdef TX_ENABLE_VFP_SUPPORT From 6f4b21105ab2a3875e016ea46e2644f600d169ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20Desbiens?= Date: Wed, 19 Aug 2026 08:37:23 -0400 Subject: [PATCH 20/33] Seeded r0 and r1 so the module shell entry has its arguments The module's shell entry takes two arguments, the thread and the thread entry information, and everything the module needs to start is behind the second one: its own entry point, its parameter, its data and code bases and the callback queue. ThreadX's thread shell passes only one argument, so the second can only arrive as a seeded register. The base port's stack build leaves both at zero, which is right for a kernel thread and wrong here: the shell entry dereferences the entry info on its first instruction, so a module faulted before running a line of its own code. The entry info pointer is read from the control block before the frame is built, because _txm_module_manager_thread_create parks it in the stack pointer field for exactly this purpose and the initial stack pointer is written over that same field further down. Assisted-by: Claude Code (Opus 5) --- .../txm_module_manager_thread_stack_build.S | 20 +++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/ports_module/cortex_r52/gnu/module_manager/src/txm_module_manager_thread_stack_build.S b/ports_module/cortex_r52/gnu/module_manager/src/txm_module_manager_thread_stack_build.S index e65c98d2a..767157d7a 100644 --- a/ports_module/cortex_r52/gnu/module_manager/src/txm_module_manager_thread_stack_build.S +++ b/ports_module/cortex_r52/gnu/module_manager/src/txm_module_manager_thread_stack_build.S @@ -130,6 +130,22 @@ _txm_module_manager_thread_stack_build: @ 0 For stack backtracing @ @ Stack Bottom: (higher memory address) */ +@ +@ The thread entry information pointer, which _txm_module_manager_thread_create +@ parked in the stack pointer position of the control block for exactly this +@ purpose. It has to be read before the frame is built, because the initial +@ stack pointer is written over that same field further down. +@ +@ The module's shell entry takes two arguments -- the thread and this pointer -- +@ and everything the module needs to start is behind it: its own entry point, +@ its parameter, its data and code bases, and the callback queue. ThreadX's +@ thread shell passes only one argument, so the second one can only arrive as a +@ seeded register, which is what the two stores below are for. Left at zero, +@ as the base port's stack build leaves them, the shell entry dereferences NULL +@ on its first instruction and the module faults before it runs a line of its +@ own code. + + LDR r12, [r0, #8] @ Pickup thread entry info pointer @ LDR r2, [r0, #16] @ Pickup end of stack area BIC r2, r2, #7 @ Ensure 8-byte alignment @@ -139,9 +155,9 @@ _txm_module_manager_thread_stack_build: @ MOV r3, #1 @ Build interrupt stack type STR r3, [r2, #0] @ Store stack type + STR r0, [r2, #8] @ Store initial r0, the thread pointer + STR r12, [r2, #12] @ Store initial r1, the entry info MOV r3, #0 @ Build initial register value - STR r3, [r2, #8] @ Store initial r0 - STR r3, [r2, #12] @ Store initial r1 STR r3, [r2, #16] @ Store initial r2 STR r3, [r2, #20] @ Store initial r3 STR r3, [r2, #24] @ Store initial r4 From c1c93964b0fce76087b3bea4896e2d2000b231b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20Desbiens?= Date: Wed, 19 Aug 2026 08:38:35 -0400 Subject: [PATCH 21/33] Moved the module's kernel gateway out of the kernel's code region txm_module_manager_user_mode_entry is the only way a module reaches the kernel, so a module needs an MPU region covering those instructions. The kernel's code region grants no EL0 access, and PMSAv8-R has no region priority, so the module's region must not overlap it -- two enabled regions matching one address is CONSTRAINED UNPREDICTABLE and on this part it aborts. With the function still inside .text the first memory access after a module's regions were loaded took a data abort in the scheduler, which is a confusing place to find the consequence of a linker decision. It now lives in its own section, placed after __code_end__ and therefore outside the region the kernel's code table describes. Alone there on purpose: whatever shared the section would become executable by every module. The two regions end up exactly adjacent rather than merely far apart, and that holds for any build. The kernel's region limit is masked down to a granule boundary, PRLAR carrying LIMIT[31:6] with the low bits inclusive, while ALIGN(64) pushes this section up to the next one -- so for __code_end__ = 64k + r the kernel's inclusive limit is 64k+63 for every r in 1..63 and 64k-1 for r = 0, which is one below this section's base in both cases. No gap to waste and no overlap to fault, without either side knowing the other's address. Assisted-by: Claude Code (Opus 5) --- .../example_build/s32z280_evb/link_module.lds | 18 ++++++++++++++++++ .../src/txm_module_manager_user_mode_entry.S | 17 ++++++++++++++++- 2 files changed, 34 insertions(+), 1 deletion(-) diff --git a/ports_module/cortex_r52/gnu/example_build/s32z280_evb/link_module.lds b/ports_module/cortex_r52/gnu/example_build/s32z280_evb/link_module.lds index 272990bd6..9b0e6d9a0 100644 --- a/ports_module/cortex_r52/gnu/example_build/s32z280_evb/link_module.lds +++ b/ports_module/cortex_r52/gnu/example_build/s32z280_evb/link_module.lds @@ -188,6 +188,24 @@ SECTIONS __code_end__ = .; } > CODE + /* The module's gateway into the kernel, deliberately after __code_end__ and + therefore outside the kernel code region, which ends there. A module gets + its own region covering exactly this function; that region would otherwise + overlap the kernel's, which PMSAv8-R does not allow. + + Alone in its own section on purpose: anything sharing it would become + executable by every module. 64-byte aligned at both ends, the PMSAv8-R + granule, so a region can start and stop on it without covering + neighbours. */ + + .txm_user_entry : ALIGN(64) + { + __txm_user_entry_start__ = .; + KEEP(*(.txm_user_entry)) + . = ALIGN(64); + __txm_user_entry_end__ = .; + } > CODE + .data : { . = ALIGN(8); diff --git a/ports_module/cortex_r52/gnu/module_manager/src/txm_module_manager_user_mode_entry.S b/ports_module/cortex_r52/gnu/module_manager/src/txm_module_manager_user_mode_entry.S index 6a338820a..c52defec4 100644 --- a/ports_module/cortex_r52/gnu/module_manager/src/txm_module_manager_user_mode_entry.S +++ b/ports_module/cortex_r52/gnu/module_manager/src/txm_module_manager_user_mode_entry.S @@ -55,7 +55,22 @@ @/**************************************************************************/ .arm - .text + +@ Its own section, placed after __code_end__ by the linker script, so that this +@ function sits OUTSIDE the kernel's code region. +@ +@ It has to. A module runs in User mode and the kernel code region grants no +@ EL0 access, so the module needs a region of its own covering these +@ instructions -- and PMSAv8-R has no region priority, so that region must not +@ overlap the kernel's. Two enabled regions matching one address is +@ CONSTRAINED UNPREDICTABLE, and on this part it aborts: with this function +@ still inside .text, the first memory access after a module's regions were +@ loaded took a data abort in the scheduler. +@ +@ Nothing else may share the section, because whatever does becomes executable +@ by every module. + + .section .txm_user_entry, "ax", %progbits @ 64-byte aligned, which is the PMSAv8-R granule and all that is needed here. @ From 4d201ae236b45ae89477d00692e2e1b84eaf08d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20Desbiens?= Date: Wed, 19 Aug 2026 08:39:00 -0400 Subject: [PATCH 22/33] Drove the manager from a thread, and gave it the object pool it needs Two ways the sample could not have worked, both of which look like the port being broken rather than the sample being wrong. Manager services must be called from a thread. Loading a module takes the manager's mutex, and a service that can block cannot be called before the scheduler runs: driven from tx_application_define the sample printed its way as far as the load and stopped there for ever. Everything now happens in manager_entry, and tx_application_define creates that thread and nothing else. The Armv8-M sample is arranged the same way for the same reason. txm_module_manager_object_pool_create is not optional for a User-mode module. The privileged half of each system call needs a kernel stack allocated from that pool, and txm_module_manager_initialize deliberately leaves it uncreated, so its absence surfaces as TX_NOT_AVAILABLE from deep inside thread create -- which is not a recognisable way to be told that a pool is missing. It is a separate allocation from the module pool, and unlike that one it can stay in .bss: the manager reads it, a module never does. The thread create status is now reported rather than discarded. A thread that was never created and a thread that was created but never scheduled produce the same silence on the console, and telling those two apart is most of the work when the port itself is what is under test. The load and start status codes and the M0-M3 markers are there for the same reason: the expected end state of this sample is a fault, so the console has to distinguish the fault that proves isolation from a fault that means the sample never got started. Also dropped an mpu.h include this file does not use. Assisted-by: Claude Code (Opus 5) --- .../sample_threadx_module_manager.c | 110 ++++++++++++++---- 1 file changed, 88 insertions(+), 22 deletions(-) diff --git a/ports_module/cortex_r52/gnu/example_build/s32z280_evb/sample_threadx_module_manager.c b/ports_module/cortex_r52/gnu/example_build/s32z280_evb/sample_threadx_module_manager.c index 3c5371c9c..c21f962da 100644 --- a/ports_module/cortex_r52/gnu/example_build/s32z280_evb/sample_threadx_module_manager.c +++ b/ports_module/cortex_r52/gnu/example_build/s32z280_evb/sample_threadx_module_manager.c @@ -79,6 +79,21 @@ extern unsigned char __module_image_start__; static unsigned char module_pool[MODULE_POOL_SIZE] __attribute__((aligned(64), section(".module_pool"))); +/* The object pool, which is a separate allocation from the module pool above and + is not optional here. A module that runs in User mode needs a kernel stack + for the privileged side of each system call, and the manager allocates that + from this pool -- txm_module_manager_initialize deliberately leaves the pool + uncreated, so without this a User-mode module fails to start with + TX_NOT_AVAILABLE from deep inside the thread create, which is not an obvious + way to be told that a pool is missing. */ + +#ifndef MODULE_OBJECT_POOL_SIZE +#define MODULE_OBJECT_POOL_SIZE (8U * 1024U) +#endif + +static unsigned char module_object_pool[MODULE_OBJECT_POOL_SIZE] + __attribute__((aligned(64))); + static TXM_MODULE_INSTANCE demo_module; /* What the fault handler saw. Read by the reporting thread after the module has @@ -126,10 +141,67 @@ static void put_hex(unsigned long value) /* Reporting. */ /**************************************************************************/ -static void report_entry(ULONG input) +static void manager_entry(ULONG input) { + UINT status; + (void) input; + /* Every manager call happens here, in a thread, and not in + tx_application_define. Loading a module takes the manager's mutex, and a + service that can block cannot be called before the scheduler runs: the + first version of this drove the manager from tx_application_define, + printed its way as far as the load, and stopped there for ever. The + Armv8-M sample does all of this from a thread for the same reason. */ + + linflexd_puts("M1 initialize\n"); + status = txm_module_manager_initialize((VOID *) module_pool, MODULE_POOL_SIZE); + + linflexd_puts("initialize = "); + put_hex(status); + linflexd_puts(" ready = "); + put_hex((ULONG) _txm_module_manager_ready); + linflexd_puts("\n"); + + /* The pool the module's kernel stacks come from. See module_object_pool. */ + + status = txm_module_manager_object_pool_create((VOID *) module_object_pool, + MODULE_OBJECT_POOL_SIZE); + + linflexd_puts("object pool create = "); + put_hex(status); + linflexd_puts("\n"); + + /* Registered before anything is loaded, so there is no window in which a + module could fault with nobody listening. */ + + linflexd_puts("M2 fault notify\n"); + txm_module_manager_memory_fault_notify(module_fault_notify); + + /* Nothing to bracket here. No kernel region covers the module area, but the + window that reaches it is owned by the scheduler, which enables it for + every thread that does not own a module and disables it for every thread + that does -- so it is already open on this thread and cannot be open at + the same time as a module's own regions. */ + + linflexd_puts("M3 in_place_load\n"); + status = txm_module_manager_in_place_load(&demo_module, "demo module", + (VOID *) &__module_image_start__); + + linflexd_puts("in_place_load = "); + put_hex(status); + linflexd_puts(" ready = "); + put_hex((ULONG) _txm_module_manager_ready); + linflexd_puts("\n"); + + if (status == TX_SUCCESS) + { + status = txm_module_manager_start(&demo_module); + linflexd_puts("module start = "); + put_hex(status); + linflexd_puts("\n"); + } + /* Give the module time to run its three steps and be terminated by the third. Bounded: if the fault never arrives, that is the result to report, not a reason to wait for ever. */ @@ -229,32 +301,26 @@ void tx_application_define(void *first_unused_memory) linflexd_puts("\n=== module manager starting ===\n"); - /* Bring the manager up on its pool, then register the fault handler before - anything is loaded. Registering afterwards would leave a window in which - a module could fault with nobody listening. */ - - txm_module_manager_initialize((VOID *) module_pool, MODULE_POOL_SIZE); - txm_module_manager_memory_fault_notify(module_fault_notify); + /* Nothing but the thread. See manager_entry for why the manager cannot be + driven from here. */ - /* Load in place, from where the linker put the image. */ + status = tx_thread_create(&report_thread, "module manager", manager_entry, 0UL, + report_stack, sizeof(report_stack), + 20U, 20U, TX_NO_TIME_SLICE, TX_AUTO_START); - status = txm_module_manager_in_place_load(&demo_module, "demo module", - (VOID *) &__module_image_start__); - - linflexd_puts("in_place_load = "); - put_hex(status); - linflexd_puts("\n"); + /* Reported rather than discarded. A thread that was never created and a + thread that was created but never scheduled produce exactly the same + silence on the console, and telling those two apart is most of the work + when the port itself is what is under test. */ if (status == TX_SUCCESS) { - status = txm_module_manager_start(&demo_module); - - linflexd_puts("module start = "); - put_hex(status); + linflexd_puts("M0 thread created\n"); + } + else + { + linflexd_puts("FAIL tx_thread_create status 0x"); + put_hex((ULONG) status); linflexd_puts("\n"); } - - (void) tx_thread_create(&report_thread, "report", report_entry, 0UL, - report_stack, sizeof(report_stack), - 20U, 20U, TX_NO_TIME_SLICE, TX_AUTO_START); } From 61b81117463515e23f8f6d505616b8b3489f5021 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20Desbiens?= Date: Wed, 19 Aug 2026 09:58:30 -0400 Subject: [PATCH 23/33] Stored the preamble's entry points relative to their own words Every entry point the manager derived from this preamble was wrong by the byte offset of the word that held it, and the module was entered that far into its shell entry -- past the prologue, so its arguments were never saved and its frame pointer never set up. The manager computes each entry as code_start + field + adjust, where the adjust comes from TXM_MODULE_GNU_SHELL_ADJUST 24, START 28, STOP 32 and CALLBACK 44. Those four numbers are exactly the byte offsets of the four words in the preamble, because the convention is that each field holds symbol - and the manager adds the offset back to recover the offset from the module base. Storing them relative to __txm_module_preamble counts the offset twice. cortex_m33, which this port was seeded from, writes "symbol - . - 0"; all four GNU ports carry the same adjust constants, so the constants were right and this file was wrong. Found on silicon, not by reading. The module thread ran once and was terminated with DFAR 0x1C, which made no sense until the faulting instruction turned out to be "ldr r3, [r3, #0x1c]" at 0x317F020C -- 0x18 into a shell entry that starts at 0x317F01F4. The captured registers were the values the stack build seeds rather than anything the module had computed (r9, r11, r12 zero, r10 the stack start), which is what a thread looks like when it enters after its own prologue: r3 was still the seeded zero, so the load went to 0x1C. After this change the three real entry points resolve to 0x317F01F4, 0x317F0080 and 0x317F030C, matching the module ELF's symbol table, and the stop field stays a literal 0 so the manager's "is there a stop function" test still reads false. Assisted-by: Claude Code (Opus 5) --- .../s32z280_evb/txm_module_preamble.S | 20 ++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/ports_module/cortex_r52/gnu/example_build/s32z280_evb/txm_module_preamble.S b/ports_module/cortex_r52/gnu/example_build/s32z280_evb/txm_module_preamble.S index f1c4b9f5a..fa4858f67 100644 --- a/ports_module/cortex_r52/gnu/example_build/s32z280_evb/txm_module_preamble.S +++ b/ports_module/cortex_r52/gnu/example_build/s32z280_evb/txm_module_preamble.S @@ -80,12 +80,26 @@ __txm_module_preamble: @ Entry points, as offsets from the preamble. - .word _txm_module_thread_shell_entry - __txm_module_preamble - .word demo_module_start - __txm_module_preamble +@ Entry points are stored relative to the word that holds them, not to the +@ start of the preamble. That is what the manager expects: it recovers the +@ offset from the module base by adding the field's own byte offset back -- +@ TXM_MODULE_GNU_SHELL_ADJUST 24, START 28, STOP 32, CALLBACK 44, which are +@ exactly the offsets of the four words below. Storing these relative to +@ __txm_module_preamble instead counts that offset twice, and the module is +@ then entered that many bytes into its shell entry: past the prologue, with +@ the arguments never saved and the frame pointer never set up, so the first +@ dereference goes through a register the stack build had zeroed. On silicon +@ that read faulted at 0x1C, which is offset 0x1C from a null r3. +@ +@ Every other GNU module port writes these the same way; cortex_m33's +@ preamble, which this port was seeded from, spells it "symbol - . - 0". + + .word _txm_module_thread_shell_entry - . + .word demo_module_start - . .word 0 @ No stop thread .word 1 @ Start/stop thread priority .word 1024 @ Start/stop thread stack size - .word _txm_module_callback_request_thread_entry - __txm_module_preamble + .word _txm_module_callback_request_thread_entry - . .word 1 @ Callback thread priority .word 1024 @ Callback thread stack size From f8ed88bb621af1418a14ee1e93091aab868df09b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20Desbiens?= Date: Wed, 19 Aug 2026 09:58:45 -0400 Subject: [PATCH 24/33] Added the silicon run and diagnostic scripts for the module demonstration Running this example on the board was a hand-assembled sequence of GTA, the Windows CCS, an EDBGREQ clear and a UART capture, which is too much to retype correctly and too easy to get subtly wrong -- a missed console capture turns a diagnosable failure into "it printed nothing". run_module_demo.sh starts the capture before the image is loaded, derives module_progress's address from the module's own ELF, and prints the console next to the register report. run_module_demo.gdb loads, clears the debug request that pins the core, enters at the T32 _start and breaks after the manager has printed its verdict. There is no bsp_done in this image to break on the way the boot example has, so it breaks on the line the report falls into, which is reached exactly once. hbreak rather than break: the MPU maps the code region read-only. diagnose_module.gdb answers the questions that come up when the verdict is not the expected one, and each was needed for a real failure: the thread run counts separate "the module thread never ran" from "it ran and died", the raw fault info shows whether an abort was captured even when the sample's counter says none, and reading the notify hook separates a hook that was never registered from a handler that never reached the call. Reading module_progress at its linked address is only sound while the module is linked absolutely and loaded in place; the script says so where it does it, because relocation changes that. Assisted-by: Claude Code (Opus 5) --- .../s32z280_evb/tools/diagnose_module.gdb | 207 ++++++++++++++++++ .../s32z280_evb/tools/run_module_demo.gdb | 184 ++++++++++++++++ .../s32z280_evb/tools/run_module_demo.sh | 123 +++++++++++ 3 files changed, 514 insertions(+) create mode 100644 ports_module/cortex_r52/gnu/example_build/s32z280_evb/tools/diagnose_module.gdb create mode 100644 ports_module/cortex_r52/gnu/example_build/s32z280_evb/tools/run_module_demo.gdb create mode 100755 ports_module/cortex_r52/gnu/example_build/s32z280_evb/tools/run_module_demo.sh diff --git a/ports_module/cortex_r52/gnu/example_build/s32z280_evb/tools/diagnose_module.gdb b/ports_module/cortex_r52/gnu/example_build/s32z280_evb/tools/diagnose_module.gdb new file mode 100644 index 000000000..ab106b4ad --- /dev/null +++ b/ports_module/cortex_r52/gnu/example_build/s32z280_evb/tools/diagnose_module.gdb @@ -0,0 +1,207 @@ +# Copyright (c) 2026 Eclipse ThreadX contributors +# SPDX-License-Identifier: MIT +# Some portions generated by Claude Code (Opus 5). +# +# Diagnostic run for the S32Z280 module manager demonstration. Same attach and +# load as run_module_demo.gdb, but instead of judging the result it dumps the +# state needed to explain one: +# +# * every ThreadX thread, with its run count -- which separates "the module +# thread never ran" from "it ran and died", the first question to answer +# when module_progress comes back zero. +# * the raw fault info, which says whether an abort was captured even though +# the notify callback never fired. +# * the module instance, so the addresses the manager decided on can be +# compared against where the module's code actually writes. +# * the live MPU regions, read back from the hardware. +# +# Usage: S32Z280_ELF= arm-none-eabi-gdb-py -batch -x diagnose_module.gdb + +py _PROBE_IP = "s32dbg:192.168.50.238" +py _SOC_NAME = "S32Z280" +py _CORE_NAME = "R52_0_0" +py _GDB_SERVER_PORT = 45000 + +source /home/fdesbiens/NXP/S32DS.3.6.10/S32DS/tools/S32Debugger/Debugger/scripts/s32z2e2/s32z2e2_generic_bareboard_all_cores.py + +py board_init() +py s32z2e2_cores.init(_CORE_NAME) +py s32z2e2_cores.start_debug_by_core_name(_CORE_NAME) + +py gta_lib.set_context(context.CORE_CTX[_SOC_NAME + "_" + _CORE_NAME]).unwrap() +py gta_lib.stop_core().unwrap() +py gta_lib.attach().unwrap() + +python +import gdb, os + +CONTROL2 = 0x4DC11044 +EDBGREQ_CORE0 = 0x00010000 + +def rd(addr): + return int(gdb.parse_and_eval("*(unsigned int *)0x%x" % addr)) & 0xFFFFFFFF + +c2 = rd(CONTROL2) +gdb.execute("set *(unsigned int *)0x%x = 0x%x" % (CONTROL2, c2 & ~EDBGREQ_CORE0)) +if rd(CONTROL2) & EDBGREQ_CORE0: + raise gdb.GdbError("CR52_RTU0_0_EDBGREQ is still set; the core will not run") + +elf = os.environ.get("S32Z280_ELF", "s32z280_module.elf") +gdb.execute("file %s" % elf) +gdb.execute("load") +start = int(gdb.parse_and_eval("(unsigned int)&_start")) & ~1 +gdb.execute("set $pc = 0x%x" % start) +gdb.execute("hbreak sample_threadx_module_manager.c:259") +gdb.execute("continue") +end + +echo \n===== THREADS =====\n +python +import gdb + +def ev(expr): + return gdb.parse_and_eval(expr) + +def u32(expr): + return int(ev(expr)) & 0xFFFFFFFF + +STATE = {0: "READY", 1: "COMPLETED", 2: "TERMINATED", 3: "SUSPENDED", + 4: "SLEEP", 5: "QUEUE_SUSP", 6: "SEMAPHORE_SUSP", 7: "EVENT_FLAG", + 8: "BLOCK_MEMORY", 9: "BYTE_MEMORY", 10: "IO_DRIVER", 11: "FILE", + 12: "TCP_IP", 13: "MUTEX_SUSP", 14: "PRIORITY_CHANGE"} + +try: + count = u32("_tx_thread_created_count") + print(" _tx_thread_created_count = %d" % count) + t = ev("_tx_thread_created_ptr") + print(" %-18s %-4s %-14s %-5s %-10s %-10s %s" + % ("name", "pri", "state", "runs", "stack_ptr", "entry", "module")) + for i in range(count): + name = t["tx_thread_name"].string() if int(t["tx_thread_name"]) else "(unnamed)" + pri = int(t["tx_thread_priority"]) + st = int(t["tx_thread_state"]) + runs = int(t["tx_thread_run_count"]) & 0xFFFFFFFF + sp = int(t["tx_thread_stack_ptr"]) & 0xFFFFFFFF + entry = int(t["tx_thread_entry"]) & 0xFFFFFFFF + try: + mod = int(t["tx_thread_module_instance_ptr"]) & 0xFFFFFFFF + except gdb.error: + mod = 0 + print(" %-18s %-4d %-14s %-5d 0x%08X 0x%08X %s" + % (name, pri, STATE.get(st, "?%d" % st), runs, sp, entry, + ("0x%08X" % mod) if mod else "-")) + t = t["tx_thread_created_next"] +except gdb.error as e: + print(" could not walk the thread list: %s" % e) + +print("") +print(" _tx_thread_current_ptr = 0x%08X" % u32("(unsigned int)_tx_thread_current_ptr")) +print(" _tx_thread_execute_ptr = 0x%08X" % u32("(unsigned int)_tx_thread_execute_ptr")) +end + +echo \n===== RAW FAULT INFO (was an abort captured at all?) =====\n +python +import gdb + +def u32(expr): + return int(gdb.parse_and_eval(expr)) & 0xFFFFFFFF + +f = "_txm_module_manager_memory_fault_info" +try: + info = gdb.parse_and_eval(f) + for field in ("thread_ptr", "code_location", "dfsr", "dfar", "ifsr", "ifar", + "sp", "r0", "r1", "r9", "r10", "r11", "r12", "lr", "spsr"): + key = "txm_module_manager_memory_fault_info_" + field + try: + v = int(info[key]) & 0xFFFFFFFF + print(" %-14s = 0x%08X" % (field, v)) + except gdb.error: + pass +except gdb.error as e: + print(" no fault info: %s" % e) + +print("") +print(" fault_count (sample's own counter) = %d" % u32("fault_count")) + +# Was the callback ever registered? The handler terminates the current thread +# and only then calls the notify hook, so a populated fault info with a zero +# counter has two explanations: the hook was never registered, or +# _tx_thread_terminate on the current thread did not return to the call site. +try: + n = u32("(unsigned int)_txm_module_manager_fault_notify") + want = u32("(unsigned int)&module_fault_notify") + print(" _txm_module_manager_fault_notify = 0x%08X" % n) + print(" &module_fault_notify (sample's) = 0x%08X" % want) + if n == 0: + print(" -> hook NEVER REGISTERED; that alone explains the zero counter") + elif n == want: + print(" -> hook IS registered, so the handler never reached the call:") + print(" _tx_thread_terminate on the current thread did not return") + else: + print(" -> hook points somewhere unexpected") +except gdb.error as e: + print(" could not read the notify hook: %s" % e) +end + +echo \n===== MODULE INSTANCE =====\n +python +import gdb + +def u32(expr): + return int(gdb.parse_and_eval(expr)) & 0xFFFFFFFF + +try: + m = gdb.parse_and_eval("demo_module") + for field in ("txm_module_instance_state", + "txm_module_instance_code_start", + "txm_module_instance_code_end", + "txm_module_instance_code_size", + "txm_module_instance_data_start", + "txm_module_instance_data_end", + "txm_module_instance_data_size", + "txm_module_instance_start_thread_entry", + "txm_module_instance_module_data_base_address", + "txm_module_instance_shell_entry_function", + "txm_module_instance_preamble_ptr", + "txm_module_instance_property_flags"): + try: + print(" %-46s = 0x%08X" % (field.replace("txm_module_instance_", ""), + int(m[field]) & 0xFFFFFFFF)) + except gdb.error: + pass +except gdb.error as e: + print(" cannot read demo_module: %s" % e) + +print("") +print(" the module's own symbols, for comparison (absolute link):") +print(" module_progress = 0x317F0584") +print(" module_scratch = (just below it)") +print(" __module_image_start__ = 0x%08X" % u32("(unsigned int)&__module_image_start__")) +print(" __module_pool_start__ = 0x%08X" % u32("(unsigned int)&__module_pool_start__")) +print("") +print(" first words of the module image, to confirm the blob is really there:") +for off in (0x00, 0x04, 0x14, 0x18): + a = 0x317F0000 + off + print(" 0x%08X = 0x%08X" % (a, u32("*(unsigned int *)0x%x" % a))) +print(" module_progress / scratch area as it stands now:") +for a in (0x317F0580, 0x317F0584, 0x317F0544, 0x317F0548): + print(" 0x%08X = 0x%08X" % (a, u32("*(unsigned int *)0x%x" % a))) +end + +echo \n===== LIVE MPU REGIONS (read back from hardware) =====\n +python +import gdb + +# Regions 0-15 have direct MCR/MRC encodings; 16 and up need PRSELR. Reading +# them back is the only way to see what the module thread was actually given, +# as opposed to what the manager computed. +print(" (read via the running core; regions 8-15 are the module block, 16 the window)") +try: + gdb.execute("p $pc") +except gdb.error: + pass +print(" NOTE: system registers are not exposed over this RSP connection, so the") +print(" region read-back needs an MRC executed on the core. The manager's own") +print(" copy of what it programmed is in the module instance above; comparing") +print(" that against a hardware read is a job for an on-target routine.") +end diff --git a/ports_module/cortex_r52/gnu/example_build/s32z280_evb/tools/run_module_demo.gdb b/ports_module/cortex_r52/gnu/example_build/s32z280_evb/tools/run_module_demo.gdb new file mode 100644 index 000000000..45a0fc341 --- /dev/null +++ b/ports_module/cortex_r52/gnu/example_build/s32z280_evb/tools/run_module_demo.gdb @@ -0,0 +1,184 @@ +# Copyright (c) 2026 Eclipse ThreadX contributors +# SPDX-License-Identifier: MIT +# Some portions generated by Claude Code (Opus 5). +# +# Load s32z280_module.elf onto the S32Z280-594EVB, run it, and report what the +# protection boundary did. +# +# Usage, normally through run_module_demo.sh which computes the addresses: +# S32Z280_ELF= \ +# S32Z280_MODULE_ELF= \ +# arm-none-eabi-gdb-py -batch -x tools/run_module_demo.gdb +# +# Requires, and does not do for you: +# * a CCS listening on 41475 -- the *Windows* CCS ("ccs.exe -noportquit"). +# The Linux CCS's pushes to the probe stall after "Sending code ... done", +# and under WSL2 mirrored networking a stray Linux CCS holds the port and +# blocks the Windows one ("pkill -x ccs", not -f: it runs as ./ccs). +# * GTA on 45000: Server/gta/gta -p 45000 -k +# * PYTHONHOME pointing at a source-built Python 3.10.11. +# * The UART already being captured to a file BEFORE this runs. The console +# is the test method here, not a convenience: this script reports the same +# verdict from memory, but only the console shows the ordering of events +# that produced it. +# +# WHAT A PASS LOOKS LIKE, and why it is a fault +# +# The sample module writes its own data, calls the kernel through SVC, and then +# deliberately reads an address the manager never granted it. The expected end +# state is therefore a data abort taken in User mode, captured by the abort +# vector, reported through the fault-notify callback, and terminating the module +# thread -- with the manager still alive to report it. A run with no fault is a +# FAILURE: it means a module reached kernel memory and was not stopped. +# +# module_progress bit 0x1 wrote and summed its own scratch expect SET +# bit 0x2 reached the kernel through SVC expect SET +# bit 0x4 about to touch the forbidden address expect SET +# bit 0x8 survived the forbidden access expect CLEAR +# +# so module_progress == 0x7 exactly, and fault_count >= 1 with SPSR mode 0x10. + +py _PROBE_IP = "s32dbg:192.168.50.238" +py _SOC_NAME = "S32Z280" +py _CORE_NAME = "R52_0_0" +py _GDB_SERVER_PORT = 45000 + +source /home/fdesbiens/NXP/S32DS.3.6.10/S32DS/tools/S32Debugger/Debugger/scripts/s32z2e2/s32z2e2_generic_bareboard_all_cores.py + +py board_init() +py s32z2e2_cores.init(_CORE_NAME) +py s32z2e2_cores.start_debug_by_core_name(_CORE_NAME) + +# Select the core context before asking about core state. NXP's core_init() +# omits this and then stop_core() reads the mode of the SoC context, which has +# none, and reports "Core can't be stopped. State is undefined/unknown". +py gta_lib.set_context(context.CORE_CTX[_SOC_NAME + "_" + _CORE_NAME]).unwrap() +py gta_lib.stop_core().unwrap() +py gta_lib.attach().unwrap() + +python +import gdb, os + +# --------------------------------------------------------------------------- +# Release the core from the debugger's external debug request. +# +# _reset_to_first_instruction() asserts MDM_AP CONTROL2[19:16] = +# CR52_RTU0_{3,2,1,0}_EDBGREQ (0x000F0000) and nothing clears them, so every +# RTU0 core is pinned in debug state and executes nothing -- registers and +# memory still respond, which is what makes it look like working hardware. +# --------------------------------------------------------------------------- +CONTROL2 = 0x4DC11044 +EDBGREQ_CORE0 = 0x00010000 + +def rd(addr): + return int(gdb.parse_and_eval("*(unsigned int *)0x%x" % addr)) & 0xFFFFFFFF + +c2 = rd(CONTROL2) +gdb.execute("set *(unsigned int *)0x%x = 0x%x" % (CONTROL2, c2 & ~EDBGREQ_CORE0)) +if rd(CONTROL2) & EDBGREQ_CORE0: + raise gdb.GdbError("CR52_RTU0_0_EDBGREQ is still set; the core will not run") +print("EDBGREQ cleared: CONTROL2 0x%08X -> 0x%08X" % (c2, rd(CONTROL2))) + +elf = os.environ.get("S32Z280_ELF", "s32z280_module.elf") +gdb.execute("file %s" % elf) +gdb.execute("load") + +# The core resets in Thumb state and CPSR cannot be written through this +# connection, so entry must be the T32 _start. Mask bit 0: GDB reports a Thumb +# function's address with it set, but PC itself must be even. +start = int(gdb.parse_and_eval("(unsigned int)&_start")) & ~1 +gdb.execute("set $pc = 0x%x" % start) +print("PC set to 0x%08X" % start) + +# Stop after the manager has printed its whole report. manager_entry ends in an +# endless sleep, so this line is reached exactly once the verdict is out -- there +# is no bsp_done in this image to break on, unlike the boot example. +# +# hbreak, not break: the code region is mapped read-only by the MPU, so a +# software breakpoint would have to write to it. +gdb.execute("hbreak sample_threadx_module_manager.c:259") +gdb.execute("continue") +end + +echo \n===== module manager run: what memory says =====\n +python +import gdb, os + +def rd(addr): + return int(gdb.parse_and_eval("*(unsigned int *)0x%x" % addr)) & 0xFFFFFFFF + +def sym(name): + return int(gdb.parse_and_eval("(unsigned int)&%s" % name)) & 0xFFFFFFFF + +failures = 0 + +# --- the module's own progress, read from the module image ------------------- +# +# module_progress belongs to the module, which is a separate link unit embedded +# as a blob, so the manager's ELF has no symbol for it. The address comes from +# the module's own ELF, and it is valid because the module is linked absolutely +# and loaded in place: it runs, and writes its data, where it lies. Once T3 +# makes modules relocatable this has to be derived from the load address +# instead, and this comment is the reminder. +progress_addr = os.environ.get("S32Z280_MODULE_PROGRESS_ADDR") + +OWN_DATA, KERNEL_CALL, ATTEMPTED_STEAL, SURVIVED_STEAL = 0x1, 0x2, 0x4, 0x8 + +if progress_addr: + progress = rd(int(progress_addr, 0)) + print(" module_progress = 0x%08X (at %s)" % (progress, progress_addr)) + for bit, name, want_set in ((OWN_DATA, "wrote its own data", True), + (KERNEL_CALL, "reached the kernel through SVC", True), + (ATTEMPTED_STEAL, "attempted the forbidden access", True), + (SURVIVED_STEAL, "SURVIVED the forbidden access", False)): + got = bool(progress & bit) + ok = (got == want_set) + if not ok: + failures += 1 + print(" 0x%X %-34s %-5s %s" % (bit, name, "set" if got else "clear", + "ok" if ok else "*** WRONG ***")) + if progress & SURVIVED_STEAL: + print(" ISOLATION FAILURE: the module read memory it was never granted.") +else: + print(" module_progress not read (S32Z280_MODULE_PROGRESS_ADDR unset)") + +# --- what the fault handler captured ---------------------------------------- +print("") +count = rd(sym("fault_count")) +print(" fault_count = %d" % count) + +if count == 0: + print(" *** FAIL: no fault was taken. The module reached outside its memory") + print(" and was not stopped, or it never got as far as trying.") + failures += 1 +else: + dfsr = rd(sym("fault_dfsr")) + dfar = rd(sym("fault_dfar")) + spsr = rd(sym("fault_spsr")) + pc = rd(sym("fault_code_location")) + print(" DFSR = 0x%08X status 0x%02X WnR %d" + % (dfsr, dfsr & 0x3F, (dfsr >> 11) & 1)) + print(" DFAR = 0x%08X" % dfar) + print(" SPSR = 0x%08X mode 0x%02X (%s)" + % (spsr, spsr & 0x1F, + {0x10: "User", 0x13: "Supervisor", 0x1A: "Hyp", + 0x1F: "System"}.get(spsr & 0x1F, "?"))) + print(" faulting pc = 0x%08X" % pc) + + # The mode field is the whole point. A fault from anywhere but User mode + # means privileged code faulted, which is not this test passing. + if (spsr & 0x1F) != 0x10: + print(" *** FAIL: the fault did not come from User mode, so it is not") + print(" a module being stopped at the boundary.") + failures += 1 + else: + print(" the fault came from User mode, which is where a module runs") + +print("") +print(" boot_stage = %d" % rd(sym("boot_stage"))) +print("") +if failures == 0: + print("===== PASS: the module was stopped at the boundary =====") +else: + print("===== FAIL: %d check(s) wrong =====" % failures) +end diff --git a/ports_module/cortex_r52/gnu/example_build/s32z280_evb/tools/run_module_demo.sh b/ports_module/cortex_r52/gnu/example_build/s32z280_evb/tools/run_module_demo.sh new file mode 100755 index 000000000..074a7abca --- /dev/null +++ b/ports_module/cortex_r52/gnu/example_build/s32z280_evb/tools/run_module_demo.sh @@ -0,0 +1,123 @@ +#!/bin/bash +# Copyright (c) 2026 Eclipse ThreadX contributors +# SPDX-License-Identifier: MIT +# Some portions generated by Claude Code (Opus 5). +# +# Run the ThreadX module manager demonstration on the S32Z280-594EVB and capture +# the evidence: the console log and the fault registers. +# +# Usage: tools/run_module_demo.sh [] +# +# The console is the test method here, not a convenience, so this captures the +# UART to a file before the image runs and prints the captured bytes afterwards. +# Both LINFlexD bugs found during bring-up were invisible from the target and +# showed up only in a byte diff of a captured log. +# +# Prerequisites this cannot do for you: +# * the *Windows* CCS listening on 41475, started as "ccs.exe -noportquit". +# The Linux CCS's pushes to the probe stall after "Sending code ... done". +# * board wiring for the console: LIN9 -> daughtercard USB-UART, jumper J248 +# at 1-2, micro-USB at J119 on the daughtercard. +set -u + +BUILD=${1:-build_mod} +EVB_BIN="$BUILD/ports/cortex_r52/gnu/example_build/s32z280_evb" +HERE=$(cd "$(dirname "$0")" && pwd) + +R=/home/fdesbiens/NXP/S32DS.3.6.10 +GDB=$R/S32DS/tools/gdb-arm/arm32-eabi/bin/arm-none-eabi-gdb-py +GTA=$R/S32DS/tools/S32Debugger/Debugger/Server/gta + +MANAGER_ELF=$EVB_BIN/s32z280_module.elf +MODULE_ELF=$EVB_BIN/s32z280_demo_module.elf +UART=${UART:-/dev/ttyUSB0} +LOG=${LOG:-/tmp/module_demo_uart.log} + +for f in "$MANAGER_ELF" "$MODULE_ELF"; do + if [ ! -f "$f" ]; then + echo "ERROR: $f not found. Build it first:" >&2 + echo " ninja -C $BUILD s32z280_module.elf" >&2 + exit 1 + fi +done + +# gdb-py needs the source-built interpreter: its embedded Python has a minimal +# builtin set and loads _struct/_socket/_ctypes as separate .so files. Compute +# the module address BEFORE exporting PYTHONHOME, since that breaks system tools. +PROGRESS_ADDR=0x$(nm "$MODULE_ELF" | awk '$3=="module_progress"{print $1}') +if [ "$PROGRESS_ADDR" = "0x" ]; then + echo "ERROR: module_progress not found in $MODULE_ELF" >&2 + exit 1 +fi +echo "module_progress lives at $PROGRESS_ADDR (from the module's own ELF)" + +if pgrep -x ccs > /dev/null; then + echo "ERROR: a Linux CCS is running (pid $(pgrep -x ccs | tr '\n' ' '))." >&2 + echo " It holds 41475 and blocks the Windows CCS. Kill it: pkill -x ccs" >&2 + exit 1 +fi +# /dev/tcp rather than a Python probe: PYTHONHOME below points at the +# source-built 3.10 for gdb-py, which would make system python3 fail and this +# check spuriously report "no CCS". +if ! timeout 5 bash -c "exec 3<>/dev/tcp/127.0.0.1/41475" 2>/dev/null; then + echo "ERROR: nothing listening on 41475." >&2 + echo " CCS quits when its client disconnects unless started with" >&2 + echo " -noportquit. Relaunch on Windows as: ccs.exe -noportquit" >&2 + exit 1 +fi + +# --- console capture, started BEFORE the image runs ------------------------- +if [ -c "$UART" ]; then + stty -F "$UART" 115200 cs8 -parenb -cstopb -crtscts raw -echo + : > "$LOG" + cat "$UART" > "$LOG" & + CAT_PID=$! + echo "capturing $UART -> $LOG (pid $CAT_PID)" +else + CAT_PID="" + echo "WARNING: $UART is not a character device; running without a console capture." >&2 +fi + +cleanup() { + if [ -n "$CAT_PID" ]; then + kill "$CAT_PID" 2>/dev/null || true + fi +} +trap cleanup EXIT + +P=$HOME/toolchains/py310-src +export PYTHONHOME=$P +export PYTHONPATH=$P/lib/python3.10:$P/lib/python3.10/lib-dynload:$P/lib/python3.10/site-packages +export S32Z280_ELF=$MANAGER_ELF +export S32Z280_MODULE_ELF=$MODULE_ELF +export S32Z280_MODULE_PROGRESS_ADDR=$PROGRESS_ADDR + +# pkill -x, not -f: these run as ./gta, so a -f pattern like 'gta/gta' never +# matches and a stale server silently serves the next attach. +pkill -x gta 2>/dev/null +sleep 2 +( cd "$GTA" && nohup ./gta -p 45000 -k > /tmp/gta_module_demo.log 2>&1 & ) +sleep 4 +if ! pgrep -x gta > /dev/null; then + echo "ERROR: GTA failed to start; see /tmp/gta_module_demo.log" >&2 + exit 1 +fi + +timeout -k 20 500 "$GDB" -batch -x "$HERE/run_module_demo.gdb" +rc=$? + +# Let the last of the console output arrive before the capture is torn down. +sleep 2 + +echo "" +echo "===== console, as captured from $UART =====" +if [ -s "$LOG" ]; then + cat "$LOG" +else + echo "(nothing arrived on the console)" + echo "Check: jumper J248 at 1-2, micro-USB at J119 on the daughtercard, and" + echo "LED J122 for board-to-host traffic." +fi +echo "===== end of console =====" +echo "=== gdb exit=$rc ===" +exit $rc From 09a5c76c8f6dfee4fcfa45a79465792faa49e581 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20Desbiens?= Date: Wed, 19 Aug 2026 15:24:14 -0400 Subject: [PATCH 25/33] Made modules relocatable, so one blob runs at any address The module was linked absolutely and loaded in place, which meant it ran only at the address it was built for. Worse, it could not run correctly at all: the manager allocates a module's data area from its byte pool and memsets it, and never copies .data anywhere, so a module's initialised globals were outside every region the module owned. Silicon faulted on the first write to one of them, at an address in the gap between the granted code and the granted data. So the module is now built position independent -- -fpic with -msingle-pic-base, which reserves r9 as the base every data reference is measured from -- and four things had to arrive together for that to work. _gcc_setup stops being a BX lr. It copies the global offset table out of the image into the module's own data, rebasing each entry by whether it points into the code segment or the data segment, then copies .data and zeroes .bss. It may not use a global itself, being the function that makes globals usable, so every address it needs is a literal pool load of a nominal address that it corrects by hand. The module's link map grows a second segment. Its two origins are fictions the module never runs at: they exist so the rebase can tell a code address from a data address with one unsigned compare, which is why they are far apart, and why neither is zero -- a zero GOT entry means one the linker never filled in, and rebasing it would turn a recognisable null into a plausible pointer. .got and .data have their addresses in the data segment, where the module runs, and their load images back in the code segment, inside the blob, where _gcc_setup can find them. The manager seeds r9. It was storing a literal 0 at frame offset 44, and -msingle-pic-base is precisely the promise that the caller sets that register up, so every module data reference was resolving to a small absolute address. The value is the data base from the thread entry info, whose declaration in txm_module.h carries the comment "Don't move this, referenced in stack build to setup module data base register". And the module is built -mno-long-calls, which is ours rather than the reference port's. The project-wide flags carry -mlong-calls because the manager's image spans TCM at 0x30000000 and code at 0x79900000, further than a direct BL reaches. But -mlong-calls under -fpic routes every call through the GOT, and one of those calls is the shell entry's call to _gcc_setup -- the function that fills the GOT in. The module would have loaded, entered its shell entry, read a zero out of the not-yet-written GOT and branched to address 0. A module needs no long calls: it is one contiguous blob under two kilobytes and never calls out of itself, since kernel services go through the dispatcher pointer in its entry info. Verified on an S32Z280-594EVB. The module now writes and reads its own data, reaches the kernel through SVC and returns, and is stopped by the MPU on the access it should not have -- DFSR 0x0000020C, a read permission fault, DFAR 0x31780000, SPSR mode 0x10, and r9 holding the data base the manager handed out. Assisted-by: Claude Code (Opus 5) --- .../example_build/s32z280_evb/CMakeLists.txt | 53 ++++- .../s32z280_evb/link_demo_module.lds | 155 ++++++++++++--- .../gnu/module_lib/src/txm_module_gcc_setup.S | 188 +++++++++++++++--- .../txm_module_manager_thread_stack_build.S | 23 ++- 4 files changed, 365 insertions(+), 54 deletions(-) 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 ab2bac6fd..e968f937b 100644 --- a/ports/cortex_r52/gnu/example_build/s32z280_evb/CMakeLists.txt +++ b/ports/cortex_r52/gnu/example_build/s32z280_evb/CMakeLists.txt @@ -353,7 +353,58 @@ if(TX_R52_BUILD_S32Z280_MODULE_EXAMPLE) ${CMAKE_SOURCE_DIR}/common/inc ) - target_compile_options(s32z280_demo_module.elf PRIVATE -g) + # Position independent, and only here. These four flags are what make the + # module relocatable, and putting any of them on the manager would be a bug: + # the manager is the resident image, linked absolutely at the address it + # boots from, and -msingle-pic-base in particular would have it treat r9 as + # a PIC base that nothing sets up. + # + # -fpic data references go through the GOT + # -msingle-pic-base r9 is the GOT base, and the caller sets + # it -- which is what the manager's + # thread stack build now does + # -mno-pic-data-is-text-relative the module's data is NOT at a fixed + # offset from its code. It is not: code + # is loaded in place in the module area + # and data is allocated from the byte + # pool, so the gap between them is + # decided at run time + # -fno-plt no procedure linkage table, which a + # module has no loader to populate + # -mno-long-calls see below -- this one is ours, not the + # reference port's, and without it the + # module cannot start at all + # + # -mno-long-calls is the interesting one, and it is here because of a bug + # this build found before silicon did. The project-wide C flags carry + # -mlong-calls, which the manager needs: its image spans TCM at 0x30000000 + # and code at 0x79900000, far beyond the +/-32 MB a direct BL reaches. But + # -mlong-calls under -fpic makes GCC route EVERY call through the GOT -- + # R_ARM_GOT32 instead of R_ARM_CALL -- and one of those calls is the shell + # entry's call to _gcc_setup, which is the function that fills the GOT in. + # The module would have loaded, entered its shell entry, loaded a zero out + # of the not-yet-written GOT and branched to address 0. + # + # A module does not need long calls. It is one contiguous blob under two + # kilobytes, every internal call is a few hundred bytes away, and it never + # calls out of itself: kernel services go through the dispatcher function + # pointer in its entry info, which is data and reached through the GOT + # either way. So the flag is pure cost here, and dropping it is what makes + # the bootstrap possible rather than a workaround for it. + # + # The same four, in the same order, are what the Cortex-A7 GNU module + # example uses. Verified on the generated code rather than assumed: with + # these flags every reference to a module global comes out as an R_ARM_GOT32 + # and loads through LDR rX, [r9, rY], while calls stay direct R_ARM_CALL. + + target_compile_options(s32z280_demo_module.elf PRIVATE + -g + -fpic + -fno-plt + -mno-pic-data-is-text-relative + -msingle-pic-base + -mno-long-calls + ) target_link_options(s32z280_demo_module.elf PRIVATE -T${MOD_EVB}/link_demo_module.lds diff --git a/ports_module/cortex_r52/gnu/example_build/s32z280_evb/link_demo_module.lds b/ports_module/cortex_r52/gnu/example_build/s32z280_evb/link_demo_module.lds index d19a4c45a..3fc17ef86 100644 --- a/ports_module/cortex_r52/gnu/example_build/s32z280_evb/link_demo_module.lds +++ b/ports_module/cortex_r52/gnu/example_build/s32z280_evb/link_demo_module.lds @@ -29,28 +29,60 @@ * called into unmapped memory and the core took a prefetch abort with nothing on * the console to say so. * - * The manager embeds the binary produced from this link at the same address it - * is linked for, so the two agree on where the module lives without either one - * needing the other's symbols. + * POSITION INDEPENDENT, and the two addresses below are fictions + * ============================================================== * - * Absolute, not position independent. This is the stepping-stone arrangement: - * the common module manager does no relocation processing -- there is no - * .rel.dyn handling anywhere in common_modules -- so a position-independent - * module would have nothing to fix up its references. Making the module - * genuinely relocatable is the next step, and it changes how the image gets - * here, not where it ends up. + * The module is built -fpic -msingle-pic-base, so every reference it makes to + * its own data goes through the global offset table with r9 as the base. It + * therefore does not run at the addresses in this file and is not meant to: the + * two segment origins are nominal, chosen only so that the loader can tell a + * code address from a data address by comparing against __data_segment_start__. + * What actually happens at run time is that _gcc_setup rewrites every GOT entry + * from these nominal addresses to the addresses the manager decided on. + * + * That is why they are far apart and why neither is zero. Far apart, because + * the whole discrimination is "below the data origin means code"; non-zero, + * because _gcc_setup treats a zero GOT entry as one the linker never filled in + * and skips it, so a real address of zero would be silently dropped. + * + * 0x01000000 code preamble, text, rodata, and the load images of the + * GOT and .data -- this is the blob the manager embeds + * 0x02000000 data the GOT the module runs against, .data and .bss, all + * of which live in memory the manager allocates + * + * The load images matter. A module's .data cannot be used where it was linked, + * because the manager never copies it there: _txm_module_manager_internal_load + * allocates the module's data area from its byte pool and TX_MEMSETs it to zero, + * and that is the only memory the module is given a region for. The module's + * own .data, wherever it was linked, is outside every region the module owns. + * On silicon that faulted at the first write to an initialised variable, with + * DFAR pointing into the gap between the granted code and the granted data. + * + * So .data and .got have their VMAs in the data segment, where the module will + * run, and their LMAs in the code segment, inside the blob, where _gcc_setup can + * find them and copy them out. AT>CODE is what says that. */ MEMORY { - /* Must match S32Z_MODULE_AREA_BASE and S32Z_MODULE_AREA_SIZE in platform.h, - and the MODULE region in the manager's link_module.lds. */ - MODULE (rwx) : ORIGIN = 0x317F0000, LENGTH = 0x00010000 /* 64 KB */ + /* Nominal. See the note above: the module runs where the manager puts it, + not here. Sized generously because nothing is reserved by being large -- + the raw binary is only as long as the sections actually emitted. */ + + CODE (rx) : ORIGIN = 0x01000000, LENGTH = 0x00100000 + DATA (rw) : ORIGIN = 0x02000000, LENGTH = 0x00100000 } +__code_segment_start__ = 0x01000000; +__data_segment_start__ = 0x02000000; + SECTIONS { - .module_image : + /* --------------------------------------------------------------------- + The code segment, which is the blob byte for byte. + --------------------------------------------------------------------- */ + + .module_image : ALIGN(64) { __module_image_start__ = .; @@ -65,32 +97,88 @@ SECTIONS *(.glue_7t) *(.rodata .rodata.*) - /* Code and data are separated here so the preamble can declare the two - sizes the manager needs, each rounded to the 64-byte PMSAv8-R granule - because that is the smallest region the manager can express. */ + . = ALIGN(4); + } >CODE AT>CODE - . = ALIGN(64); - __txm_module_code_end__ = .; + /* --------------------------------------------------------------------- + The data segment. VMAs here, load images back in the code segment. + --------------------------------------------------------------------- */ + /* The GOT, and it must be first in the data segment. r9 is the GOT base as + far as the compiler is concerned -- every R_ARM_GOT32 is an offset from + it -- and the manager sets r9 to txm_module_instance_module_data_base_address, + which is the start of the module's data area. So GOT origin and data + origin have to be the same address, or every offset the compiler emitted + is measured from the wrong place. __data_segment_start__ above is that + address on the nominal side; r9 is that address on the real side, and + _gcc_setup's rebase is the difference between the two. */ + + .got : ALIGN(4) + { + __new_got_start__ = .; + *(.got.plt) + *(.igot.plt) + *(.got) + __new_got_end__ = .; + } >DATA AT>CODE + + __got_load_start__ = LOADADDR(.got); + + .data : ALIGN(4) + { + __data_start__ = .; *(.data .data.*) + *(.gnu.linkonce.d.*) + . = ALIGN(4); + __data_end__ = .; + } >DATA AT>CODE - /* .bss last, and deliberately inside the image rather than discarded. - An in-place load does not zero it -- the manager copies nothing and - runs the module where it lies -- so a module that relies on .bss being - zero would read whatever the SRAM held. The sample initialises what - it reads for that reason. */ + __data_load_start__ = LOADADDR(.data); - *(.bss .bss.* COMMON) + /* NOLOAD, so .bss contributes nothing to the blob. The manager has already + zeroed the whole data allocation by the time the module runs -- but + _gcc_setup zeroes .bss anyway, because "the manager happens to memset it" + is a property of one loader and not something a module may rely on. */ + .bss (NOLOAD) : ALIGN(4) + { + __bss_start__ = .; + *(.bss .bss.*) + *(.gnu.linkonce.b.*) + *(COMMON) . = ALIGN(64); - __module_image_end__ = .; - } > MODULE + __bss_end__ = .; + } >DATA + + /* --------------------------------------------------------------------- + The two sizes the preamble declares. + --------------------------------------------------------------------- */ - __txm_module_code_size = __txm_module_code_end__ - __module_image_start__; - __txm_module_data_size = __module_image_end__ - __txm_module_code_end__; + /* Code covers everything in the blob, the load images of the GOT and .data + included, because the module has to be able to read them to copy them out + and the code region is the only mapping it has over the blob. Ending it + at .rodata instead would put the GOT template outside every region the + module owns and _gcc_setup would fault on its first read. */ + + __txm_module_code_end__ = __data_load_start__ + SIZEOF(.data); + __txm_module_code_size = __txm_module_code_end__ - __code_segment_start__; + + /* Data covers the GOT, .data and .bss. Not the thread stacks: the manager + adds the start/stop and callback stack sizes to this figure itself, from + the two stack-size words further down the preamble. Counting them here + as well would double them. */ + + __txm_module_data_size = __bss_end__ - __data_segment_start__; /* Unwind tables and toolchain notes would otherwise land at address zero and - be carried into the raw binary. A module has no unwinder. */ + be carried into the raw binary. A module has no unwinder. + + .rel.dyn and friends are discarded because this is a static link: ld + resolves every R_ARM_GOT32 itself and writes the finished address into the + GOT, so there is nothing left for a dynamic loader to do. If one of these + ever stops being empty, the assumption above has broken and the module + needs relocation processing rather than a rebase -- so they are listed + explicitly, to be found by whoever goes looking. */ /DISCARD/ : { @@ -98,5 +186,14 @@ SECTIONS *(.ARM.extab*) *(.comment) *(.note.*) + *(.dynsym) + *(.dynstr) + *(.hash) + *(.gnu.hash) + *(.dynamic) + *(.interp) + *(.rel.dyn) + *(.rel.plt) + *(.plt) } } diff --git a/ports_module/cortex_r52/gnu/module_lib/src/txm_module_gcc_setup.S b/ports_module/cortex_r52/gnu/module_lib/src/txm_module_gcc_setup.S index ef6c39355..8d2b64d1f 100644 --- a/ports_module/cortex_r52/gnu/module_lib/src/txm_module_gcc_setup.S +++ b/ports_module/cortex_r52/gnu/module_lib/src/txm_module_gcc_setup.S @@ -26,29 +26,58 @@ @/* DESCRIPTION */ @/* */ @/* Where a position-independent module fixes up its own data */ -@/* references, and currently a no-op. */ -@/* */ -@/* The module shell entry calls this before running the module, passing */ -@/* the address the module's code was loaded at. On the Armv8-M module */ -@/* port the equivalent copies the global offset table and rewrites every */ -@/* entry so the module's data references point at the memory the manager */ -@/* allocated, rather than wherever the module was linked. */ -@/* */ -@/* This port does not need that yet, because the module is linked */ -@/* absolutely at the address it runs from and loaded in place there. */ -@/* Nothing needs relocating, so there is nothing to do. */ -@/* */ -@/* That is a stepping stone and not the destination. Absolute linking */ -@/* means one instance of a module, at one fixed address, decided at */ -@/* build time -- which is not how modules are used. Making them */ -@/* genuinely relocatable means building position independent, having the */ -@/* manager process the relocations, and implementing this function for */ -@/* real. The hook is here so that work has somewhere to land and so */ -@/* the shell entry does not have to change when it does. */ +@/* references, before it has run a line of its own code. */ +@/* */ +@/* The module's shell entry calls this once, on the start thread only, */ +@/* passing the address the module's code was actually loaded at. Three */ +@/* things happen here, and the module cannot touch a single global */ +@/* until all three have: */ +@/* */ +@/* 1. The global offset table is copied out of the image into the */ +@/* module's own data, and every entry in it is rewritten from the */ +@/* nominal address the linker chose to the address the manager */ +@/* decided on. */ +@/* 2. .data is copied out of the image into the module's own data. */ +@/* 3. .bss is zeroed. */ +@/* */ +@/* Why any of this is needed: the module is built -fpic with */ +@/* -msingle-pic-base, so a reference to a global compiles to */ +@/* LDR rX, [r9, #offset] -- a load of a GOT entry, at a fixed offset */ +@/* from whatever r9 holds. The manager seeds r9 with the module's data */ +@/* base when it builds the thread's stack frame, so the offsets land in */ +@/* the right place; but the entries there are zero until this function */ +@/* writes them, because the manager memsets the data area and nothing */ +@/* else populates it. */ +@/* */ +@/* And .data has to be copied for the same reason. The manager does */ +@/* not copy it: _txm_module_manager_internal_load allocates a data area */ +@/* from its byte pool and zeroes it, so the module's linked .data -- */ +@/* wherever it sits -- is outside every region the module owns. A */ +@/* module reading an initialised global before this ran would fault, or */ +@/* read a zero, depending on where it looked. */ +@/* */ +@/* This function may not use a global itself, which is the constraint */ +@/* that shapes it: it is what makes the GOT usable, so it cannot use */ +@/* the GOT. Every address it needs comes from a literal pool load of a */ +@/* linker-supplied nominal address, rebased by hand. That is also why */ +@/* it is assembly and not C. */ @/* */ @/* INPUT */ @/* */ -@/* r0 Address the module's code was loaded at */ +@/* r0 Address the module's code was actually loaded at */ +@/* r9 Address of the module's data area, already seeded by */ +@/* _txm_module_manager_thread_stack_build. Also the base the */ +@/* compiler measures every GOT offset from, so the GOT must go */ +@/* at exactly this address and nowhere else. */ +@/* */ +@/* OUTPUT */ +@/* */ +@/* None. r9 is unchanged, and must be: it is the module's PIC base for */ +@/* the rest of the thread's life. */ +@/* */ +@/* CALLED BY */ +@/* */ +@/* _txm_module_thread_shell_entry, on the start thread only */ @/* */ @/**************************************************************************/ @@ -60,10 +89,123 @@ .global _gcc_setup .type _gcc_setup, %function +@ Supplied by the module's linker script. The two segment origins are nominal +@ addresses the module never runs at -- see link_demo_module.lds. + + .extern __code_segment_start__ + .extern __data_segment_start__ + .extern __got_load_start__ + .extern __new_got_start__ + .extern __new_got_end__ + .extern __data_load_start__ + .extern __data_start__ + .extern __data_end__ + .extern __bss_start__ + .extern __bss_end__ + _gcc_setup: -@ Nothing to relocate while modules are linked absolutely. r0 holds the load -@ address and is deliberately ignored rather than removed from the interface, -@ because the real implementation needs it. + PUSH {r4-r7, lr} + +@ The two deltas every address below is corrected by. Everything the linker +@ emitted is expressed against the nominal origins; everything that exists at +@ run time is at the same offset from the real base. + + LDR r3, =__code_segment_start__ + SUB r4, r0, r3 @ r4 = code delta, real - nominal + LDR r3, =__data_segment_start__ + SUB r5, r9, r3 @ r5 = data delta, real - nominal + +@ --------------------------------------------------------------------------- +@ 1. The global offset table. +@ --------------------------------------------------------------------------- +@ +@ Read from the image, written to the module's data area, one entry at a time, +@ with each entry rebased according to which segment it points into. The test +@ is a single unsigned compare against the nominal data origin, which is what +@ the linker script's far-apart origins exist to make sound: everything below +@ it is code or read-only data and moves with the code, everything at or above +@ it is writable data and moves with the data area. +@ +@ A zero entry is left alone. The linker leaves an entry zero when it had +@ nothing to put there, and rebasing it would turn a recognisable null into a +@ plausible-looking pointer into the middle of the module. + + LDR r1, =__got_load_start__ + ADD r1, r1, r4 @ Source, in the image + LDR r2, =__new_got_start__ + ADD r2, r2, r5 @ Destination, in the data area + LDR r3, =__new_got_end__ + ADD r3, r3, r5 @ One past the last entry + LDR r7, =__data_segment_start__ @ The code / data dividing line + +got_loop: + CMP r2, r3 + BHS got_done + LDR r6, [r1], #4 @ Pickup the entry as linked + CMP r6, #0 + BEQ got_store @ Never filled in; leave it alone + CMP r6, r7 + ADDLO r6, r6, r4 @ Below the line: a code address + ADDHS r6, r6, r5 @ At or above: a data address +got_store: + STR r6, [r2], #4 + B got_loop +got_done: + +@ --------------------------------------------------------------------------- +@ 2. Initialised data. +@ --------------------------------------------------------------------------- +@ +@ Byte at a time. The run-time destination is the manager's data base plus a +@ linker offset, and while both happen to be word aligned today, a byte copy +@ does not have to care -- and this runs once per module start, where the cost +@ of not having to think about it is nothing. + + LDR r1, =__data_load_start__ + ADD r1, r1, r4 @ Source, in the image + LDR r2, =__data_start__ + ADD r2, r2, r5 @ Destination, in the data area + LDR r3, =__data_end__ + ADD r3, r3, r5 +data_loop: + CMP r2, r3 + BHS data_done + LDRB r6, [r1], #1 + STRB r6, [r2], #1 + B data_loop +data_done: + +@ --------------------------------------------------------------------------- +@ 3. Zero-initialised data. +@ --------------------------------------------------------------------------- +@ +@ The manager has already zeroed the whole allocation, so this is redundant +@ with the loader this port ships. It is here because that is the loader's +@ behaviour and not the module's contract: a module that only works because +@ its loader happens to memset is a module that breaks on the next loader. + + LDR r1, =__bss_start__ + ADD r1, r1, r5 + LDR r2, =__bss_end__ + ADD r2, r2, r5 + MOV r6, #0 + +bss_loop: + CMP r1, r2 + BHS bss_done + STRB r6, [r1], #1 + B bss_loop +bss_done: + +@ The module's data is now readable and writable through r9, and the module +@ proper can run. A data barrier because the GOT and .data were just written +@ as data and are about to be read as the module's working set; no instruction +@ barrier, because nothing executable was modified. + + DSB + POP {r4-r7, lr} BX lr + + .size _gcc_setup, . - _gcc_setup diff --git a/ports_module/cortex_r52/gnu/module_manager/src/txm_module_manager_thread_stack_build.S b/ports_module/cortex_r52/gnu/module_manager/src/txm_module_manager_thread_stack_build.S index 767157d7a..9f8ebfbf0 100644 --- a/ports_module/cortex_r52/gnu/module_manager/src/txm_module_manager_thread_stack_build.S +++ b/ports_module/cortex_r52/gnu/module_manager/src/txm_module_manager_thread_stack_build.S @@ -165,7 +165,28 @@ _txm_module_manager_thread_stack_build: STR r3, [r2, #32] @ Store initial r6 STR r3, [r2, #36] @ Store initial r7 STR r3, [r2, #40] @ Store initial r8 - STR r3, [r2, #44] @ Store initial r9 +@ +@ r9 is the module's PIC base, and seeding it is not optional for a module +@ built -fpic -msingle-pic-base. Every reference the module makes to one of +@ its own globals compiles to LDR rX, [r9, #offset] -- an offset into the +@ global offset table, measured from whatever r9 holds -- and -msingle-pic-base +@ is precisely the promise that nothing in the module will set r9 up, because +@ the caller already did. Here is the caller. +@ +@ The value is the module's data base, which is also where _gcc_setup puts the +@ GOT, because the compiler measures its offsets from the GOT origin and the +@ linker script puts the GOT first in the data segment for that reason. Offset +@ 8 in the thread entry info is txm_module_thread_entry_info_data_base_address, +@ whose declaration in txm_module.h carries the comment "Don't move this, +@ referenced in stack build to setup module data base register." This is the +@ reference it means. +@ +@ Left at zero, as the base port's stack build leaves it and as this port did +@ until now, every module data reference reads address 0 + a small offset. +@ Silicon showed exactly that: r9 was 0 in every fault captured on the S32Z280. +@ + LDR r3, [r12, #8] @ Pickup the module's data base + STR r3, [r2, #44] @ Store initial r9, the PIC base LDR r3, [r0, #12] @ Pickup stack starting address STR r3, [r2, #48] @ Store initial r10 (sl) LDR r3,=_tx_thread_schedule @ Pickup address of _tx_thread_schedule for GDB backtrace From 08776d4a6f09b18039ff0141c13fc5c64439b17d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20Desbiens?= Date: Wed, 19 Aug 2026 15:24:35 -0400 Subject: [PATCH 26/33] Proved the relocation by running the same blob from two addresses A module that runs correctly at the address it was linked for proves nothing about relocation: the GOT rebase maps every address to itself, so a rebase that did nothing would look identical to one that worked. The example therefore loads the same blob twice -- once where the linker put it, once from a staging area reserved in the module area -- and compares the two runs. Pass 1 is left loaded while pass 2 loads, so pass 2's data is allocated after pass 1's and the data base moves too. Both halves of the rebase are then exercised, not just the code half. The verdict is readable from the console alone, with no debugger and without the manager knowing a single module symbol: * the same instruction faults in both passes -- equal offsets from each pass's own code base, at two different absolute addresses * DFAR is the forbidden address in both. The module obtained that value by reading one of its own initialised globals through the rebased GOT, so one register proves the GOT was rewritten and .data was copied * r9 at the fault is the data base the manager handed out * SPSR says User mode, so the boundary held The sample now waits on the captured fault info rather than on the notify callback, and this is a correction rather than a refinement. The handler terminates the offending thread before calling the notify hook, and _tx_thread_terminate does not return when the thread being terminated is the running one -- which it always is here. So the hook, though correctly registered, never runs, fault_count stays 0, and the previous version waited out its timeout and reported that no fault had occurred for a module that had been stopped exactly as intended. The abort vector's fault info is filled in before any of that and is the evidence that actually arrives; the callback counter is still reported, as a statement about the notify path. That defect is untouched here and belongs to the fault-path task. Staged code gets cache maintenance. The copy is data writes to memory about to be fetched as instructions, and the module area is Normal write-back, so the bytes can sit in dirty D-cache lines while the instruction side fetches what main memory still holds. It ran correctly before this was added, which is the problem: a cold I-cache over a never-executed address happens to work until the staging area is reused. An overlap check comes with the second load address. A module's code and data regions are enabled together, PMSAv8-R has no region priority, and the staging area shares its 64 KB with the byte pool the data comes from. Nothing in the manager checks, so the example does. The harness breaks on manager_done instead of a line number. It used to break on sample_threadx_module_manager.c:259, and every edit to that file moved the line out from under the breakpoint, after which the run stopped somewhere arbitrary and reported whatever memory happened to hold. It also computes module_progress as an offset into the module's data segment added to the data base read off the target, because a relocated module's progress flag has no fixed address any more. Verified on an S32Z280-594EVB: module_progress 0x7 in both passes, code bases 0x317F0000 and 0x317F4680, data bases 0x317F0EC0 and 0x317F1780, both faulting at offset 0x19C into their own image. FVP suite still 5/5. Assisted-by: Claude Code (Opus 5) --- .../example_build/s32z280_evb/link_module.lds | 26 + .../sample_threadx_module_manager.c | 522 ++++++++++++++++-- .../s32z280_evb/tools/run_module_demo.gdb | 238 ++++++-- .../s32z280_evb/tools/run_module_demo.sh | 24 +- 4 files changed, 691 insertions(+), 119 deletions(-) diff --git a/ports_module/cortex_r52/gnu/example_build/s32z280_evb/link_module.lds b/ports_module/cortex_r52/gnu/example_build/s32z280_evb/link_module.lds index 9b0e6d9a0..b10acd827 100644 --- a/ports_module/cortex_r52/gnu/example_build/s32z280_evb/link_module.lds +++ b/ports_module/cortex_r52/gnu/example_build/s32z280_evb/link_module.lds @@ -150,6 +150,32 @@ SECTIONS __module_pool_end__ = .; } > MODULE + /* A second place to load the same module from, which is how this example + proves the module is relocatable rather than merely built to be. + + A module that runs correctly at the address it was linked for proves + nothing about relocation -- the GOT rebase would produce the same + addresses it started with and a broken rebase would look identical. So + the manager copies the blob here and loads it a second time, and the two + runs are compared. Any address in the module area would do; what matters + is that it is not __module_image_start__ and that it does not overlap the + pool, because the code region of the second instance and the data regions + the pool hands out are live at the same time and PMSAv8-R does not permit + two enabled regions to overlap. + + NOLOAD, and deliberately not initialised at link time: if the blob were + placed here by the linker as well, a failure to copy it would be invisible + because the right bytes would already be present. */ + + .module_stage (NOLOAD) : + { + . = ALIGN(64); + __module_stage_start__ = .; + . = . + 0x1000; + . = ALIGN(64); + __module_stage_end__ = .; + } > MODULE + /* The code and data sizes are not computed here any more. They belong to the module's own link, which is where its preamble is assembled and where the boundary between its code and its data is actually known. */ diff --git a/ports_module/cortex_r52/gnu/example_build/s32z280_evb/sample_threadx_module_manager.c b/ports_module/cortex_r52/gnu/example_build/s32z280_evb/sample_threadx_module_manager.c index c21f962da..7e3fa6995 100644 --- a/ports_module/cortex_r52/gnu/example_build/s32z280_evb/sample_threadx_module_manager.c +++ b/ports_module/cortex_r52/gnu/example_build/s32z280_evb/sample_threadx_module_manager.c @@ -25,8 +25,8 @@ /* */ /* DESCRIPTION */ /* */ -/* Loads the sample module, lets it misbehave, and reports what the */ -/* hardware did about it. */ +/* Loads the sample module twice, from two different addresses, lets it */ +/* misbehave both times, and reports what the hardware did about it. */ /* */ /* The result this example exists to produce is the fault. A module */ /* that starts and runs proves the loader works; a module that is */ @@ -35,6 +35,32 @@ /* an error path here, it is the expected outcome, and its absence is */ /* the failure. */ /* */ +/* TWO passes, because one proves nothing about relocation. The module */ +/* is position independent: it is linked against nominal addresses it */ +/* never runs at, and _gcc_setup rewrites its global offset table to */ +/* wherever the manager actually put it. A single run at the linked */ +/* address would exercise a rebase whose input and output are the same */ +/* number, and would look identical if the rebase did nothing at all. */ +/* */ +/* So pass 1 loads the blob where the linker placed it and pass 2 loads */ +/* a byte-for-byte copy of it from the staging area, with pass 1 still */ +/* holding its pool memory so that pass 2's data lands somewhere else */ +/* too. Both the code base and the data base therefore differ between */ +/* the passes, which is what makes the comparison at the end mean */ +/* something: */ +/* */ +/* * the same instruction faults in both passes -- equal offsets from */ +/* each pass's own code base, at two different absolute addresses. */ +/* The module ran relocated. */ +/* * DFAR is the forbidden address in both passes. The module got */ +/* that value by reading one of its own initialised globals through */ +/* the rebased GOT, so this single register proves the GOT was */ +/* rewritten and .data was copied. */ +/* * SPSR says User mode in both passes. The boundary held. */ +/* */ +/* All of which is checked on the console, with no help from a debugger */ +/* and without the manager needing to know one symbol of the module. */ +/* */ /* What the module image is and where it comes from: it is linked into */ /* this application as a separate section and loaded in place, so */ /* nothing is copied and no filesystem or download path is needed. */ @@ -49,6 +75,7 @@ #include "linflexd.h" #include "platform.h" #include "thread_mpu.h" +#include "cache.h" /* The fault information the abort vector captured. Declared here because the module manager expands it into the port's fault handler through the @@ -61,6 +88,24 @@ extern TXM_MODULE_MANAGER_MEMORY_FAULT_INFO _txm_module_manager_memory_fault module's preamble first so this address is also the preamble address. */ extern unsigned char __module_image_start__; +extern unsigned char __module_image_end__; + +/* The second address the same blob is loaded from, so relocation can be shown + rather than assumed. Reserved by the linker script and left empty by it -- + see .module_stage in link_module.lds for why it is not pre-filled. */ + +extern unsigned char __module_stage_start__; +extern unsigned char __module_stage_end__; + +/* The address the module is going to reach for and must not be allowed to have. + Declared here only so the console check can name it; the value lives in the + module's own initialised data, which is the point -- the module reads it + through its rebased GOT, so seeing it arrive in DFAR proves the rebase and + the .data copy both worked. Kept in step with sample_threadx_module.c by + hand, and checked at run time rather than trusted: if the two ever disagree + the relocation verdict says so instead of quietly passing. */ + +#define MODULE_FORBIDDEN_ADDRESS 0x31780000UL /* Memory the manager hands out to modules: object memory, and the data region a module's own variables live in. It has to be outside every region a module is @@ -94,7 +139,39 @@ static unsigned char module_pool[MODULE_POOL_SIZE] static unsigned char module_object_pool[MODULE_OBJECT_POOL_SIZE] __attribute__((aligned(64))); -static TXM_MODULE_INSTANCE demo_module; +/* One instance per pass. Both are loaded at once for part of the run: pass 1 is + stopped but still holds its data allocation while pass 2 loads, which is what + pushes pass 2's data base somewhere different. */ + +#define MODULE_PASSES 2U + +static TXM_MODULE_INSTANCE demo_module[MODULE_PASSES]; + +/* What each pass produced. Recorded rather than printed as it happens, because + the comparison between the two passes is the actual result and it cannot be + made until both have run. */ + +typedef struct PASS_RESULT_STRUCT +{ + CHAR *pass_name; + ULONG pass_load_status; + ULONG pass_start_status; + ULONG pass_stop_status; + ULONG pass_code_start; + ULONG pass_code_end; + ULONG pass_data_start; + ULONG pass_data_end; + ULONG pass_data_base; + ULONG pass_faults; + ULONG pass_captured; + ULONG pass_fault_r9; + ULONG pass_dfsr; + ULONG pass_dfar; + ULONG pass_spsr; + ULONG pass_code_location; +} PASS_RESULT; + +static PASS_RESULT pass_results[MODULE_PASSES]; /* What the fault handler saw. Read by the reporting thread after the module has been terminated. */ @@ -137,13 +214,195 @@ static void put_hex(unsigned long value) } +static void put_field(const char *label, unsigned long value) +{ + linflexd_puts(label); + put_hex(value); + linflexd_puts("\n"); +} + + +/**************************************************************************/ +/* A byte copy, written out rather than called for. */ +/* */ +/* The manager links -nostdlib, and reaching for memcpy would pull in a */ +/* libc whose presence this example does not otherwise depend on. The */ +/* blob is under two kilobytes and this runs once. */ +/**************************************************************************/ + +static void copy_bytes(unsigned char *destination, const unsigned char *source, + unsigned long length) +{ + unsigned long i; + + for (i = 0UL; i < length; i++) + { + destination[i] = source[i]; + } +} + + +/**************************************************************************/ +/* One pass: load the blob from a given address, start it, wait for the */ +/* fault it is written to provoke, and stop it. */ +/* */ +/* The module is left loaded. Its data allocation is what moves the next */ +/* pass's data base, and releasing it here would defeat half the test. */ +/* Unloading happens after both passes have run. */ +/**************************************************************************/ + +/* name is CHAR * and not const CHAR *, because txm_module_manager_in_place_load + takes it that way -- the manager stores the pointer in the instance and the API + has never promised not to write through it. */ + +static void run_one_pass(UINT index, CHAR *name, VOID *location) +{ + PASS_RESULT *result = &pass_results[index]; + TXM_MODULE_INSTANCE *instance = &demo_module[index]; + unsigned int waited; + + result -> pass_name = name; + + /* Cleared before the module starts, so a fault counted here belongs to this + pass and not the previous one. */ + + fault_count = 0UL; + + result -> pass_load_status = (ULONG) txm_module_manager_in_place_load(instance, name, location); + + if (result -> pass_load_status != (ULONG) TX_SUCCESS) + { + return; + } + + /* Read from the instance rather than computed here. Where the code ended up + is what the manager decided, and the whole point of the comparison at the + end is to check the module against the manager's own numbers. */ + + result -> pass_code_start = (ULONG) instance -> txm_module_instance_code_start; + result -> pass_code_end = (ULONG) instance -> txm_module_instance_code_end; + result -> pass_data_start = (ULONG) instance -> txm_module_instance_data_start; + result -> pass_data_end = (ULONG) instance -> txm_module_instance_data_end; + result -> pass_data_base = (ULONG) instance -> txm_module_instance_module_data_base_address; + + result -> pass_start_status = (ULONG) txm_module_manager_start(instance); + + if (result -> pass_start_status != (ULONG) TX_SUCCESS) + { + return; + } + + /* Wait for the abort vector to capture a fault belonging to THIS pass, and + not for the notify callback to be told about one. + + Those are different events, and on this port only the first of them + happens. _txm_module_manager_memory_fault_handler terminates the + offending thread and only then calls the notify hook, and + _tx_thread_terminate does not return when the thread it is terminating is + the one running -- which it always is here. So the hook, though correctly + registered, is never reached, fault_count stays 0, and a sample that waits + on it waits out its whole timeout and then declares that no fault + occurred. That is what the first version of this file did, and it + reported FAIL for a module that had been stopped exactly as intended. + See the fault-path item in the task list; the handler is byte-for-byte + identical to the cortex_m33, a7 and m7 ports, so the assumption being + violated is upstream's rather than this port's. + + The fault info itself is filled in by the port's own capture code before + any of that, so it is the evidence that actually arrives. Attribution is + by thread pointer: each pass has its own module instance and therefore its + own start thread, so a captured fault naming this pass's thread cannot be + a leftover from the previous pass. */ + + waited = 0U; + while ((_txm_module_manager_memory_fault_info.txm_module_manager_memory_fault_info_thread_ptr + != &(instance -> txm_module_instance_start_stop_thread)) && + (waited < 50U)) + { + tx_thread_sleep(2UL); + waited++; + } + + if (_txm_module_manager_memory_fault_info.txm_module_manager_memory_fault_info_thread_ptr + == &(instance -> txm_module_instance_start_stop_thread)) + { + result -> pass_captured = 1UL; + result -> pass_dfsr = _txm_module_manager_memory_fault_info.txm_module_manager_memory_fault_info_dfsr; + result -> pass_dfar = _txm_module_manager_memory_fault_info.txm_module_manager_memory_fault_info_dfar; + result -> pass_spsr = _txm_module_manager_memory_fault_info.txm_module_manager_memory_fault_info_spsr; + result -> pass_fault_r9 = _txm_module_manager_memory_fault_info.txm_module_manager_memory_fault_info_r9; + result -> pass_code_location = (ULONG) _txm_module_manager_memory_fault_info.txm_module_manager_memory_fault_info_code_location; + } + + /* Recorded for what it is worth, which is currently a report on the notify + path rather than on the module. */ + + result -> pass_faults = fault_count; + + /* Stopped, not unloaded. The fault terminated the module's start thread but + left the module STARTED, and unload refuses anything that is not LOADED or + STOPPED -- so this is required, not tidiness. */ + + result -> pass_stop_status = (ULONG) txm_module_manager_stop(instance); +} + + +static void report_one_pass(const PASS_RESULT *result) +{ + linflexd_puts("\n--- "); + linflexd_puts(result -> pass_name); + linflexd_puts(" ---\n"); + + put_field(" load status = ", result -> pass_load_status); + put_field(" start status = ", result -> pass_start_status); + put_field(" stop status = ", result -> pass_stop_status); + put_field(" code region = ", result -> pass_code_start); + put_field(" .. to = ", result -> pass_code_end); + put_field(" data region = ", result -> pass_data_start); + put_field(" .. to = ", result -> pass_data_end); + put_field(" data base (r9) = ", result -> pass_data_base); + put_field(" fault captured = ", result -> pass_captured); + put_field(" notify callbacks = ", result -> pass_faults); + put_field(" r9 at the fault = ", result -> pass_fault_r9); + put_field(" DFSR = ", result -> pass_dfsr); + put_field(" DFAR = ", result -> pass_dfar); + put_field(" SPSR = ", result -> pass_spsr); + put_field(" faulting pc = ", result -> pass_code_location); + put_field(" pc - code base = ", result -> pass_code_location - result -> pass_code_start); +} + + +/**************************************************************************/ +/* Where the debugger stops. */ +/* */ +/* A symbol and not a line number in the report loop. The harness used */ +/* to break on sample_threadx_module_manager.c:259, and every edit to */ +/* this file moved that line -- after which the run stops somewhere */ +/* arbitrary and reports whatever memory happens to hold, which looks */ +/* like a result rather than a mistake. This does not move. */ +/* */ +/* Not static, and noinline, so it survives to the symbol table with an */ +/* address a breakpoint can be set on. */ +/**************************************************************************/ + +__attribute__((noinline)) void manager_done(void) +{ + __asm__ volatile("nop"); +} + + /**************************************************************************/ /* Reporting. */ /**************************************************************************/ static void manager_entry(ULONG input) { - UINT status; + UINT status; + UINT i; + unsigned long blob_size; + ULONG offset_0; + ULONG offset_1; + UINT failures = 0U; (void) input; @@ -178,82 +437,239 @@ static void manager_entry(ULONG input) linflexd_puts("M2 fault notify\n"); txm_module_manager_memory_fault_notify(module_fault_notify); - /* Nothing to bracket here. No kernel region covers the module area, but the - window that reaches it is owned by the scheduler, which enables it for - every thread that does not own a module and disables it for every thread - that does -- so it is already open on this thread and cannot be open at - the same time as a module's own regions. */ + /* The copy that makes pass 2 a relocation test. The staging area is left + empty by the linker on purpose: if the blob were already there, a failure + to copy would be invisible because the right bytes would be present + anyway. */ - linflexd_puts("M3 in_place_load\n"); - status = txm_module_manager_in_place_load(&demo_module, "demo module", - (VOID *) &__module_image_start__); + blob_size = (unsigned long) (&__module_image_end__ - &__module_image_start__); + + linflexd_puts("M3 staging the blob\n"); + put_field(" blob size = ", blob_size); + put_field(" from = ", (unsigned long) &__module_image_start__); + put_field(" to = ", (unsigned long) &__module_stage_start__); + + if (blob_size > (unsigned long) (&__module_stage_end__ - &__module_stage_start__)) + { + /* Reported rather than allowed to overrun. The staging area is a fixed + size in the linker script and the module is free to grow. */ + + linflexd_puts("FAIL the module image does not fit the staging area\n"); + failures++; + } + else + { + copy_bytes(&__module_stage_start__, &__module_image_start__, blob_size); + + /* Those were data writes to memory that is about to be fetched as + instructions, and the module area is mapped Normal write-back + (MAIR attribute 0 is 0xFF). So the copied bytes may sit in dirty D + cache lines while the instruction side, which is not coherent with + the D cache on this core, fetches whatever main memory still holds. + Clean first so memory is correct, then invalidate the I cache so no + stale line from a previous image can be served. + + This is not a precaution the passing run justifies. It ran correctly + before this was added, which is exactly the problem: a cold I cache + over a never-executed address happens to work, and it keeps happening + to work until the staging area is reused or an eviction lands + differently. Any loader that copies code owes this pair of + operations, and by-range variants would be tighter than these + all-cache sweeps -- fine here, where it runs once at startup. */ + + cache_clean_all(); + cache_invalidate_icache_all(); + + /* Nothing to bracket for the MPU here. No kernel region covers the + module area, but the window that reaches it is owned by the scheduler, + which enables it for every thread that does not own a module and + disables it for every thread that does -- so it is already open on + this thread and cannot be open while a module's own regions are. */ + + linflexd_puts("M4 pass 1, at the linked address\n"); + run_one_pass(0U, "linked address", (VOID *) &__module_image_start__); + + linflexd_puts("M5 pass 2, relocated\n"); + run_one_pass(1U, "relocated", (VOID *) &__module_stage_start__); + + /* Both passes are done with their memory now. */ + + for (i = 0U; i < MODULE_PASSES; i++) + { + (void) txm_module_manager_unload(&demo_module[i]); + } + } + + /* ------------------------------------------------------------------ + The verdict. + ------------------------------------------------------------------ */ + + linflexd_puts("\n=== ThreadX modules on S32Z280: relocation and protection ===\n"); + + for (i = 0U; i < MODULE_PASSES; i++) + { + report_one_pass(&pass_results[i]); + } - linflexd_puts("in_place_load = "); - put_hex(status); - linflexd_puts(" ready = "); - put_hex((ULONG) _txm_module_manager_ready); linflexd_puts("\n"); - if (status == TX_SUCCESS) + /* Before anything else: a module's code and data regions are enabled at the + same time, and PMSAv8-R has no region priority -- two enabled regions that + overlap are CONSTRAINED UNPREDICTABLE, and on this part that means aborts + from addresses that look perfectly legal. Nothing in the manager checks + for it, so this example does. + + The risk is real here and not theoretical: pass 2 loads its code from the + staging area, which sits in the same 64 KB as the byte pool its data comes + out of. A module grown large enough, or a pool sized differently, and the + two would meet. */ + + for (i = 0U; i < MODULE_PASSES; i++) { - status = txm_module_manager_start(&demo_module); - linflexd_puts("module start = "); - put_hex(status); - linflexd_puts("\n"); + const PASS_RESULT *result = &pass_results[i]; + + if (result -> pass_load_status != (ULONG) TX_SUCCESS) + { + continue; + } + + if ((result -> pass_code_start <= result -> pass_data_end) && + (result -> pass_data_start <= result -> pass_code_end)) + { + linflexd_puts("FAIL "); + linflexd_puts(result -> pass_name); + linflexd_puts(": its code and data regions overlap\n"); + failures++; + } } - /* Give the module time to run its three steps and be terminated by the - third. Bounded: if the fault never arrives, that is the result to - report, not a reason to wait for ever. */ + /* Each pass on its own: it has to have loaded, started, faulted once, and + faulted in User mode at the address it was told not to touch. */ + for (i = 0U; i < MODULE_PASSES; i++) { - unsigned int waited = 0U; + const PASS_RESULT *result = &pass_results[i]; - while ((fault_count == 0UL) && (waited < 50U)) + if ((result -> pass_load_status != (ULONG) TX_SUCCESS) || + (result -> pass_start_status != (ULONG) TX_SUCCESS)) { - tx_thread_sleep(2UL); - waited++; + linflexd_puts("FAIL "); + linflexd_puts(result -> pass_name); + linflexd_puts(": did not load and start\n"); + failures++; + } + else if (result -> pass_captured == 0UL) + { + linflexd_puts("FAIL "); + linflexd_puts(result -> pass_name); + linflexd_puts(": reached outside its memory and was not stopped\n"); + failures++; + } + else if (result -> pass_fault_r9 != result -> pass_data_base) + { + /* r9 is the module's PIC base and the manager seeded it from the + thread entry info. If the value captured at the fault is not the + data base the manager handed out, the seeding is wrong and every + data reference the module made went somewhere unintended -- so + check it explicitly rather than inferring it from a symptom. */ + + linflexd_puts("FAIL "); + linflexd_puts(result -> pass_name); + linflexd_puts(": r9 was not the module's data base\n"); + failures++; + } + else if ((result -> pass_spsr & 0x1FUL) != 0x10UL) + { + /* A fault from anywhere but User mode is not this test passing: the + module runs unprivileged, so a privileged fault means the fault + came from the kernel and something else is wrong. */ + + linflexd_puts("FAIL "); + linflexd_puts(result -> pass_name); + linflexd_puts(": faulted, but not from User mode\n"); + failures++; + } + else if (result -> pass_dfar != MODULE_FORBIDDEN_ADDRESS) + { + /* The module got this address out of its own initialised data, + through its rebased GOT. Any other value means it faulted + somewhere unintended -- most likely on its own data, which is what + a bad rebase looks like. */ + + linflexd_puts("FAIL "); + linflexd_puts(result -> pass_name); + linflexd_puts(": faulted at the wrong address, so .data or the GOT is wrong\n"); + failures++; + } + else + { + linflexd_puts("PASS "); + linflexd_puts(result -> pass_name); + linflexd_puts(": faulted in User mode at the forbidden address\n"); } } - linflexd_puts("\n=== ThreadX modules on S32Z280: protection boundary ===\n"); + /* And the two passes against each other, which is the relocation result. */ - linflexd_puts("module faults = "); - put_hex(fault_count); - linflexd_puts("\n"); + offset_0 = pass_results[0].pass_code_location - pass_results[0].pass_code_start; + offset_1 = pass_results[1].pass_code_location - pass_results[1].pass_code_start; - if (fault_count == 0UL) + if ((pass_results[0].pass_captured == 0UL) || (pass_results[1].pass_captured == 0UL)) + { + linflexd_puts("FAIL relocation: a pass did not fault, nothing to compare\n"); + failures++; + } + else if (pass_results[0].pass_code_start == pass_results[1].pass_code_start) { - linflexd_puts("FAIL the module reached outside its memory and was not stopped\n"); + linflexd_puts("FAIL relocation: both passes ran from the same address\n"); + failures++; + } + else if (offset_0 != offset_1) + { + linflexd_puts("FAIL relocation: the two passes faulted at different offsets\n"); + failures++; } else { - linflexd_puts("DFSR = "); - put_hex(fault_dfsr); - linflexd_puts("\nDFAR = "); - put_hex(fault_dfar); - linflexd_puts("\nSPSR = "); - put_hex(fault_spsr); - linflexd_puts("\nfaulting pc = "); - put_hex(fault_code_location); - linflexd_puts("\n"); - - /* SPSR's mode field says who faulted. 0x10 is User mode, which is where - a protected module runs; anything else means the fault came from - privileged code and this is not the test passing. */ + linflexd_puts("PASS relocation: the same blob ran correctly from two addresses\n"); - if ((fault_spsr & 0x1FUL) == 0x10UL) + if (pass_results[0].pass_data_base == pass_results[1].pass_data_base) { - linflexd_puts("PASS the module faulted in User mode and was terminated\n"); - } - else - { - linflexd_puts("FAIL a fault was taken, but not from User mode\n"); + /* Not a failure -- the code rebase is still proven -- but worth + saying, because it means the data rebase produced the same + numbers twice and was not exercised as thoroughly. */ + + linflexd_puts("NOTE both passes shared a data base; the data rebase was not varied\n"); } } + /* Reported apart from the verdict above, because it is a known defect in the + fault-notification path and not a property of this run. The module was + stopped either way -- that is what the captured fault says -- but an + application asking to be told about it is not being told. */ + + if ((pass_results[0].pass_captured != 0UL) && (pass_results[0].pass_faults == 0UL)) + { + linflexd_puts("KNOWN the fault was captured but the notify callback never ran\n"); + } + + put_field("\nfailures = ", (unsigned long) failures); + + if (failures == 0U) + { + linflexd_puts("=== PASS ===\n"); + } + else + { + linflexd_puts("=== FAIL ===\n"); + } + linflexd_puts("=== end ===\n"); + /* Everything worth reading is in memory now. */ + + manager_done(); + while (1) { tx_thread_sleep(100UL); diff --git a/ports_module/cortex_r52/gnu/example_build/s32z280_evb/tools/run_module_demo.gdb b/ports_module/cortex_r52/gnu/example_build/s32z280_evb/tools/run_module_demo.gdb index 45a0fc341..d180a6ff9 100644 --- a/ports_module/cortex_r52/gnu/example_build/s32z280_evb/tools/run_module_demo.gdb +++ b/ports_module/cortex_r52/gnu/example_build/s32z280_evb/tools/run_module_demo.gdb @@ -37,6 +37,21 @@ # bit 0x8 survived the forbidden access expect CLEAR # # so module_progress == 0x7 exactly, and fault_count >= 1 with SPSR mode 0x10. +# +# AND IT HAPPENS TWICE, from two different load addresses +# +# The module is position independent, so a single run at the address it was +# linked for would prove nothing: the GOT rebase would map every address to +# itself and a rebase that did nothing would look the same. The sample therefore +# loads the same blob twice -- once where the linker put it, once from a staging +# area -- and this script checks both. +# +# Finding module_progress is no longer a matter of reading the module's ELF. A +# relocated module's data lives wherever the manager allocated it, so the address +# is (this pass's data base) + (the nominal offset of module_progress within the +# module's data segment). The first number is read from the module instance on +# the target, the second from the module's ELF, and neither is a constant that +# can be written down here. py _PROBE_IP = "s32dbg:192.168.50.238" py _SOC_NAME = "S32Z280" @@ -90,13 +105,15 @@ start = int(gdb.parse_and_eval("(unsigned int)&_start")) & ~1 gdb.execute("set $pc = 0x%x" % start) print("PC set to 0x%08X" % start) -# Stop after the manager has printed its whole report. manager_entry ends in an -# endless sleep, so this line is reached exactly once the verdict is out -- there -# is no bsp_done in this image to break on, unlike the boot example. +# Stop after the manager has printed its whole report. manager_done exists for +# this and nothing else: the previous version of this script broke on +# sample_threadx_module_manager.c:259, and every edit to that file moved the line +# out from under the breakpoint, after which the run stopped somewhere arbitrary +# and reported whatever memory happened to hold. # # hbreak, not break: the code region is mapped read-only by the MPU, so a # software breakpoint would have to write to it. -gdb.execute("hbreak sample_threadx_module_manager.c:259") +gdb.execute("hbreak manager_done") gdb.execute("continue") end @@ -107,78 +124,177 @@ import gdb, os def rd(addr): return int(gdb.parse_and_eval("*(unsigned int *)0x%x" % addr)) & 0xFFFFFFFF -def sym(name): - return int(gdb.parse_and_eval("(unsigned int)&%s" % name)) & 0xFFFFFFFF +def ev(expr): + return int(gdb.parse_and_eval(expr)) & 0xFFFFFFFF failures = 0 -# --- the module's own progress, read from the module image ------------------- -# -# module_progress belongs to the module, which is a separate link unit embedded -# as a blob, so the manager's ELF has no symbol for it. The address comes from -# the module's own ELF, and it is valid because the module is linked absolutely -# and loaded in place: it runs, and writes its data, where it lies. Once T3 -# makes modules relocatable this has to be derived from the load address -# instead, and this comment is the reminder. -progress_addr = os.environ.get("S32Z280_MODULE_PROGRESS_ADDR") - OWN_DATA, KERNEL_CALL, ATTEMPTED_STEAL, SURVIVED_STEAL = 0x1, 0x2, 0x4, 0x8 +FORBIDDEN = 0x31780000 +MODE = {0x10: "User", 0x13: "Supervisor", 0x1A: "Hyp", 0x1F: "System"} + +# The nominal offset of module_progress inside the module's data segment. Both +# halves come from the module's own ELF and neither is an address the module ever +# runs at -- see link_demo_module.lds. The run-time address is this offset plus +# whatever data base the manager handed the module, which differs per pass. +progress_offset = os.environ.get("S32Z280_MODULE_PROGRESS_OFFSET") +progress_offset = int(progress_offset, 0) if progress_offset else None -if progress_addr: - progress = rd(int(progress_addr, 0)) - print(" module_progress = 0x%08X (at %s)" % (progress, progress_addr)) - for bit, name, want_set in ((OWN_DATA, "wrote its own data", True), - (KERNEL_CALL, "reached the kernel through SVC", True), - (ATTEMPTED_STEAL, "attempted the forbidden access", True), - (SURVIVED_STEAL, "SURVIVED the forbidden access", False)): - got = bool(progress & bit) - ok = (got == want_set) - if not ok: - failures += 1 - print(" 0x%X %-34s %-5s %s" % (bit, name, "set" if got else "clear", - "ok" if ok else "*** WRONG ***")) - if progress & SURVIVED_STEAL: - print(" ISOLATION FAILURE: the module read memory it was never granted.") +if progress_offset is None: + print(" NOTE S32Z280_MODULE_PROGRESS_OFFSET unset; skipping the progress checks") else: - print(" module_progress not read (S32Z280_MODULE_PROGRESS_ADDR unset)") + print(" module_progress sits %d bytes into the module's data segment" % progress_offset) -# --- what the fault handler captured ---------------------------------------- -print("") -count = rd(sym("fault_count")) -print(" fault_count = %d" % count) +passes = [] +for i in range(2): + p = {} + for field in ("pass_load_status", "pass_start_status", "pass_stop_status", + "pass_code_start", "pass_code_end", "pass_data_start", + "pass_data_end", "pass_data_base", "pass_faults", + "pass_captured", "pass_fault_r9", + "pass_dfsr", "pass_dfar", "pass_spsr", "pass_code_location"): + p[field] = ev("pass_results[%d].%s" % (i, field)) + try: + p["name"] = gdb.parse_and_eval("pass_results[%d].pass_name" % i).string() + except gdb.error: + p["name"] = "pass %d" % (i + 1) + passes.append(p) -if count == 0: - print(" *** FAIL: no fault was taken. The module reached outside its memory") - print(" and was not stopped, or it never got as far as trying.") - failures += 1 -else: - dfsr = rd(sym("fault_dfsr")) - dfar = rd(sym("fault_dfar")) - spsr = rd(sym("fault_spsr")) - pc = rd(sym("fault_code_location")) - print(" DFSR = 0x%08X status 0x%02X WnR %d" +for i, p in enumerate(passes): + print("") + print(" --- pass %d: %s ---" % (i + 1, p["name"])) + print(" load / start / stop = 0x%08X / 0x%08X / 0x%08X" + % (p["pass_load_status"], p["pass_start_status"], p["pass_stop_status"])) + print(" code region = 0x%08X .. 0x%08X" + % (p["pass_code_start"], p["pass_code_end"])) + print(" data region = 0x%08X .. 0x%08X" + % (p["pass_data_start"], p["pass_data_end"])) + print(" data base (r9) = 0x%08X" % p["pass_data_base"]) + + if p["pass_load_status"] != 0 or p["pass_start_status"] != 0: + print(" *** FAIL: did not load and start") + failures += 1 + continue + + # Code and data regions are enabled together and PMSAv8-R has no region + # priority, so an overlap is CONSTRAINED UNPREDICTABLE rather than merely + # untidy. Checked here as well as on the target because a debugger can say + # which two regions met, and the target can only say that something did. + if (p["pass_code_start"] <= p["pass_data_end"] + and p["pass_data_start"] <= p["pass_code_end"]): + print(" *** FAIL: this module's code and data regions overlap") + failures += 1 + + if progress_offset is not None: + addr = p["pass_data_base"] + progress_offset + progress = rd(addr) + print(" module_progress = 0x%08X (at 0x%08X)" % (progress, addr)) + for bit, name, want_set in ((OWN_DATA, "wrote its own data", True), + (KERNEL_CALL, "reached the kernel through SVC", True), + (ATTEMPTED_STEAL, "attempted the forbidden access", True), + (SURVIVED_STEAL, "SURVIVED the forbidden access", False)): + got = bool(progress & bit) + ok = (got == want_set) + if not ok: + failures += 1 + print(" 0x%X %-34s %-5s %s" + % (bit, name, "set" if got else "clear", + "ok" if ok else "*** WRONG ***")) + if progress & SURVIVED_STEAL: + print(" ISOLATION FAILURE: the module read memory it was never granted.") + + # "captured" and "notified" are different events, and only the first of them + # happens on this port. The fault handler terminates the offending thread + # before calling the notify hook, and _tx_thread_terminate does not return + # when the thread being terminated is the running one -- which it always is + # here. So the hook never runs, and a check written against the callback + # counter reports "no fault" for a module that was stopped correctly. + # The abort vector's captured fault info is the evidence that arrives. + print(" fault captured = %d" % p["pass_captured"]) + print(" notify callbacks = %d" % p["pass_faults"]) + if p["pass_captured"] == 0: + print(" *** FAIL: no fault was captured. The module reached outside its") + print(" memory and was not stopped, or never got that far.") + failures += 1 + continue + if p["pass_faults"] == 0: + print(" KNOWN: captured but never notified -- the fault-notify path is") + print(" broken independently of anything this run exercises.") + + dfsr, dfar, spsr, pc = (p["pass_dfsr"], p["pass_dfar"], + p["pass_spsr"], p["pass_code_location"]) + print(" DFSR = 0x%08X status 0x%02X WnR %d" % (dfsr, dfsr & 0x3F, (dfsr >> 11) & 1)) - print(" DFAR = 0x%08X" % dfar) - print(" SPSR = 0x%08X mode 0x%02X (%s)" - % (spsr, spsr & 0x1F, - {0x10: "User", 0x13: "Supervisor", 0x1A: "Hyp", - 0x1F: "System"}.get(spsr & 0x1F, "?"))) - print(" faulting pc = 0x%08X" % pc) - - # The mode field is the whole point. A fault from anywhere but User mode - # means privileged code faulted, which is not this test passing. + print(" DFAR = 0x%08X" % dfar) + print(" SPSR = 0x%08X mode 0x%02X (%s)" + % (spsr, spsr & 0x1F, MODE.get(spsr & 0x1F, "?"))) + print(" faulting pc = 0x%08X" % pc) + print(" pc - code base = 0x%08X" % ((pc - p["pass_code_start"]) & 0xFFFFFFFF)) + print(" r9 at the fault = 0x%08X" % p["pass_fault_r9"]) + + # r9 is the module's PIC base, seeded by the manager's thread stack build from + # the thread entry info. Before T3 it was left at 0, which is what made every + # module data reference resolve to a small absolute address. Checking it + # directly turns that class of bug into one line instead of a fault to explain. + if p["pass_fault_r9"] != p["pass_data_base"]: + print(" *** FAIL: r9 should be the data base 0x%08X" % p["pass_data_base"]) + failures += 1 + if (spsr & 0x1F) != 0x10: - print(" *** FAIL: the fault did not come from User mode, so it is not") - print(" a module being stopped at the boundary.") + print(" *** FAIL: the fault did not come from User mode, so it is not") + print(" a module being stopped at the boundary.") + failures += 1 + + # DFAR is the strongest single check in this script. The module obtained + # this address by reading one of its own initialised globals through the + # rebased GOT, so the right value here means the GOT was rewritten AND .data + # was copied out of the image. A wrong value usually means the module + # faulted on its own data instead, which is what a bad rebase looks like. + if dfar != FORBIDDEN: + print(" *** FAIL: expected DFAR 0x%08X, the address the module was told" % FORBIDDEN) + print(" not to touch. A different one means .data was not") + print(" copied or the GOT was not rebased.") failures += 1 - else: - print(" the fault came from User mode, which is where a module runs") + +# --- the relocation result, which is the comparison between the passes -------- +print("") +print(" ===== relocation =====") +a, b = passes[0], passes[1] +off_a = (a["pass_code_location"] - a["pass_code_start"]) & 0xFFFFFFFF +off_b = (b["pass_code_location"] - b["pass_code_start"]) & 0xFFFFFFFF + +if a["pass_captured"] == 0 or b["pass_captured"] == 0: + print(" *** FAIL: a pass did not fault, so there is nothing to compare") + failures += 1 +elif a["pass_code_start"] == b["pass_code_start"]: + print(" *** FAIL: both passes ran from the same address 0x%08X" + % a["pass_code_start"]) + failures += 1 +elif off_a != off_b: + print(" *** FAIL: the passes faulted at different offsets, 0x%08X and 0x%08X" + % (off_a, off_b)) + failures += 1 +else: + print(" code bases 0x%08X and 0x%08X, %d bytes apart" + % (a["pass_code_start"], b["pass_code_start"], + (b["pass_code_start"] - a["pass_code_start"]) & 0xFFFFFFFF)) + print(" both faulted at offset 0x%08X into their own image" % off_a) + print(" PASS: the same blob ran correctly from two different addresses") + if a["pass_data_base"] == b["pass_data_base"]: + print(" NOTE both passes shared a data base 0x%08X, so the data half of" + % a["pass_data_base"]) + print(" the rebase produced the same numbers twice") print("") -print(" boot_stage = %d" % rd(sym("boot_stage"))) +# Cast, because this symbol has no debug type in the manager image and gdb +# refuses to read an untyped symbol without being told its width. +try: + print(" boot_stage = %d" % ev("*(unsigned int *)&boot_stage")) +except gdb.error as e: + print(" boot_stage = unavailable (%s)" % e) print("") if failures == 0: - print("===== PASS: the module was stopped at the boundary =====") + print("===== PASS: relocated, and stopped at the boundary, both times =====") else: print("===== FAIL: %d check(s) wrong =====" % failures) end diff --git a/ports_module/cortex_r52/gnu/example_build/s32z280_evb/tools/run_module_demo.sh b/ports_module/cortex_r52/gnu/example_build/s32z280_evb/tools/run_module_demo.sh index 074a7abca..d9cac3d2b 100755 --- a/ports_module/cortex_r52/gnu/example_build/s32z280_evb/tools/run_module_demo.sh +++ b/ports_module/cortex_r52/gnu/example_build/s32z280_evb/tools/run_module_demo.sh @@ -44,12 +44,26 @@ done # gdb-py needs the source-built interpreter: its embedded Python has a minimal # builtin set and loads _struct/_socket/_ctypes as separate .so files. Compute # the module address BEFORE exporting PYTHONHOME, since that breaks system tools. -PROGRESS_ADDR=0x$(nm "$MODULE_ELF" | awk '$3=="module_progress"{print $1}') -if [ "$PROGRESS_ADDR" = "0x" ]; then - echo "ERROR: module_progress not found in $MODULE_ELF" >&2 +# What is wanted is an OFFSET, not an address. The module is position +# independent: the address nm reports is a nominal one from a segment the module +# never runs at, and its real data lives wherever the manager allocated it -- a +# different place in each of the two passes. So the offset of module_progress +# within the module's data segment is computed here, and the gdb script adds it +# to each pass's data base, which it reads off the target. +PROGRESS_SYM=$(nm "$MODULE_ELF" | awk '$3=="module_progress"{print $1}') +DATA_SEG=$(nm "$MODULE_ELF" | awk '$3=="__data_segment_start__"{print $1}') +if [ -z "$PROGRESS_SYM" ] || [ -z "$DATA_SEG" ]; then + echo "ERROR: module_progress or __data_segment_start__ not found in $MODULE_ELF" >&2 exit 1 fi -echo "module_progress lives at $PROGRESS_ADDR (from the module's own ELF)" +PROGRESS_OFFSET=$(( 0x$PROGRESS_SYM - 0x$DATA_SEG )) +if [ "$PROGRESS_OFFSET" -lt 0 ]; then + echo "ERROR: module_progress is below the module's data segment; the link map" >&2 + echo " and this script disagree about where the module's data starts." >&2 + exit 1 +fi +echo "module_progress is $PROGRESS_OFFSET bytes into the module's data segment" +echo " (nominal 0x$PROGRESS_SYM, data segment 0x$DATA_SEG -- neither is a real address)" if pgrep -x ccs > /dev/null; then echo "ERROR: a Linux CCS is running (pid $(pgrep -x ccs | tr '\n' ' '))." >&2 @@ -90,7 +104,7 @@ export PYTHONHOME=$P export PYTHONPATH=$P/lib/python3.10:$P/lib/python3.10/lib-dynload:$P/lib/python3.10/site-packages export S32Z280_ELF=$MANAGER_ELF export S32Z280_MODULE_ELF=$MODULE_ELF -export S32Z280_MODULE_PROGRESS_ADDR=$PROGRESS_ADDR +export S32Z280_MODULE_PROGRESS_OFFSET=$PROGRESS_OFFSET # pkill -x, not -f: these run as ./gta, so a -f pattern like 'gta/gta' never # matches and a stale server silently serves the next attach. From 89bc0fe8aa2dc859730a778987da32a7c77d0363 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20Desbiens?= Date: Wed, 19 Aug 2026 16:59:15 -0400 Subject: [PATCH 27/33] Fixed the module fault-notify path and covered both abort types The shared _txm_module_manager_memory_fault_handler calls the application's fault-notify callback after terminating the faulting thread, and that call was never reached on this port. It is not an upstream bug: terminating the running thread returns only when the kernel believes it is inside an exception, because _tx_thread_terminate ends in _tx_thread_system_preempt_check, which calls _tx_thread_system_return whenever _tx_thread_system_state and _tx_thread_preempt_disable are both zero -- and on AArch32 that switches context and never comes back. cortex_m33 is safe because its _tx_thread_system_return only pends PendSV; cortex_a7, the port shaped like this one, increments _tx_thread_system_state in its abort vector. This port's vector did not. So the fix belongs in the vector, not the handler, whose code stays byte for byte identical to cortex_m33, cortex_a7 and cortex_m7. The capture now increments _tx_thread_system_state before any kernel service runs, balances its own push, and on return decrements it, clears _tx_thread_current_ptr and enters _tx_thread_schedule through an exception return in System mode. That also stops an Abort stack leak. The _tx_thread_system_return that ran in the callback's place did so from Abort mode, saving a solicited frame nothing reclaimed on top of a capture push that was read rather than popped: about 56 bytes of a 1 KB Abort stack per module fault, for the life of the run. The sample now exercises the prefetch abort as well as the data abort, since only one of the two vectors had ever run. Which violation the module commits is chosen by the manager through the application-defined module ID, so one blob covers both cases and the manager still knows no symbol of the module. A third pass takes the prefetch abort from the staging area, after the first two have been unloaded, so it is also a load that follows two faults and two unloads. The notify callback is checked rather than printed: exactly one call per pass, with the faulting thread and module instance compared against what was recorded before the start. The harness reads each pass's module_progress at a new pass_done() symbol while that pass still holds its data. Reading all three at the end of the run attributed pass 3's progress to pass 1, because the byte pool had handed pass 3 pass 1's freed block. Verified on S32Z280-594EVB silicon, RTU0 core 0: three passes, both abort types, IFSR/IFAR 0x0000020C/0x31780000 for the prefetch pass and DFSR/DFAR 0x0000020C/0x31780000 for the data passes, all faults in User mode, one correct notification each, and the relocation result unchanged at offset 0x1F8 from two code bases. Target verdict and gdb harness both green. AR1 FVP regression 5/5. Assisted-by: Claude Code (Opus 5) --- .../s32z280_evb/sample_threadx_module.c | 74 ++++- .../sample_threadx_module_manager.c | 310 +++++++++++++++--- .../s32z280_evb/tools/run_module_demo.gdb | 239 +++++++++++--- .../s32z280_evb/tools/run_module_demo.sh | 2 +- .../src/txm_module_manager_fault_capture.S | 107 +++++- .../txm_module_manager_memory_fault_handler.c | 25 +- 6 files changed, 634 insertions(+), 123 deletions(-) diff --git a/ports_module/cortex_r52/gnu/example_build/s32z280_evb/sample_threadx_module.c b/ports_module/cortex_r52/gnu/example_build/s32z280_evb/sample_threadx_module.c index 470071f7d..5bd5adec9 100644 --- a/ports_module/cortex_r52/gnu/example_build/s32z280_evb/sample_threadx_module.c +++ b/ports_module/cortex_r52/gnu/example_build/s32z280_evb/sample_threadx_module.c @@ -38,7 +38,8 @@ /* 2. Makes a kernel call, which must succeed -- proving a module in */ /* User mode can reach the kernel through the supervisor call */ /* boundary and come back. */ -/* 3. Reads the manager's memory, which must fault. */ +/* 3. Violates its protection in one of the two ways the hardware */ +/* distinguishes, which must fault. */ /* 4. Never reaches step 4, because step 3 terminates it. */ /* */ /* Steps 1 and 2 passing without step 3 faulting would mean the module */ @@ -46,6 +47,16 @@ /* detect. A module that only ever touched its own memory would pass */ /* identically with the MPU switched off. */ /* */ +/* TWO KINDS OF VIOLATION, because the core reports them differently. */ +/* Reading outside the data region is a DATA abort, reported through */ +/* DFSR and DFAR; branching outside the code region is a PREFETCH abort,*/ +/* reported through IFSR and IFAR. Both arrive at the same handler by */ +/* different vectors, and a module that only ever does the first leaves */ +/* half of the port's fault path unexecuted. Which one this module does */ +/* is chosen by the manager, through the application-defined module ID */ +/* that the manager passes to the start thread -- the module needs no */ +/* build-time variant and the manager needs no symbol of the module. */ +/* */ /* The module cannot print. The console belongs to the board support */ /* package, outside every region a module owns, so reaching it would */ /* fault as surely as step 3 does. Progress is recorded in the */ @@ -62,6 +73,17 @@ #define MODULE_PROGRESS_KERNEL_CALL 0x00000002UL #define MODULE_PROGRESS_ATTEMPTED_STEAL 0x00000004UL #define MODULE_PROGRESS_SURVIVED_STEAL 0x00000008UL +#define MODULE_PROGRESS_ATTEMPTED_JUMP 0x00000010UL +#define MODULE_PROGRESS_SURVIVED_JUMP 0x00000020UL + +/* Which violation to commit, taken from the low byte of the module ID the + manager passes to the start thread. The rest of that word is left alone: the + preamble ships it as 0x52520001, so a manager that sets nothing still gets a + working data-abort test rather than a module that does nothing. */ + +#define MODULE_TEST_MASK 0x000000FFUL +#define MODULE_TEST_DATA_ABORT 0x00000001UL +#define MODULE_TEST_PREFETCH_ABORT 0x00000002UL volatile ULONG module_progress; volatile ULONG module_scratch[16]; @@ -81,13 +103,19 @@ volatile ULONG module_scratch[16]; volatile ULONG module_forbidden_address = 0x31780000UL; +/* The same address serves the prefetch test. On this board it is the base of + the kernel's data region, which is mapped execute-never and privileged-only, + so a User-mode instruction fetch there is a permission fault whichever way it + is looked at. An address in no region at all would fault too, but this one + also proves the kernel's own memory is what a module cannot reach -- which is + the property being demonstrated, rather than the absence of a mapping. */ + + void demo_module_start(ULONG id) { ULONG i; ULONG sum = 0UL; - (void) id; - /* 1. The module's own data. If this faults, the data region is wrong and nothing else in this file will be reached. */ @@ -117,18 +145,42 @@ void demo_module_start(ULONG id) module_progress |= MODULE_PROGRESS_KERNEL_CALL; } - /* 3. Somebody else's memory. This must fault, and the fault must terminate - this thread. Marked before the access rather than after, because after - is not reached if the port is working. */ + /* 3. The violation, and 4. the flag that says it was tolerated. Each is + marked before the access rather than after, because after is not + reached if the port is working. */ + + if ((id & MODULE_TEST_MASK) == MODULE_TEST_PREFETCH_ABORT) + { + /* Branch outside the code region: a prefetch abort, reported through + IFSR and IFAR. Nothing is executed at the target -- the fault is on + the fetch itself, so the thread never arrives. */ + + module_progress |= MODULE_PROGRESS_ATTEMPTED_JUMP; - module_progress |= MODULE_PROGRESS_ATTEMPTED_STEAL; + /* MISRA C:2012 Rule 11.1 (no conversion between a pointer to a function + and any other type) is deliberately violated here, and Rule 11.6 + (no conversion between an integer and a pointer to void) with it. + Provoking a fetch from an address that holds no function is the entire + purpose of these three lines; there is no conforming way to write it. + The address is read from the module's own initialised data so that the + value arriving in IFAR also proves the GOT rebase, exactly as the data + case uses DFAR. */ - sum += *((volatile ULONG *) module_forbidden_address); + ((void (*)(void)) module_forbidden_address)(); - /* 4. Only reached if the access above was permitted, which means the module - is running unprotected. The manager treats this flag as a failure. */ + module_progress |= MODULE_PROGRESS_SURVIVED_JUMP; + } + else + { + /* Read outside the data region: a data abort, reported through DFSR and + DFAR. */ - module_progress |= MODULE_PROGRESS_SURVIVED_STEAL; + module_progress |= MODULE_PROGRESS_ATTEMPTED_STEAL; + + sum += *((volatile ULONG *) module_forbidden_address); + + module_progress |= MODULE_PROGRESS_SURVIVED_STEAL; + } /* Keep the compiler from discarding the read above. */ diff --git a/ports_module/cortex_r52/gnu/example_build/s32z280_evb/sample_threadx_module_manager.c b/ports_module/cortex_r52/gnu/example_build/s32z280_evb/sample_threadx_module_manager.c index 7e3fa6995..93d720d31 100644 --- a/ports_module/cortex_r52/gnu/example_build/s32z280_evb/sample_threadx_module_manager.c +++ b/ports_module/cortex_r52/gnu/example_build/s32z280_evb/sample_threadx_module_manager.c @@ -25,8 +25,8 @@ /* */ /* DESCRIPTION */ /* */ -/* Loads the sample module twice, from two different addresses, lets it */ -/* misbehave both times, and reports what the hardware did about it. */ +/* Loads the sample module three times, lets it misbehave every time, */ +/* and reports what the hardware did about it. */ /* */ /* The result this example exists to produce is the fault. A module */ /* that starts and runs proves the loader works; a module that is */ @@ -61,6 +61,35 @@ /* All of which is checked on the console, with no help from a debugger */ /* and without the manager needing to know one symbol of the module. */ /* */ +/* THEN A THIRD PASS, which faults the other way. The two passes above */ +/* make the module read an address it does not own: a data abort, */ +/* reported through DFSR and DFAR. A module can equally leave its code */ +/* region, which is a prefetch abort reported through IFSR and IFAR and */ +/* arrives at the handler by a different vector. Both halves of the */ +/* port's fault path are therefore exercised, and neither is inferred */ +/* from the other. */ +/* */ +/* Which violation a pass commits is chosen here, not in the module: the */ +/* manager writes the application-defined module ID in the instance */ +/* after loading it, and the manager passes that word to the module's */ +/* start thread. So one blob covers both cases and the manager needs no */ +/* symbol of the module to select between them. */ +/* */ +/* The third pass runs after the first two have been unloaded, which is */ +/* the other half of what this file demonstrates: a module fault must */ +/* leave the manager able to load and run the next module. A fault that */ +/* kills the manager is not isolation, and a fault that leaves the MPU */ +/* in a state where the next load misbehaves is not either. */ +/* */ +/* AND THE NOTIFICATION IS CHECKED, not merely printed. The manager */ +/* registers a fault-notify callback, and every pass must see it run */ +/* exactly once with the faulting thread and the right module instance. */ +/* That path used to be dead on this port -- the shared fault handler */ +/* terminates the thread before calling the hook, which only returns if */ +/* the port's abort vector tells the kernel it is inside an exception, */ +/* and this port's did not. It does now, so the hook is a result rather */ +/* than a known defect. */ +/* */ /* What the module image is and where it comes from: it is linked into */ /* this application as a separate section and loaded in place, so */ /* nothing is copied and no filesystem or download path is needed. */ @@ -107,6 +136,16 @@ extern unsigned char __module_stage_end__; #define MODULE_FORBIDDEN_ADDRESS 0x31780000UL +/* Which violation a pass tells the module to commit. Written into the module + instance's application-defined ID after the load and before the start, because + that word is what the manager hands the module's start thread. The high half + is the fingerprint the preamble ships; only the low byte selects the test, so a + module started without this still runs the data-abort case. */ + +#define MODULE_ID_BASE 0x52520000UL +#define MODULE_TEST_DATA_ABORT 0x00000001UL +#define MODULE_TEST_PREFETCH_ABORT 0x00000002UL + /* Memory the manager hands out to modules: object memory, and the data region a module's own variables live in. It has to be outside every region a module is given, or a module could reach another module's data. */ @@ -139,11 +178,13 @@ static unsigned char module_pool[MODULE_POOL_SIZE] static unsigned char module_object_pool[MODULE_OBJECT_POOL_SIZE] __attribute__((aligned(64))); -/* One instance per pass. Both are loaded at once for part of the run: pass 1 is - stopped but still holds its data allocation while pass 2 loads, which is what - pushes pass 2's data base somewhere different. */ +/* One instance per pass. Passes 1 and 2 are loaded at once for part of the run: + pass 1 is stopped but still holds its data allocation while pass 2 loads, which + is what pushes pass 2's data base somewhere different. Pass 3 runs after both + have been unloaded, which is deliberate -- see the header. */ -#define MODULE_PASSES 2U +#define MODULE_PASSES 3U +#define MODULE_RELOCATION_PASSES 2U static TXM_MODULE_INSTANCE demo_module[MODULE_PASSES]; @@ -154,6 +195,7 @@ static TXM_MODULE_INSTANCE demo_module[MODULE_PASSES]; typedef struct PASS_RESULT_STRUCT { CHAR *pass_name; + ULONG pass_test; /* MODULE_TEST_*: which abort is expected */ ULONG pass_load_status; ULONG pass_start_status; ULONG pass_stop_status; @@ -167,8 +209,19 @@ typedef struct PASS_RESULT_STRUCT ULONG pass_fault_r9; ULONG pass_dfsr; ULONG pass_dfar; + ULONG pass_ifsr; + ULONG pass_ifar; ULONG pass_spsr; ULONG pass_code_location; + + /* What the notify callback was told, against what it should have been told. + Recorded as plain words rather than pointers so the console can print them + and a mismatch names both values. */ + + ULONG pass_notify_thread; + ULONG pass_notify_instance; + ULONG pass_expect_thread; + ULONG pass_expect_instance; } PASS_RESULT; static PASS_RESULT pass_results[MODULE_PASSES]; @@ -179,30 +232,51 @@ static PASS_RESULT pass_results[MODULE_PASSES]; static volatile ULONG fault_count; static volatile ULONG fault_dfsr; static volatile ULONG fault_dfar; +static volatile ULONG fault_ifsr; +static volatile ULONG fault_ifar; static volatile ULONG fault_spsr; static volatile ULONG fault_code_location; +static volatile ULONG fault_notify_thread; +static volatile ULONG fault_notify_instance; static TX_THREAD report_thread; static unsigned char report_stack[2048] __attribute__((aligned(8))); +/* The two symbols the debug harness stops on, declared here because they have + external linkage and are called before they are defined -- MISRA C:2012 Rule + 8.4 wants a visible declaration either way. Both are defined below; see each + for why it exists rather than a line number in a loop. */ + +void manager_done(void); +void pass_done(void); + /**************************************************************************/ /* Fault notification. */ /* */ /* Called by the module manager after it has terminated the offending */ -/* thread. Records rather than prints: this runs in the fault path, and */ -/* the console is a polled driver that spins waiting for a transmit to */ -/* complete. */ +/* thread. Records rather than prints, for two reasons: this runs in the */ +/* fault path, where the console is a polled driver that spins waiting */ +/* for a transmit to complete -- and it runs in Abort mode on the Abort */ +/* stack, which is a kilobyte on this board and already carries the */ +/* terminate underneath this frame. A callback that printed would work */ +/* and would still be the wrong shape to copy. */ +/* */ +/* Its two arguments are the point of the hook, so they are recorded and */ +/* checked rather than discarded: an application is being told WHICH */ +/* thread and WHICH module faulted, and a callback that fires with the */ +/* wrong pair is no more use than one that never fires. */ /**************************************************************************/ static void module_fault_notify(TX_THREAD *thread_ptr, TXM_MODULE_INSTANCE *module_instance) { - (void) thread_ptr; - (void) module_instance; - fault_count++; + fault_notify_thread = (ULONG) thread_ptr; + fault_notify_instance = (ULONG) module_instance; fault_dfsr = _txm_module_manager_memory_fault_info.txm_module_manager_memory_fault_info_dfsr; fault_dfar = _txm_module_manager_memory_fault_info.txm_module_manager_memory_fault_info_dfar; + fault_ifsr = _txm_module_manager_memory_fault_info.txm_module_manager_memory_fault_info_ifsr; + fault_ifar = _txm_module_manager_memory_fault_info.txm_module_manager_memory_fault_info_ifar; fault_spsr = _txm_module_manager_memory_fault_info.txm_module_manager_memory_fault_info_spsr; fault_code_location = (ULONG) _txm_module_manager_memory_fault_info.txm_module_manager_memory_fault_info_code_location; } @@ -255,18 +329,21 @@ static void copy_bytes(unsigned char *destination, const unsigned char *source, takes it that way -- the manager stores the pointer in the instance and the API has never promised not to write through it. */ -static void run_one_pass(UINT index, CHAR *name, VOID *location) +static void run_one_pass(UINT index, CHAR *name, VOID *location, ULONG test) { PASS_RESULT *result = &pass_results[index]; TXM_MODULE_INSTANCE *instance = &demo_module[index]; unsigned int waited; result -> pass_name = name; + result -> pass_test = test; /* Cleared before the module starts, so a fault counted here belongs to this pass and not the previous one. */ - fault_count = 0UL; + fault_count = 0UL; + fault_notify_thread = 0UL; + fault_notify_instance = 0UL; result -> pass_load_status = (ULONG) txm_module_manager_in_place_load(instance, name, location); @@ -275,6 +352,19 @@ static void run_one_pass(UINT index, CHAR *name, VOID *location) return; } + /* Which violation this pass provokes. Written after the load, which is what + filled the field in from the preamble, and before the start, which is what + hands it to the module's start thread. */ + + instance -> txm_module_instance_application_module_id = MODULE_ID_BASE | test; + + /* What the notify callback must be told, recorded before the module runs so + the comparison afterwards is against an expectation and not against + whatever the callback happened to write. */ + + result -> pass_expect_thread = (ULONG) &(instance -> txm_module_instance_start_stop_thread); + result -> pass_expect_instance = (ULONG) instance; + /* Read from the instance rather than computed here. Where the code ended up is what the manager decided, and the whole point of the comparison at the end is to check the module against the manager's own numbers. */ @@ -292,27 +382,21 @@ static void run_one_pass(UINT index, CHAR *name, VOID *location) return; } - /* Wait for the abort vector to capture a fault belonging to THIS pass, and - not for the notify callback to be told about one. - - Those are different events, and on this port only the first of them - happens. _txm_module_manager_memory_fault_handler terminates the - offending thread and only then calls the notify hook, and - _tx_thread_terminate does not return when the thread it is terminating is - the one running -- which it always is here. So the hook, though correctly - registered, is never reached, fault_count stays 0, and a sample that waits - on it waits out its whole timeout and then declares that no fault - occurred. That is what the first version of this file did, and it - reported FAIL for a module that had been stopped exactly as intended. - See the fault-path item in the task list; the handler is byte-for-byte - identical to the cortex_m33, a7 and m7 ports, so the assumption being - violated is upstream's rather than this port's. - - The fault info itself is filled in by the port's own capture code before - any of that, so it is the evidence that actually arrives. Attribution is - by thread pointer: each pass has its own module instance and therefore its - own start thread, so a captured fault naming this pass's thread cannot be - a leftover from the previous pass. */ + /* Wait for the abort vector to capture a fault belonging to THIS pass. + + The captured fault info is waited on rather than the notify callback, even + though the callback now works. The capture happens in the abort vector + before anything else runs, so it is the earliest and most direct evidence + that a fault occurred; the callback is a later consequence of the same + event, and this file checks it separately. Waiting on the earlier of the + two means a regression in the callback path shows up as "notified 0" next + to a captured fault, rather than as "no fault occurred" -- which is the + opposite of the truth and is exactly what an earlier version of this file + reported. + + Attribution is by thread pointer: each pass has its own module instance and + therefore its own start thread, so a captured fault naming this pass's + thread cannot be a leftover from the previous pass. */ waited = 0U; while ((_txm_module_manager_memory_fault_info.txm_module_manager_memory_fault_info_thread_ptr @@ -329,21 +413,30 @@ static void run_one_pass(UINT index, CHAR *name, VOID *location) result -> pass_captured = 1UL; result -> pass_dfsr = _txm_module_manager_memory_fault_info.txm_module_manager_memory_fault_info_dfsr; result -> pass_dfar = _txm_module_manager_memory_fault_info.txm_module_manager_memory_fault_info_dfar; + result -> pass_ifsr = _txm_module_manager_memory_fault_info.txm_module_manager_memory_fault_info_ifsr; + result -> pass_ifar = _txm_module_manager_memory_fault_info.txm_module_manager_memory_fault_info_ifar; result -> pass_spsr = _txm_module_manager_memory_fault_info.txm_module_manager_memory_fault_info_spsr; result -> pass_fault_r9 = _txm_module_manager_memory_fault_info.txm_module_manager_memory_fault_info_r9; result -> pass_code_location = (ULONG) _txm_module_manager_memory_fault_info.txm_module_manager_memory_fault_info_code_location; } - /* Recorded for what it is worth, which is currently a report on the notify - path rather than on the module. */ + /* What the notify callback saw. Read after the wait above, so a callback + that ran is recorded and one that did not shows as zero. */ - result -> pass_faults = fault_count; + result -> pass_faults = fault_count; + result -> pass_notify_thread = fault_notify_thread; + result -> pass_notify_instance = fault_notify_instance; /* Stopped, not unloaded. The fault terminated the module's start thread but left the module STARTED, and unload refuses anything that is not LOADED or STOPPED -- so this is required, not tidiness. */ result -> pass_stop_status = (ULONG) txm_module_manager_stop(instance); + + /* Still loaded, so the module's own memory is still where this pass's data + base says it is. See pass_done. */ + + pass_done(); } @@ -353,6 +446,16 @@ static void report_one_pass(const PASS_RESULT *result) linflexd_puts(result -> pass_name); linflexd_puts(" ---\n"); + linflexd_puts(" expected abort = "); + if (result -> pass_test == MODULE_TEST_PREFETCH_ABORT) + { + linflexd_puts("prefetch (IFSR/IFAR)\n"); + } + else + { + linflexd_puts("data (DFSR/DFAR)\n"); + } + put_field(" load status = ", result -> pass_load_status); put_field(" start status = ", result -> pass_start_status); put_field(" stop status = ", result -> pass_stop_status); @@ -363,12 +466,26 @@ static void report_one_pass(const PASS_RESULT *result) put_field(" data base (r9) = ", result -> pass_data_base); put_field(" fault captured = ", result -> pass_captured); put_field(" notify callbacks = ", result -> pass_faults); + put_field(" notified thread = ", result -> pass_notify_thread); + put_field(" should be = ", result -> pass_expect_thread); + put_field(" notified module = ", result -> pass_notify_instance); + put_field(" should be = ", result -> pass_expect_instance); put_field(" r9 at the fault = ", result -> pass_fault_r9); put_field(" DFSR = ", result -> pass_dfsr); put_field(" DFAR = ", result -> pass_dfar); + put_field(" IFSR = ", result -> pass_ifsr); + put_field(" IFAR = ", result -> pass_ifar); put_field(" SPSR = ", result -> pass_spsr); put_field(" faulting pc = ", result -> pass_code_location); - put_field(" pc - code base = ", result -> pass_code_location - result -> pass_code_start); + + /* Meaningful for a data abort, where the module faulted inside its own code. + A prefetch abort faults ON the address it branched to, so the faulting pc + is outside the module and the difference is not an offset into it. */ + + if (result -> pass_test != MODULE_TEST_PREFETCH_ABORT) + { + put_field(" pc - code base = ", result -> pass_code_location - result -> pass_code_start); + } } @@ -391,6 +508,31 @@ __attribute__((noinline)) void manager_done(void) } +/**************************************************************************/ +/* Where the debugger stops after each pass. */ +/* */ +/* A module's data is read back by the harness, not by the manager: the */ +/* manager deliberately knows no symbol of the module, so it cannot find */ +/* module_progress, while a debugger can compute its offset from the */ +/* module's ELF and add it to the data base this pass recorded. */ +/* */ +/* But it has to read it WHILE THIS PASS STILL HOLDS THAT MEMORY. The */ +/* byte pool reuses a freed block, so once a later pass has loaded, an */ +/* earlier pass's data base points at the later pass's data -- and reading */ +/* every pass at the end of the run reports the last writer's progress for */ +/* all of them. That is not a hypothetical: pass 3 loads after passes 1 */ +/* and 2 are unloaded and lands exactly where pass 1 was. */ +/* */ +/* So this exists to be broken on, once per pass, after the pass has */ +/* faulted and been stopped and before anything is unloaded. */ +/**************************************************************************/ + +__attribute__((noinline)) void pass_done(void) +{ + __asm__ volatile("nop"); +} + + /**************************************************************************/ /* Reporting. */ /**************************************************************************/ @@ -487,17 +629,33 @@ static void manager_entry(ULONG input) this thread and cannot be open while a module's own regions are. */ linflexd_puts("M4 pass 1, at the linked address\n"); - run_one_pass(0U, "linked address", (VOID *) &__module_image_start__); + run_one_pass(0U, "linked address, data abort", (VOID *) &__module_image_start__, + MODULE_TEST_DATA_ABORT); linflexd_puts("M5 pass 2, relocated\n"); - run_one_pass(1U, "relocated", (VOID *) &__module_stage_start__); + run_one_pass(1U, "relocated, data abort", (VOID *) &__module_stage_start__, + MODULE_TEST_DATA_ABORT); - /* Both passes are done with their memory now. */ + /* Both relocation passes are done with their memory now. Unloaded here + rather than at the end, so that the pass below is a load that follows + two faults and two unloads -- which is the state a manager is in after + a module has misbehaved, and the state the next load has to work in. */ - for (i = 0U; i < MODULE_PASSES; i++) + for (i = 0U; i < MODULE_RELOCATION_PASSES; i++) { (void) txm_module_manager_unload(&demo_module[i]); } + + /* The other abort type, from the staging area so that it is also a + relocated module: the address arriving in IFAR came out of the module's + own initialised data through its rebased GOT, exactly as DFAR does + above. */ + + linflexd_puts("M6 pass 3, prefetch abort\n"); + run_one_pass(2U, "relocated, prefetch abort", (VOID *) &__module_stage_start__, + MODULE_TEST_PREFETCH_ABORT); + + (void) txm_module_manager_unload(&demo_module[2]); } /* ------------------------------------------------------------------ @@ -589,27 +747,67 @@ static void manager_entry(ULONG input) linflexd_puts(": faulted, but not from User mode\n"); failures++; } - else if (result -> pass_dfar != MODULE_FORBIDDEN_ADDRESS) + else if (((result -> pass_test == MODULE_TEST_PREFETCH_ABORT) ? + result -> pass_ifar : result -> pass_dfar) + != MODULE_FORBIDDEN_ADDRESS) { /* The module got this address out of its own initialised data, through its rebased GOT. Any other value means it faulted somewhere unintended -- most likely on its own data, which is what - a bad rebase looks like. */ + a bad rebase looks like. + + Which register carries it depends on the abort: a data abort + reports the address it tried to touch in DFAR, a prefetch abort + reports the address it tried to fetch in IFAR. Checking the wrong + one of the two passes for a stale value left by an earlier fault, + which is why this is selected on the expected type rather than on + whichever register happens to be non-zero. */ linflexd_puts("FAIL "); linflexd_puts(result -> pass_name); linflexd_puts(": faulted at the wrong address, so .data or the GOT is wrong\n"); failures++; } + else if (result -> pass_faults != 1UL) + { + /* The notify callback is a public API of the module manager, so it is + checked and not merely reported. Zero here with a captured fault + above means the fault path reached the hardware's evidence and + never reached the application: on this port that used to be the + case for every fault, because the shared fault handler terminates + the offending thread before calling the hook and the terminate only + returns if the abort vector has told the kernel it is inside an + exception. More than one means a single violation was reported + twice. */ + + linflexd_puts("FAIL "); + linflexd_puts(result -> pass_name); + linflexd_puts(": the fault-notify callback did not run exactly once\n"); + failures++; + } + else if ((result -> pass_notify_thread != result -> pass_expect_thread) || + (result -> pass_notify_instance != result -> pass_expect_instance)) + { + /* Being told that something faulted is not the service; being told + which thread and which module is. */ + + linflexd_puts("FAIL "); + linflexd_puts(result -> pass_name); + linflexd_puts(": notified about the wrong thread or module\n"); + failures++; + } else { linflexd_puts("PASS "); linflexd_puts(result -> pass_name); - linflexd_puts(": faulted in User mode at the forbidden address\n"); + linflexd_puts(": faulted in User mode at the forbidden address, and was reported\n"); } } - /* And the two passes against each other, which is the relocation result. */ + /* And the first two passes against each other, which is the relocation + result. The third is not in this comparison: it faults on the address it + branched to rather than inside its own code, so its faulting pc is not an + offset into the module and there is nothing to compare. */ offset_0 = pass_results[0].pass_code_location - pass_results[0].pass_code_start; offset_1 = pass_results[1].pass_code_location - pass_results[1].pass_code_start; @@ -643,14 +841,20 @@ static void manager_entry(ULONG input) } } - /* Reported apart from the verdict above, because it is a known defect in the - fault-notification path and not a property of this run. The module was - stopped either way -- that is what the captured fault says -- but an - application asking to be told about it is not being told. */ + /* Both abort types, stated as its own line. Each pass above already checked + its own registers; this says that between them the two vectors were both + taken, which is the claim a reader of the log wants to be able to make + without working out what each pass did. */ - if ((pass_results[0].pass_captured != 0UL) && (pass_results[0].pass_faults == 0UL)) + if ((pass_results[0].pass_captured != 0UL) && (pass_results[0].pass_dfsr != 0UL) && + (pass_results[2].pass_captured != 0UL) && (pass_results[2].pass_ifsr != 0UL)) { - linflexd_puts("KNOWN the fault was captured but the notify callback never ran\n"); + linflexd_puts("PASS both abort types: data through DFSR/DFAR, prefetch through IFSR/IFAR\n"); + } + else + { + linflexd_puts("FAIL only one kind of abort was exercised\n"); + failures++; } put_field("\nfailures = ", (unsigned long) failures); diff --git a/ports_module/cortex_r52/gnu/example_build/s32z280_evb/tools/run_module_demo.gdb b/ports_module/cortex_r52/gnu/example_build/s32z280_evb/tools/run_module_demo.gdb index d180a6ff9..4d0c4b3fa 100644 --- a/ports_module/cortex_r52/gnu/example_build/s32z280_evb/tools/run_module_demo.gdb +++ b/ports_module/cortex_r52/gnu/example_build/s32z280_evb/tools/run_module_demo.gdb @@ -25,26 +25,45 @@ # WHAT A PASS LOOKS LIKE, and why it is a fault # # The sample module writes its own data, calls the kernel through SVC, and then -# deliberately reads an address the manager never granted it. The expected end -# state is therefore a data abort taken in User mode, captured by the abort -# vector, reported through the fault-notify callback, and terminating the module -# thread -- with the manager still alive to report it. A run with no fault is a -# FAILURE: it means a module reached kernel memory and was not stopped. +# deliberately violates its protection. The expected end state is therefore an +# abort taken in User mode, captured by the abort vector, reported through the +# fault-notify callback, and terminating the module thread -- with the manager +# still alive to report it. A run with no fault is a FAILURE: it means a module +# reached kernel memory and was not stopped. # -# module_progress bit 0x1 wrote and summed its own scratch expect SET -# bit 0x2 reached the kernel through SVC expect SET -# bit 0x4 about to touch the forbidden address expect SET -# bit 0x8 survived the forbidden access expect CLEAR +# module_progress bit 0x01 wrote and summed its own scratch expect SET +# bit 0x02 reached the kernel through SVC expect SET +# bit 0x04 about to READ the forbidden address data pass only +# bit 0x08 survived the forbidden read expect CLEAR +# bit 0x10 about to BRANCH to it prefetch pass only +# bit 0x20 survived the forbidden branch expect CLEAR # -# so module_progress == 0x7 exactly, and fault_count >= 1 with SPSR mode 0x10. +# so module_progress == 0x7 for a data-abort pass and 0x13 for the prefetch pass. # -# AND IT HAPPENS TWICE, from two different load addresses +# THREE PASSES, because one run proves neither relocation nor both abort types # # The module is position independent, so a single run at the address it was # linked for would prove nothing: the GOT rebase would map every address to -# itself and a rebase that did nothing would look the same. The sample therefore -# loads the same blob twice -- once where the linker put it, once from a staging -# area -- and this script checks both. +# itself and a rebase that did nothing would look the same. So passes 1 and 2 +# run the same blob from two addresses -- once where the linker put it, once from +# a staging area -- and this script compares them. +# +# Pass 3 faults the other way. Passes 1 and 2 read an address they do not own, +# which is a DATA abort reported through DFSR and DFAR. Pass 3 branches outside +# its code region, which is a PREFETCH abort reported through IFSR and IFAR and +# arrives at the handler by a different vector. It runs after passes 1 and 2 have +# been unloaded, so it also shows that a module fault leaves the manager able to +# load and run the next module. +# +# THE NOTIFY CALLBACK IS A CHECK NOW, not a note +# +# It used to be dead on this port: the shared fault handler terminates the +# offending thread and only then calls the hook, and _tx_thread_terminate returns +# for the running thread only if the abort vector has told the kernel it is inside +# an exception -- which this port's vector did not do. It does now, so every pass +# must see the callback run exactly once, with the faulting thread and the right +# module instance. Zero notifications next to a captured fault is a regression in +# that bracket and nothing else. # # Finding module_progress is no longer a matter of reading the module's ELF. A # relocated module's data lives wherever the manager allocated it, so the address @@ -52,6 +71,13 @@ # module's data segment). The first number is read from the module instance on # the target, the second from the module's ELF, and neither is a constant that # can be written down here. +# +# AND IT HAS TO BE READ WHILE THE PASS STILL OWNS THE MEMORY. The byte pool +# reuses freed blocks, so pass 3 -- which loads after passes 1 and 2 are unloaded +# -- lands exactly where pass 1 was. Reading all three at the end of the run +# therefore reports pass 3's progress as pass 1's, and pass 1 looks like it took +# the wrong branch. So this script breaks on pass_done once per pass and reads +# that pass's word there, before anything is unloaded. py _PROBE_IP = "s32dbg:192.168.50.238" py _SOC_NAME = "S32Z280" @@ -113,7 +139,35 @@ print("PC set to 0x%08X" % start) # # hbreak, not break: the code region is mapped read-only by the MPU, so a # software breakpoint would have to write to it. +gdb.execute("hbreak pass_done") gdb.execute("hbreak manager_done") + +# Each pass's module_progress, read at pass_done while that pass still holds its +# data. Kept in a global the reporting block below picks up; gdb's python runs in +# one persistent namespace, and the reader falls back to "skipped" rather than a +# traceback if that ever stops being true. +PROGRESS_BY_PASS = [] + +_offset = os.environ.get("S32Z280_MODULE_PROGRESS_OFFSET") +_offset = int(_offset, 0) if _offset else None + +for _i in range(3): + gdb.execute("continue") + if _offset is None: + PROGRESS_BY_PASS.append(None) + continue + _base = int(gdb.parse_and_eval("pass_results[%d].pass_data_base" % _i)) & 0xFFFFFFFF + if _base == 0: + # The pass never loaded, so there is no data base and nothing to read. + PROGRESS_BY_PASS.append(None) + else: + _addr = _base + _offset + PROGRESS_BY_PASS.append( + int(gdb.parse_and_eval("*(unsigned int *)0x%x" % _addr)) & 0xFFFFFFFF) + print("pass %d: module_progress 0x%08X at 0x%08X (read while loaded)" + % (_i + 1, PROGRESS_BY_PASS[-1], _addr)) + +# On to the report the manager prints for itself. gdb.execute("continue") end @@ -129,10 +183,18 @@ def ev(expr): failures = 0 -OWN_DATA, KERNEL_CALL, ATTEMPTED_STEAL, SURVIVED_STEAL = 0x1, 0x2, 0x4, 0x8 +OWN_DATA, KERNEL_CALL = 0x01, 0x02 +ATTEMPTED_STEAL, SURVIVED_STEAL = 0x04, 0x08 +ATTEMPTED_JUMP, SURVIVED_JUMP = 0x10, 0x20 FORBIDDEN = 0x31780000 MODE = {0x10: "User", 0x13: "Supervisor", 0x1A: "Hyp", 0x1F: "System"} +# Which violation each pass was told to commit, from the low byte of the module ID +# the manager writes into the instance before starting it. +TEST_DATA_ABORT, TEST_PREFETCH_ABORT = 0x1, 0x2 +PASSES = 3 +RELOCATION_PASSES = 2 + # The nominal offset of module_progress inside the module's data segment. Both # halves come from the module's own ELF and neither is an address the module ever # runs at -- see link_demo_module.lds. The run-time address is this offset plus @@ -146,13 +208,16 @@ else: print(" module_progress sits %d bytes into the module's data segment" % progress_offset) passes = [] -for i in range(2): +for i in range(PASSES): p = {} - for field in ("pass_load_status", "pass_start_status", "pass_stop_status", + for field in ("pass_test", "pass_load_status", "pass_start_status", "pass_stop_status", "pass_code_start", "pass_code_end", "pass_data_start", "pass_data_end", "pass_data_base", "pass_faults", "pass_captured", "pass_fault_r9", - "pass_dfsr", "pass_dfar", "pass_spsr", "pass_code_location"): + "pass_dfsr", "pass_dfar", "pass_ifsr", "pass_ifar", + "pass_spsr", "pass_code_location", + "pass_notify_thread", "pass_notify_instance", + "pass_expect_thread", "pass_expect_instance"): p[field] = ev("pass_results[%d].%s" % (i, field)) try: p["name"] = gdb.parse_and_eval("pass_results[%d].pass_name" % i).string() @@ -161,8 +226,11 @@ for i in range(2): passes.append(p) for i, p in enumerate(passes): + prefetch = (p["pass_test"] == TEST_PREFETCH_ABORT) print("") print(" --- pass %d: %s ---" % (i + 1, p["name"])) + print(" expected abort = %s" + % ("prefetch, through IFSR/IFAR" if prefetch else "data, through DFSR/DFAR")) print(" load / start / stop = 0x%08X / 0x%08X / 0x%08X" % (p["pass_load_status"], p["pass_start_status"], p["pass_stop_status"])) print(" code region = 0x%08X .. 0x%08X" @@ -185,31 +253,41 @@ for i, p in enumerate(passes): print(" *** FAIL: this module's code and data regions overlap") failures += 1 - if progress_offset is not None: + progress = globals().get("PROGRESS_BY_PASS", [None] * PASSES)[i] + if progress is None: + print(" module_progress = not read") + else: addr = p["pass_data_base"] + progress_offset - progress = rd(addr) - print(" module_progress = 0x%08X (at 0x%08X)" % (progress, addr)) - for bit, name, want_set in ((OWN_DATA, "wrote its own data", True), - (KERNEL_CALL, "reached the kernel through SVC", True), - (ATTEMPTED_STEAL, "attempted the forbidden access", True), - (SURVIVED_STEAL, "SURVIVED the forbidden access", False)): + print(" module_progress = 0x%08X (read at 0x%08X while this pass" + % (progress, addr)) + print(" still held that memory)") + + # Every bit is checked, including the ones belonging to the violation this + # pass was NOT told to commit: a module that took both routes, or the + # wrong one, is not the module this pass asked for and the result would + # not mean what the register checks below claim. + wanted = [(OWN_DATA, "wrote its own data", True), + (KERNEL_CALL, "reached the kernel through SVC", True), + (ATTEMPTED_STEAL, "attempted the forbidden read", not prefetch), + (SURVIVED_STEAL, "SURVIVED the forbidden read", False), + (ATTEMPTED_JUMP, "attempted the forbidden branch", prefetch), + (SURVIVED_JUMP, "SURVIVED the forbidden branch", False)] + for bit, name, want_set in wanted: got = bool(progress & bit) ok = (got == want_set) if not ok: failures += 1 - print(" 0x%X %-34s %-5s %s" + print(" 0x%02X %-33s %-5s %s" % (bit, name, "set" if got else "clear", "ok" if ok else "*** WRONG ***")) - if progress & SURVIVED_STEAL: - print(" ISOLATION FAILURE: the module read memory it was never granted.") - - # "captured" and "notified" are different events, and only the first of them - # happens on this port. The fault handler terminates the offending thread - # before calling the notify hook, and _tx_thread_terminate does not return - # when the thread being terminated is the running one -- which it always is - # here. So the hook never runs, and a check written against the callback - # counter reports "no fault" for a module that was stopped correctly. - # The abort vector's captured fault info is the evidence that arrives. + if progress & (SURVIVED_STEAL | SURVIVED_JUMP): + print(" ISOLATION FAILURE: the module reached memory it was never granted.") + + # "captured" and "notified" are different events: the abort vector records the + # fault registers before anything else runs, and the notify callback is a + # later consequence of the same fault. Both are required. Reporting them + # apart is what keeps a broken notify path from being read as "no fault + # occurred", which is the opposite of the truth. print(" fault captured = %d" % p["pass_captured"]) print(" notify callbacks = %d" % p["pass_faults"]) if p["pass_captured"] == 0: @@ -217,19 +295,53 @@ for i, p in enumerate(passes): print(" memory and was not stopped, or never got that far.") failures += 1 continue - if p["pass_faults"] == 0: - print(" KNOWN: captured but never notified -- the fault-notify path is") - print(" broken independently of anything this run exercises.") + + # The callback is a public API of the module manager, so its arguments are + # checked and not merely counted. Zero notifications beside a captured fault + # means the fault reached the hardware's evidence and never reached the + # application: on this port that is the system-state bracket in + # txm_module_manager_fault_capture.S having gone missing again. + if p["pass_faults"] != 1: + print(" *** FAIL: the notify callback ran %d times, not once." + % p["pass_faults"]) + failures += 1 + elif (p["pass_notify_thread"] != p["pass_expect_thread"] + or p["pass_notify_instance"] != p["pass_expect_instance"]): + print(" *** FAIL: notified about thread 0x%08X / module 0x%08X," + % (p["pass_notify_thread"], p["pass_notify_instance"])) + print(" expected thread 0x%08X / module 0x%08X" + % (p["pass_expect_thread"], p["pass_expect_instance"])) + failures += 1 + else: + print(" notified about = thread 0x%08X, module 0x%08X (both correct)" + % (p["pass_notify_thread"], p["pass_notify_instance"])) dfsr, dfar, spsr, pc = (p["pass_dfsr"], p["pass_dfar"], p["pass_spsr"], p["pass_code_location"]) - print(" DFSR = 0x%08X status 0x%02X WnR %d" - % (dfsr, dfsr & 0x3F, (dfsr >> 11) & 1)) + ifsr, ifar = p["pass_ifsr"], p["pass_ifar"] + + # Both pairs are printed for both kinds of pass, because the capture records + # both and the pair that does not belong to this fault is a leftover the + # hardware never cleared -- worth seeing, and worth not mistaking for a + # result. Which pair is meaningful is decided by the expected abort type, + # not by which of them looks plausible. + print(" DFSR = 0x%08X status 0x%02X WnR %d%s" + % (dfsr, dfsr & 0x3F, (dfsr >> 11) & 1, + "" if not prefetch else " (stale: this pass took a prefetch abort)")) print(" DFAR = 0x%08X" % dfar) + print(" IFSR = 0x%08X status 0x%02X%s" + % (ifsr, ifsr & 0x3F, + " (stale: this pass took a data abort)" if not prefetch else "")) + print(" IFAR = 0x%08X" % ifar) print(" SPSR = 0x%08X mode 0x%02X (%s)" % (spsr, spsr & 0x1F, MODE.get(spsr & 0x1F, "?"))) print(" faulting pc = 0x%08X" % pc) - print(" pc - code base = 0x%08X" % ((pc - p["pass_code_start"]) & 0xFFFFFFFF)) + if prefetch: + # The fault is on the fetch AT the branch target, so the faulting pc is + # the target and not a place inside the module. + print(" (the faulting pc is the branch target, outside the module)") + else: + print(" pc - code base = 0x%08X" % ((pc - p["pass_code_start"]) & 0xFFFFFFFF)) print(" r9 at the fault = 0x%08X" % p["pass_fault_r9"]) # r9 is the module's PIC base, seeded by the manager's thread stack build from @@ -245,18 +357,47 @@ for i, p in enumerate(passes): print(" a module being stopped at the boundary.") failures += 1 - # DFAR is the strongest single check in this script. The module obtained - # this address by reading one of its own initialised globals through the + # The faulting address is the strongest single check in this script. The + # module obtained it by reading one of its own initialised globals through the # rebased GOT, so the right value here means the GOT was rewritten AND .data - # was copied out of the image. A wrong value usually means the module - # faulted on its own data instead, which is what a bad rebase looks like. - if dfar != FORBIDDEN: - print(" *** FAIL: expected DFAR 0x%08X, the address the module was told" % FORBIDDEN) + # was copied out of the image. A wrong value usually means the module faulted + # on its own data instead, which is what a bad rebase looks like. + # + # A data abort reports it in DFAR, a prefetch abort in IFAR. Both registers + # are sticky, so checking the pair that does not belong to this fault would + # pass on a value some earlier fault left behind -- including the boot probes, + # which take a prefetch abort of their own before ThreadX starts. + reg_name, reg_value = ("IFAR", ifar) if prefetch else ("DFAR", dfar) + if reg_value != FORBIDDEN: + print(" *** FAIL: expected %s 0x%08X, the address the module was told" + % (reg_name, FORBIDDEN)) print(" not to touch. A different one means .data was not") print(" copied or the GOT was not rebased.") failures += 1 + # A prefetch abort with nothing in IFSR would mean the vector reached the + # capture without the core having recorded an instruction fault, which is the + # one way this pass could look right and be meaningless. + if prefetch and (ifsr & 0x3F) == 0: + print(" *** FAIL: IFSR reports no instruction fault status") + failures += 1 + # --- the relocation result, which is the comparison between the passes -------- +print("") +print(" ===== both abort types =====") +data_passes = [q for q in passes if q["pass_test"] != TEST_PREFETCH_ABORT + and q["pass_captured"] and (q["pass_dfsr"] & 0x3F)] +pabt_passes = [q for q in passes if q["pass_test"] == TEST_PREFETCH_ABORT + and q["pass_captured"] and (q["pass_ifsr"] & 0x3F)] +print(" data aborts captured = %d" % len(data_passes)) +print(" prefetch aborts captured = %d" % len(pabt_passes)) +if not data_passes or not pabt_passes: + print(" *** FAIL: only one kind of abort was exercised, so half of the") + print(" port's fault path never ran") + failures += 1 +else: + print(" PASS: both vectors were taken and both register pairs reported") + print("") print(" ===== relocation =====") a, b = passes[0], passes[1] @@ -294,7 +435,7 @@ except gdb.error as e: print(" boot_stage = unavailable (%s)" % e) print("") if failures == 0: - print("===== PASS: relocated, and stopped at the boundary, both times =====") + print("===== PASS: relocated, stopped at the boundary both ways, and reported =====") else: print("===== FAIL: %d check(s) wrong =====" % failures) end diff --git a/ports_module/cortex_r52/gnu/example_build/s32z280_evb/tools/run_module_demo.sh b/ports_module/cortex_r52/gnu/example_build/s32z280_evb/tools/run_module_demo.sh index d9cac3d2b..eb9be3c04 100755 --- a/ports_module/cortex_r52/gnu/example_build/s32z280_evb/tools/run_module_demo.sh +++ b/ports_module/cortex_r52/gnu/example_build/s32z280_evb/tools/run_module_demo.sh @@ -47,7 +47,7 @@ done # What is wanted is an OFFSET, not an address. The module is position # independent: the address nm reports is a nominal one from a segment the module # never runs at, and its real data lives wherever the manager allocated it -- a -# different place in each of the two passes. So the offset of module_progress +# different place in each pass. So the offset of module_progress # within the module's data segment is computed here, and the gdb script adds it # to each pass's data base, which it reads off the target. PROGRESS_SYM=$(nm "$MODULE_ELF" | awk '$3=="module_progress"{print $1}') diff --git a/ports_module/cortex_r52/gnu/module_manager/src/txm_module_manager_fault_capture.S b/ports_module/cortex_r52/gnu/module_manager/src/txm_module_manager_fault_capture.S index 02cc58fed..48090ff33 100644 --- a/ports_module/cortex_r52/gnu/module_manager/src/txm_module_manager_fault_capture.S +++ b/ports_module/cortex_r52/gnu/module_manager/src/txm_module_manager_fault_capture.S @@ -42,6 +42,38 @@ @/* abort is also taken in Abort mode, with its own banked sp and lr, so */ @/* C cannot be entered until a stack is known good. */ @/* */ +@/* THE CONTRACT WITH THE SHARED C HANDLER */ +@/* */ +@/* _txm_module_manager_memory_fault_handler is common to every module */ +@/* port and it terminates the faulting thread and then calls the */ +@/* application's fault-notify callback. For the second half of that to */ +@/* happen, _tx_thread_terminate has to RETURN -- and terminating the */ +@/* running thread only returns if the kernel believes it is inside an */ +@/* exception. _tx_thread_terminate ends in */ +@/* _tx_thread_system_preempt_check, which calls */ +@/* _tx_thread_system_return whenever _tx_thread_system_state and */ +@/* _tx_thread_preempt_disable are both zero; on this architecture that */ +@/* switches context immediately and never comes back. */ +@/* */ +@/* So this routine owes the handler three things, exactly as the */ +@/* Cortex-A7 module port's abort vector does: */ +@/* */ +@/* 1. _tx_thread_system_state incremented across the call, so the */ +@/* terminate returns instead of scheduling from Abort mode. */ +@/* 2. _tx_thread_current_ptr cleared afterwards -- the thread it */ +@/* names is terminated and the scheduler must not save into it. */ +@/* 3. an exception return into _tx_thread_schedule in System mode, */ +@/* which is where the next thread is chosen. */ +@/* */ +@/* Without step 1 the notify callback is unreachable, and the */ +@/* _tx_thread_system_return that runs in its place saves a solicited */ +@/* frame on the ABORT stack and writes that Abort-mode sp into the */ +@/* dead thread's stack pointer. It looks like it works, because the */ +@/* thread is never resumed -- but nothing gives the Abort stack back, */ +@/* so each module fault costs it about 56 bytes for the life of the */ +@/* run. The Cortex-M ports do not need step 1: there */ +@/* _tx_thread_system_return only pends PendSV and returns. */ +@/* */ @/**************************************************************************/ .arm @@ -53,6 +85,8 @@ .extern _txm_module_manager_memory_fault_info .extern _txm_module_manager_memory_fault_handler .extern _tx_thread_current_ptr + .extern _tx_thread_system_state + .extern _tx_thread_schedule @ Offsets into TXM_MODULE_MANAGER_MEMORY_FAULT_INFO. Checked against offsetof at @ compile time in txm_module_manager_offset_check.c. @@ -68,6 +102,18 @@ .equ FAULT_LR, 0x50 .equ FAULT_SPSR, 0x54 +@ Processor mode and state bits. System mode is used for the return because +@ every kernel thread in this port runs in it, and it is the mode +@ _tx_thread_schedule expects to choose the next thread from. + + .equ MODE_MASK, 0x1F + .equ SYS_MODE_BITS, 0x1F + .equ THUMB_BIT, 0x20 + +@ Bytes pushed on the Abort stack by the capture below, so it can be given back. + + .equ CAPTURE_FRAME_SIZE, 20 + @/**************************************************************************/ @/* Data abort: a module wrote or read outside a region it owns. */ @@ -107,6 +153,15 @@ _txm_module_manager_fault_common: STMDB sp!, {r0-r3, r12} +@ Tell the kernel it is inside an exception, before any kernel service runs. +@ This is what makes _tx_thread_terminate return to the C handler rather than +@ scheduling from Abort mode; see THE CONTRACT WITH THE SHARED C HANDLER above. + + LDR r0, =_tx_thread_system_state + LDR r1, [r0] + ADD r1, r1, #1 + STR r1, [r0] + LDR r0, =_txm_module_manager_memory_fault_info @ Which thread was running, and where it was. @@ -180,15 +235,51 @@ _txm_module_manager_fault_common: ADD r1, r0, #(FAULT_R0 + 16) STMIA r1, {r4-r11} -@ Hand over. The handler terminates the thread and notifies the application; -@ it does not return here. +@ Give the Abort stack back. The five words above were read out of it rather +@ than popped, because the copies needed them in place; nothing needs them now. +@ This routine does not return to the faulting code, so an unbalanced sp here +@ would never be corrected and every module fault would cost the Abort stack +@ twenty bytes until it ran out. + + ADD sp, sp, #CAPTURE_FRAME_SIZE + +@ Hand over. The handler terminates the offending thread and then calls the +@ application's fault-notify callback, and it DOES return here -- the +@ incremented system state above is what makes that true. BL _txm_module_manager_memory_fault_handler -@ Not reached. The handler terminates the offending thread and the scheduler -@ runs something else, so it does not come back. If it ever did, returning -@ would resume the instruction that faulted and fault again immediately, for -@ ever -- so stop here instead, where a debugger can see why. +@ Out of the exception. Balanced against the increment above. -__txm_module_fault_unexpected_return: - B __txm_module_fault_unexpected_return + LDR r0, =_tx_thread_system_state + LDR r1, [r0] + SUB r1, r1, #1 + STR r1, [r0] + +@ The current thread is terminated, so there is no context to save into it and +@ the scheduler must not try. Clearing the pointer is how this port says "no +@ thread is running", the same state _tx_thread_system_return leaves behind. + + MOV r1, #0 + LDR r0, =_tx_thread_current_ptr + STR r1, [r0] + +@ Return into the scheduler rather than to the faulting instruction, which +@ would fault again immediately and for ever. An exception return, not a +@ branch: it leaves Abort mode properly and restores the interrupt mask the +@ faulting context had, where a branch would run the scheduler in Abort mode +@ on the Abort stack. +@ +@ SPSR is rewritten for the mode this port's kernel threads run in, with the +@ Thumb bit cleared because _tx_thread_schedule is A32. The value recorded in +@ the fault info was stored before this, so what an application reads is still +@ the mode the module faulted in. + + MRS r0, SPSR + BIC r0, r0, #MODE_MASK + ORR r0, r0, #SYS_MODE_BITS + BIC r0, r0, #THUMB_BIT + MSR SPSR_cxsf, r0 + + LDR lr, =_tx_thread_schedule + SUBS pc, lr, #0 diff --git a/ports_module/cortex_r52/gnu/module_manager/src/txm_module_manager_memory_fault_handler.c b/ports_module/cortex_r52/gnu/module_manager/src/txm_module_manager_memory_fault_handler.c index 03e92d696..cac30d2fb 100644 --- a/ports_module/cortex_r52/gnu/module_manager/src/txm_module_manager_memory_fault_handler.c +++ b/ports_module/cortex_r52/gnu/module_manager/src/txm_module_manager_memory_fault_handler.c @@ -20,6 +20,8 @@ /**************************************************************************/ /**************************************************************************/ +// Some portions generated by Claude Code (Opus 5). + #define TX_SOURCE_CODE #include "tx_api.h" @@ -37,7 +39,28 @@ Data aborts and prefetch aborts both arrive here. A module can violate its protection either way: writing outside its data region, or branching outside its code region. Which pair of registers is meaningful depends on which it - was, and the fault info structure carries both. */ + was, and the fault info structure carries both. + + THIS FUNCTION RETURNS, AND THAT IS A REQUIREMENT ON THE ABORT VECTOR. + + It terminates the faulting thread and then calls the application's notify + callback. The second of those is only reached if _tx_thread_terminate returns, + and terminating the RUNNING thread returns only when the kernel believes it is + inside an exception: _tx_thread_terminate ends in + _tx_thread_system_preempt_check, which calls _tx_thread_system_return whenever + _tx_thread_system_state and _tx_thread_preempt_disable are both zero. On this + architecture _tx_thread_system_return switches context immediately and never + comes back, so the callback would be unreachable. + + The body below is therefore left exactly as the cortex_m33, cortex_a7 and + cortex_m7 ports have it, and the requirement is met where it belongs -- in + txm_module_manager_fault_capture.S, which increments _tx_thread_system_state + around this call, clears _tx_thread_current_ptr afterwards and returns into + the scheduler. The Cortex-M ports need no such bracket because their + _tx_thread_system_return only pends PendSV and returns. Anyone tempted to + reorder the two statements below to "fix" a notify callback that does not fire + should look at the abort vector first: the ordering here is upstream's and it + is not the defect. */ /* Define the user's fault notification callback function pointer. This is setup via the txm_module_manager_memory_fault_notify API. */ From 7636b0c79eef50bd690dfee968e9baa41a908ad3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20Desbiens?= Date: Thu, 20 Aug 2026 08:54:40 -0400 Subject: [PATCH 28/33] Masked the shared region's limit to the protection granule _txm_module_manager_external_memory_enable built PRLAR from (address + length - 1) without masking off the low six bits, where PRLAR carries RES0[5:4], AttrIndx[3:1] and EN[0] rather than address. For a granule-sized grant the limit ends in 0x3F, so those bits land on top of the attributes: AttrIndx comes out 7 instead of 0, selecting an unwritten MAIR byte and giving the shared region Device memory type, and both RES0 bits are set as well. Masking with TXM_MODULE_MPU_ADDRESS_MASK describes exactly the same inclusive end address, because the hardware reads the limit back as LIMIT concatenated with 0x3F. It is what txm_module_manager_mm_register_setup.c already does for the code and data regions; only this file was missing it. Found by using the function for the first time, in the FVP module example. The region still works well enough to pass a functional test with the wrong memory type, which is why the reasoning is now written down beside the mask. The S32Z280 module image is rebuilt by this change and its behaviour is not: that example grants no shared regions, so it never calls this function. Assisted-by: Claude Code (Opus 5) --- ...xm_module_manager_external_memory_enable.c | 23 ++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/ports_module/cortex_r52/gnu/module_manager/src/txm_module_manager_external_memory_enable.c b/ports_module/cortex_r52/gnu/module_manager/src/txm_module_manager_external_memory_enable.c index 76a7f06ce..f63b4c145 100644 --- a/ports_module/cortex_r52/gnu/module_manager/src/txm_module_manager_external_memory_enable.c +++ b/ports_module/cortex_r52/gnu/module_manager/src/txm_module_manager_external_memory_enable.c @@ -9,6 +9,8 @@ * SPDX-License-Identifier: MIT **************************************************************************/ +// Some portions generated by Claude Code (Opus 5). + /**************************************************************************/ /**************************************************************************/ @@ -42,7 +44,10 @@ /* DESCRIPTION */ /* */ /* This function creates an entry in the MPU table for a shared */ -/* memory space. The start_address must be 32-byte aligned. */ +/* memory space. The start_address must be aligned to the PMSAv8-R */ +/* protection granule, which is 64 bytes on Cortex-R52 -- the comment */ +/* inherited from the Armv8-M port says 32, and TXM_MODULE_MPU_ALIGNMENT*/ +/* below is what is actually enforced. */ /* */ /* INPUT */ /* */ @@ -140,8 +145,20 @@ ULONG shared_index; /* Set base address register with start address, sanitized attributes and execute never. */ module_instance -> txm_module_instance_mpu_registers[shared_index].txm_module_mpu_region_base_address = address | (attributes & TXM_MODULE_ATTRIBUTE_MASK) | TXM_MODULE_ATTRIBUTE_EXECUTE_NEVER; - /* Set the limit address (data start + length-1), attribute index, and enable bit. */ - module_instance -> txm_module_instance_mpu_registers[shared_index].txm_module_mpu_region_limit_address = (address + length-1) | TXM_MODULE_ATTRIBUTE_INDEX | TXM_MODULE_ATTRIBUTE_REGION_ENABLE; + /* Set the limit address (data start + length-1), attribute index, and enable bit. + + The limit is MASKED to the protection granule before the attributes are + ORed in, exactly as txm_module_manager_mm_register_setup.c does for the + code and data regions, and this is not cosmetic. PRLAR holds LIMIT[31:6] + with the low six bits carrying RES0[5:4], AttrIndx[3:1] and EN[0], and the + hardware reads the limit back as LIMIT concatenated with 0x3F -- so the + masked value describes exactly the same inclusive end address. Without + the mask, a granule-sized region ends in 0x3F and those bits land on top + of the attributes: AttrIndx comes out 7 instead of 0, which selects an + unwritten MAIR byte and gives the shared region Device memory type, and + the two RES0 bits are set as well. The region still works well enough to + pass a functional test, which is why this was worth writing down. */ + module_instance -> txm_module_instance_mpu_registers[shared_index].txm_module_mpu_region_limit_address = ((address + length - 1) & TXM_MODULE_MPU_ADDRESS_MASK) | TXM_MODULE_ATTRIBUTE_INDEX | TXM_MODULE_ATTRIBUTE_REGION_ENABLE; /* Keep track of shared memory address and length in module instance. */ module_instance -> txm_module_instance_shared_memory_address[module_instance -> txm_module_instance_shared_memory_count] = address; From 483c1731fea3d57718e8b792e1cf5e393cb7731b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20Desbiens?= Date: Thu, 20 Aug 2026 08:55:11 -0400 Subject: [PATCH 29/33] Added the module isolation regression on the Armv8-R AEM FVP AR2's deliverable was an example module and manager on the model plus an automated isolation regression. Until now the module port was silicon only and judged by a person reading a console through a debug probe, where AR1 has ctest. fvp_module.elf runs the same three passes the S32Z280 example does -- two data-abort passes from two different load addresses, then a prefetch-abort pass after both have been unloaded -- and reports one result line the existing run_fvp_test.py matches. ctest from the build root is now 6/6. The board support gains what a manager build needs and nothing else, all of it behind TXM_MODULE_MANAGER: entry.S routes the EL1 supervisor call to the module boundary and sends a User-mode abort of either kind to the manager's capture, and mpu.c programs the load window over the module area as region 16. link_module.lds ends the kernel's DRAM region where the module area begins, because a module's regions may not overlap a kernel one and PMSAv8-R has no priority to resolve it if they do. The count of MPU regions is asked of MPUIR rather than assumed, and a build that cannot supply seventeen refuses to enable protection at all. This matters more than it looks: the model reports 32 EL1 regions, which the Cortex-R52 TRM does not permit for this core -- MPUIR.DREGION is 16, 20 or 24 -- so the model is the generic AEMv8-R and is strictly more permissive than every real part. A green run here cannot tell you the region budget fits silicon, and a legal 16-region R52 cannot run this port at all. Both facts are printed by the image and stated in its header rather than left to be inferred from a pass. How the test knows what the module achieved, with no debugger in reach: the FVP exposes an Iris server and no GDB stub, so the harness that reads the module's progress out of its data area on silicon has no counterpart here. The manager instead grants each pass a shared MPU region over one granule at a fixed address and the module records its progress there. That is what lets this image fail on MODULE_PROGRESS_SURVIVED_STEAL and _SURVIVED_JUMP by name, rather than only on the absence of a fault, and it is the first exercise this port has given txm_module_manager_external_memory_enable. Verified on FVP_BaseR_AEMv8R 11.32.19. Six of six ctest green in 8.5 s; the module image reports failures = 0 with all three passes faulting in User mode at the forbidden address, one correct notification each, and equal fault offsets from two different code bases. Checked negatively as well: pointing the module at an address it is allowed to touch makes it survive, and the run then prints "the module SURVIVED its violation -- it is not protected", reports MODULE RESULT: FAILED and exits non-zero. AR1 alone still 5/5 with the module option off, and the S32Z280 example builds clean, 596 targets and no warnings. The two module examples duplicate their sample, preamble, blob wrapper and module link script rather than sharing them. That is deliberate: the two board examples are kept independent so silicon bring-up cannot perturb the FVP regression, and the copies are structurally identical so diff is the tool for telling whether they have drifted. Assisted-by: Claude Code (Opus 5) --- .../fvp_baser_aemv8r/CMakeLists.txt | 292 +++++ .../example_build/fvp_baser_aemv8r/cache.c | 149 +++ .../example_build/fvp_baser_aemv8r/cache.h | 72 ++ .../example_build/fvp_baser_aemv8r/entry.S | 51 + .../gnu/example_build/fvp_baser_aemv8r/mpu.c | 102 ++ .../gnu/example_build/fvp_baser_aemv8r/mpu.h | 50 + .../example_build/fvp_baser_aemv8r/platform.h | 40 + .../example_build/s32z280_evb/CMakeLists.txt | 9 + .../fvp_baser_aemv8r/link_demo_module.lds | 208 ++++ .../fvp_baser_aemv8r/link_module.lds | 275 +++++ .../fvp_baser_aemv8r/module_blob.S | 69 ++ .../fvp_baser_aemv8r/sample_threadx_module.c | 219 ++++ .../sample_threadx_module_manager.c | 1018 +++++++++++++++++ .../fvp_baser_aemv8r/txm_module_preamble.S | 137 +++ 14 files changed, 2691 insertions(+) create mode 100644 ports/cortex_r52/gnu/example_build/fvp_baser_aemv8r/cache.c create mode 100644 ports/cortex_r52/gnu/example_build/fvp_baser_aemv8r/cache.h create mode 100644 ports_module/cortex_r52/gnu/example_build/fvp_baser_aemv8r/link_demo_module.lds create mode 100644 ports_module/cortex_r52/gnu/example_build/fvp_baser_aemv8r/link_module.lds create mode 100644 ports_module/cortex_r52/gnu/example_build/fvp_baser_aemv8r/module_blob.S create mode 100644 ports_module/cortex_r52/gnu/example_build/fvp_baser_aemv8r/sample_threadx_module.c create mode 100644 ports_module/cortex_r52/gnu/example_build/fvp_baser_aemv8r/sample_threadx_module_manager.c create mode 100644 ports_module/cortex_r52/gnu/example_build/fvp_baser_aemv8r/txm_module_preamble.S diff --git a/ports/cortex_r52/gnu/example_build/fvp_baser_aemv8r/CMakeLists.txt b/ports/cortex_r52/gnu/example_build/fvp_baser_aemv8r/CMakeLists.txt index c8339fa70..41510a620 100644 --- a/ports/cortex_r52/gnu/example_build/fvp_baser_aemv8r/CMakeLists.txt +++ b/ports/cortex_r52/gnu/example_build/fvp_baser_aemv8r/CMakeLists.txt @@ -303,6 +303,285 @@ if(TX_R52_CONSOLE_PL011) endforeach() endif() +# --------------------------------------------------------------------------- +# ThreadX modules: the module manager, with a sample module linked into the +# module area and loaded in place from there. +# +# This is the AR2 deliverable that the S32Z280 example cannot be: the silicon +# module demonstration is judged by a person reading a console through a debug +# probe, and this one is judged by ctest. The image below is the same port +# under the same three passes, reporting one result line the runner matches. +# +# This target deliberately does NOT link the threadx library's port assembly. +# The module port carries its own copies of the scheduler, context restore and +# stack build, because the region switch happens in the scheduler and a module +# thread starts in a different processor mode. Linking both would give two +# definitions of every one of them. +# +# Guarded by an option, off by default, because it is the only target that needs +# TXM_MODULE_MANAGER in entry.S and the only one that links common_modules. +# --------------------------------------------------------------------------- + +option(TX_R52_BUILD_FVP_MODULE_EXAMPLE + "Build the Armv8-R AEM FVP ThreadX module manager example" OFF) + +if(TX_R52_BUILD_FVP_MODULE_EXAMPLE) + + set(MOD_DIR ${CMAKE_SOURCE_DIR}/ports_module/cortex_r52/gnu) + set(MOD_FVP ${MOD_DIR}/example_build/fvp_baser_aemv8r) + + # --------------------------------------------------------------------- + # A second ThreadX library, built with the module port's headers. + # + # This is not optional and not a convenience. The module port's tx_port.h + # adds the owning module instance to TX_QUEUE, TX_SEMAPHORE, + # TX_EVENT_FLAGS_GROUP and TX_TIMER, and the module fields to TX_THREAD. + # Measured: TX_QUEUE is 68 bytes against the base port's 60, TX_SEMAPHORE 40 + # against 32, TX_THREAD 236 against 184. A kernel compiled against the base + # port and a manager compiled against this one disagree about every object, + # and they link without complaint, because C linking does not compare struct + # layouts. The result would be memory corruption at run time with nothing in + # the build to hint at it. + # + # Guarded by if(NOT TARGET) because the S32Z280 module example defines the + # same library from its own directory, and the two example options can be on + # at once. The definitions are identical -- both describe the module port, + # not a board -- so whichever directory CMake reaches first may create it. + # --------------------------------------------------------------------- + + if(NOT TARGET threadx_module) + + file(GLOB TX_COMMON_SRC ${CMAKE_SOURCE_DIR}/common/src/*.c) + + set(TX_R52_PORT_SRC ${CMAKE_SOURCE_DIR}/ports/cortex_r52/gnu/src) + + # The base port's assembly is reused except for the five files the module + # port replaces, and tx_port_offset_check.c is included deliberately: if + # the module headers moved any offset that assembly depends on, that check + # is what says so. + add_library(threadx_module STATIC EXCLUDE_FROM_ALL + ${TX_COMMON_SRC} + ${TX_R52_PORT_SRC}/tx_port_offset_check.c + ${TX_R52_PORT_SRC}/tx_thread_fiq_context_restore.S + ${TX_R52_PORT_SRC}/tx_thread_fiq_context_save.S + ${TX_R52_PORT_SRC}/tx_thread_fiq_nesting_end.S + ${TX_R52_PORT_SRC}/tx_thread_fiq_nesting_start.S + ${TX_R52_PORT_SRC}/tx_thread_interrupt_control.S + ${TX_R52_PORT_SRC}/tx_thread_interrupt_disable.S + ${TX_R52_PORT_SRC}/tx_thread_interrupt_restore.S + ${TX_R52_PORT_SRC}/tx_thread_irq_nesting_end.S + ${TX_R52_PORT_SRC}/tx_thread_irq_nesting_start.S + ${TX_R52_PORT_SRC}/tx_thread_vectored_context_save.S + ${TX_R52_PORT_SRC}/tx_timer_interrupt.S + ) + + target_include_directories(threadx_module PUBLIC + ${MOD_DIR}/inc + ${CMAKE_SOURCE_DIR}/common/inc + ${CMAKE_SOURCE_DIR}/common_modules/module_manager/inc + ${CMAKE_SOURCE_DIR}/common_modules/module_lib/inc + ${CMAKE_SOURCE_DIR}/common_modules/inc + ) + + target_compile_definitions(threadx_module PUBLIC TXM_MODULE_MANAGER) + + endif() + + # The portable half of the module manager. Globbed rather than listed: it is + # a whole upstream component, not a selection from one, and a file added to it + # upstream should not need a change here to be compiled. + + file(GLOB TXM_MANAGER_SRC ${CMAKE_SOURCE_DIR}/common_modules/module_manager/src/*.c) + + # The module-side library: the API shims that run inside the module. These + # belong to the module's image and must not reach the manager's link -- they + # define the same names as the kernel's own entry points, and as objects they + # beat the kernel's static library, so in one link every service call the + # manager makes is redirected into the module. + + file(GLOB TXM_MODULE_LIB_SRC ${CMAKE_SOURCE_DIR}/common_modules/module_lib/src/*.c) + + # ------------------------------------------------------------------------ + # The module, built as its own image and reduced to a raw binary. + # ------------------------------------------------------------------------ + + add_executable(fvp_demo_module.elf EXCLUDE_FROM_ALL + ${MOD_FVP}/txm_module_preamble.S + ${MOD_FVP}/sample_threadx_module.c + ${MOD_DIR}/module_lib/src/txm_module_thread_shell_entry.c + ${MOD_DIR}/module_lib/src/txm_module_gcc_setup.S + ${TXM_MODULE_LIB_SRC} + ) + + # No TXM_MODULE_MANAGER here: that define selects the kernel side of the + # shared headers, and this is the other side. + + target_include_directories(fvp_demo_module.elf PRIVATE + ${FVP_DIR} + ${MOD_DIR}/inc + ${CMAKE_SOURCE_DIR}/common_modules/module_lib/inc + ${CMAKE_SOURCE_DIR}/common_modules/inc + ${CMAKE_SOURCE_DIR}/common/inc + ) + + # Position independent, and only here. These five flags are what make the + # module relocatable, and putting any of them on the manager would be a bug: + # the manager is the resident image, linked absolutely at the address it + # boots from, and -msingle-pic-base in particular would have it treat r9 as + # a PIC base that nothing sets up. + # + # -fpic data references go through the GOT + # -msingle-pic-base r9 is the GOT base, and the caller sets + # it -- which is what the manager's + # thread stack build does + # -mno-pic-data-is-text-relative the module's data is NOT at a fixed + # offset from its code: code is loaded in + # place in the module area and data is + # allocated from the byte pool, so the gap + # between them is decided at run time + # -fno-plt no procedure linkage table, which a + # module has no loader to populate + # -mno-long-calls see below + # + # -mno-long-calls is the interesting one. The project-wide C flags carry + # -mlong-calls, and under -fpic that makes GCC route EVERY call through the + # GOT -- R_ARM_GOT32 instead of R_ARM_CALL -- including the shell entry's + # call to _gcc_setup, which is the function that fills the GOT in. The + # module would load, enter its shell entry, read a zero out of the + # not-yet-written GOT and branch to address 0. A module never needs long + # calls: it is one contiguous blob and it never calls out of itself, because + # kernel services go through the dispatcher function pointer in its entry + # info. + + target_compile_options(fvp_demo_module.elf PRIVATE + -g + -fpic + -fno-plt + -mno-pic-data-is-text-relative + -msingle-pic-base + -mno-long-calls + ) + + target_link_options(fvp_demo_module.elf PRIVATE + -T${MOD_FVP}/link_demo_module.lds + -nostartfiles + -nostdlib + -Wl,-Map=fvp_demo_module.map + ${R52_LINK_QUIET_RWX} + ) + + set_target_properties(fvp_demo_module.elf PROPERTIES + LINK_DEPENDS ${MOD_FVP}/link_demo_module.lds) + + # The raw image the manager embeds. Written into the binary directory + # because that is the include path module_blob.S searches. + + add_custom_command( + OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/demo_module.bin + COMMAND ${CMAKE_OBJCOPY} -O binary + $ + ${CMAKE_CURRENT_BINARY_DIR}/demo_module.bin + DEPENDS fvp_demo_module.elf + COMMENT "Converting the demonstration module to a raw image" + VERBATIM + ) + + add_custom_target(fvp_demo_module_bin + DEPENDS ${CMAKE_CURRENT_BINARY_DIR}/demo_module.bin + ) + + # ------------------------------------------------------------------------ + # The manager image. + # ------------------------------------------------------------------------ + + add_executable(fvp_module.elf EXCLUDE_FROM_ALL + # Board support, shared with the other FVP examples + ${FVP_DIR}/entry.S + ${FVP_DIR}/console.c + ${FVP_DIR}/uart_pl011.c + ${FVP_DIR}/mpu.c + ${FVP_DIR}/cache.c + ${FVP_DIR}/gicv3.c + ${FVP_DIR}/timer.c + ${FVP_DIR}/irq_dispatch.c + ${FVP_DIR}/tx_initialize_low_level.S + + # Module manager port + ${MOD_DIR}/module_manager/src/tx_thread_schedule.S + ${MOD_DIR}/module_manager/src/tx_thread_context_save.S + ${MOD_DIR}/module_manager/src/tx_thread_context_restore.S + ${MOD_DIR}/module_manager/src/tx_thread_stack_build.S + ${MOD_DIR}/module_manager/src/tx_thread_system_return.S + ${MOD_DIR}/module_manager/src/txm_module_manager_svc_handler.S + ${MOD_DIR}/module_manager/src/txm_module_manager_fault_capture.S + ${MOD_DIR}/module_manager/src/txm_module_manager_user_mode_entry.S + ${MOD_DIR}/module_manager/src/txm_module_manager_thread_stack_build.S + ${MOD_DIR}/module_manager/src/txm_module_manager_mm_register_setup.c + ${MOD_DIR}/module_manager/src/txm_module_manager_memory_fault_handler.c + ${MOD_DIR}/module_manager/src/txm_module_manager_memory_fault_notify.c + ${MOD_DIR}/module_manager/src/txm_module_manager_alignment_adjust.c + ${MOD_DIR}/module_manager/src/txm_module_manager_external_memory_enable.c + ${MOD_DIR}/module_manager/src/txm_module_manager_offset_check.c + + # The module, as bytes rather than as objects. module_blob.S includes the + # raw image built by fvp_demo_module.elf, so none of the module's symbols + # enter this link. + ${MOD_FVP}/module_blob.S + + # The application + ${MOD_FVP}/sample_threadx_module_manager.c + + # The portable module manager + ${TXM_MANAGER_SRC} + ) + + target_include_directories(fvp_module.elf PRIVATE + ${FVP_DIR} + ${MOD_DIR}/inc + ${CMAKE_SOURCE_DIR}/common_modules/module_manager/inc + ${CMAKE_SOURCE_DIR}/common_modules/module_lib/inc + ${CMAKE_SOURCE_DIR}/common_modules/inc + ) + + # TX_R52_ENABLE_MPU is not optional for this image, unlike the AR1 demos + # where it is a switch: without protection there is no module boundary to + # test, and the manager's load window over the module area is programmed by + # mpu_init. TX_R52_USE_THREADX_IRQ brings up the tick, which the module + # needs for the tx_thread_sleep that proves a kernel call returns. + + target_compile_definitions(fvp_module.elf PRIVATE + TXM_MODULE_MANAGER + TX_R52_USE_THREADX_IRQ + TX_R52_ENABLE_MPU + ) + + target_compile_options(fvp_module.elf PRIVATE -g) + + # .incbin searches the assembler's include paths, not the source tree, and + # demo_module.bin is generated -- so the build directory has to be on that + # path or module_blob.S cannot find the image. + + set_source_files_properties(${MOD_FVP}/module_blob.S PROPERTIES + OBJECT_DEPENDS ${CMAKE_CURRENT_BINARY_DIR}/demo_module.bin + COMPILE_OPTIONS "-Wa,-I${CMAKE_CURRENT_BINARY_DIR}" + ) + + add_dependencies(fvp_module.elf fvp_demo_module_bin) + + target_link_options(fvp_module.elf PRIVATE + -T${MOD_FVP}/link_module.lds + -nostartfiles + -Wl,-Map=fvp_module.map + ${R52_LINK_QUIET_RWX} + ) + + set_target_properties(fvp_module.elf PROPERTIES + LINK_DEPENDS ${MOD_FVP}/link_module.lds) + + target_link_libraries(fvp_module.elf PRIVATE threadx_module) + +endif() + # Run a target on the FVP. Each image exits by itself through the # semihosting SYS_EXIT call, so no host-side timeout is needed. UART0 is # routed to stdout unconditionally so that PL011-console images are visible @@ -334,6 +613,10 @@ if(FVP_BASER_AEMV8R) "Running AR1/M3 tick and preemption demo on FVP_BaseR_AEMv8R...") threadx_r52_add_fvp_run(demo_threadx.elf run-demo-threadx-r52 "Running the standard ThreadX demo on FVP_BaseR_AEMv8R...") + if(TX_R52_BUILD_FVP_MODULE_EXAMPLE) + threadx_r52_add_fvp_run(fvp_module.elf run-module-r52 + "Running the AR2 module isolation example on FVP_BaseR_AEMv8R...") + endif() # Automated checks. The runner judges each image by its self-reported # result and treats a missing result line as failure, so a hang cannot @@ -344,6 +627,15 @@ if(FVP_BASER_AEMV8R) set(R52_RUNNER ${FVP_DIR}/test/run_fvp_test.py) set(R52_TEST_IMAGES ${R52_IMAGES}) + # AR2. Kept out of R52_IMAGES above rather than added to it, because + # that list is what the TX_R52_ENABLE_MPU and TX_R52_CONSOLE_PL011 + # options are applied to and neither is optional for this image: the + # module boundary IS the MPU, and the result line has to reach the + # runner whichever console the rest of the suite was built with. + if(TX_R52_BUILD_FVP_MODULE_EXAMPLE) + list(APPEND R52_TEST_IMAGES fvp_module.elf) + endif() + enable_testing() foreach(image IN LISTS R52_TEST_IMAGES) add_test(NAME r52-fvp-${image} diff --git a/ports/cortex_r52/gnu/example_build/fvp_baser_aemv8r/cache.c b/ports/cortex_r52/gnu/example_build/fvp_baser_aemv8r/cache.c new file mode 100644 index 000000000..6632096ad --- /dev/null +++ b/ports/cortex_r52/gnu/example_build/fvp_baser_aemv8r/cache.c @@ -0,0 +1,149 @@ +/*************************************************************************** + * 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 */ +/* */ +/* cache.c Cortex-R52/GNU */ +/* 6.5.2 */ +/* AUTHOR */ +/* */ +/* Frédéric Desbiens, Eclipse Foundation */ +/* */ +/* DESCRIPTION */ +/* */ +/* Cache maintenance for a loader that copies code. See cache.h for */ +/* why only these three functions exist. */ +/* */ +/* MISRA C:2012 deviations (justified) */ +/* */ +/* Directive 4.3 -- cache maintenance and CTR are reachable only */ +/* through CP15; every access is encapsulated in a one-line accessor */ +/* below and nowhere else in this file. */ +/* Rule 11.6 (conversion between a pointer and an integer) -- a cache */ +/* maintenance operation takes a virtual address as a register value, */ +/* so the conversion is what the instruction requires. */ +/* */ +/**************************************************************************/ + +#include "cache.h" + +/* The smallest data-cache line in the machine, from CTR.DminLine. Held as a + log2 of the number of 32-bit words, so the byte count is 4 << DminLine. + DminLine and not the L1 geometry: a maintenance-by-address walk has to step + by the smallest line any level implements, or it skips lines in that level. */ + +#define CTR_DMINLINE_SHIFT 16U +#define CTR_DMINLINE_MASK 0xFUL + + +/**************************************************************************/ +/* CP15 accessors. The only assembly in this file (MISRA C:2012 Dir 4.3).*/ +/**************************************************************************/ + +static unsigned long read_ctr(void) +{ + unsigned long value; + __asm__ volatile("mrc p15, 0, %0, c0, c0, 1" : "=r"(value)); + return value; +} + +static void clean_dcache_line(unsigned long address) +{ + __asm__ volatile("mcr p15, 0, %0, c7, c10, 1" :: "r"(address) : "memory"); +} + +static void invalidate_icache_all_op(void) +{ + unsigned long zero = 0UL; + __asm__ volatile("mcr p15, 0, %0, c7, c5, 0" :: "r"(zero) : "memory"); +} + +static void data_sync_barrier(void) +{ + __asm__ volatile("dsb sy" ::: "memory"); +} + +static void instruction_barrier(void) +{ + __asm__ volatile("isb" ::: "memory"); +} + + +/**************************************************************************/ +/* cache_dcache_line_bytes */ +/**************************************************************************/ + +unsigned long cache_dcache_line_bytes(void) +{ + return 4UL << ((read_ctr() >> CTR_DMINLINE_SHIFT) & CTR_DMINLINE_MASK); +} + + +/**************************************************************************/ +/* cache_clean_range */ +/* */ +/* Clean by virtual address over [start, start + length). */ +/* */ +/* The start is rounded DOWN to a line boundary and the walk continues */ +/* past the end until the last line containing a requested byte has been */ +/* cleaned. Rounding the start up instead would leave the first partial */ +/* line dirty, which is the whole failure this exists to prevent and is */ +/* invisible whenever the caller happens to be line aligned. */ +/**************************************************************************/ + +void cache_clean_range(const void *start_address, unsigned long length) +{ + unsigned long line = cache_dcache_line_bytes(); + unsigned long address; + unsigned long end; + + if (length == 0UL) + { + return; + } + + address = (unsigned long) start_address; + end = address + length; + + address &= ~(line - 1UL); + + while (address < end) + { + clean_dcache_line(address); + address += line; + } + + /* The stores have to be complete before anything fetches from the range. */ + + data_sync_barrier(); +} + + +/**************************************************************************/ +/* cache_invalidate_icache_all */ +/**************************************************************************/ + +void cache_invalidate_icache_all(void) +{ + invalidate_icache_all_op(); + + /* DSB then ISB: the invalidate must complete, and the pipeline must be + flushed of anything fetched before it did. */ + + data_sync_barrier(); + instruction_barrier(); +} diff --git a/ports/cortex_r52/gnu/example_build/fvp_baser_aemv8r/cache.h b/ports/cortex_r52/gnu/example_build/fvp_baser_aemv8r/cache.h new file mode 100644 index 000000000..0ed8dcfc0 --- /dev/null +++ b/ports/cortex_r52/gnu/example_build/fvp_baser_aemv8r/cache.h @@ -0,0 +1,72 @@ +/*************************************************************************** + * 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 */ +/* */ +/* cache.h Cortex-R52/GNU */ +/* 6.5.2 */ +/* AUTHOR */ +/* */ +/* Frédéric Desbiens, Eclipse Foundation */ +/* */ +/* DESCRIPTION */ +/* */ +/* The cache maintenance a loader owes the instruction side, and */ +/* nothing else. */ +/* */ +/* The S32Z280 board support carries a full cache driver -- enable, */ +/* disable, geometry, set/way sweeps -- because silicon bring-up needed */ +/* to ask the hardware what it had. None of that is needed here: the */ +/* caches are turned on once in mpu_init and never turned off, and the */ +/* model's geometry is not in question. What IS needed is the pair of */ +/* operations that make copied code executable, so that is all this is. */ +/* */ +/* Why it is needed at all: the module area is Normal write-back memory, */ +/* so a byte copy of module code leaves the bytes in dirty data-cache */ +/* lines while the instruction side -- which is not coherent with the */ +/* data cache on this core -- fetches whatever main memory still holds. */ +/* Cleaning by address range and then invalidating the instruction cache */ +/* is what closes that gap. */ +/* */ +/* By range rather than by set/way, deliberately. A set/way sweep needs */ +/* the cache geometry and touches every line in the machine; the loader */ +/* knows exactly which bytes it wrote, so the range form is both tighter */ +/* and shorter to get right. */ +/* */ +/**************************************************************************/ + +#ifndef CACHE_H +#define CACHE_H + +/* Clean (write back) the data cache over one address range, so that main + memory holds what the copy wrote. Rounds outwards to cache-line + boundaries; a length of zero does nothing. */ + +void cache_clean_range(const void *start_address, unsigned long length); + +/* Invalidate the whole instruction cache, so no stale line from a previous + image at the same address can be served. Invalidate-all rather than by + range because it is one register write and this runs once per load. */ + +void cache_invalidate_icache_all(void); + +/* Data cache line length in bytes, from CTR.DminLine. Published because the + range walk above depends on it and a caller may want to report it. */ + +unsigned long cache_dcache_line_bytes(void); + +#endif /* CACHE_H */ diff --git a/ports/cortex_r52/gnu/example_build/fvp_baser_aemv8r/entry.S b/ports/cortex_r52/gnu/example_build/fvp_baser_aemv8r/entry.S index d59183f8f..77d813361 100644 --- a/ports/cortex_r52/gnu/example_build/fvp_baser_aemv8r/entry.S +++ b/ports/cortex_r52/gnu/example_build/fvp_baser_aemv8r/entry.S @@ -101,6 +101,20 @@ .equ HSR_EC_SHIFT, 26 .equ HSR_EC_HVC, 0x12 /* HVC executed in AArch32 */ +#ifdef TXM_MODULE_MANAGER + +/* The module manager owns three of the EL1 vectors in a manager build: the + supervisor call, which is the privilege boundary a module crosses to reach + the kernel, and the two aborts, which are how a module that reaches outside + its regions is caught. Declared rather than merely referenced so that a + build with the manager half-configured fails at the link with a name in it. */ + + .extern __tx_module_svc_interrupt + .extern _txm_module_manager_data_abort + .extern _txm_module_manager_prefetch_abort + +#endif + /* Report which vector was taken, then halt. The FVP offers no GDB stub (Iris only), so a self-identifying fault is the primary debugging tool for this port. Uses no stack: only r0/r1 and a semihosting call. */ @@ -144,7 +158,11 @@ el2_vectors: el1_vectors: b el1_trap_reset /* 0x00 reset */ b el1_trap_undef /* 0x04 undefined instruction */ +#ifdef TXM_MODULE_MANAGER + b __tx_module_svc_interrupt /* 0x08 module kernel boundary */ +#else b el1_trap_svc /* 0x08 supervisor call */ +#endif b el1_trap_pabt /* 0x0C prefetch abort */ b el1_trap_dabt /* 0x10 data abort */ b el1_trap_reserved /* 0x14 reserved */ @@ -476,6 +494,26 @@ el1_trap_svc: FAULT_REPORT msg_el1_svc and this handler returns there, restoring the pre-fault mode from SPSR. */ el1_trap_pabt: +#ifdef TXM_MODULE_MANAGER + + /* A fault from User mode is a module reaching outside its code region, and + it belongs to the module manager, which records it and terminates the + thread. A fault from a privileged mode falls through to the paths below, + which the MPU self-test depends on. + + r0 is pushed and popped around the test rather than simply used: the + capture in the manager records the faulting registers, and clobbering one + here would have it record ours instead. LDM does not affect the flags, + so the comparison still holds after the pop. */ + + stmdb sp!, {r0} + mrs r0, spsr + and r0, r0, #0x1F + cmp r0, #0x10 /* User mode? */ + ldmia sp!, {r0} + beq _txm_module_manager_prefetch_abort + +#endif #ifdef TX_R52_MPU_FAULT_TEST push {r0, r1} ldr r0, =mpu_expect_pabt @@ -532,6 +570,19 @@ mpu_try_execute_land: Runs on the Abort-mode stack set up above. */ el1_trap_dabt: +#ifdef TXM_MODULE_MANAGER + + /* A fault from User mode is a module reaching outside its data region. See + el1_trap_pabt above for why r0 is saved across the test. */ + + stmdb sp!, {r0} + mrs r0, spsr + and r0, r0, #0x1F + cmp r0, #0x10 /* User mode? */ + ldmia sp!, {r0} + beq _txm_module_manager_data_abort + +#endif #ifdef TX_R52_MPU_FAULT_TEST push {r0, r1} ldr r0, =mpu_expect_abort diff --git a/ports/cortex_r52/gnu/example_build/fvp_baser_aemv8r/mpu.c b/ports/cortex_r52/gnu/example_build/fvp_baser_aemv8r/mpu.c index 29f9fe25f..07c9ff907 100644 --- a/ports/cortex_r52/gnu/example_build/fvp_baser_aemv8r/mpu.c +++ b/ports/cortex_r52/gnu/example_build/fvp_baser_aemv8r/mpu.c @@ -294,6 +294,80 @@ static void program_region(unsigned int index, const MPU_REGION *region_ptr) } +#ifdef TXM_MODULE_MANAGER + +/**************************************************************************/ +/* mpu_module_window_init */ +/* */ +/* A window over the module area, for the manager to load through. */ +/* */ +/* No region in the table above covers the module area, which is what */ +/* stops every thread from reaching a module's memory -- but the manager */ +/* has to read the preamble and write the module's data to load it at */ +/* all. Without this the load faults on its first read of the image. */ +/* */ +/* Not opened and closed around the load. It is left enabled here and */ +/* the SCHEDULER owns it from then on: it turns this region on for every */ +/* thread that owns no module and off for every thread that does, which */ +/* is what keeps it and a module's own regions -- which cover the same */ +/* memory -- from ever being enabled together. PMSAv8-R has no region */ +/* priority, so two enabled regions over one address are CONSTRAINED */ +/* UNPREDICTABLE, and mutual exclusion by ownership is the only form of */ +/* it that does not depend on remembering to bracket a call. */ +/* */ +/* EL1 read/write with no EL0 access, and execute-never: the manager can */ +/* load through it, and a module cannot use it to reach anything. */ +/**************************************************************************/ + +unsigned long mpu_module_window_prbar; +unsigned long mpu_module_window_prlar; + + +unsigned int mpu_module_window_init(void) +{ + MPU_REGION window; + + /* Asked of the hardware rather than assumed. See MPU_MODULE_REGIONS_REQUIRED + in mpu.h: this model reports a region count no real Cortex-R52 can have, + so the number is checked here and the shortfall is a refusal. */ + + if (mpu_region_count() < MPU_MODULE_REGIONS_REQUIRED) + { + return 0U; + } + + window.mpu_region_base = FVP_MODULE_AREA_BASE; + window.mpu_region_limit = FVP_MODULE_AREA_BASE + + FVP_MODULE_AREA_SIZE - 1UL; + window.mpu_region_ap = MPU_AP_RW_EL1; + window.mpu_region_execute_never = 1U; + window.mpu_region_shareability = MPU_SH_NON; + window.mpu_region_attr_index = MPU_ATTR_NORMAL_WB; + window.mpu_region_name = "module window RW NX EL1"; + + /* Published for the scheduler, which turns this region on and off on every + dispatch and has no business computing register layouts in assembly. */ + + mpu_module_window_prbar = (window.mpu_region_base & 0xFFFFFFC0UL) + | (((unsigned long) window.mpu_region_shareability & 0x3UL) << 3) + | (((unsigned long) window.mpu_region_ap & 0x3UL) << 1) + | ((unsigned long) window.mpu_region_execute_never & 0x1UL); + + mpu_module_window_prlar = (window.mpu_region_limit & 0xFFFFFFC0UL) + | (((unsigned long) window.mpu_region_attr_index & 0x7UL) << 1) + | 1UL; + + program_region(MPU_MODULE_LOAD_REGION, &window); + + data_sync_barrier(); + instruction_barrier(); + + return 1U; +} + +#endif /* TXM_MODULE_MANAGER */ + + /**************************************************************************/ /* mpu_init */ /**************************************************************************/ @@ -311,6 +385,20 @@ unsigned int mpu_init(void) return 0U; } +#ifdef TXM_MODULE_MANAGER + + /* A module manager build needs regions the table above does not describe: + the eight handed to a module and the window over the module area. If the + implementation cannot supply them, stop here rather than enable a + protection map that is missing the half a module depends on. */ + + if (available < MPU_MODULE_REGIONS_REQUIRED) + { + return 0U; + } + +#endif + /* Memory types first: a region's attribute index is meaningless until MAIR is populated. */ @@ -332,6 +420,20 @@ unsigned int mpu_init(void) write_prlar(0UL); } +#ifdef TXM_MODULE_MANAGER + + /* After the loop above, which would otherwise disable it again: the module + window lives above the regions this table uses, so it counts as unused + here. The region count was checked at the top of this function, so the + call cannot fail at this point. */ + + if (mpu_module_window_init() == 0U) + { + return 0U; + } + +#endif + data_sync_barrier(); /* Caches are invalidated before enabling. The instruction cache has a diff --git a/ports/cortex_r52/gnu/example_build/fvp_baser_aemv8r/mpu.h b/ports/cortex_r52/gnu/example_build/fvp_baser_aemv8r/mpu.h index aaca31761..d52a03a6e 100644 --- a/ports/cortex_r52/gnu/example_build/fvp_baser_aemv8r/mpu.h +++ b/ports/cortex_r52/gnu/example_build/fvp_baser_aemv8r/mpu.h @@ -116,4 +116,54 @@ const MPU_REGION *mpu_region_table(unsigned int *count_ptr); void mpu_read_region(unsigned int index, unsigned long *prbar_ptr, unsigned long *prlar_ptr); +/* --------------------------------------------------------------------------- + ThreadX modules. + + Kept in this header rather than behind TXM_MODULE_MANAGER because + txm_module_manager_offset_check.c includes it to check the region number + against the one tx_thread_schedule.S writes in assembly, and a definition + that appears only in some builds cannot be checked in the others. + --------------------------------------------------------------------------- */ + +/* Region index for the manager's load window over the module area. Above the + kernel's 0-2 and above the eight the manager hands to a module (8-15), so + neither the boot table nor the scheduler's per-thread region load can + disturb it. tx_thread_schedule.S spells the same number in an MCR to + PRSELR; the offset check asserts the two agree. */ + +#define MPU_MODULE_LOAD_REGION 16U + +/* How many EL1 regions a module manager build needs: 0-2 kernel, 8-15 for the + module block, 16 for the window. Region 16 is the highest index used, so + seventeen regions is the requirement. + + Checked against MPUIR rather than assumed, even though the AEMv8-R model + reports 32. The model reports a region count NO Cortex-R52 can have -- the + TRM gives MPUIR.DREGION as 16, 20 or 24 -- so a green run here says nothing + about whether the budget fits silicon, and a part configured with 16 regions + cannot run this port at all. The check is here so that the shortfall is a + refusal rather than a region that silently does not exist. */ + +#define MPU_MODULE_REGIONS_REQUIRED 17U + +/* The module area window, which is what lets privileged code reach module + memory at all -- no other kernel region covers it. + + PMSAv8-R has no region priority, so this must never be enabled at the same + time as the regions a module is given, which cover the same memory. That is + guaranteed by who owns it rather than by careful calling: the scheduler turns + this region on for every thread that is not a module thread and off for every + thread that is, so the window is enabled exactly when no module regions are + loaded. The two register words are published for it below. + + Enabled at boot, because everything before the first module thread is + privileged code that may need to reach module memory. Returns 0 if the + implementation has fewer than MPU_MODULE_REGIONS_REQUIRED regions, in which + case nothing was programmed. */ + +unsigned int mpu_module_window_init(void); + +extern unsigned long mpu_module_window_prbar; +extern unsigned long mpu_module_window_prlar; + #endif /* MPU_H */ diff --git a/ports/cortex_r52/gnu/example_build/fvp_baser_aemv8r/platform.h b/ports/cortex_r52/gnu/example_build/fvp_baser_aemv8r/platform.h index 913befdb8..c3d5cf6d1 100644 --- a/ports/cortex_r52/gnu/example_build/fvp_baser_aemv8r/platform.h +++ b/ports/cortex_r52/gnu/example_build/fvp_baser_aemv8r/platform.h @@ -71,6 +71,46 @@ #define SYSTEM_COUNTER_HZ 100000000 +/* --------------------------------------------------------------------------- + Memory for ThreadX modules. + + Only the module manager build uses this, but the addresses are stated here + rather than in the module example because they are a property of the board's + memory map: a module's code and data are handed to it as MPU regions, and + PMSAv8-R will not allow those to overlap a kernel region. So module memory + has to be memory NO kernel region covers, and deciding that is the memory + map's job. + + link_module.lds ends the kernel's DRAM region at this base and gives the + 64 KB above it to the module area; mpu.c covers exactly the same range with + the manager's load window (region 16), which is the only mapping privileged + code has over it. The three have to agree, so all three read these two + values. + + Chosen at 0x003F0000 -- just under 4 MB -- because the manager image with its + stacks and its 1 MB of heap is far smaller than that, and the link script + asserts the two do not meet rather than trusting the margin. */ + +#define FVP_MODULE_AREA_BASE 0x003F0000 +#define FVP_MODULE_AREA_SIZE 0x00010000 /* 64 KB */ + +/* The status word the sample module reports its progress through, at the base + of the module area. + + It exists because the FVP has no debugger seam: on silicon a GDB harness + reads the module's own progress variable out of its data area, and here + nothing outside the image can read anything. So the manager grants the + module a shared region over this word and reads it back afterwards, which is + what lets the FVP test judge what the module actually managed to do rather + than only that it faulted. + + A fixed address on both sides, and checked at run time rather than trusted: + the manager grants the region at the linker's symbol and refuses to continue + if that is not this address. */ + +#define FVP_MODULE_STATUS_BASE 0x003F0000 +#define FVP_MODULE_STATUS_SIZE 0x40 /* one MPU granule */ + #ifndef __ASSEMBLER__ /* 32-bit device register access. */ 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 e968f937b..ae0a8b73f 100644 --- a/ports/cortex_r52/gnu/example_build/s32z280_evb/CMakeLists.txt +++ b/ports/cortex_r52/gnu/example_build/s32z280_evb/CMakeLists.txt @@ -286,6 +286,13 @@ if(TX_R52_BUILD_S32Z280_MODULE_EXAMPLE) # so, and it is better to find out here than on the board. # --------------------------------------------------------------------- + # Guarded by if(NOT TARGET) because the FVP module example defines the same + # library from its own directory, and the two example options can be on at + # once. The definitions are identical -- both describe the module port, not + # a board -- so whichever directory CMake reaches first may create it. + + if(NOT TARGET threadx_module) + file(GLOB TX_COMMON_SRC ${CMAKE_SOURCE_DIR}/common/src/*.c) set(TX_R52_PORT_SRC ${CMAKE_SOURCE_DIR}/ports/cortex_r52/gnu/src) @@ -316,6 +323,8 @@ if(TX_R52_BUILD_S32Z280_MODULE_EXAMPLE) target_compile_definitions(threadx_module PUBLIC TXM_MODULE_MANAGER) + endif() + # The portable half of the module manager. Globbed rather than listed: it is # a whole upstream component, not a selection from one, and a file added to it # upstream should not need a change here to be compiled. diff --git a/ports_module/cortex_r52/gnu/example_build/fvp_baser_aemv8r/link_demo_module.lds b/ports_module/cortex_r52/gnu/example_build/fvp_baser_aemv8r/link_demo_module.lds new file mode 100644 index 000000000..dbe1e069f --- /dev/null +++ b/ports_module/cortex_r52/gnu/example_build/fvp_baser_aemv8r/link_demo_module.lds @@ -0,0 +1,208 @@ +/*************************************************************************** + * 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 + **************************************************************************/ + +/* Link map for the demonstration module alone, for the Armv8-R AEM FVP. + * + * Nothing in this file is board specific: the module is position independent + * and the two segment origins below are fictions. It is a separate copy from + * the S32Z280 one all the same, because the two examples are deliberately + * independent -- silicon bring-up must not be able to perturb the FVP + * regression, and vice versa -- and because a module example is free to change + * its own layout. The two are byte-identical apart from this paragraph today, + * so `diff` is the right way to see whether they have drifted. + * + * The module is a separate link unit from the manager, and it has to be. Both + * sides define the ThreadX API: the kernel defines the real _txe_* entry points + * and the module library defines shims of the same names that trap into the + * kernel instead. Linking them together does not produce a duplicate-symbol + * error, because the kernel arrives as a static library and the module library + * as ordinary objects, and an object always beats an archive member. The module + * shims therefore win for the whole image, and the manager's own calls to + * tx_thread_create and friends are quietly redirected into the module. + * + * That was not a theory. On the S32Z280 it put _txe_thread_create inside the + * module area, which no kernel MPU region covers -- so tx_application_define + * called into unmapped memory and the core took a prefetch abort with nothing on + * the console to say so. + * + * POSITION INDEPENDENT, and the two addresses below are fictions + * ============================================================== + * + * The module is built -fpic -msingle-pic-base, so every reference it makes to + * its own data goes through the global offset table with r9 as the base. It + * therefore does not run at the addresses in this file and is not meant to: the + * two segment origins are nominal, chosen only so that the loader can tell a + * code address from a data address by comparing against __data_segment_start__. + * What actually happens at run time is that _gcc_setup rewrites every GOT entry + * from these nominal addresses to the addresses the manager decided on. + * + * That is why they are far apart and why neither is zero. Far apart, because + * the whole discrimination is "below the data origin means code"; non-zero, + * because _gcc_setup treats a zero GOT entry as one the linker never filled in + * and skips it, so a real address of zero would be silently dropped. + * + * 0x01000000 code preamble, text, rodata, and the load images of the + * GOT and .data -- this is the blob the manager embeds + * 0x02000000 data the GOT the module runs against, .data and .bss, all + * of which live in memory the manager allocates + * + * The load images matter. A module's .data cannot be used where it was linked, + * because the manager never copies it there: _txm_module_manager_internal_load + * allocates the module's data area from its byte pool and TX_MEMSETs it to zero, + * and that is the only memory the module is given a region for. The module's + * own .data, wherever it was linked, is outside every region the module owns. + * On silicon that faulted at the first write to an initialised variable, with + * DFAR pointing into the gap between the granted code and the granted data. + * The same arrangement fails the same way on the model. + * + * So .data and .got have their VMAs in the data segment, where the module will + * run, and their LMAs in the code segment, inside the blob, where _gcc_setup can + * find them and copy them out. AT>CODE is what says that. + */ + +MEMORY +{ + /* Nominal. See the note above: the module runs where the manager puts it, + not here. Sized generously because nothing is reserved by being large -- + the raw binary is only as long as the sections actually emitted. */ + + CODE (rx) : ORIGIN = 0x01000000, LENGTH = 0x00100000 + DATA (rw) : ORIGIN = 0x02000000, LENGTH = 0x00100000 +} + +__code_segment_start__ = 0x01000000; +__data_segment_start__ = 0x02000000; + +SECTIONS +{ + /* --------------------------------------------------------------------- + The code segment, which is the blob byte for byte. + --------------------------------------------------------------------- */ + + .module_image : ALIGN(64) + { + __module_image_start__ = .; + + /* The preamble is first because that is where the manager looks for it: + it reads the properties, the entry points and the two sizes from the + first words of the image. KEEP because nothing references it. */ + + KEEP(*(.txm_module_preamble)) + + *(.text .text.*) + *(.glue_7) + *(.glue_7t) + *(.rodata .rodata.*) + + . = ALIGN(4); + } >CODE AT>CODE + + /* --------------------------------------------------------------------- + The data segment. VMAs here, load images back in the code segment. + --------------------------------------------------------------------- */ + + /* The GOT, and it must be first in the data segment. r9 is the GOT base as + far as the compiler is concerned -- every R_ARM_GOT32 is an offset from + it -- and the manager sets r9 to txm_module_instance_module_data_base_address, + which is the start of the module's data area. So GOT origin and data + origin have to be the same address, or every offset the compiler emitted + is measured from the wrong place. __data_segment_start__ above is that + address on the nominal side; r9 is that address on the real side, and + _gcc_setup's rebase is the difference between the two. */ + + .got : ALIGN(4) + { + __new_got_start__ = .; + *(.got.plt) + *(.igot.plt) + *(.got) + __new_got_end__ = .; + } >DATA AT>CODE + + __got_load_start__ = LOADADDR(.got); + + .data : ALIGN(4) + { + __data_start__ = .; + *(.data .data.*) + *(.gnu.linkonce.d.*) + . = ALIGN(4); + __data_end__ = .; + } >DATA AT>CODE + + __data_load_start__ = LOADADDR(.data); + + /* NOLOAD, so .bss contributes nothing to the blob. The manager has already + zeroed the whole data allocation by the time the module runs -- but + _gcc_setup zeroes .bss anyway, because "the manager happens to memset it" + is a property of one loader and not something a module may rely on. */ + + .bss (NOLOAD) : ALIGN(4) + { + __bss_start__ = .; + *(.bss .bss.*) + *(.gnu.linkonce.b.*) + *(COMMON) + . = ALIGN(64); + __bss_end__ = .; + } >DATA + + /* --------------------------------------------------------------------- + The two sizes the preamble declares. + --------------------------------------------------------------------- */ + + /* Code covers everything in the blob, the load images of the GOT and .data + included, because the module has to be able to read them to copy them out + and the code region is the only mapping it has over the blob. Ending it + at .rodata instead would put the GOT template outside every region the + module owns and _gcc_setup would fault on its first read. */ + + __txm_module_code_end__ = __data_load_start__ + SIZEOF(.data); + __txm_module_code_size = __txm_module_code_end__ - __code_segment_start__; + + /* Data covers the GOT, .data and .bss. Not the thread stacks: the manager + adds the start/stop and callback stack sizes to this figure itself, from + the two stack-size words further down the preamble. Counting them here + as well would double them. */ + + __txm_module_data_size = __bss_end__ - __data_segment_start__; + + /* Unwind tables and toolchain notes would otherwise land at address zero and + be carried into the raw binary. A module has no unwinder. + + .rel.dyn and friends are discarded because this is a static link: ld + resolves every R_ARM_GOT32 itself and writes the finished address into the + GOT, so there is nothing left for a dynamic loader to do. If one of these + ever stops being empty, the assumption above has broken and the module + needs relocation processing rather than a rebase -- so they are listed + explicitly, to be found by whoever goes looking. */ + + /DISCARD/ : + { + *(.ARM.exidx*) + *(.ARM.extab*) + *(.comment) + *(.note.*) + *(.dynsym) + *(.dynstr) + *(.hash) + *(.gnu.hash) + *(.dynamic) + *(.interp) + *(.rel.dyn) + *(.rel.plt) + *(.plt) + } +} diff --git a/ports_module/cortex_r52/gnu/example_build/fvp_baser_aemv8r/link_module.lds b/ports_module/cortex_r52/gnu/example_build/fvp_baser_aemv8r/link_module.lds new file mode 100644 index 000000000..cbc34798c --- /dev/null +++ b/ports_module/cortex_r52/gnu/example_build/fvp_baser_aemv8r/link_module.lds @@ -0,0 +1,275 @@ +/*************************************************************************** + * 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 + **************************************************************************/ + +/* Link map for the module manager on the Armv8-R AEM FVP (BaseR platform). + * + * The BaseR memory map is the Base platform map with its two 2 GB halves + * swapped: Base DRAM at 0x80000000 appears at 0x00000000 here, and the upper + * half holds peripherals and is not executable by default. Code therefore has + * to be linked into low DRAM -- linking at 0x80000000 produces a silent fault + * loop with no output. + * + * This is the AR1 link.lds with three differences, and each of them is a + * requirement of the module port rather than a preference. + * + * 1. DRAM stops below a MODULE region. A module's code and data are handed + * to it as MPU regions, and PMSAv8-R has no region priority: two enabled + * regions covering one address are CONSTRAINED UNPREDICTABLE. So module + * memory has to be memory no kernel region covers, and the only way to + * guarantee that is to end the kernel's data region where the module area + * begins. __data_end__ below is ORIGIN(MODULE) for exactly that reason, + * not the "1 MB of heap past the image" the AR1 script uses. + * + * 2. .txm_user_entry sits between the code region and the data region, in a + * gap belonging to neither. It holds the module's gateway into the + * kernel, and a module is given a region covering exactly it -- which + * would overlap the kernel's code region if it were inside .text. + * + * 3. The MODULE region is subdivided into four fixed pieces. See each. + * + * The two addresses here are also in platform.h as FVP_MODULE_AREA_BASE and + * FVP_MODULE_AREA_SIZE, because mpu.c programs the manager's load window over + * the same range. A linker script cannot include a C header, so the ASSERT at + * the bottom of this file pins the two spellings together at link time. + */ + +ENTRY(_start) + +__hyp_stack_size__ = 0x0800; +__svc_stack_size__ = 0x1000; +__irq_stack_size__ = 0x0800; +__fiq_stack_size__ = 0x0400; +__abt_stack_size__ = 0x0400; +__und_stack_size__ = 0x0400; +__sys_stack_size__ = 0x0800; + +MEMORY +{ + DRAM (rwx) : ORIGIN = 0x00000000, LENGTH = 0x003F0000 /* ~4 MB less the module area */ + MODULE (rwx) : ORIGIN = 0x003F0000, LENGTH = 0x00010000 /* 64 KB */ +} + +SECTIONS +{ + /* PMSAv8-R regions have a 64-byte granule and their limits are inclusive, + so every boundary the MPU has to describe is aligned to it. */ + + . = ALIGN(64); + __code_start__ = .; + + .text : + { + KEEP(*(.vectors_el2)) + KEEP(*(.vectors_el1)) + *(.text*) + *(.glue_7) + *(.glue_7t) + } > DRAM + + .rodata : + { + . = ALIGN(4); + *(.rodata*) + . = ALIGN(4); + } > DRAM + + /* The module's gateway into the kernel. + * + * __code_end__ is taken at the START of this section, so the kernel's code + * region stops just below it: the region limit is __code_end__ - 1, masked + * down to the granule by PRLAR, and PRLAR's limit is inclusive with the low + * six bits implied as ones -- so the region ends at __code_end__ - 1 exactly + * and this section is outside it. A module gets its own region covering + * exactly this function, and that region would otherwise overlap the + * kernel's. + * + * Alone in its own section on purpose: anything sharing it would become + * executable by every module. ALIGN(64) after the colon aligns the SECTION, + * which is what is wanted. Written as ".txm_user_entry ALIGN(64) :" the + * expression would be the section's ADDRESS instead, and while that form + * happens to work in a region based at zero, it silently overrides "> DRAM" + * and is the wrong habit to leave in a file that will be copied. + */ + + .txm_user_entry : ALIGN(64) + { + __code_end__ = .; + __txm_user_entry_start__ = .; + KEEP(*(.txm_user_entry)) + . = ALIGN(64); + __txm_user_entry_end__ = .; + } > DRAM + + .data : ALIGN(64) + { + __data_start__ = .; + *(.data*) + . = ALIGN(4); + } > DRAM + + .bss (NOLOAD) : + { + . = ALIGN(4); + __bss_start__ = .; + *(.bss*) + *(COMMON) + . = ALIGN(4); + __bss_end__ = .; + } > DRAM + + /* One stack per AArch32 processor mode. SP must stay 8-byte aligned + (AAPCS), so each region is aligned before its top symbol is taken. */ + + .stacks (NOLOAD) : + { + . = ALIGN(8); + . = . + __hyp_stack_size__; + __hyp_stack_top = .; + + . = ALIGN(8); + . = . + __svc_stack_size__; + __svc_stack_top = .; + + . = ALIGN(8); + . = . + __irq_stack_size__; + __irq_stack_top = .; + + . = ALIGN(8); + . = . + __fiq_stack_size__; + __fiq_stack_top = .; + + . = ALIGN(8); + . = . + __abt_stack_size__; + __abt_stack_top = .; + + . = ALIGN(8); + . = . + __und_stack_size__; + __und_stack_top = .; + + . = ALIGN(8); + . = . + __sys_stack_size__; + __sys_stack_top = .; + } > DRAM + + . = ALIGN(8); + _end = .; + PROVIDE(end = .); + + /* ------------------------------------------------------------------ + The module area. Four pieces, none of which may overlap another: + each becomes an enabled MPU region while a module runs. + ------------------------------------------------------------------ */ + + /* The status word the module reports its progress through. + + This exists because the FVP has no debugger seam. On silicon a GDB + harness reads the module's own progress variable out of the data area the + manager allocated for it; here nothing outside the image can read + anything, and the manager deliberately knows no symbol of the module. So + the manager grants the module a shared MPU region over this word and + reads it back afterwards, which is what lets the test judge what the + module managed to do rather than only that it faulted. + + First in the module area, so its address is ORIGIN(MODULE) and the module + can carry it as a constant. Exactly one granule: a shared region is a + region like any other and cannot overlap the image below it. */ + + .module_status (NOLOAD) : + { + __module_status_start__ = .; + . = . + 0x40; + . = ALIGN(64); + __module_status_end__ = .; + } > MODULE + + /* The module image. + + The module is not linked here. It is built as its own image by the + fvp_demo_module.elf target, objcopied to a raw binary and included as + bytes by module_blob.S; this section only decides where those bytes land. + Keeping the module out of the link is not tidiness: the module library + defines shims named after the same ThreadX entry points the kernel + defines, and in one link the shims win, because they arrive as objects and + the kernel arrives as a static library. The manager's own service calls + would then be redirected into the module. */ + + .module_image : + { + . = ALIGN(64); + __module_image_start__ = .; + KEEP(*(.module_blob)) + . = ALIGN(64); + __module_image_end__ = .; + } > MODULE + + /* The pool the manager allocates module data from, in the module area for + the same reason the image is: whatever the manager hands to a module + becomes an MPU region, and that region may not overlap a kernel one. + + NOLOAD because nothing is copied in -- the byte pool writes its own + headers when it is created. */ + + .module_pool (NOLOAD) : + { + . = ALIGN(64); + __module_pool_start__ = .; + *(.module_pool) + . = ALIGN(64); + __module_pool_end__ = .; + } > MODULE + + /* A second place to load the same module from, which is how this example + proves the module is relocatable rather than merely built to be. + + A module that runs correctly at the address it was linked for proves + nothing: the GOT rebase would produce the addresses it started with, and a + rebase that did nothing would look identical. So the manager copies the + blob here and loads it a second time. + + NOLOAD, and deliberately not filled by the linker: if the blob were placed + here too, a failure to copy would be invisible because the right bytes + would already be present. */ + + .module_stage (NOLOAD) : + { + . = ALIGN(64); + __module_stage_start__ = .; + . = . + 0x1000; + . = ALIGN(64); + __module_stage_end__ = .; + } > MODULE + + /* The writable area the MPU describes: data, bss, the stacks and everything + tx_application_define carves out above _end, ending where the module area + begins. Not "_end plus a heap": the kernel's data region has to stop + below the module area or it overlaps every region a module is given. */ + + __data_end__ = ORIGIN(MODULE); + + /* The image must not have grown into the module area. Without this the + overlap appears as a module whose data the kernel also owns, which + PMSAv8-R reports as an abort from an address that looks perfectly legal. */ + + ASSERT(_end <= ORIGIN(MODULE), + "the manager image has grown into the module area; move FVP_MODULE_AREA_BASE up") + + /* The module carries the status address as a constant and platform.h names + it as FVP_MODULE_STATUS_BASE. Neither can see this script, so the + agreement is pinned here. The manager checks the same thing again at run + time, against the symbol, so a build that skipped this file still says so. */ + + ASSERT(__module_status_start__ == 0x003F0000, + "the module status word is not at FVP_MODULE_STATUS_BASE") +} diff --git a/ports_module/cortex_r52/gnu/example_build/fvp_baser_aemv8r/module_blob.S b/ports_module/cortex_r52/gnu/example_build/fvp_baser_aemv8r/module_blob.S new file mode 100644 index 000000000..b3c2d44df --- /dev/null +++ b/ports_module/cortex_r52/gnu/example_build/fvp_baser_aemv8r/module_blob.S @@ -0,0 +1,69 @@ +@/*************************************************************************** +@ * 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 +@ **************************************************************************/ +@ +@/**************************************************************************/ +@/* */ +@/* MODULE MANAGER RELEASE */ +@/* */ +@/* module_blob.S Cortex-R52/GNU */ +@/* 6.5.2 */ +@/* AUTHOR */ +@/* */ +@/* Frédéric Desbiens, Eclipse Foundation */ +@/* */ +@/* DESCRIPTION */ +@/* */ +@/* Carries the demonstration module into the manager image as data. */ +@/* */ +@/* The module is built as its own link unit and objcopied to a raw */ +@/* binary, which is included here verbatim. It has to be a separate */ +@/* link: the module library defines shims named after the ThreadX API */ +@/* entry points the kernel also defines, and in one link the shims win */ +@/* -- objects beat archive members -- so the manager's own service calls */ +@/* end up trapping into the module. */ +@/* */ +@/* Included as bytes rather than linked as objects, so the module's */ +@/* symbols never enter the manager's link at all. Nothing here is */ +@/* called: the manager finds the preamble at the start of the image and */ +@/* reaches everything else through that. */ +@/* */ +@/* A separate copy from the S32Z280 one, and identical to it: the two */ +@/* module examples are deliberately independent builds, and each names */ +@/* its own raw image through its own assembler include path. */ +@/* */ +@/**************************************************************************/ + + .section .module_blob, "a" + +@ 64-byte aligned, the PMSAv8-R granule, so the image starts where a region +@ can start. The linker script places this section at the module area base, +@ which is the address the module itself was linked for. + + .align 6 + + .global __demo_module_image + .global __demo_module_image_end + +__demo_module_image: + +@ demo_module.bin is produced by the fvp_demo_module.elf target. The +@ assembler finds it through an include path pointing at the build directory, +@ set in CMakeLists.txt -- .incbin searches the -I paths, not the source tree. + + .incbin "demo_module.bin" + +__demo_module_image_end: + + .align 6 diff --git a/ports_module/cortex_r52/gnu/example_build/fvp_baser_aemv8r/sample_threadx_module.c b/ports_module/cortex_r52/gnu/example_build/fvp_baser_aemv8r/sample_threadx_module.c new file mode 100644 index 000000000..d11a7b26c --- /dev/null +++ b/ports_module/cortex_r52/gnu/example_build/fvp_baser_aemv8r/sample_threadx_module.c @@ -0,0 +1,219 @@ +/*************************************************************************** + * 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 + **************************************************************************/ + +/**************************************************************************/ +/* */ +/* SAMPLE MODULE RELEASE */ +/* */ +/* sample_threadx_module.c Cortex-R52/GNU */ +/* 6.5.2 */ +/* AUTHOR */ +/* */ +/* Frédéric Desbiens, Eclipse Foundation */ +/* */ +/* DESCRIPTION */ +/* */ +/* A module that exercises the protection boundary rather than */ +/* demonstrating features, for the Armv8-R AEM FVP. */ +/* */ +/* The same four steps as the S32Z280 module, because it is the same */ +/* port under test: */ +/* */ +/* 1. Writes and reads its own data, which must succeed. */ +/* 2. Makes a kernel call, which must succeed -- proving a module in */ +/* User mode can reach the kernel through the supervisor call */ +/* boundary and come back. */ +/* 3. Violates its protection in one of the two ways the hardware */ +/* distinguishes, which must fault. */ +/* 4. Never reaches step 4, because step 3 terminates it. */ +/* */ +/* Steps 1 and 2 passing without step 3 faulting would mean the module */ +/* is running unprotected, which is the failure this example exists to */ +/* detect. A module that only ever touched its own memory would pass */ +/* identically with the MPU switched off. */ +/* */ +/* ONE DIFFERENCE FROM THE SILICON MODULE, and it is what makes this */ +/* one testable without a person: progress is written to a shared word */ +/* as well as to the module's own data. */ +/* */ +/* A module cannot print -- the console belongs to the board support */ +/* package, outside every region a module owns, so reaching it would */ +/* fault as surely as step 3 does. On silicon that is answered by a */ +/* GDB harness, which reads the module's own progress variable out of */ +/* the data area the manager allocated for it. The FVP has no such */ +/* seam: it exposes an Iris server and no GDB stub, so whatever the run */ +/* is going to report has to be reported by the image itself. */ +/* */ +/* So the manager grants this module a shared MPU region over one */ +/* 64-byte granule at a fixed address and the module records its */ +/* progress there, where the manager can read it back after the fault. */ +/* The address is a constant on both sides -- the module has no loader */ +/* to tell it anything and the manager deliberately knows no symbol of */ +/* the module -- and the manager checks the two agree rather than */ +/* trusting them to. */ +/* */ +/* The module's own progress variable is kept as well, unread. It */ +/* costs nothing, it keeps this file next to the silicon one, and it */ +/* means the two writes are the same code path: if the shared write */ +/* were the only one, a module that could not reach its own data would */ +/* still report progress. */ +/* */ +/**************************************************************************/ + +#include "txm_module.h" + +/* Progress, recorded in two places. See the header: the shared copy is what + the manager reads, and the module's own copy is what proves it could write + its own data at all. */ + +#define MODULE_PROGRESS_OWN_DATA 0x00000001UL +#define MODULE_PROGRESS_KERNEL_CALL 0x00000002UL +#define MODULE_PROGRESS_ATTEMPTED_STEAL 0x00000004UL +#define MODULE_PROGRESS_SURVIVED_STEAL 0x00000008UL +#define MODULE_PROGRESS_ATTEMPTED_JUMP 0x00000010UL +#define MODULE_PROGRESS_SURVIVED_JUMP 0x00000020UL + +/* Which violation to commit, taken from the low byte of the module ID the + manager passes to the start thread. The rest of that word is left alone: the + preamble ships it as 0x52520001, so a manager that sets nothing still gets a + working data-abort test rather than a module that does nothing. */ + +#define MODULE_TEST_MASK 0x000000FFUL +#define MODULE_TEST_DATA_ABORT 0x00000001UL +#define MODULE_TEST_PREFETCH_ABORT 0x00000002UL + +/* The shared status word, at the base of the module area. + + A literal address rather than anything reached through the GOT, because it is + not the module's own memory: it is memory the manager granted, and the module + has no channel through which to be told where it is. The same number is + FVP_MODULE_STATUS_BASE in platform.h and ORIGIN(MODULE) in link_module.lds; + the manager checks all three agree before it starts this module, so a + disagreement is a reported failure rather than a fault at the first write. + + MISRA C:2012 Rule 11.6 (conversion between an integer and a pointer to void) + is deliberately violated: an address agreed between two separately linked + images can only be written as a literal, and there is no conforming way to + express it. */ + +#define MODULE_STATUS_ADDRESS 0x003F0000UL + +#define MODULE_STATUS (*((volatile ULONG *) MODULE_STATUS_ADDRESS)) + +volatile ULONG module_progress; +volatile ULONG module_scratch[16]; + +/* An address the module has no business touching, used by step 3. + + The last protection granule below the module area, which on this map is + inside the manager's data region: that region is EL1 read/write and + execute-never, so a User-mode read of it is a permission fault and a + User-mode instruction fetch from it is one too. Both are what step 3 needs, + and both prove the KERNEL'S memory is what a module cannot reach -- which is + the property being demonstrated, rather than the absence of a mapping. + + Held in initialised data rather than written as a literal, deliberately and + unlike the status address above. The module reads it through its rebased + GOT, so the value arriving in DFAR or IFAR also proves the GOT was rewritten + and .data was copied. The manager knows the same number and checks it. */ + +volatile ULONG module_forbidden_address = 0x003EFFC0UL; + + +void demo_module_start(ULONG id) +{ + ULONG i; + ULONG sum = 0UL; + + /* 1. The module's own data. If this faults, the data region is wrong and + nothing else in this file will be reached. */ + + for (i = 0UL; i < 16UL; i++) + { + module_scratch[i] = i * 3UL; + } + + for (i = 0UL; i < 16UL; i++) + { + sum += module_scratch[i]; + } + + if (sum == 360UL) /* 3 * (0 + 1 + ... + 15) */ + { + module_progress |= MODULE_PROGRESS_OWN_DATA; + MODULE_STATUS |= MODULE_PROGRESS_OWN_DATA; + } + + /* 2. A kernel call, which leaves User mode through the supervisor call + boundary and must come back. A sleep is used because it is the + simplest service with an observable effect and it yields, so the + scheduler runs a module thread and reloads its regions on the way + back -- exercising the region switch as well as the call. */ + + if (tx_thread_sleep(2UL) == TX_SUCCESS) + { + module_progress |= MODULE_PROGRESS_KERNEL_CALL; + MODULE_STATUS |= MODULE_PROGRESS_KERNEL_CALL; + } + + /* 3. The violation, and 4. the flag that says it was tolerated. Each is + marked before the access rather than after, because after is not + reached if the port is working. The SURVIVED flags are what the + manager fails on: they can only appear if the hardware allowed an + access it was configured to refuse. */ + + if ((id & MODULE_TEST_MASK) == MODULE_TEST_PREFETCH_ABORT) + { + /* Branch outside the code region: a prefetch abort, reported through + IFSR and IFAR. Nothing is executed at the target -- the fault is on + the fetch itself, so the thread never arrives. */ + + module_progress |= MODULE_PROGRESS_ATTEMPTED_JUMP; + MODULE_STATUS |= MODULE_PROGRESS_ATTEMPTED_JUMP; + + /* MISRA C:2012 Rule 11.1 (no conversion between a pointer to a function + and any other type) is deliberately violated here, and Rule 11.6 + (no conversion between an integer and a pointer to void) with it. + Provoking a fetch from an address that holds no function is the entire + purpose of these three lines; there is no conforming way to write it. */ + + ((void (*)(void)) module_forbidden_address)(); + + module_progress |= MODULE_PROGRESS_SURVIVED_JUMP; + MODULE_STATUS |= MODULE_PROGRESS_SURVIVED_JUMP; + } + else + { + /* Read outside the data region: a data abort, reported through DFSR and + DFAR. */ + + module_progress |= MODULE_PROGRESS_ATTEMPTED_STEAL; + MODULE_STATUS |= MODULE_PROGRESS_ATTEMPTED_STEAL; + + sum += *((volatile ULONG *) module_forbidden_address); + + module_progress |= MODULE_PROGRESS_SURVIVED_STEAL; + MODULE_STATUS |= MODULE_PROGRESS_SURVIVED_STEAL; + } + + /* Keep the compiler from discarding the read above. */ + + module_scratch[0] = sum; + + while (1) + { + (void) tx_thread_sleep(100UL); + } +} diff --git a/ports_module/cortex_r52/gnu/example_build/fvp_baser_aemv8r/sample_threadx_module_manager.c b/ports_module/cortex_r52/gnu/example_build/fvp_baser_aemv8r/sample_threadx_module_manager.c new file mode 100644 index 000000000..ecd092016 --- /dev/null +++ b/ports_module/cortex_r52/gnu/example_build/fvp_baser_aemv8r/sample_threadx_module_manager.c @@ -0,0 +1,1018 @@ +/*************************************************************************** + * 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 + **************************************************************************/ + +/**************************************************************************/ +/* */ +/* MODULE MANAGER SAMPLE RELEASE */ +/* */ +/* sample_threadx_module_manager.c Cortex-R52/GNU */ +/* 6.5.2 */ +/* AUTHOR */ +/* */ +/* Frédéric Desbiens, Eclipse Foundation */ +/* */ +/* DESCRIPTION */ +/* */ +/* Loads the sample module three times, lets it misbehave every time, */ +/* and reports what the hardware did about it -- on the Armv8-R AEM */ +/* FVP, with no debugger and no person in the loop. */ +/* */ +/* This is the S32Z280 module demonstration turned into a regression. */ +/* The silicon version is judged by a human reading a console and by a */ +/* GDB harness that reads the module's memory between passes; neither */ +/* exists here. The model offers an Iris server and no GDB stub, so */ +/* the image has to judge itself and say so in one line the runner can */ +/* match. Everything below that differs from the silicon sample */ +/* differs for that reason. */ +/* */ +/* THE RESULT THIS EXAMPLE EXISTS TO PRODUCE IS THE FAULT. A module */ +/* that starts and runs proves the loader works; a module that is */ +/* stopped by the memory protection unit when it reaches outside its */ +/* own memory proves the port works. So the fault notification is not */ +/* an error path here, it is the expected outcome, and its absence is */ +/* the failure. */ +/* */ +/* TWO PASSES, because one proves nothing about relocation. The module */ +/* is position independent: it is linked against nominal addresses it */ +/* never runs at, and _gcc_setup rewrites its global offset table to */ +/* wherever the manager actually put it. A single run at the linked */ +/* address would exercise a rebase whose input and output are the same */ +/* number, and would look identical if the rebase did nothing at all. */ +/* */ +/* So pass 1 loads the blob where the linker placed it and pass 2 loads */ +/* a byte-for-byte copy of it from the staging area, with pass 1 still */ +/* holding its pool memory so that pass 2's data lands somewhere else */ +/* too. Both bases therefore differ between the passes, which is what */ +/* makes the comparison at the end mean something. */ +/* */ +/* THEN A THIRD PASS, which faults the other way. The two passes above */ +/* make the module read an address it does not own: a data abort, */ +/* reported through DFSR and DFAR. A module can equally leave its code */ +/* region, which is a prefetch abort reported through IFSR and IFAR and */ +/* arrives at the handler by a different vector. Both halves of the */ +/* port's fault path are therefore exercised, and neither is inferred */ +/* from the other. The third pass also runs after the first two have */ +/* been unloaded, which is the other half of what this file shows: a */ +/* module fault must leave the manager able to load and run the next */ +/* module. */ +/* */ +/* AND WHAT THE MODULE ACHIEVED IS READ BACK, not inferred. The manager */ +/* grants each pass a shared MPU region over one granule at a fixed */ +/* address and the module records its progress there. That is what */ +/* replaces the GDB harness, and it is what lets this file fail on the */ +/* two flags that matter -- SURVIVED_STEAL and SURVIVED_JUMP -- by name */ +/* rather than only on the absence of a fault. The two are the same */ +/* event seen from both ends, and a regression that showed one without */ +/* the other would be worth knowing about. */ +/* */ +/* WHAT A GREEN RUN HERE DOES NOT PROVE. The model reports 32 EL1 MPU */ +/* regions. The Cortex-R52 TRM gives MPUIR.DREGION as 16, 20 or 24, so */ +/* 32 is not an architecturally permitted value for this core and the */ +/* model is the generic AEMv8-R rather than an R52. It is strictly more */ +/* permissive than every real part: this port needs seventeen regions, */ +/* which the S32Z280's twenty supply and a legal 16-region R52 does not, */ +/* and no result from this image can tell you that. The count is */ +/* reported below so the log says what it was rather than implying it. */ +/* */ +/**************************************************************************/ + +#include "tx_api.h" +#include "txm_module.h" +#include "console.h" +#include "platform.h" +#include "mpu.h" +#include "cache.h" + +/* The fault information the abort vector captured. Declared here because the + module manager expands it into the port's fault handler through the + TXM_MODULE_MANAGER_FAULT_INFO macro rather than declaring it in a header, so an + application that wants to read it has to say so itself. */ + +extern TXM_MODULE_MANAGER_MEMORY_FAULT_INFO _txm_module_manager_memory_fault_info; + +/* Where the module image sits. Provided by the linker script, which places the + module's preamble first so this address is also the preamble address. */ + +extern unsigned char __module_image_start__; +extern unsigned char __module_image_end__; + +/* The second address the same blob is loaded from, so relocation can be shown + rather than assumed. Reserved by the linker script and left empty by it -- + see .module_stage in link_module.lds for why it is not pre-filled. */ + +extern unsigned char __module_stage_start__; +extern unsigned char __module_stage_end__; + +/* The shared status granule the module reports its progress through. */ + +extern unsigned char __module_status_start__; +extern unsigned char __module_status_end__; + +/* The kernel's own writable region, used to check that the address the module + is told to reach for really is memory it must not have. */ + +extern unsigned char __data_start__; +extern unsigned char __data_end__; + +/* The address the module is going to reach for and must not be allowed to have. + The value lives in the module's own initialised data, which is the point -- + the module reads it through its rebased GOT, so seeing it arrive in DFAR + proves the rebase and the .data copy both worked. Kept in step with + sample_threadx_module.c by hand, and checked at run time twice: that it is + what the module faulted on, and that it lies inside the kernel's data region + in the first place. An address in no region at all would fault too, and + would prove much less. */ + +#define MODULE_FORBIDDEN_ADDRESS 0x003EFFC0UL + +/* The shared status word. The same number is FVP_MODULE_STATUS_BASE in + platform.h, ORIGIN(MODULE) in link_module.lds and MODULE_STATUS_ADDRESS in + the module. Three spellings of one address, checked against the linker + symbol below rather than trusted. */ + +#define MODULE_STATUS_ADDRESS FVP_MODULE_STATUS_BASE +#define MODULE_STATUS_LENGTH FVP_MODULE_STATUS_SIZE + +/* The progress bits the module sets, mirrored from sample_threadx_module.c. + The two SURVIVED flags can only ever be set if the hardware allowed an access + it was configured to refuse, so they are checked for by name. */ + +#define MODULE_PROGRESS_OWN_DATA 0x00000001UL +#define MODULE_PROGRESS_KERNEL_CALL 0x00000002UL +#define MODULE_PROGRESS_ATTEMPTED_STEAL 0x00000004UL +#define MODULE_PROGRESS_SURVIVED_STEAL 0x00000008UL +#define MODULE_PROGRESS_ATTEMPTED_JUMP 0x00000010UL +#define MODULE_PROGRESS_SURVIVED_JUMP 0x00000020UL + +#define MODULE_PROGRESS_SURVIVED (MODULE_PROGRESS_SURVIVED_STEAL | \ + MODULE_PROGRESS_SURVIVED_JUMP) + +/* Which violation a pass tells the module to commit. Written into the module + instance's application-defined ID after the load and before the start, because + that word is what the manager hands the module's start thread. The high half + is the fingerprint the preamble ships; only the low byte selects the test. */ + +#define MODULE_ID_BASE 0x52520000UL +#define MODULE_TEST_DATA_ABORT 0x00000001UL +#define MODULE_TEST_PREFETCH_ABORT 0x00000002UL + +/* Memory the manager hands out to modules: object memory, and the data region a + module's own variables live in. It has to be outside every region a module is + given, or a module could reach another module's data. */ + +#ifndef MODULE_POOL_SIZE +#define MODULE_POOL_SIZE (16U * 1024U) +#endif + +/* In the module area, not in .bss. The manager carves a module's data out of + this pool, and that data is handed to the module as an MPU region -- so if the + pool lived in .bss it would sit inside the kernel's data region and the two + would overlap, which PMSAv8-R does not allow. */ + +static unsigned char module_pool[MODULE_POOL_SIZE] + __attribute__((aligned(64), section(".module_pool"))); + +/* The object pool, which is a separate allocation from the module pool above and + is not optional here. A module that runs in User mode needs a kernel stack + for the privileged side of each system call, and the manager allocates that + from this pool -- txm_module_manager_initialize deliberately leaves the pool + uncreated, so without this a User-mode module fails to start with + TX_NOT_AVAILABLE from deep inside the thread create. */ + +#ifndef MODULE_OBJECT_POOL_SIZE +#define MODULE_OBJECT_POOL_SIZE (8U * 1024U) +#endif + +static unsigned char module_object_pool[MODULE_OBJECT_POOL_SIZE] + __attribute__((aligned(64))); + +/* One instance per pass. Passes 1 and 2 are loaded at once for part of the run: + pass 1 is stopped but still holds its data allocation while pass 2 loads, which + is what pushes pass 2's data base somewhere different. */ + +#define MODULE_PASSES 3U +#define MODULE_RELOCATION_PASSES 2U + +static TXM_MODULE_INSTANCE demo_module[MODULE_PASSES]; + +/* What each pass produced. Recorded rather than printed as it happens, because + the comparison between the passes is the actual result and it cannot be made + until they have all run. */ + +typedef struct PASS_RESULT_STRUCT +{ + CHAR *pass_name; + ULONG pass_test; /* MODULE_TEST_*: which abort is expected */ + ULONG pass_load_status; + ULONG pass_share_status; + ULONG pass_start_status; + ULONG pass_stop_status; + ULONG pass_code_start; + ULONG pass_code_end; + ULONG pass_data_start; + ULONG pass_data_end; + ULONG pass_data_base; + ULONG pass_faults; + ULONG pass_captured; + ULONG pass_progress; + ULONG pass_expect_progress; + ULONG pass_fault_r9; + ULONG pass_dfsr; + ULONG pass_dfar; + ULONG pass_ifsr; + ULONG pass_ifar; + ULONG pass_spsr; + ULONG pass_code_location; + + /* What the notify callback was told, against what it should have been told. + Recorded as plain words rather than pointers so the console can print them + and a mismatch names both values. */ + + ULONG pass_notify_thread; + ULONG pass_notify_instance; + ULONG pass_expect_thread; + ULONG pass_expect_instance; +} PASS_RESULT; + +static PASS_RESULT pass_results[MODULE_PASSES]; + +/* What the fault handler saw. Read by the reporting thread after the module has + been terminated. */ + +static volatile ULONG fault_count; +static volatile ULONG fault_notify_thread; +static volatile ULONG fault_notify_instance; + +static TX_THREAD report_thread; +static unsigned char report_stack[2048] __attribute__((aligned(8))); + + +/**************************************************************************/ +/* Fault notification. */ +/* */ +/* Called by the module manager after it has terminated the offending */ +/* thread. Records rather than prints: this runs in Abort mode on the */ +/* Abort stack, which is a kilobyte and already carries the terminate */ +/* underneath this frame. A callback that printed would work here and */ +/* would still be the wrong shape to copy. */ +/* */ +/* Its two arguments are the point of the hook, so they are recorded and */ +/* checked rather than discarded: an application is being told WHICH */ +/* thread and WHICH module faulted, and a callback that fires with the */ +/* wrong pair is no more use than one that never fires. */ +/**************************************************************************/ + +static void module_fault_notify(TX_THREAD *thread_ptr, TXM_MODULE_INSTANCE *module_instance) +{ + fault_count++; + fault_notify_thread = (ULONG) thread_ptr; + fault_notify_instance = (ULONG) module_instance; +} + + +static void put_field(const char *label, unsigned long value) +{ + console_puts(label); + console_puthex(value); + console_puts("\n"); +} + + +/**************************************************************************/ +/* The shared status word, reached through the manager's load window. */ +/* */ +/* No kernel region covers the module area; region 16 does, and the */ +/* scheduler enables it for every thread that owns no module. This */ +/* thread owns none, so the window is open on it and these two functions */ +/* need no bracketing of their own -- which is the whole reason the */ +/* window is owned by the scheduler rather than by whoever calls. */ +/* */ +/* MISRA C:2012 Rule 11.6 is deliberately violated: the address is an */ +/* agreement between two separately linked images. */ +/**************************************************************************/ + +static void module_status_clear(void) +{ + *((volatile ULONG *) MODULE_STATUS_ADDRESS) = 0UL; +} + + +static ULONG module_status_read(void) +{ + return *((volatile ULONG *) MODULE_STATUS_ADDRESS); +} + + +/**************************************************************************/ +/* A byte copy, written out rather than called for. */ +/* */ +/* The manager links -nostartfiles and nothing else in this image reaches */ +/* for a C library, so calling memcpy would pull one in for a single */ +/* copy. The blob is under two kilobytes and this runs once. */ +/**************************************************************************/ + +static void copy_bytes(unsigned char *destination, const unsigned char *source, + unsigned long length) +{ + unsigned long i; + + for (i = 0UL; i < length; i++) + { + destination[i] = source[i]; + } +} + + +/**************************************************************************/ +/* One pass: load the blob from a given address, grant it the status */ +/* granule, start it, wait for the fault it is written to provoke, and */ +/* stop it. */ +/* */ +/* The module is left loaded. Its data allocation is what moves the next */ +/* pass's data base, and releasing it here would defeat half the test. */ +/**************************************************************************/ + +/* name is CHAR * and not const CHAR *, because txm_module_manager_in_place_load + takes it that way -- the manager stores the pointer in the instance and the API + has never promised not to write through it. */ + +static void run_one_pass(UINT index, CHAR *name, VOID *location, ULONG test) +{ + PASS_RESULT *result = &pass_results[index]; + TXM_MODULE_INSTANCE *instance = &demo_module[index]; + unsigned int waited; + + result -> pass_name = name; + result -> pass_test = test; + + /* What the module should manage before the hardware stops it: its own data, + a kernel call, and the attempt. Not the SURVIVED bit for either test -- + that one is the failure. */ + + result -> pass_expect_progress = MODULE_PROGRESS_OWN_DATA + | MODULE_PROGRESS_KERNEL_CALL + | ((test == MODULE_TEST_PREFETCH_ABORT) + ? MODULE_PROGRESS_ATTEMPTED_JUMP + : MODULE_PROGRESS_ATTEMPTED_STEAL); + + /* Cleared before the module starts, so a fault or a progress bit counted + here belongs to this pass and not the previous one. */ + + fault_count = 0UL; + fault_notify_thread = 0UL; + fault_notify_instance = 0UL; + module_status_clear(); + + result -> pass_load_status = (ULONG) txm_module_manager_in_place_load(instance, name, location); + + if (result -> pass_load_status != (ULONG) TX_SUCCESS) + { + return; + } + + /* The shared granule the module reports through. Granted while the module + is LOADED and before it is STARTED, which is the only window the manager + accepts, and granted per pass because each pass is its own instance with + its own region table. */ + + result -> pass_share_status = + (ULONG) txm_module_manager_external_memory_enable(instance, + (VOID *) &__module_status_start__, + MODULE_STATUS_LENGTH, + TXM_MODULE_ATTRIBUTE_READ_WRITE); + + if (result -> pass_share_status != (ULONG) TX_SUCCESS) + { + return; + } + + /* Which violation this pass provokes. Written after the load, which is what + filled the field in from the preamble, and before the start, which is what + hands it to the module's start thread. */ + + instance -> txm_module_instance_application_module_id = MODULE_ID_BASE | test; + + /* What the notify callback must be told, recorded before the module runs so + the comparison afterwards is against an expectation and not against + whatever the callback happened to write. */ + + result -> pass_expect_thread = (ULONG) &(instance -> txm_module_instance_start_stop_thread); + result -> pass_expect_instance = (ULONG) instance; + + /* Read from the instance rather than computed here. Where the code ended up + is what the manager decided, and the whole point of the comparison at the + end is to check the module against the manager's own numbers. */ + + result -> pass_code_start = (ULONG) instance -> txm_module_instance_code_start; + result -> pass_code_end = (ULONG) instance -> txm_module_instance_code_end; + result -> pass_data_start = (ULONG) instance -> txm_module_instance_data_start; + result -> pass_data_end = (ULONG) instance -> txm_module_instance_data_end; + result -> pass_data_base = (ULONG) instance -> txm_module_instance_module_data_base_address; + + result -> pass_start_status = (ULONG) txm_module_manager_start(instance); + + if (result -> pass_start_status != (ULONG) TX_SUCCESS) + { + return; + } + + /* Wait for the abort vector to capture a fault belonging to THIS pass. + + The captured fault info is waited on rather than the notify callback, even + though the callback works. The capture happens in the abort vector before + anything else runs, so it is the earliest and most direct evidence that a + fault occurred; the callback is a later consequence of the same event, and + this file checks it separately. Waiting on the earlier of the two means a + regression in the callback path shows up as "notified 0" next to a + captured fault, rather than as "no fault occurred" -- which is the opposite + of the truth. + + Attribution is by thread pointer: each pass has its own module instance and + therefore its own start thread, so a captured fault naming this pass's + thread cannot be a leftover from the previous pass. */ + + waited = 0U; + while ((_txm_module_manager_memory_fault_info.txm_module_manager_memory_fault_info_thread_ptr + != &(instance -> txm_module_instance_start_stop_thread)) && + (waited < 50U)) + { + tx_thread_sleep(2UL); + waited++; + } + + if (_txm_module_manager_memory_fault_info.txm_module_manager_memory_fault_info_thread_ptr + == &(instance -> txm_module_instance_start_stop_thread)) + { + result -> pass_captured = 1UL; + result -> pass_dfsr = _txm_module_manager_memory_fault_info.txm_module_manager_memory_fault_info_dfsr; + result -> pass_dfar = _txm_module_manager_memory_fault_info.txm_module_manager_memory_fault_info_dfar; + result -> pass_ifsr = _txm_module_manager_memory_fault_info.txm_module_manager_memory_fault_info_ifsr; + result -> pass_ifar = _txm_module_manager_memory_fault_info.txm_module_manager_memory_fault_info_ifar; + result -> pass_spsr = _txm_module_manager_memory_fault_info.txm_module_manager_memory_fault_info_spsr; + result -> pass_fault_r9 = _txm_module_manager_memory_fault_info.txm_module_manager_memory_fault_info_r9; + result -> pass_code_location = (ULONG) _txm_module_manager_memory_fault_info.txm_module_manager_memory_fault_info_code_location; + } + + /* What the module reached, and what the notify callback saw. Both read + after the wait above, so what ran is recorded and what did not shows as + zero. The status word is read while THIS pass is still the last thing to + have written it, which is the same discipline the silicon harness needs + for a different reason: there, the byte pool reuses a freed block and a + later pass's data lands where an earlier one's was. */ + + result -> pass_progress = module_status_read(); + result -> pass_faults = fault_count; + result -> pass_notify_thread = fault_notify_thread; + result -> pass_notify_instance = fault_notify_instance; + + /* Stopped, not unloaded. The fault terminated the module's start thread but + left the module STARTED, and unload refuses anything that is not LOADED or + STOPPED -- so this is required, not tidiness. */ + + result -> pass_stop_status = (ULONG) txm_module_manager_stop(instance); +} + + +static void report_one_pass(const PASS_RESULT *result) +{ + console_puts("\n--- "); + console_puts(result -> pass_name); + console_puts(" ---\n"); + + console_puts(" expected abort = "); + if (result -> pass_test == MODULE_TEST_PREFETCH_ABORT) + { + console_puts("prefetch (IFSR/IFAR)\n"); + } + else + { + console_puts("data (DFSR/DFAR)\n"); + } + + put_field(" load status = ", result -> pass_load_status); + put_field(" share status = ", result -> pass_share_status); + put_field(" start status = ", result -> pass_start_status); + put_field(" stop status = ", result -> pass_stop_status); + put_field(" code region = ", result -> pass_code_start); + put_field(" .. to = ", result -> pass_code_end); + put_field(" data region = ", result -> pass_data_start); + put_field(" .. to = ", result -> pass_data_end); + put_field(" data base (r9) = ", result -> pass_data_base); + put_field(" fault captured = ", result -> pass_captured); + put_field(" module progress = ", result -> pass_progress); + put_field(" should be = ", result -> pass_expect_progress); + put_field(" notify callbacks = ", result -> pass_faults); + put_field(" notified thread = ", result -> pass_notify_thread); + put_field(" should be = ", result -> pass_expect_thread); + put_field(" notified module = ", result -> pass_notify_instance); + put_field(" should be = ", result -> pass_expect_instance); + put_field(" r9 at the fault = ", result -> pass_fault_r9); + put_field(" DFSR = ", result -> pass_dfsr); + put_field(" DFAR = ", result -> pass_dfar); + put_field(" IFSR = ", result -> pass_ifsr); + put_field(" IFAR = ", result -> pass_ifar); + put_field(" SPSR = ", result -> pass_spsr); + put_field(" faulting pc = ", result -> pass_code_location); + + /* Meaningful for a data abort, where the module faulted inside its own code. + A prefetch abort faults ON the address it branched to, so the faulting pc + is outside the module and the difference is not an offset into it. */ + + if (result -> pass_test != MODULE_TEST_PREFETCH_ABORT) + { + put_field(" pc - code base = ", result -> pass_code_location - result -> pass_code_start); + } +} + + +/**************************************************************************/ +/* Per-pass verdict. Returns the number of failures it found. */ +/**************************************************************************/ + +static UINT judge_one_pass(const PASS_RESULT *result) +{ + if ((result -> pass_load_status != (ULONG) TX_SUCCESS) || + (result -> pass_share_status != (ULONG) TX_SUCCESS) || + (result -> pass_start_status != (ULONG) TX_SUCCESS)) + { + console_puts("FAIL "); + console_puts(result -> pass_name); + console_puts(": did not load, share and start\n"); + return 1U; + } + + /* First, and named explicitly: the module must not have survived what it + was told to attempt. This is the one result that means the protection + did not hold, and it is checked before anything else so that a run in + which the MPU did nothing says so in those words rather than through + some downstream symptom. */ + + if ((result -> pass_progress & MODULE_PROGRESS_SURVIVED) != 0UL) + { + console_puts("FAIL "); + console_puts(result -> pass_name); + console_puts(": the module SURVIVED its violation -- it is not protected\n"); + return 1U; + } + + if (result -> pass_captured == 0UL) + { + console_puts("FAIL "); + console_puts(result -> pass_name); + console_puts(": reached outside its memory and was not stopped\n"); + return 1U; + } + + /* What it did manage, from the shared granule. A module that faulted + without having read its own data or made a kernel call faulted for the + wrong reason, and the fault alone would not have said so. */ + + if (result -> pass_progress != result -> pass_expect_progress) + { + console_puts("FAIL "); + console_puts(result -> pass_name); + console_puts(": did not reach the point it was supposed to fault at\n"); + return 1U; + } + + /* r9 is the module's PIC base and the manager seeded it from the thread + entry info. If the value captured at the fault is not the data base the + manager handed out, the seeding is wrong and every data reference the + module made went somewhere unintended. */ + + if (result -> pass_fault_r9 != result -> pass_data_base) + { + console_puts("FAIL "); + console_puts(result -> pass_name); + console_puts(": r9 was not the module's data base\n"); + return 1U; + } + + /* A fault from anywhere but User mode is not this test passing: the module + runs unprivileged, so a privileged fault means the fault came from the + kernel and something else is wrong. */ + + if ((result -> pass_spsr & 0x1FUL) != 0x10UL) + { + console_puts("FAIL "); + console_puts(result -> pass_name); + console_puts(": faulted, but not from User mode\n"); + return 1U; + } + + /* The module got this address out of its own initialised data, through its + rebased GOT. Any other value means it faulted somewhere unintended -- + most likely on its own data, which is what a bad rebase looks like. + + Which register carries it depends on the abort: a data abort reports the + address it tried to touch in DFAR, a prefetch abort reports the address it + tried to fetch in IFAR. Checking the wrong one of the two passes for a + stale value left by an earlier fault, which is why this is selected on the + expected type rather than on whichever register happens to be non-zero. */ + + if (((result -> pass_test == MODULE_TEST_PREFETCH_ABORT) + ? result -> pass_ifar : result -> pass_dfar) != MODULE_FORBIDDEN_ADDRESS) + { + console_puts("FAIL "); + console_puts(result -> pass_name); + console_puts(": faulted at the wrong address, so .data or the GOT is wrong\n"); + return 1U; + } + + /* The notify callback is a public API of the module manager, so it is + checked and not merely reported. Zero here with a captured fault above + means the fault path reached the hardware's evidence and never reached the + application: on this port that used to be the case for every fault, + because the shared fault handler terminates the offending thread before + calling the hook and the terminate only returns if the abort vector has + told the kernel it is inside an exception. More than one means a single + violation was reported twice. */ + + if (result -> pass_faults != 1UL) + { + console_puts("FAIL "); + console_puts(result -> pass_name); + console_puts(": the fault-notify callback did not run exactly once\n"); + return 1U; + } + + /* Being told that something faulted is not the service; being told which + thread and which module is. */ + + if ((result -> pass_notify_thread != result -> pass_expect_thread) || + (result -> pass_notify_instance != result -> pass_expect_instance)) + { + console_puts("FAIL "); + console_puts(result -> pass_name); + console_puts(": notified about the wrong thread or module\n"); + return 1U; + } + + console_puts("PASS "); + console_puts(result -> pass_name); + console_puts(": ran, faulted in User mode at the forbidden address, and was reported\n"); + + return 0U; +} + + +/**************************************************************************/ +/* The manager thread. */ +/**************************************************************************/ + +static void manager_entry(ULONG input) +{ + UINT status; + UINT i; + unsigned long blob_size; + ULONG offset_0; + ULONG offset_1; + UINT failures = 0U; + + (void) input; + + /* Every manager call happens here, in a thread, and not in + tx_application_define. Loading a module takes the manager's mutex, and a + service that can block cannot be called before the scheduler runs. */ + + /* ------------------------------------------------------------------ + What the platform actually provides. Reported before anything is + loaded, because a shortfall here explains every failure below it. + ------------------------------------------------------------------ */ + + put_field("MPUIR regions available = ", (unsigned long) mpu_region_count()); + put_field("regions this port needs = ", (unsigned long) MPU_MODULE_REGIONS_REQUIRED); + put_field("D-cache line bytes = ", cache_dcache_line_bytes()); + + if (mpu_region_count() < MPU_MODULE_REGIONS_REQUIRED) + { + console_puts("FAIL the implementation has too few MPU regions for this port\n"); + failures++; + } + + /* The three spellings of the shared status address, and the forbidden + address, checked rather than trusted. Both are constants agreed between + separately linked images, and a disagreement in either would present as a + module faulting somewhere unexpected -- which is exactly what the pass + verdicts are trying to distinguish. */ + + if ((unsigned long) &__module_status_start__ != MODULE_STATUS_ADDRESS) + { + console_puts("FAIL the linker put the status word somewhere the module will not look\n"); + failures++; + } + + if ((unsigned long) (&__module_status_end__ - &__module_status_start__) + < MODULE_STATUS_LENGTH) + { + console_puts("FAIL the status granule is smaller than the region granted over it\n"); + failures++; + } + + if ((MODULE_FORBIDDEN_ADDRESS < (unsigned long) &__data_start__) || + (MODULE_FORBIDDEN_ADDRESS >= (unsigned long) &__data_end__)) + { + /* The point of the test is that a module cannot reach the KERNEL'S + memory. An address in no region at all would fault too, and would + prove much less, so the address is checked to be inside the kernel's + writable region before anything relies on the fault it causes. */ + + console_puts("FAIL the forbidden address is not inside the kernel's data region\n"); + failures++; + } + + console_puts("\nM1 initialize\n"); + status = txm_module_manager_initialize((VOID *) module_pool, MODULE_POOL_SIZE); + + put_field("initialize = ", status); + put_field("ready = ", (unsigned long) _txm_module_manager_ready); + + /* The pool the module's kernel stacks come from. See module_object_pool. */ + + status = txm_module_manager_object_pool_create((VOID *) module_object_pool, + MODULE_OBJECT_POOL_SIZE); + + put_field("object pool create = ", status); + + /* Registered before anything is loaded, so there is no window in which a + module could fault with nobody listening. */ + + console_puts("M2 fault notify\n"); + txm_module_manager_memory_fault_notify(module_fault_notify); + + /* The copy that makes pass 2 a relocation test. The staging area is left + empty by the linker on purpose: if the blob were already there, a failure + to copy would be invisible because the right bytes would be present. */ + + blob_size = (unsigned long) (&__module_image_end__ - &__module_image_start__); + + console_puts("M3 staging the blob\n"); + put_field(" blob size = ", blob_size); + put_field(" from = ", (unsigned long) &__module_image_start__); + put_field(" to = ", (unsigned long) &__module_stage_start__); + + if (blob_size > (unsigned long) (&__module_stage_end__ - &__module_stage_start__)) + { + /* Reported rather than allowed to overrun. The staging area is a fixed + size in the linker script and the module is free to grow. */ + + console_puts("FAIL the module image does not fit the staging area\n"); + failures++; + } + else + { + copy_bytes(&__module_stage_start__, &__module_image_start__, blob_size); + + /* Those were data writes to memory that is about to be fetched as + instructions, and the module area is mapped Normal write-back. So the + copied bytes may sit in dirty D-cache lines while the instruction + side, which is not coherent with the D cache on this core, fetches + whatever main memory still holds. Clean the range so memory is + correct, then invalidate the I cache so no stale line from a previous + image can be served. + + Not a precaution the passing run justifies: a cold I cache over a + never-executed address happens to work, and keeps happening to work + until the staging area is reused or an eviction lands differently. + Any loader that copies code owes this pair. */ + + cache_clean_range(&__module_stage_start__, blob_size); + cache_invalidate_icache_all(); + + console_puts("M4 pass 1, at the linked address\n"); + run_one_pass(0U, "linked address, data abort", (VOID *) &__module_image_start__, + MODULE_TEST_DATA_ABORT); + + console_puts("M5 pass 2, relocated\n"); + run_one_pass(1U, "relocated, data abort", (VOID *) &__module_stage_start__, + MODULE_TEST_DATA_ABORT); + + /* Both relocation passes are done with their memory now. Unloaded here + rather than at the end, so that the pass below is a load that follows + two faults and two unloads -- which is the state a manager is in after + a module has misbehaved, and the state the next load has to work in. */ + + for (i = 0U; i < MODULE_RELOCATION_PASSES; i++) + { + (void) txm_module_manager_unload(&demo_module[i]); + } + + /* The other abort type, from the staging area so that it is also a + relocated module: the address arriving in IFAR came out of the + module's own initialised data through its rebased GOT, exactly as DFAR + does above. */ + + console_puts("M6 pass 3, prefetch abort\n"); + run_one_pass(2U, "relocated, prefetch abort", (VOID *) &__module_stage_start__, + MODULE_TEST_PREFETCH_ABORT); + + (void) txm_module_manager_unload(&demo_module[2]); + } + + /* ------------------------------------------------------------------ + The verdict. + ------------------------------------------------------------------ */ + + console_puts("\n=== ThreadX modules on the Armv8-R AEM FVP: " + "isolation and relocation ===\n"); + + for (i = 0U; i < MODULE_PASSES; i++) + { + report_one_pass(&pass_results[i]); + } + + console_puts("\n"); + + /* Before anything else: a module's code, data and shared regions are enabled + at the same time, and PMSAv8-R has no region priority -- two enabled + regions that overlap are CONSTRAINED UNPREDICTABLE, and that means aborts + from addresses that look perfectly legal. Nothing in the manager checks + for it, so this example does. + + The risk is real here and not theoretical: the image, the pool and the + staging area all live in one 64 KB area. A module grown large enough, or + a pool sized differently, and two of them would meet. */ + + for (i = 0U; i < MODULE_PASSES; i++) + { + const PASS_RESULT *result = &pass_results[i]; + + if (result -> pass_load_status != (ULONG) TX_SUCCESS) + { + continue; + } + + if ((result -> pass_code_start <= result -> pass_data_end) && + (result -> pass_data_start <= result -> pass_code_end)) + { + console_puts("FAIL "); + console_puts(result -> pass_name); + console_puts(": its code and data regions overlap\n"); + failures++; + } + + if ((result -> pass_code_start <= (MODULE_STATUS_ADDRESS + MODULE_STATUS_LENGTH - 1UL)) && + (MODULE_STATUS_ADDRESS <= result -> pass_code_end)) + { + console_puts("FAIL "); + console_puts(result -> pass_name); + console_puts(": the shared status region overlaps its code\n"); + failures++; + } + + if ((result -> pass_data_start <= (MODULE_STATUS_ADDRESS + MODULE_STATUS_LENGTH - 1UL)) && + (MODULE_STATUS_ADDRESS <= result -> pass_data_end)) + { + console_puts("FAIL "); + console_puts(result -> pass_name); + console_puts(": the shared status region overlaps its data\n"); + failures++; + } + } + + /* Each pass on its own. */ + + for (i = 0U; i < MODULE_PASSES; i++) + { + failures += judge_one_pass(&pass_results[i]); + } + + /* And the first two passes against each other, which is the relocation + result. The third is not in this comparison: it faults on the address it + branched to rather than inside its own code, so its faulting pc is not an + offset into the module and there is nothing to compare. */ + + offset_0 = pass_results[0].pass_code_location - pass_results[0].pass_code_start; + offset_1 = pass_results[1].pass_code_location - pass_results[1].pass_code_start; + + if ((pass_results[0].pass_captured == 0UL) || (pass_results[1].pass_captured == 0UL)) + { + console_puts("FAIL relocation: a pass did not fault, nothing to compare\n"); + failures++; + } + else if (pass_results[0].pass_code_start == pass_results[1].pass_code_start) + { + console_puts("FAIL relocation: both passes ran from the same address\n"); + failures++; + } + else if (offset_0 != offset_1) + { + console_puts("FAIL relocation: the two passes faulted at different offsets\n"); + failures++; + } + else + { + console_puts("PASS relocation: the same blob ran correctly from two addresses\n"); + + if (pass_results[0].pass_data_base == pass_results[1].pass_data_base) + { + /* Not a failure -- the code rebase is still proven -- but worth + saying, because it means the data rebase produced the same numbers + twice and was not exercised as thoroughly. */ + + console_puts("NOTE both passes shared a data base; the data rebase was not varied\n"); + } + } + + /* Both abort types, stated as its own line. Each pass above already checked + its own registers; this says that between them the two vectors were both + taken, which is the claim a reader of the log wants to be able to make + without working out what each pass did. */ + + if ((pass_results[0].pass_captured != 0UL) && (pass_results[0].pass_dfsr != 0UL) && + (pass_results[2].pass_captured != 0UL) && (pass_results[2].pass_ifsr != 0UL)) + { + console_puts("PASS both abort types: data through DFSR/DFAR, prefetch through IFSR/IFAR\n"); + } + else + { + console_puts("FAIL only one kind of abort was exercised\n"); + failures++; + } + + /* The manager is still alive to print this, which is itself the last + result: three module faults did not take the kernel with them. */ + + put_field("\nfailures = ", (unsigned long) failures); + + if (failures == 0U) + { + console_puts("\nMODULE RESULT: ALL CHECKS PASSED\n"); + } + else + { + console_puts("\nMODULE RESULT: FAILED\n"); + } + + /* Terminates the model. Every FVP image in this port exits through + semihosting SYS_EXIT, so the runner needs no host-side timeout and a hang + cannot be mistaken for a slow run. */ + + console_exit(failures); +} + + +/**************************************************************************/ +/* Application definition. */ +/**************************************************************************/ + +void tx_application_define(void *first_unused_memory) +{ + UINT status; + + (void) first_unused_memory; + + console_puts("\n=== module manager starting ===\n"); + + /* Nothing but the thread. See manager_entry for why the manager cannot be + driven from here. */ + + status = tx_thread_create(&report_thread, "module manager", manager_entry, 0UL, + report_stack, sizeof(report_stack), + 20U, 20U, TX_NO_TIME_SLICE, TX_AUTO_START); + + /* Reported rather than discarded. A thread that was never created and a + thread that was created but never scheduled produce exactly the same + silence on the console, and telling those two apart is most of the work + when the port itself is what is under test. */ + + if (status == TX_SUCCESS) + { + console_puts("M0 thread created\n"); + } + else + { + put_field("FAIL tx_thread_create status = ", (unsigned long) status); + } +} + + +/**************************************************************************/ +/* bsp_main -- entered at EL1 from entry.S. Does not return. */ +/**************************************************************************/ + +void bsp_main(void) +{ + console_puts("\n=== ThreadX modules :: Armv8-R AEM FVP (Cortex-R52) ===\n"); + console_puts("entering kernel\n"); + + tx_kernel_enter(); + + /* Not reached. */ + + console_puts("FAIL tx_kernel_enter returned\n"); + + console_exit(1U); +} diff --git a/ports_module/cortex_r52/gnu/example_build/fvp_baser_aemv8r/txm_module_preamble.S b/ports_module/cortex_r52/gnu/example_build/fvp_baser_aemv8r/txm_module_preamble.S new file mode 100644 index 000000000..fa4858f67 --- /dev/null +++ b/ports_module/cortex_r52/gnu/example_build/fvp_baser_aemv8r/txm_module_preamble.S @@ -0,0 +1,137 @@ +@/*************************************************************************** +@ * 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 +@ **************************************************************************/ +@ +@/**************************************************************************/ +@/* */ +@/* MODULE PREAMBLE RELEASE */ +@/* */ +@/* txm_module_preamble.S Cortex-R52/GNU */ +@/* 6.5.2 */ +@/* AUTHOR */ +@/* */ +@/* Frédéric Desbiens, Eclipse Foundation */ +@/* */ +@/* DESCRIPTION */ +@/* */ +@/* The header the module manager reads before it will load a module. */ +@/* */ +@/* It must be the first thing in the module image, which is what the */ +@/* linker script arranges, and every entry point in it is an offset */ +@/* from the start of the preamble rather than an address. A module is */ +@/* position independent: the manager decides where it lands, so the */ +@/* module cannot know its own addresses at link time. */ +@/* */ +@/* Code and data sizes come from the linker script rather than being */ +@/* written in by hand. The Cortex-R4 preamble carries literal numbers */ +@/* for them, which is a standing invitation to grow a module past its */ +@/* declared size and have the manager map less memory than it uses -- */ +@/* a fault in a module that did nothing wrong, whose cause is a */ +@/* constant in a file nobody thought to change. */ +@/* */ +@/**************************************************************************/ + + .syntax unified + .arm + + .global __txm_module_preamble + + .extern _txm_module_thread_shell_entry + .extern _txm_module_callback_request_thread_entry + .extern demo_module_start + +@ Supplied by the module's linker script. + + .extern __txm_module_code_size + .extern __txm_module_data_size + +@ Properties. The compiler field tells the manager which set of entry-point +@ adjustments to apply, and the option bits say what the module is asking for: +@ user mode and memory protection, which together are the point of this port. +@ +@ 0x02000000 TXM_MODULE_GNU_COMPILER +@ 0x00000001 TXM_MODULE_USER_MODE +@ 0x00000002 TXM_MODULE_MEMORY_PROTECTION + + .equ MODULE_PROPERTIES, 0x02000003 + + .section .txm_module_preamble, "a" + .align 6 + +__txm_module_preamble: + + .word 0x4D4F4455 @ Module ID, "MODU" + .word 0x6 @ Major version + .word 0x1 @ Minor version + .word 32 @ Preamble size, 32-bit words + .word 0x52520001 @ Application-defined ID + .word MODULE_PROPERTIES @ Properties, see above + +@ Entry points, as offsets from the preamble. + +@ Entry points are stored relative to the word that holds them, not to the +@ start of the preamble. That is what the manager expects: it recovers the +@ offset from the module base by adding the field's own byte offset back -- +@ TXM_MODULE_GNU_SHELL_ADJUST 24, START 28, STOP 32, CALLBACK 44, which are +@ exactly the offsets of the four words below. Storing these relative to +@ __txm_module_preamble instead counts that offset twice, and the module is +@ then entered that many bytes into its shell entry: past the prologue, with +@ the arguments never saved and the frame pointer never set up, so the first +@ dereference goes through a register the stack build had zeroed. On silicon +@ that read faulted at 0x1C, which is offset 0x1C from a null r3. +@ +@ Every other GNU module port writes these the same way; cortex_m33's +@ preamble, which this port was seeded from, spells it "symbol - . - 0". + + .word _txm_module_thread_shell_entry - . + .word demo_module_start - . + .word 0 @ No stop thread + .word 1 @ Start/stop thread priority + .word 1024 @ Start/stop thread stack size + .word _txm_module_callback_request_thread_entry - . + .word 1 @ Callback thread priority + .word 1024 @ Callback thread stack size + +@ Sizes, from the linker script. The manager rounds both up to the 64-byte MPU +@ granule and maps exactly this much; anything the module touches beyond it +@ faults, which is the intended behaviour and not a bug to work around by +@ inflating these numbers. + + .word __txm_module_code_size + .word __txm_module_data_size + + .word 0 @ Reserved 0 + .word 0 @ Reserved 1 + .word 0 @ Reserved 2 + .word 0 @ Reserved 3 + .word 0 @ Reserved 4 + .word 0 @ Reserved 5 + .word 0 @ Reserved 6 + .word 0 @ Reserved 7 + .word 0 @ Reserved 8 + .word 0 @ Reserved 9 + .word 0 @ Reserved 10 + .word 0 @ Reserved 11 + .word 0 @ Reserved 12 + .word 0 @ Reserved 13 + .word 0 @ Reserved 14 + .word 0 @ Reserved 15 + +@ The preamble declares its own length in its fourth word, and the manager +@ believes it. If the two ever disagree the manager reads entry points from the +@ wrong offsets, so the assembler checks rather than the reader. + + .if (. - __txm_module_preamble) != (32 * 4) + .error "txm_module_preamble is not the 32 words its size field declares" + .endif From 87ee71ee132e16c39035a4eaa947807607e17e51 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20Desbiens?= Date: Thu, 20 Aug 2026 10:01:49 -0400 Subject: [PATCH 30/33] Added the shared-region regression to both module examples A fourth pass on the FVP and on the S32Z280 exercises the shared-memory half of the module port, which until now had one region granted and four of its five entries never used at all. The shared area becomes six 64-byte granules at the base of the module region: five the pass is granted, one entry each, and granule 2 -- sandwiched between two granted ones -- which the manager never grants. The module writes every granule it was given, reads all of them back, then writes the gap, which must fault. The gap sits between two grants deliberately: the defect worth catching is a region covering one granule too many, and it reaches the gap from either direction. The readback matters as much as the write, because an entry programmed with the wrong base accepts the store and simply puts it somewhere else. The same pass probes the two refusals nothing had ever called. An unaligned grant must return TXM_MODULE_ALIGNMENT_ERROR and one grant past the entry count must return TX_NO_MEMORY. The unaligned probe runs first, because external_memory_enable checks the entry count before it checks alignment and would otherwise answer TX_NO_MEMORY and say nothing about alignment. The sixth-grant probe aims at the gap, so a check that ever failed would be reported twice: once as a status and once by name as the module surviving its violation. No port source changed. external_memory_enable and alignment_adjust were already correct once the limit mask landed in 7636b0c7, so this is the evidence rather than a fix. What alignment_adjust produces is now checked directly: code_start, data_start and data_end + 1 must be granule-aligned in every pass. Not code_end, which is the true end of the image and is rounded to nothing. Fixed one real defect that the negative runs found. Both examples read the instance's region addresses after the shared grant, so a grant failure left them zero and the end-of-run overlap comparison then reported "its code and data regions overlap" -- a false second failure pointing at the wrong thing. They are read straight after the load now, which is what decided them. The S32Z280 module now reports progress through a shared granule as well, as the FVP one already did, and its manager reads that on the target. The gdb harness still reads the module's own copy out of the pass's data area, and the two readings are cross-checked against each other. The two sample modules are back to differing only by the board named in the header and the two addresses at the bottom, so diff remains the way to spot drift. diagnose_module.gdb lost the hardcoded 0x317F05xx addresses it had carried since before the module became relocatable. FVP: 603 targets, zero warnings, ctest 6/6. All three negative runs were done -- an over-wide grant, an alignment probe made aligned, and the alignment check made to require 128 bytes -- each failing loudly and by name. S32Z280: 596 targets, zero warnings, === PASS === with failures = 0 from the target and PASS from the gdb harness. Pass 4 on the part reported DFSR 0x00000A04 with WnR set, DFAR 0x317F0084, SPSR mode 0x10, five grants, five entries held, 0xF0 for the unaligned grant, 0x10 for the sixth, each granted granule holding its own mark and granule 2 still zero. Assisted-by: Claude Code (Opus 5) --- .../example_build/fvp_baser_aemv8r/platform.h | 35 +- .../gnu/example_build/s32z280_evb/platform.h | 29 + .../fvp_baser_aemv8r/link_module.lds | 47 +- .../fvp_baser_aemv8r/sample_threadx_module.c | 146 ++++- .../sample_threadx_module_manager.c | 463 ++++++++++++-- .../example_build/s32z280_evb/link_module.lds | 51 +- .../s32z280_evb/sample_threadx_module.c | 206 ++++-- .../sample_threadx_module_manager.c | 600 +++++++++++++++++- .../s32z280_evb/tools/diagnose_module.gdb | 32 +- .../s32z280_evb/tools/run_module_demo.gdb | 162 ++++- 10 files changed, 1577 insertions(+), 194 deletions(-) diff --git a/ports/cortex_r52/gnu/example_build/fvp_baser_aemv8r/platform.h b/ports/cortex_r52/gnu/example_build/fvp_baser_aemv8r/platform.h index c3d5cf6d1..151f732d4 100644 --- a/ports/cortex_r52/gnu/example_build/fvp_baser_aemv8r/platform.h +++ b/ports/cortex_r52/gnu/example_build/fvp_baser_aemv8r/platform.h @@ -94,22 +94,35 @@ #define FVP_MODULE_AREA_BASE 0x003F0000 #define FVP_MODULE_AREA_SIZE 0x00010000 /* 64 KB */ -/* The status word the sample module reports its progress through, at the base - of the module area. - - It exists because the FVP has no debugger seam: on silicon a GDB harness - reads the module's own progress variable out of its data area, and here - nothing outside the image can read anything. So the manager grants the - module a shared region over this word and reads it back afterwards, which is - what lets the FVP test judge what the module actually managed to do rather +/* The shared granules the sample module reports its progress through, at the + base of the module area. + + The first of them exists because the FVP has no debugger seam: on silicon a + GDB harness reads the module's own progress variable out of its data area, + and here nothing outside the image can read anything. So the manager grants + the module a shared region over this word and reads it back afterwards, which + is what lets the FVP test judge what the module actually managed to do rather than only that it faulted. - A fixed address on both sides, and checked at run time rather than trusted: - the manager grants the region at the linker's symbol and refuses to continue - if that is not this address. */ + The rest exist to exercise the shared-region machinery itself. A module may + be granted TXM_MODULE_MPU_SHARED_ENTRIES regions, and one grant proves only + that the first entry works, so the area holds one granule per entry -- plus + one more that the manager NEVER grants, sandwiched between two that it does. + A limit register masked the wrong way, or a base off by a granule, leaks into + that gap from one side or the other, and a module that can write it is a + module whose grant covered more than was asked for. + + FVP_MODULE_STATUS_SIZE is the size of ONE granule, which is the length of + each individual grant; the area is FVP_MODULE_STATUS_GRANULES of them. + + Fixed addresses on both sides, and checked at run time rather than trusted: + the manager grants the regions from the linker's symbol and refuses to + continue if that symbol is not this address. */ #define FVP_MODULE_STATUS_BASE 0x003F0000 #define FVP_MODULE_STATUS_SIZE 0x40 /* one MPU granule */ +#define FVP_MODULE_STATUS_GRANULES 6 /* five granted, one not */ +#define FVP_MODULE_STATUS_UNGRANTED 2 /* the one never granted */ #ifndef __ASSEMBLER__ 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 0ad91cecb..1440fbd10 100644 --- a/ports/cortex_r52/gnu/example_build/s32z280_evb/platform.h +++ b/ports/cortex_r52/gnu/example_build/s32z280_evb/platform.h @@ -150,6 +150,35 @@ #define S32Z_MODULE_AREA_BASE 0x317F0000UL #define S32Z_MODULE_AREA_SIZE 0x00010000UL /* 64 KB */ +/* The shared granules the sample module reports its progress through, at the + base of the module area. + + A module cannot print -- the console belongs to the board support package and + lies outside every region a module owns -- so the manager grants it shared + regions and reads them back. On this board a GDB harness reads the module's + own progress variable as well, out of the data area the manager allocated; + these granules are the channel that needs no debugger, and having both means + the two agree or the run says so. + + Six granules rather than one, because a module may be granted + TXM_MODULE_MPU_SHARED_ENTRIES regions and one grant only ever exercises the + first of them. Five are granted, one granule per entry, and + S32Z_MODULE_STATUS_UNGRANTED -- index 2, so a granted granule sits on either + side of it -- is never granted to anything. A limit register masked the wrong + way, or a base off by a granule, extends a region into that gap from one side + or the other, and a module that can write it was given more than was asked + for. + + S32Z_MODULE_STATUS_SIZE is the size of ONE granule, which is also the length + of each individual grant; the area is GRANULES of them. Fixed addresses on + both sides, checked at run time against the linker's symbol rather than + trusted. */ + +#define S32Z_MODULE_STATUS_BASE 0x317F0000UL +#define S32Z_MODULE_STATUS_SIZE 0x40UL /* one MPU granule */ +#define S32Z_MODULE_STATUS_GRANULES 6UL /* five granted, one not */ +#define S32Z_MODULE_STATUS_UNGRANTED 2UL /* the one never granted */ + /* What is left of the full-speed pair for the kernel itself. link.lds sizes its DATA region from this, so the linker cannot place kernel data in the module area by accident. */ diff --git a/ports_module/cortex_r52/gnu/example_build/fvp_baser_aemv8r/link_module.lds b/ports_module/cortex_r52/gnu/example_build/fvp_baser_aemv8r/link_module.lds index cbc34798c..11b6a778b 100644 --- a/ports_module/cortex_r52/gnu/example_build/fvp_baser_aemv8r/link_module.lds +++ b/ports_module/cortex_r52/gnu/example_build/fvp_baser_aemv8r/link_module.lds @@ -172,24 +172,35 @@ SECTIONS each becomes an enabled MPU region while a module runs. ------------------------------------------------------------------ */ - /* The status word the module reports its progress through. - - This exists because the FVP has no debugger seam. On silicon a GDB - harness reads the module's own progress variable out of the data area the - manager allocated for it; here nothing outside the image can read - anything, and the manager deliberately knows no symbol of the module. So - the manager grants the module a shared MPU region over this word and - reads it back afterwards, which is what lets the test judge what the - module managed to do rather than only that it faulted. - - First in the module area, so its address is ORIGIN(MODULE) and the module - can carry it as a constant. Exactly one granule: a shared region is a - region like any other and cannot overlap the image below it. */ + /* The shared granules the module reports its progress through, and the one + it is not allowed to reach. + + The first granule exists because the FVP has no debugger seam. On + silicon a GDB harness reads the module's own progress variable out of the + data area the manager allocated for it; here nothing outside the image + can read anything, and the manager deliberately knows no symbol of the + module. So the manager grants the module a shared MPU region over this + word and reads it back afterwards, which is what lets the test judge what + the module managed to do rather than only that it faulted. + + Six granules rather than one, because a module may be granted five shared + regions and granting a single one exercises only the first entry of the + five. Five of these are granted, one granule per entry, and the sixth -- + FVP_MODULE_STATUS_UNGRANTED, index 2, so that a granted granule sits on + either side of it -- is never granted to anything. The module writes + every granted granule, reads them all back, and then writes the gap, + which must fault. A limit masked the wrong way or a base off by a + granule leaks into that gap from one side or the other. + + First in the module area, so the base address is ORIGIN(MODULE) and the + module can carry it as a constant. A shared region is a region like any + other and cannot overlap the image below it, hence whole granules and the + alignment at the end. */ .module_status (NOLOAD) : { __module_status_start__ = .; - . = . + 0x40; + . = . + 0x180; /* six 64-byte granules */ . = ALIGN(64); __module_status_end__ = .; } > MODULE @@ -272,4 +283,12 @@ SECTIONS ASSERT(__module_status_start__ == 0x003F0000, "the module status word is not at FVP_MODULE_STATUS_BASE") + + /* And its size, for the same reason: the manager grants one region per + granule from this base and the module addresses them by index, so an area + that is not exactly FVP_MODULE_STATUS_GRANULES granules long would leave + a grant, or a module write, outside it. */ + + ASSERT(__module_status_end__ - __module_status_start__ == 0x180, + "the module status area is not FVP_MODULE_STATUS_GRANULES granules long") } diff --git a/ports_module/cortex_r52/gnu/example_build/fvp_baser_aemv8r/sample_threadx_module.c b/ports_module/cortex_r52/gnu/example_build/fvp_baser_aemv8r/sample_threadx_module.c index d11a7b26c..f93a8d9a8 100644 --- a/ports_module/cortex_r52/gnu/example_build/fvp_baser_aemv8r/sample_threadx_module.c +++ b/ports_module/cortex_r52/gnu/example_build/fvp_baser_aemv8r/sample_threadx_module.c @@ -28,47 +28,55 @@ /* A module that exercises the protection boundary rather than */ /* demonstrating features, for the Armv8-R AEM FVP. */ /* */ -/* The same four steps as the S32Z280 module, because it is the same */ -/* port under test: */ +/* The S32Z280 copy of this file is the same module for the same port; */ +/* what differs is the two addresses at the bottom and the board named */ +/* here, so `diff` is the tool for telling whether the two have drifted. */ +/* */ +/* Four steps: */ /* */ /* 1. Writes and reads its own data, which must succeed. */ /* 2. Makes a kernel call, which must succeed -- proving a module in */ /* User mode can reach the kernel through the supervisor call */ /* boundary and come back. */ -/* 3. Violates its protection in one of the two ways the hardware */ -/* distinguishes, which must fault. */ +/* 3. Violates its protection in one of three ways the manager */ +/* selects, which must fault. */ /* 4. Never reaches step 4, because step 3 terminates it. */ /* */ +/* THE THREE VIOLATIONS. Two of them are the two aborts the hardware */ +/* distinguishes: reading the kernel's data is a DATA abort reported */ +/* through DFSR and DFAR, branching out of the code region is a */ +/* PREFETCH abort reported through IFSR and IFAR. The third writes a */ +/* granule of the SHARED area that the manager deliberately did not */ +/* grant, after writing and reading back every granule it did -- so it */ +/* is the shared-region machinery under test rather than the kernel's */ +/* own memory, and a grant that covered one granule too many is what it */ +/* is looking for. */ +/* */ /* Steps 1 and 2 passing without step 3 faulting would mean the module */ /* is running unprotected, which is the failure this example exists to */ /* detect. A module that only ever touched its own memory would pass */ /* identically with the MPU switched off. */ /* */ -/* ONE DIFFERENCE FROM THE SILICON MODULE, and it is what makes this */ -/* one testable without a person: progress is written to a shared word */ -/* as well as to the module's own data. */ -/* */ -/* A module cannot print -- the console belongs to the board support */ -/* package, outside every region a module owns, so reaching it would */ -/* fault as surely as step 3 does. On silicon that is answered by a */ -/* GDB harness, which reads the module's own progress variable out of */ -/* the data area the manager allocated for it. The FVP has no such */ -/* seam: it exposes an Iris server and no GDB stub, so whatever the run */ -/* is going to report has to be reported by the image itself. */ +/* HOW PROGRESS GETS OUT. A module cannot print: the console belongs */ +/* to the board support package, outside every region a module owns, so */ +/* reaching it would fault as surely as step 3 does. So progress is */ +/* recorded twice -- in the module's own data, and in the first granule */ +/* of the shared area the manager granted it. */ /* */ -/* So the manager grants this module a shared MPU region over one */ -/* 64-byte granule at a fixed address and the module records its */ -/* progress there, where the manager can read it back after the fault. */ -/* The address is a constant on both sides -- the module has no loader */ -/* to tell it anything and the manager deliberately knows no symbol of */ -/* the module -- and the manager checks the two agree rather than */ -/* trusting them to. */ +/* Which of the two can be read depends on the board. On silicon a GDB */ +/* harness reads the module's own copy out of the data area the manager */ +/* allocated for it; the FVP has no such seam -- it exposes an Iris */ +/* server and no GDB stub -- so there only the shared copy is readable */ +/* and everything the run reports has to be reported by the image */ +/* itself. Both writes are kept on both boards deliberately: if the */ +/* shared write were the only one, a module that could not reach its own */ +/* data would still report progress. */ /* */ -/* The module's own progress variable is kept as well, unread. It */ -/* costs nothing, it keeps this file next to the silicon one, and it */ -/* means the two writes are the same code path: if the shared write */ -/* were the only one, a module that could not reach its own data would */ -/* still report progress. */ +/* The shared address is a literal on this side. The module has no */ +/* loader to tell it anything and the manager deliberately knows no */ +/* symbol of the module, so the two agree by convention -- and the */ +/* manager checks that they do, against the linker's own symbol, rather */ +/* than trusting them to. */ /* */ /**************************************************************************/ @@ -84,6 +92,9 @@ #define MODULE_PROGRESS_SURVIVED_STEAL 0x00000008UL #define MODULE_PROGRESS_ATTEMPTED_JUMP 0x00000010UL #define MODULE_PROGRESS_SURVIVED_JUMP 0x00000020UL +#define MODULE_PROGRESS_SHARED_WROTE 0x00000040UL +#define MODULE_PROGRESS_ATTEMPTED_GAP 0x00000080UL +#define MODULE_PROGRESS_SURVIVED_GAP 0x00000100UL /* Which violation to commit, taken from the low byte of the module ID the manager passes to the start thread. The rest of that word is left alone: the @@ -93,6 +104,7 @@ #define MODULE_TEST_MASK 0x000000FFUL #define MODULE_TEST_DATA_ABORT 0x00000001UL #define MODULE_TEST_PREFETCH_ABORT 0x00000002UL +#define MODULE_TEST_SHARED_ABORT 0x00000003UL /* The shared status word, at the base of the module area. @@ -112,6 +124,33 @@ #define MODULE_STATUS (*((volatile ULONG *) MODULE_STATUS_ADDRESS)) +/* And the rest of the shared area, which exists to exercise the shared-region + machinery rather than to report anything. + + The manager may grant a module TXM_MODULE_MPU_SHARED_ENTRIES regions and one + grant only ever proves the first entry works, so the area holds one granule + per entry -- five granted, and a sixth that the manager never grants. The + ungranted one is index 2, which puts a granted granule on either side of it: + a limit register masked the wrong way, or a base off by a granule, leaks into + that gap from below or from above, and either way a module that can write it + was given more than was asked for. + + The mark for each granule goes at WORD 1, because word 0 of granule 0 is the + progress word above. Uniform across all six so the address the gap write + faults on is one expression on both sides of the agreement. */ + +#define MODULE_STATUS_GRANULE 0x40UL +#define MODULE_STATUS_GRANULES 6UL +#define MODULE_STATUS_UNGRANTED 2UL +#define MODULE_STATUS_MARK_OFFSET 4UL + +#define MODULE_SHARED_SIGNATURE 0x5A5A0000UL + +#define MODULE_SHARED_MARK(index) \ + (*((volatile ULONG *) (MODULE_STATUS_ADDRESS \ + + ((index) * MODULE_STATUS_GRANULE) \ + + MODULE_STATUS_MARK_OFFSET))) + volatile ULONG module_progress; volatile ULONG module_scratch[16]; @@ -136,6 +175,7 @@ void demo_module_start(ULONG id) { ULONG i; ULONG sum = 0UL; + ULONG shared_ok = 1UL; /* 1. The module's own data. If this faults, the data region is wrong and nothing else in this file will be reached. */ @@ -174,7 +214,57 @@ void demo_module_start(ULONG id) manager fails on: they can only appear if the hardware allowed an access it was configured to refuse. */ - if ((id & MODULE_TEST_MASK) == MODULE_TEST_PREFETCH_ABORT) + if ((id & MODULE_TEST_MASK) == MODULE_TEST_SHARED_ABORT) + { + /* Write every granule the manager granted, read all of them back, and + then write the one it did not grant. + + The readback matters as much as the write. A shared entry programmed + with the wrong base still accepts a store -- it just lands somewhere + else -- so a write that is never read back proves only that the core + did not object. Reading each granule's own mark out of its own + granule is what says the five entries describe five distinct + extents. */ + + for (i = 0UL; i < MODULE_STATUS_GRANULES; i++) + { + if (i != MODULE_STATUS_UNGRANTED) + { + MODULE_SHARED_MARK(i) = MODULE_SHARED_SIGNATURE | i; + } + } + + for (i = 0UL; i < MODULE_STATUS_GRANULES; i++) + { + if ((i != MODULE_STATUS_UNGRANTED) && + (MODULE_SHARED_MARK(i) != (MODULE_SHARED_SIGNATURE | i))) + { + shared_ok = 0UL; + } + } + + if (shared_ok != 0UL) + { + module_progress |= MODULE_PROGRESS_SHARED_WROTE; + MODULE_STATUS |= MODULE_PROGRESS_SHARED_WROTE; + } + + /* The gap. A WRITE rather than a read, so DFSR reports WnR set and the + fault cannot be confused with the read the data-abort passes do. It + lies between two granules this module was granted, so surviving it + means a grant covered a granule nobody asked for -- which is the + whole reason this pass exists. */ + + module_progress |= MODULE_PROGRESS_ATTEMPTED_GAP; + MODULE_STATUS |= MODULE_PROGRESS_ATTEMPTED_GAP; + + MODULE_SHARED_MARK(MODULE_STATUS_UNGRANTED) = + MODULE_SHARED_SIGNATURE | MODULE_STATUS_UNGRANTED; + + module_progress |= MODULE_PROGRESS_SURVIVED_GAP; + MODULE_STATUS |= MODULE_PROGRESS_SURVIVED_GAP; + } + else if ((id & MODULE_TEST_MASK) == MODULE_TEST_PREFETCH_ABORT) { /* Branch outside the code region: a prefetch abort, reported through IFSR and IFAR. Nothing is executed at the target -- the fault is on diff --git a/ports_module/cortex_r52/gnu/example_build/fvp_baser_aemv8r/sample_threadx_module_manager.c b/ports_module/cortex_r52/gnu/example_build/fvp_baser_aemv8r/sample_threadx_module_manager.c index ecd092016..869ba72db 100644 --- a/ports_module/cortex_r52/gnu/example_build/fvp_baser_aemv8r/sample_threadx_module_manager.c +++ b/ports_module/cortex_r52/gnu/example_build/fvp_baser_aemv8r/sample_threadx_module_manager.c @@ -25,7 +25,7 @@ /* */ /* DESCRIPTION */ /* */ -/* Loads the sample module three times, lets it misbehave every time, */ +/* Loads the sample module four times, lets it misbehave every time, */ /* and reports what the hardware did about it -- on the Armv8-R AEM */ /* FVP, with no debugger and no person in the loop. */ /* */ @@ -77,6 +77,30 @@ /* event seen from both ends, and a regression that showed one without */ /* the other would be worth knowing about. */ /* */ +/* AND A FOURTH PASS FOR THE SHARED REGIONS. The three above are each */ +/* granted one shared region -- the status granule -- which exercises */ +/* the first of the five shared entries the port provides and says */ +/* nothing about the other four. The fourth pass is granted all five, */ +/* one 64-byte granule each, and is NOT granted the granule that sits */ +/* between two of them. It writes every granule it was given, reads */ +/* every one of them back, and then writes the gap, which must fault. */ +/* */ +/* That shape is chosen against a specific defect. A limit register */ +/* masked the wrong way, or a base off by one granule, extends a region */ +/* past what was asked for -- and with the gap sandwiched between two */ +/* granted granules it is reachable from either side if that happens. */ +/* The readback matters as much as the write: a region programmed with */ +/* the wrong base accepts a store and puts it elsewhere, so five marks */ +/* read out of five granules is what says five distinct extents were */ +/* programmed rather than one of them five times. */ +/* */ +/* The same pass probes the two ways the manager refuses a grant, which */ +/* nothing had ever called: an unaligned address must come back */ +/* TXM_MODULE_ALIGNMENT_ERROR, and one grant past the entry count must */ +/* come back TX_NO_MEMORY. Both are checked by name. The order is not */ +/* free -- the entry-count check runs before the alignment check, so the */ +/* unaligned probe has to happen while entries remain. */ +/* */ /* WHAT A GREEN RUN HERE DOES NOT PROVE. The model reports 32 EL1 MPU */ /* regions. The Cortex-R52 TRM gives MPUIR.DREGION as 16, 20 or 24, so */ /* 32 is not an architecturally permitted value for this core and the */ @@ -145,6 +169,41 @@ extern unsigned char __data_end__; #define MODULE_STATUS_ADDRESS FVP_MODULE_STATUS_BASE #define MODULE_STATUS_LENGTH FVP_MODULE_STATUS_SIZE +/* The rest of the shared area, which is there to exercise the shared-region + machinery rather than to report anything. + + MODULE_STATUS_LENGTH is one granule and also the length of one grant. The + area is MODULE_STATUS_GRANULES of them: five the shared pass is granted, one + entry each, and MODULE_STATUS_UNGRANTED -- index 2, so that a granted granule + sits on either side of it -- which is never granted to anything. Both sides + of the agreement compute a granule's address the same way from the same base, + and each granule's mark goes at word 1 because word 0 of granule 0 is the + progress word. */ + +#define MODULE_STATUS_GRANULES FVP_MODULE_STATUS_GRANULES +#define MODULE_STATUS_UNGRANTED FVP_MODULE_STATUS_UNGRANTED +#define MODULE_STATUS_AREA_LENGTH (MODULE_STATUS_GRANULES * MODULE_STATUS_LENGTH) +#define MODULE_STATUS_MARK_OFFSET 4UL + +#define MODULE_SHARED_SIGNATURE 0x5A5A0000UL + +/* How many grants the shared pass makes, which must be every entry the port + provides -- granting fewer would leave an entry unexercised, and the point of + the pass is that all of them work. */ + +#define MODULE_SHARED_GRANTS (MODULE_STATUS_GRANULES - 1U) + +/* MISRA C:2012 Rule 11.6 is deliberately violated by the second of these: the + address is an agreement between two separately linked images and there is no + conforming way to express it. */ + +#define MODULE_GRANULE_ADDRESS(index) \ + (MODULE_STATUS_ADDRESS + ((index) * MODULE_STATUS_LENGTH)) + +#define MODULE_SHARED_MARK(index) \ + (*((volatile ULONG *) (MODULE_GRANULE_ADDRESS(index) \ + + MODULE_STATUS_MARK_OFFSET))) + /* The progress bits the module sets, mirrored from sample_threadx_module.c. The two SURVIVED flags can only ever be set if the hardware allowed an access it was configured to refuse, so they are checked for by name. */ @@ -155,9 +214,13 @@ extern unsigned char __data_end__; #define MODULE_PROGRESS_SURVIVED_STEAL 0x00000008UL #define MODULE_PROGRESS_ATTEMPTED_JUMP 0x00000010UL #define MODULE_PROGRESS_SURVIVED_JUMP 0x00000020UL +#define MODULE_PROGRESS_SHARED_WROTE 0x00000040UL +#define MODULE_PROGRESS_ATTEMPTED_GAP 0x00000080UL +#define MODULE_PROGRESS_SURVIVED_GAP 0x00000100UL #define MODULE_PROGRESS_SURVIVED (MODULE_PROGRESS_SURVIVED_STEAL | \ - MODULE_PROGRESS_SURVIVED_JUMP) + MODULE_PROGRESS_SURVIVED_JUMP | \ + MODULE_PROGRESS_SURVIVED_GAP) /* Which violation a pass tells the module to commit. Written into the module instance's application-defined ID after the load and before the start, because @@ -167,6 +230,7 @@ extern unsigned char __data_end__; #define MODULE_ID_BASE 0x52520000UL #define MODULE_TEST_DATA_ABORT 0x00000001UL #define MODULE_TEST_PREFETCH_ABORT 0x00000002UL +#define MODULE_TEST_SHARED_ABORT 0x00000003UL /* Memory the manager hands out to modules: object memory, and the data region a module's own variables live in. It has to be outside every region a module is @@ -202,7 +266,7 @@ static unsigned char module_object_pool[MODULE_OBJECT_POOL_SIZE] pass 1 is stopped but still holds its data allocation while pass 2 loads, which is what pushes pass 2's data base somewhere different. */ -#define MODULE_PASSES 3U +#define MODULE_PASSES 4U #define MODULE_RELOCATION_PASSES 2U static TXM_MODULE_INSTANCE demo_module[MODULE_PASSES]; @@ -228,6 +292,23 @@ typedef struct PASS_RESULT_STRUCT ULONG pass_captured; ULONG pass_progress; ULONG pass_expect_progress; + + /* The shared-region results. Only the shared pass fills these in, so the + judge reads them only for that pass -- zero is TX_SUCCESS and a pass that + never probed would otherwise read as one that probed and was refused + nothing. */ + + ULONG pass_shared_grants; /* grants that succeeded */ + ULONG pass_shared_count; /* entries the instance holds after */ + ULONG pass_align_status; /* refusing an unaligned grant */ + ULONG pass_exhaust_status; /* refusing one grant too many */ + ULONG pass_marks[MODULE_STATUS_GRANULES]; + + /* The address this pass must fault on. Per pass rather than one constant, + because the shared pass faults on the granule it was not granted and the + other three fault on the kernel's data. */ + + ULONG pass_expect_fault; ULONG pass_fault_r9; ULONG pass_dfsr; ULONG pass_dfar; @@ -305,7 +386,18 @@ static void put_field(const char *label, unsigned long value) static void module_status_clear(void) { - *((volatile ULONG *) MODULE_STATUS_ADDRESS) = 0UL; + ULONG granule; + + /* The WHOLE area, not just the progress word. The gap granule has to start + at a known zero, because "the module never wrote it" is one of the results + and a leftover mark from the previous pass would read as a module that + reached memory it was never granted -- or hide one that did. */ + + for (granule = 0UL; granule < MODULE_STATUS_GRANULES; granule++) + { + *((volatile ULONG *) MODULE_GRANULE_ADDRESS(granule)) = 0UL; + MODULE_SHARED_MARK(granule) = 0UL; + } } @@ -336,9 +428,107 @@ static void copy_bytes(unsigned char *destination, const unsigned char *source, /**************************************************************************/ -/* One pass: load the blob from a given address, grant it the status */ -/* granule, start it, wait for the fault it is written to provoke, and */ -/* stop it. */ +/* The shared grants a pass gets, and the two ways a grant is refused. */ +/* */ +/* Every pass is granted the first granule, which is the progress word it */ +/* reports through. The shared pass is granted one granule per shared */ +/* entry the port provides, skipping the gap, because a single grant only */ +/* ever exercises the first of the five and this port has never run the */ +/* other four. */ +/* */ +/* It also probes the two ways a grant is refused, which can only be done */ +/* on a LOADED instance. ORDER MATTERS: the manager checks the entry */ +/* count BEFORE it checks alignment, so the unaligned probe has to happen */ +/* while entries remain -- after five grants it would come back */ +/* TX_NO_MEMORY and say nothing about alignment at all. */ +/* */ +/* Returns the first grant status that was not TX_SUCCESS, or TX_SUCCESS. */ +/**************************************************************************/ + +static UINT grant_shared_regions(TXM_MODULE_INSTANCE *instance, PASS_RESULT *result) +{ + UINT status; + UINT first_failure = TX_SUCCESS; + ULONG granule; + + if (result -> pass_test != MODULE_TEST_SHARED_ABORT) + { + /* The progress word, and nothing else. */ + + return txm_module_manager_external_memory_enable(instance, + (VOID *) &__module_status_start__, + MODULE_STATUS_LENGTH, + TXM_MODULE_ATTRIBUTE_READ_WRITE); + } + + /* Refused for alignment. One word into the first granule: an address + inside the area, so what is being tested is the 64-byte granule rule and + not the range. It must also consume no entry, which the five grants + below prove by all succeeding. */ + + result -> pass_align_status = + (ULONG) txm_module_manager_external_memory_enable( + instance, + (VOID *) (&__module_status_start__ + MODULE_STATUS_MARK_OFFSET), + MODULE_STATUS_LENGTH, + TXM_MODULE_ATTRIBUTE_READ_WRITE); + + /* One region per granule, every entry the port has, and never the gap. */ + + for (granule = 0UL; granule < MODULE_STATUS_GRANULES; granule++) + { + if (granule != MODULE_STATUS_UNGRANTED) + { + status = txm_module_manager_external_memory_enable( + instance, + (VOID *) (&__module_status_start__ + (granule * MODULE_STATUS_LENGTH)), + MODULE_STATUS_LENGTH, + TXM_MODULE_ATTRIBUTE_READ_WRITE); + + if (status == TX_SUCCESS) + { + result -> pass_shared_grants++; + } + else if (first_failure == TX_SUCCESS) + { + first_failure = status; + } + else + { + /* Already recorded; the first failure is the informative one. */ + } + } + } + + /* And one grant too many, which must be refused. Not cosmetic: the entry + written is TXM_MODULE_MPU_SHARED_INDEX plus the count, so a sixth grant + that got past the check would write one past the end of the region table + and into the instance fields that follow it. + + Aimed at the GAP, deliberately. It overlaps nothing, so a refusal costs + nothing -- and if the check ever failed, the gap would become a granted + region and the module would go on to survive writing it. The same defect + would then be reported twice, once as this status and once by name as + MODULE_PROGRESS_SURVIVED_GAP, rather than only as a status nobody reads. */ + + result -> pass_exhaust_status = + (ULONG) txm_module_manager_external_memory_enable( + instance, + (VOID *) (&__module_status_start__ + + (MODULE_STATUS_UNGRANTED * MODULE_STATUS_LENGTH)), + MODULE_STATUS_LENGTH, + TXM_MODULE_ATTRIBUTE_READ_WRITE); + + result -> pass_shared_count = instance -> txm_module_instance_shared_memory_count; + + return first_failure; +} + + +/**************************************************************************/ +/* One pass: load the blob from a given address, grant it the shared */ +/* granules it is entitled to, start it, wait for the fault it is written */ +/* to provoke, and stop it. */ /* */ /* The module is left loaded. Its data allocation is what moves the next */ /* pass's data base, and releasing it here would defeat half the test. */ @@ -353,19 +543,47 @@ static void run_one_pass(UINT index, CHAR *name, VOID *location, ULONG test) PASS_RESULT *result = &pass_results[index]; TXM_MODULE_INSTANCE *instance = &demo_module[index]; unsigned int waited; + ULONG granule; result -> pass_name = name; result -> pass_test = test; /* What the module should manage before the hardware stops it: its own data, - a kernel call, and the attempt. Not the SURVIVED bit for either test -- - that one is the failure. */ + a kernel call, and the attempt -- plus, for the shared pass, the five + granules it wrote and read back on the way. Never a SURVIVED bit; that + one is the failure. - result -> pass_expect_progress = MODULE_PROGRESS_OWN_DATA - | MODULE_PROGRESS_KERNEL_CALL - | ((test == MODULE_TEST_PREFETCH_ABORT) - ? MODULE_PROGRESS_ATTEMPTED_JUMP - : MODULE_PROGRESS_ATTEMPTED_STEAL); + The address it must fault on goes with it, because the three tests do not + share one. Two of them reach into the kernel's data; the shared pass + writes the mark word of the granule it was not granted, and that address + is computed here exactly as the module computes it. */ + + if (test == MODULE_TEST_SHARED_ABORT) + { + result -> pass_expect_progress = MODULE_PROGRESS_OWN_DATA + | MODULE_PROGRESS_KERNEL_CALL + | MODULE_PROGRESS_SHARED_WROTE + | MODULE_PROGRESS_ATTEMPTED_GAP; + + result -> pass_expect_fault = MODULE_GRANULE_ADDRESS(MODULE_STATUS_UNGRANTED) + + MODULE_STATUS_MARK_OFFSET; + } + else if (test == MODULE_TEST_PREFETCH_ABORT) + { + result -> pass_expect_progress = MODULE_PROGRESS_OWN_DATA + | MODULE_PROGRESS_KERNEL_CALL + | MODULE_PROGRESS_ATTEMPTED_JUMP; + + result -> pass_expect_fault = MODULE_FORBIDDEN_ADDRESS; + } + else + { + result -> pass_expect_progress = MODULE_PROGRESS_OWN_DATA + | MODULE_PROGRESS_KERNEL_CALL + | MODULE_PROGRESS_ATTEMPTED_STEAL; + + result -> pass_expect_fault = MODULE_FORBIDDEN_ADDRESS; + } /* Cleared before the module starts, so a fault or a progress bit counted here belongs to this pass and not the previous one. */ @@ -382,16 +600,30 @@ static void run_one_pass(UINT index, CHAR *name, VOID *location, ULONG test) return; } - /* The shared granule the module reports through. Granted while the module - is LOADED and before it is STARTED, which is the only window the manager - accepts, and granted per pass because each pass is its own instance with - its own region table. */ + /* Read from the instance rather than computed here. Where the code ended up + is what the manager decided, and the whole point of the comparison at the + end is to check the module against the manager's own numbers. - result -> pass_share_status = - (ULONG) txm_module_manager_external_memory_enable(instance, - (VOID *) &__module_status_start__, - MODULE_STATUS_LENGTH, - TXM_MODULE_ATTRIBUTE_READ_WRITE); + Recorded HERE, immediately after the load and before anything that can + fail, because the load is what decided them. Taken any later, a pass that + failed to be granted its shared regions would carry zeros into the overlap + comparison at the end of the run -- and zero against zero overlaps, so a + grant failure was reported a second time as "its code and data regions + overlap", which is not true and points at the wrong thing. */ + + result -> pass_code_start = (ULONG) instance -> txm_module_instance_code_start; + result -> pass_code_end = (ULONG) instance -> txm_module_instance_code_end; + result -> pass_data_start = (ULONG) instance -> txm_module_instance_data_start; + result -> pass_data_end = (ULONG) instance -> txm_module_instance_data_end; + result -> pass_data_base = (ULONG) instance -> txm_module_instance_module_data_base_address; + + /* The shared granules the module reports through, and for the shared pass + every other entry as well. Granted while the module is LOADED and before + it is STARTED, which is the only window the manager accepts, and granted + per pass because each pass is its own instance with its own region table + -- an instance is memset by the load, so nothing carries over. */ + + result -> pass_share_status = (ULONG) grant_shared_regions(instance, result); if (result -> pass_share_status != (ULONG) TX_SUCCESS) { @@ -411,16 +643,6 @@ static void run_one_pass(UINT index, CHAR *name, VOID *location, ULONG test) result -> pass_expect_thread = (ULONG) &(instance -> txm_module_instance_start_stop_thread); result -> pass_expect_instance = (ULONG) instance; - /* Read from the instance rather than computed here. Where the code ended up - is what the manager decided, and the whole point of the comparison at the - end is to check the module against the manager's own numbers. */ - - result -> pass_code_start = (ULONG) instance -> txm_module_instance_code_start; - result -> pass_code_end = (ULONG) instance -> txm_module_instance_code_end; - result -> pass_data_start = (ULONG) instance -> txm_module_instance_data_start; - result -> pass_data_end = (ULONG) instance -> txm_module_instance_data_end; - result -> pass_data_base = (ULONG) instance -> txm_module_instance_module_data_base_address; - result -> pass_start_status = (ULONG) txm_module_manager_start(instance); if (result -> pass_start_status != (ULONG) TX_SUCCESS) @@ -477,6 +699,17 @@ static void run_one_pass(UINT index, CHAR *name, VOID *location, ULONG test) result -> pass_notify_thread = fault_notify_thread; result -> pass_notify_instance = fault_notify_instance; + /* Every granule's mark, granted or not. The granted ones say the module + wrote and read back the granule it meant to, which is what proves five + distinct entries were programmed rather than one entry five times; the + gap's says whether it stayed untouched. Read here, while this pass is + still the last thing to have written the area. */ + + for (granule = 0UL; granule < MODULE_STATUS_GRANULES; granule++) + { + result -> pass_marks[granule] = MODULE_SHARED_MARK(granule); + } + /* Stopped, not unloaded. The fault terminated the module's start thread but left the module STARTED, and unload refuses anything that is not LOADED or STOPPED -- so this is required, not tidiness. */ @@ -496,6 +729,10 @@ static void report_one_pass(const PASS_RESULT *result) { console_puts("prefetch (IFSR/IFAR)\n"); } + else if (result -> pass_test == MODULE_TEST_SHARED_ABORT) + { + console_puts("data (DFSR/DFAR), writing an ungranted shared granule\n"); + } else { console_puts("data (DFSR/DFAR)\n"); @@ -513,6 +750,7 @@ static void report_one_pass(const PASS_RESULT *result) put_field(" fault captured = ", result -> pass_captured); put_field(" module progress = ", result -> pass_progress); put_field(" should be = ", result -> pass_expect_progress); + put_field(" fault address = ", result -> pass_expect_fault); put_field(" notify callbacks = ", result -> pass_faults); put_field(" notified thread = ", result -> pass_notify_thread); put_field(" should be = ", result -> pass_expect_thread); @@ -534,6 +772,35 @@ static void report_one_pass(const PASS_RESULT *result) { put_field(" pc - code base = ", result -> pass_code_location - result -> pass_code_start); } + + /* The shared-region half, reported only by the pass that produces it. */ + + if (result -> pass_test == MODULE_TEST_SHARED_ABORT) + { + ULONG granule; + + put_field(" shared grants = ", result -> pass_shared_grants); + put_field(" should be = ", (unsigned long) MODULE_SHARED_GRANTS); + put_field(" entries held = ", result -> pass_shared_count); + put_field(" unaligned grant = ", result -> pass_align_status); + put_field(" should be = ", (unsigned long) TXM_MODULE_ALIGNMENT_ERROR); + put_field(" one grant too many = ", result -> pass_exhaust_status); + put_field(" should be = ", (unsigned long) TX_NO_MEMORY); + + for (granule = 0UL; granule < MODULE_STATUS_GRANULES; granule++) + { + if (granule == MODULE_STATUS_UNGRANTED) + { + put_field(" gap granule mark = ", result -> pass_marks[granule]); + put_field(" should be = ", 0UL); + } + else + { + put_field(" granule mark = ", result -> pass_marks[granule]); + put_field(" should be = ", MODULE_SHARED_SIGNATURE | granule); + } + } + } } @@ -623,7 +890,7 @@ static UINT judge_one_pass(const PASS_RESULT *result) expected type rather than on whichever register happens to be non-zero. */ if (((result -> pass_test == MODULE_TEST_PREFETCH_ABORT) - ? result -> pass_ifar : result -> pass_dfar) != MODULE_FORBIDDEN_ADDRESS) + ? result -> pass_ifar : result -> pass_dfar) != result -> pass_expect_fault) { console_puts("FAIL "); console_puts(result -> pass_name); @@ -631,6 +898,86 @@ static UINT judge_one_pass(const PASS_RESULT *result) return 1U; } + /* The shared-region results, for the one pass that produces them. Checked + after the progress word above, so a pass that never got as far as the + granules is reported as that rather than as a grant problem. */ + + if (result -> pass_test == MODULE_TEST_SHARED_ABORT) + { + ULONG granule; + + /* Every entry the port provides, granted. Fewer means an entry past + the first is not usable, which is the state this port was in before + this pass existed. */ + + if (result -> pass_shared_grants != (ULONG) MODULE_SHARED_GRANTS) + { + console_puts("FAIL "); + console_puts(result -> pass_name); + console_puts(": not every shared entry could be granted\n"); + return 1U; + } + + if (result -> pass_shared_count != (ULONG) TXM_MODULE_MPU_SHARED_ENTRIES) + { + console_puts("FAIL "); + console_puts(result -> pass_name); + console_puts(": the instance does not hold the entries it granted\n"); + return 1U; + } + + /* An unaligned grant must be refused BY NAME. Accepting it would not + fault: the low six bits of PRBAR are the shareability, permission and + execute-never fields, so an under-aligned base silently changes what + the region permits instead of where it is. */ + + if (result -> pass_align_status != (ULONG) TXM_MODULE_ALIGNMENT_ERROR) + { + console_puts("FAIL "); + console_puts(result -> pass_name); + console_puts(": an unaligned shared grant was not refused as such\n"); + return 1U; + } + + if (result -> pass_exhaust_status != (ULONG) TX_NO_MEMORY) + { + console_puts("FAIL "); + console_puts(result -> pass_name); + console_puts(": one grant too many was not refused\n"); + return 1U; + } + + /* And what the module actually managed to write where. Each granted + granule must hold its own mark -- a region programmed with the wrong + base accepts the store and puts it somewhere else -- and the gap must + still hold the zero the manager left in it. */ + + for (granule = 0UL; granule < MODULE_STATUS_GRANULES; granule++) + { + if (granule == MODULE_STATUS_UNGRANTED) + { + if (result -> pass_marks[granule] != 0UL) + { + console_puts("FAIL "); + console_puts(result -> pass_name); + console_puts(": the ungranted granule was written -- a grant covered too much\n"); + return 1U; + } + } + else if (result -> pass_marks[granule] != (MODULE_SHARED_SIGNATURE | granule)) + { + console_puts("FAIL "); + console_puts(result -> pass_name); + console_puts(": a granted granule does not hold its own mark\n"); + return 1U; + } + else + { + /* This granule is as it should be. */ + } + } + } + /* The notify callback is a public API of the module manager, so it is checked and not merely reported. Zero here with a captured fault above means the fault path reached the hardware's evidence and never reached the @@ -715,9 +1062,9 @@ static void manager_entry(ULONG input) } if ((unsigned long) (&__module_status_end__ - &__module_status_start__) - < MODULE_STATUS_LENGTH) + < MODULE_STATUS_AREA_LENGTH) { - console_puts("FAIL the status granule is smaller than the region granted over it\n"); + console_puts("FAIL the status area is smaller than the granules granted over it\n"); failures++; } @@ -819,6 +1166,19 @@ static void manager_entry(ULONG input) MODULE_TEST_PREFETCH_ABORT); (void) txm_module_manager_unload(&demo_module[2]); + + /* The shared-region pass. Granted every entry the port provides, one + granule each, and never the granule between two of them -- which it + then writes. This is the only pass that exercises a shared entry + past the first, the granule-alignment refusal, and what happens when + the entries run out; the three passes above grant one region each and + would look identical if four of the five entries did not work. */ + + console_puts("M7 pass 4, shared regions\n"); + run_one_pass(3U, "relocated, shared-region gap", (VOID *) &__module_stage_start__, + MODULE_TEST_SHARED_ABORT); + + (void) txm_module_manager_unload(&demo_module[3]); } /* ------------------------------------------------------------------ @@ -863,21 +1223,44 @@ static void manager_entry(ULONG input) failures++; } - if ((result -> pass_code_start <= (MODULE_STATUS_ADDRESS + MODULE_STATUS_LENGTH - 1UL)) && + if ((result -> pass_code_start <= (MODULE_STATUS_ADDRESS + MODULE_STATUS_AREA_LENGTH - 1UL)) && (MODULE_STATUS_ADDRESS <= result -> pass_code_end)) { console_puts("FAIL "); console_puts(result -> pass_name); - console_puts(": the shared status region overlaps its code\n"); + console_puts(": the shared status area overlaps its code\n"); failures++; } - if ((result -> pass_data_start <= (MODULE_STATUS_ADDRESS + MODULE_STATUS_LENGTH - 1UL)) && + if ((result -> pass_data_start <= (MODULE_STATUS_ADDRESS + MODULE_STATUS_AREA_LENGTH - 1UL)) && (MODULE_STATUS_ADDRESS <= result -> pass_data_end)) { console_puts("FAIL "); console_puts(result -> pass_name); - console_puts(": the shared status region overlaps its data\n"); + console_puts(": the shared status area overlaps its data\n"); + failures++; + } + + /* And what _txm_module_manager_alignment_adjust produced. It is called + by every load and has never been checked, because nothing it gets + wrong faults: it rounds a module's code and data sizes up to the + protection granule and declares the granule as their alignment, and + the loader allocates on that. Get it wrong and the region bases are + written into PRBAR with their low six bits landing on the shareability, + permission and execute-never fields -- so the module runs with + attributes nobody asked for rather than at the wrong address. + + The data region's end is checked as well as its base, because the data + region's length is the rounded size; the CODE region's end is not, it + is the true end of the image and is not rounded to anything. */ + + if (((result -> pass_code_start & (TXM_MODULE_MPU_ALIGNMENT - 1UL)) != 0UL) || + ((result -> pass_data_start & (TXM_MODULE_MPU_ALIGNMENT - 1UL)) != 0UL) || + (((result -> pass_data_end + 1UL) & (TXM_MODULE_MPU_ALIGNMENT - 1UL)) != 0UL)) + { + console_puts("FAIL "); + console_puts(result -> pass_name); + console_puts(": its regions are not aligned to the protection granule\n"); failures++; } } diff --git a/ports_module/cortex_r52/gnu/example_build/s32z280_evb/link_module.lds b/ports_module/cortex_r52/gnu/example_build/s32z280_evb/link_module.lds index b10acd827..a8962763e 100644 --- a/ports_module/cortex_r52/gnu/example_build/s32z280_evb/link_module.lds +++ b/ports_module/cortex_r52/gnu/example_build/s32z280_evb/link_module.lds @@ -95,14 +95,41 @@ SECTIONS KEEP(*(.vectors_el1)) } > CODE - /* The module image, at a fixed address in the module area. + /* The shared granules the module reports its progress through, and the one + it is not allowed to reach. + + FIRST in the module area, so the base address is ORIGIN(MODULE) and the + module can carry it as a constant -- the manager deliberately knows no + symbol of the module and has no channel through which to tell it anything. + S32Z_MODULE_STATUS_BASE in platform.h is the same number, and the ASSERTs + at the bottom of this file pin the two together at link time. + + Six granules. Five are granted, one per shared entry the port provides, + because granting a single region only ever exercises the first of the five; + the sixth -- index 2, so a granted granule sits on either side of it -- is + never granted, and the module writes it to prove it still faults. + + NOLOAD: the manager clears the area before every pass, and a granule that + arrived pre-filled would make a module that never wrote one look as though + it had. */ + + .module_status (NOLOAD) : + { + __module_status_start__ = .; + . = . + 0x180; /* six 64-byte granules */ + . = ALIGN(64); + __module_status_end__ = .; + } > MODULE + + /* The module image, in the module area, just above the shared granules. The module is not linked here. It is built as its own image by the s32z280_demo_module.elf target, objcopied to a raw binary and included as bytes by module_blob.S, and this section only decides where those bytes - land. The address has to agree with what the module was linked for -- - both come from S32Z_MODULE_AREA_BASE -- because an in-place load runs the - module where it lies. + land. Which address that is does not matter any more: since the module + became relocatable it is linked against nominal addresses it never runs + at and _gcc_setup rebases it to wherever it was put, so this only has to + be inside the module area and disjoint from everything else in it. The earlier arrangement compiled the module's sources into this image and gathered them here by object name. That cannot work. The module library @@ -298,4 +325,20 @@ SECTIONS PROVIDE(end = .); PROVIDE(_end = .); + + /* The module carries the status base as a constant and platform.h names it + as S32Z_MODULE_STATUS_BASE. Neither can see this script, so the agreement + is pinned here; the manager checks the same thing again at run time + against the symbol, so a build that skipped this file still says so. */ + + ASSERT(__module_status_start__ == 0x317F0000, + "the module status area is not at S32Z_MODULE_STATUS_BASE") + + /* And its size: the manager grants one region per granule from that base and + the module addresses them by index, so an area that is not exactly + S32Z_MODULE_STATUS_GRANULES granules long would leave a grant, or a module + write, outside it. */ + + ASSERT(__module_status_end__ - __module_status_start__ == 0x180, + "the module status area is not S32Z_MODULE_STATUS_GRANULES granules long") } diff --git a/ports_module/cortex_r52/gnu/example_build/s32z280_evb/sample_threadx_module.c b/ports_module/cortex_r52/gnu/example_build/s32z280_evb/sample_threadx_module.c index 5bd5adec9..5a2afaba9 100644 --- a/ports_module/cortex_r52/gnu/example_build/s32z280_evb/sample_threadx_module.c +++ b/ports_module/cortex_r52/gnu/example_build/s32z280_evb/sample_threadx_module.c @@ -26,48 +26,65 @@ /* DESCRIPTION */ /* */ /* A module that exercises the protection boundary rather than */ -/* demonstrating features. */ +/* demonstrating features, for the NXP S32Z280-594EVB. */ /* */ -/* The Armv8-M sample module creates eight threads, a queue, a */ -/* semaphore, a mutex, event flags and two pools. That shows the API */ -/* works; it does not show that the boundary works, and the boundary is */ -/* the only thing this port adds. So this module does four things and */ -/* stops: */ +/* The FVP copy of this file is the same module for the same port; what */ +/* differs is the two addresses at the bottom and the board named here, */ +/* so `diff` is the tool for telling whether the two have drifted. */ +/* */ +/* Four steps: */ /* */ /* 1. Writes and reads its own data, which must succeed. */ /* 2. Makes a kernel call, which must succeed -- proving a module in */ /* User mode can reach the kernel through the supervisor call */ /* boundary and come back. */ -/* 3. Violates its protection in one of the two ways the hardware */ -/* distinguishes, which must fault. */ +/* 3. Violates its protection in one of three ways the manager */ +/* selects, which must fault. */ /* 4. Never reaches step 4, because step 3 terminates it. */ /* */ +/* THE THREE VIOLATIONS. Two of them are the two aborts the hardware */ +/* distinguishes: reading the kernel's data is a DATA abort reported */ +/* through DFSR and DFAR, branching out of the code region is a */ +/* PREFETCH abort reported through IFSR and IFAR. The third writes a */ +/* granule of the SHARED area that the manager deliberately did not */ +/* grant, after writing and reading back every granule it did -- so it */ +/* is the shared-region machinery under test rather than the kernel's */ +/* own memory, and a grant that covered one granule too many is what it */ +/* is looking for. */ +/* */ /* Steps 1 and 2 passing without step 3 faulting would mean the module */ /* is running unprotected, which is the failure this example exists to */ /* detect. A module that only ever touched its own memory would pass */ /* identically with the MPU switched off. */ /* */ -/* TWO KINDS OF VIOLATION, because the core reports them differently. */ -/* Reading outside the data region is a DATA abort, reported through */ -/* DFSR and DFAR; branching outside the code region is a PREFETCH abort,*/ -/* reported through IFSR and IFAR. Both arrive at the same handler by */ -/* different vectors, and a module that only ever does the first leaves */ -/* half of the port's fault path unexecuted. Which one this module does */ -/* is chosen by the manager, through the application-defined module ID */ -/* that the manager passes to the start thread -- the module needs no */ -/* build-time variant and the manager needs no symbol of the module. */ +/* HOW PROGRESS GETS OUT. A module cannot print: the console belongs */ +/* to the board support package, outside every region a module owns, so */ +/* reaching it would fault as surely as step 3 does. So progress is */ +/* recorded twice -- in the module's own data, and in the first granule */ +/* of the shared area the manager granted it. */ +/* */ +/* Which of the two can be read depends on the board. On silicon a GDB */ +/* harness reads the module's own copy out of the data area the manager */ +/* allocated for it; the FVP has no such seam -- it exposes an Iris */ +/* server and no GDB stub -- so there only the shared copy is readable */ +/* and everything the run reports has to be reported by the image */ +/* itself. Both writes are kept on both boards deliberately: if the */ +/* shared write were the only one, a module that could not reach its own */ +/* data would still report progress. */ /* */ -/* The module cannot print. The console belongs to the board support */ -/* package, outside every region a module owns, so reaching it would */ -/* fault as surely as step 3 does. Progress is recorded in the */ -/* module's own data and read back by the manager, which can print. */ +/* The shared address is a literal on this side. The module has no */ +/* loader to tell it anything and the manager deliberately knows no */ +/* symbol of the module, so the two agree by convention -- and the */ +/* manager checks that they do, against the linker's own symbol, rather */ +/* than trusting them to. */ /* */ /**************************************************************************/ #include "txm_module.h" -/* Progress, in the module's own data. The manager reads this after the module - faults, which is how a module with no console reports what it managed to do. */ +/* Progress, recorded in two places. See the header: the shared copy is what + the manager reads, and the module's own copy is what proves it could write + its own data at all. */ #define MODULE_PROGRESS_OWN_DATA 0x00000001UL #define MODULE_PROGRESS_KERNEL_CALL 0x00000002UL @@ -75,6 +92,9 @@ #define MODULE_PROGRESS_SURVIVED_STEAL 0x00000008UL #define MODULE_PROGRESS_ATTEMPTED_JUMP 0x00000010UL #define MODULE_PROGRESS_SURVIVED_JUMP 0x00000020UL +#define MODULE_PROGRESS_SHARED_WROTE 0x00000040UL +#define MODULE_PROGRESS_ATTEMPTED_GAP 0x00000080UL +#define MODULE_PROGRESS_SURVIVED_GAP 0x00000100UL /* Which violation to commit, taken from the low byte of the module ID the manager passes to the start thread. The rest of that word is left alone: the @@ -84,37 +104,78 @@ #define MODULE_TEST_MASK 0x000000FFUL #define MODULE_TEST_DATA_ABORT 0x00000001UL #define MODULE_TEST_PREFETCH_ABORT 0x00000002UL +#define MODULE_TEST_SHARED_ABORT 0x00000003UL + +/* The shared status word, at the base of the module area. + + A literal address rather than anything reached through the GOT, because it is + not the module's own memory: it is memory the manager granted, and the module + has no channel through which to be told where it is. The same number is + S32Z_MODULE_STATUS_BASE in platform.h and ORIGIN(MODULE) in link_module.lds; + the manager checks all three agree before it starts this module, so a + disagreement is a reported failure rather than a fault at the first write. + + MISRA C:2012 Rule 11.6 (conversion between an integer and a pointer to void) + is deliberately violated: an address agreed between two separately linked + images can only be written as a literal, and there is no conforming way to + express it. */ + +#define MODULE_STATUS_ADDRESS 0x317F0000UL + +#define MODULE_STATUS (*((volatile ULONG *) MODULE_STATUS_ADDRESS)) + +/* And the rest of the shared area, which exists to exercise the shared-region + machinery rather than to report anything. + + The manager may grant a module TXM_MODULE_MPU_SHARED_ENTRIES regions and one + grant only ever proves the first entry works, so the area holds one granule + per entry -- five granted, and a sixth that the manager never grants. The + ungranted one is index 2, which puts a granted granule on either side of it: + a limit register masked the wrong way, or a base off by a granule, leaks into + that gap from below or from above, and either way a module that can write it + was given more than was asked for. + + The mark for each granule goes at WORD 1, because word 0 of granule 0 is the + progress word above. Uniform across all six so the address the gap write + faults on is one expression on both sides of the agreement. */ + +#define MODULE_STATUS_GRANULE 0x40UL +#define MODULE_STATUS_GRANULES 6UL +#define MODULE_STATUS_UNGRANTED 2UL +#define MODULE_STATUS_MARK_OFFSET 4UL + +#define MODULE_SHARED_SIGNATURE 0x5A5A0000UL + +#define MODULE_SHARED_MARK(index) \ + (*((volatile ULONG *) (MODULE_STATUS_ADDRESS \ + + ((index) * MODULE_STATUS_GRANULE) \ + + MODULE_STATUS_MARK_OFFSET))) volatile ULONG module_progress; volatile ULONG module_scratch[16]; /* An address the module has no business touching, used by step 3. - It defaults to the base of DRAM0, which is where the manager's own data and - stacks live on this board and is covered by a region the module does not own. - The manager can overwrite this before starting the module if it wants to aim - the test somewhere else, but it does not have to: locating a symbol inside a - loaded module means walking its image, and an example should not need that to - make its point. + The base of DRAM0, where the manager's own data and stacks live on this + board: that region is EL1 read/write and execute-never, so a User-mode read + of it is a permission fault and a User-mode instruction fetch from it is one + too. Both are what step 3 needs, and both prove the KERNEL'S memory is what + a module cannot reach -- which is the property being demonstrated, rather + than the absence of a mapping. - Whatever it holds must be outside every region the module owns, or step 3 - succeeds and the test silently proves nothing. */ + Held in initialised data rather than written as a literal, deliberately and + unlike the status address above. The module reads it through its rebased + GOT, so the value arriving in DFAR or IFAR also proves the GOT was rewritten + and .data was copied. The manager knows the same number and checks it. */ volatile ULONG module_forbidden_address = 0x31780000UL; -/* The same address serves the prefetch test. On this board it is the base of - the kernel's data region, which is mapped execute-never and privileged-only, - so a User-mode instruction fetch there is a permission fault whichever way it - is looked at. An address in no region at all would fault too, but this one - also proves the kernel's own memory is what a module cannot reach -- which is - the property being demonstrated, rather than the absence of a mapping. */ - - void demo_module_start(ULONG id) { ULONG i; ULONG sum = 0UL; + ULONG shared_ok = 1UL; /* 1. The module's own data. If this faults, the data region is wrong and nothing else in this file will be reached. */ @@ -132,6 +193,7 @@ void demo_module_start(ULONG id) if (sum == 360UL) /* 3 * (0 + 1 + ... + 15) */ { module_progress |= MODULE_PROGRESS_OWN_DATA; + MODULE_STATUS |= MODULE_PROGRESS_OWN_DATA; } /* 2. A kernel call, which leaves User mode through the supervisor call @@ -143,32 +205,84 @@ void demo_module_start(ULONG id) if (tx_thread_sleep(2UL) == TX_SUCCESS) { module_progress |= MODULE_PROGRESS_KERNEL_CALL; + MODULE_STATUS |= MODULE_PROGRESS_KERNEL_CALL; } /* 3. The violation, and 4. the flag that says it was tolerated. Each is marked before the access rather than after, because after is not - reached if the port is working. */ + reached if the port is working. The SURVIVED flags are what the + manager fails on: they can only appear if the hardware allowed an + access it was configured to refuse. */ - if ((id & MODULE_TEST_MASK) == MODULE_TEST_PREFETCH_ABORT) + if ((id & MODULE_TEST_MASK) == MODULE_TEST_SHARED_ABORT) + { + /* Write every granule the manager granted, read all of them back, and + then write the one it did not grant. + + The readback matters as much as the write. A shared entry programmed + with the wrong base still accepts a store -- it just lands somewhere + else -- so a write that is never read back proves only that the core + did not object. Reading each granule's own mark out of its own + granule is what says the five entries describe five distinct + extents. */ + + for (i = 0UL; i < MODULE_STATUS_GRANULES; i++) + { + if (i != MODULE_STATUS_UNGRANTED) + { + MODULE_SHARED_MARK(i) = MODULE_SHARED_SIGNATURE | i; + } + } + + for (i = 0UL; i < MODULE_STATUS_GRANULES; i++) + { + if ((i != MODULE_STATUS_UNGRANTED) && + (MODULE_SHARED_MARK(i) != (MODULE_SHARED_SIGNATURE | i))) + { + shared_ok = 0UL; + } + } + + if (shared_ok != 0UL) + { + module_progress |= MODULE_PROGRESS_SHARED_WROTE; + MODULE_STATUS |= MODULE_PROGRESS_SHARED_WROTE; + } + + /* The gap. A WRITE rather than a read, so DFSR reports WnR set and the + fault cannot be confused with the read the data-abort passes do. It + lies between two granules this module was granted, so surviving it + means a grant covered a granule nobody asked for -- which is the + whole reason this pass exists. */ + + module_progress |= MODULE_PROGRESS_ATTEMPTED_GAP; + MODULE_STATUS |= MODULE_PROGRESS_ATTEMPTED_GAP; + + MODULE_SHARED_MARK(MODULE_STATUS_UNGRANTED) = + MODULE_SHARED_SIGNATURE | MODULE_STATUS_UNGRANTED; + + module_progress |= MODULE_PROGRESS_SURVIVED_GAP; + MODULE_STATUS |= MODULE_PROGRESS_SURVIVED_GAP; + } + else if ((id & MODULE_TEST_MASK) == MODULE_TEST_PREFETCH_ABORT) { /* Branch outside the code region: a prefetch abort, reported through IFSR and IFAR. Nothing is executed at the target -- the fault is on the fetch itself, so the thread never arrives. */ module_progress |= MODULE_PROGRESS_ATTEMPTED_JUMP; + MODULE_STATUS |= MODULE_PROGRESS_ATTEMPTED_JUMP; /* MISRA C:2012 Rule 11.1 (no conversion between a pointer to a function and any other type) is deliberately violated here, and Rule 11.6 (no conversion between an integer and a pointer to void) with it. Provoking a fetch from an address that holds no function is the entire - purpose of these three lines; there is no conforming way to write it. - The address is read from the module's own initialised data so that the - value arriving in IFAR also proves the GOT rebase, exactly as the data - case uses DFAR. */ + purpose of these three lines; there is no conforming way to write it. */ ((void (*)(void)) module_forbidden_address)(); module_progress |= MODULE_PROGRESS_SURVIVED_JUMP; + MODULE_STATUS |= MODULE_PROGRESS_SURVIVED_JUMP; } else { @@ -176,10 +290,12 @@ void demo_module_start(ULONG id) DFAR. */ module_progress |= MODULE_PROGRESS_ATTEMPTED_STEAL; + MODULE_STATUS |= MODULE_PROGRESS_ATTEMPTED_STEAL; sum += *((volatile ULONG *) module_forbidden_address); module_progress |= MODULE_PROGRESS_SURVIVED_STEAL; + MODULE_STATUS |= MODULE_PROGRESS_SURVIVED_STEAL; } /* Keep the compiler from discarding the read above. */ diff --git a/ports_module/cortex_r52/gnu/example_build/s32z280_evb/sample_threadx_module_manager.c b/ports_module/cortex_r52/gnu/example_build/s32z280_evb/sample_threadx_module_manager.c index 93d720d31..742959d85 100644 --- a/ports_module/cortex_r52/gnu/example_build/s32z280_evb/sample_threadx_module_manager.c +++ b/ports_module/cortex_r52/gnu/example_build/s32z280_evb/sample_threadx_module_manager.c @@ -25,7 +25,7 @@ /* */ /* DESCRIPTION */ /* */ -/* Loads the sample module three times, lets it misbehave every time, */ +/* Loads the sample module four times, lets it misbehave every time, */ /* and reports what the hardware did about it. */ /* */ /* The result this example exists to produce is the fault. A module */ @@ -90,6 +90,38 @@ /* and this port's did not. It does now, so the hook is a result rather */ /* than a known defect. */ /* */ +/* AND A FOURTH PASS FOR THE SHARED REGIONS. The three above are each */ +/* granted one shared region -- the status granule they report their */ +/* progress through -- which exercises the first of the five shared */ +/* entries the port provides and says nothing about the other four. */ +/* The fourth pass is granted all five, one 64-byte granule each, and */ +/* is NOT granted the granule that sits between two of them. It writes */ +/* every granule it was given, reads every one of them back, and then */ +/* writes the gap, which must fault. */ +/* */ +/* That shape is chosen against a specific defect. A limit register */ +/* masked the wrong way, or a base off by one granule, extends a region */ +/* past what was asked for -- and with the gap sandwiched between two */ +/* granted granules it is reachable from either side if that happens. */ +/* The readback matters as much as the write: a region programmed with */ +/* the wrong base accepts a store and puts it elsewhere, so five marks */ +/* read out of five granules is what says five distinct extents were */ +/* programmed rather than one of them five times. */ +/* */ +/* The same pass probes the two ways the manager refuses a grant, which */ +/* nothing had ever called: an unaligned address must come back */ +/* TXM_MODULE_ALIGNMENT_ERROR, and one grant past the entry count must */ +/* come back TX_NO_MEMORY. Both are checked by name. The order is not */ +/* free -- the entry-count check runs before the alignment check, so the */ +/* unaligned probe has to happen while entries remain. */ +/* */ +/* A SECOND READING OF THE PROGRESS WORD comes with those granules. The */ +/* module records what it managed twice: in its own data, which the GDB */ +/* harness reads, and in the first shared granule, which this file now */ +/* reads on the target. The two are independent readings of the same */ +/* event, so the console judges progress without a debugger and the */ +/* harness still cross-checks it against the module's own copy. */ +/* */ /* What the module image is and where it comes from: it is linked into */ /* this application as a separate section and loaded in place, so */ /* nothing is copied and no filesystem or download path is needed. */ @@ -126,6 +158,12 @@ extern unsigned char __module_image_end__; extern unsigned char __module_stage_start__; extern unsigned char __module_stage_end__; +/* The shared granules the module reports its progress through, and the one it + is not allowed to reach. See S32Z_MODULE_STATUS_BASE in platform.h. */ + +extern unsigned char __module_status_start__; +extern unsigned char __module_status_end__; + /* The address the module is going to reach for and must not be allowed to have. Declared here only so the console check can name it; the value lives in the module's own initialised data, which is the point -- the module reads it @@ -136,6 +174,61 @@ extern unsigned char __module_stage_end__; #define MODULE_FORBIDDEN_ADDRESS 0x31780000UL +/* The shared area. The same numbers are S32Z_MODULE_STATUS_* in platform.h, + ORIGIN(MODULE) in link_module.lds and MODULE_STATUS_ADDRESS in the module; + the ASSERTs in the link script pin three of them together at link time and + the manager checks the linker's symbol at run time rather than trusting it. + + MODULE_STATUS_LENGTH is one granule and also the length of one grant. The + area is MODULE_STATUS_GRANULES of them: five the shared pass is granted, one + entry each, and MODULE_STATUS_UNGRANTED -- index 2, so a granted granule sits + on either side of it -- which is never granted to anything. Each granule's + mark goes at word 1, because word 0 of granule 0 is the progress word. + + MISRA C:2012 Rule 11.6 is deliberately violated by the accessors: an address + agreed between two separately linked images can only be written as a literal + and there is no conforming way to express it. */ + +#define MODULE_STATUS_ADDRESS S32Z_MODULE_STATUS_BASE +#define MODULE_STATUS_LENGTH S32Z_MODULE_STATUS_SIZE +#define MODULE_STATUS_GRANULES S32Z_MODULE_STATUS_GRANULES +#define MODULE_STATUS_UNGRANTED S32Z_MODULE_STATUS_UNGRANTED +#define MODULE_STATUS_AREA_LENGTH (MODULE_STATUS_GRANULES * MODULE_STATUS_LENGTH) +#define MODULE_STATUS_MARK_OFFSET 4UL + +#define MODULE_SHARED_SIGNATURE 0x5A5A0000UL + +/* How many grants the shared pass makes, which is every entry the port + provides -- granting fewer would leave an entry unexercised, and that the + other four work at all is what the pass exists to establish. */ + +#define MODULE_SHARED_GRANTS (MODULE_STATUS_GRANULES - 1UL) + +#define MODULE_GRANULE_ADDRESS(index) \ + (MODULE_STATUS_ADDRESS + ((index) * MODULE_STATUS_LENGTH)) + +#define MODULE_SHARED_MARK(index) \ + (*((volatile ULONG *) (MODULE_GRANULE_ADDRESS(index) \ + + MODULE_STATUS_MARK_OFFSET))) + +/* The progress bits the module sets, mirrored from sample_threadx_module.c. + The three SURVIVED flags can only ever be set if the hardware allowed an + access it was configured to refuse, so they are checked for by name. */ + +#define MODULE_PROGRESS_OWN_DATA 0x00000001UL +#define MODULE_PROGRESS_KERNEL_CALL 0x00000002UL +#define MODULE_PROGRESS_ATTEMPTED_STEAL 0x00000004UL +#define MODULE_PROGRESS_SURVIVED_STEAL 0x00000008UL +#define MODULE_PROGRESS_ATTEMPTED_JUMP 0x00000010UL +#define MODULE_PROGRESS_SURVIVED_JUMP 0x00000020UL +#define MODULE_PROGRESS_SHARED_WROTE 0x00000040UL +#define MODULE_PROGRESS_ATTEMPTED_GAP 0x00000080UL +#define MODULE_PROGRESS_SURVIVED_GAP 0x00000100UL + +#define MODULE_PROGRESS_SURVIVED (MODULE_PROGRESS_SURVIVED_STEAL | \ + MODULE_PROGRESS_SURVIVED_JUMP | \ + MODULE_PROGRESS_SURVIVED_GAP) + /* Which violation a pass tells the module to commit. Written into the module instance's application-defined ID after the load and before the start, because that word is what the manager hands the module's start thread. The high half @@ -145,6 +238,7 @@ extern unsigned char __module_stage_end__; #define MODULE_ID_BASE 0x52520000UL #define MODULE_TEST_DATA_ABORT 0x00000001UL #define MODULE_TEST_PREFETCH_ABORT 0x00000002UL +#define MODULE_TEST_SHARED_ABORT 0x00000003UL /* Memory the manager hands out to modules: object memory, and the data region a module's own variables live in. It has to be outside every region a module is @@ -183,7 +277,7 @@ static unsigned char module_object_pool[MODULE_OBJECT_POOL_SIZE] is what pushes pass 2's data base somewhere different. Pass 3 runs after both have been unloaded, which is deliberate -- see the header. */ -#define MODULE_PASSES 3U +#define MODULE_PASSES 4U #define MODULE_RELOCATION_PASSES 2U static TXM_MODULE_INSTANCE demo_module[MODULE_PASSES]; @@ -197,6 +291,7 @@ typedef struct PASS_RESULT_STRUCT CHAR *pass_name; ULONG pass_test; /* MODULE_TEST_*: which abort is expected */ ULONG pass_load_status; + ULONG pass_share_status; ULONG pass_start_status; ULONG pass_stop_status; ULONG pass_code_start; @@ -214,6 +309,30 @@ typedef struct PASS_RESULT_STRUCT ULONG pass_spsr; ULONG pass_code_location; + /* What the module reported through the shared granules. Read on the target + rather than only by the debug harness: the harness reads the module's own + copy of the same word out of its data area, so the two are independent + readings of the same event and a disagreement is worth seeing. */ + + ULONG pass_progress; + ULONG pass_expect_progress; + + /* The shared-region results. Only the shared pass fills these in, so they + are judged only for that pass -- zero is TX_SUCCESS, and a pass that never + probed would otherwise read as one that probed and was refused nothing. */ + + ULONG pass_shared_grants; /* grants that succeeded */ + ULONG pass_shared_count; /* entries the instance holds after */ + ULONG pass_align_status; /* refusing an unaligned grant */ + ULONG pass_exhaust_status; /* refusing one grant too many */ + ULONG pass_marks[S32Z_MODULE_STATUS_GRANULES]; + + /* The address this pass must fault on. Per pass rather than one constant, + because the shared pass faults on the granule it was not granted and the + other three fault on the kernel's data. */ + + ULONG pass_expect_fault; + /* What the notify callback was told, against what it should have been told. Recorded as plain words rather than pointers so the console can print them and a mismatch names both values. */ @@ -317,8 +436,140 @@ static void copy_bytes(unsigned char *destination, const unsigned char *source, /**************************************************************************/ -/* One pass: load the blob from a given address, start it, wait for the */ -/* fault it is written to provoke, and stop it. */ +/* The shared granules, reached through the manager's load window. */ +/* */ +/* No kernel region covers the module area; region 16 does, and the */ +/* scheduler enables it for every thread that owns no module. This */ +/* thread owns none, so the window is open on it and these functions */ +/* need no bracketing of their own -- which is the whole reason the */ +/* window is owned by the scheduler rather than by whoever calls. */ +/**************************************************************************/ + +static void module_status_clear(void) +{ + ULONG granule; + + /* The WHOLE area, not just the progress word. The gap granule has to start + at a known zero, because "the module never wrote it" is one of the results + and a leftover mark from the previous pass would read as a module that + reached memory it was never granted -- or hide one that did. */ + + for (granule = 0UL; granule < MODULE_STATUS_GRANULES; granule++) + { + *((volatile ULONG *) MODULE_GRANULE_ADDRESS(granule)) = 0UL; + MODULE_SHARED_MARK(granule) = 0UL; + } +} + + +static ULONG module_status_read(void) +{ + return *((volatile ULONG *) MODULE_STATUS_ADDRESS); +} + + +/**************************************************************************/ +/* The shared grants a pass gets, and the two ways a grant is refused. */ +/* */ +/* Every pass is granted the first granule, which is the progress word it */ +/* reports through. The shared pass is granted one granule per shared */ +/* entry the port provides, skipping the gap, because a single grant only */ +/* ever exercises the first of the five and this port had never run the */ +/* other four. */ +/* */ +/* It also probes the two ways a grant is refused, which can only be done */ +/* on a LOADED instance. ORDER MATTERS: the manager checks the entry */ +/* count BEFORE it checks alignment, so the unaligned probe has to happen */ +/* while entries remain -- after five grants it would come back */ +/* TX_NO_MEMORY and say nothing about alignment at all. */ +/* */ +/* Returns the first grant status that was not TX_SUCCESS, or TX_SUCCESS. */ +/**************************************************************************/ + +static UINT grant_shared_regions(TXM_MODULE_INSTANCE *instance, PASS_RESULT *result) +{ + UINT status; + UINT first_failure = TX_SUCCESS; + ULONG granule; + + if (result -> pass_test != MODULE_TEST_SHARED_ABORT) + { + /* The progress word, and nothing else. */ + + return txm_module_manager_external_memory_enable(instance, + (VOID *) &__module_status_start__, + MODULE_STATUS_LENGTH, + TXM_MODULE_ATTRIBUTE_READ_WRITE); + } + + /* Refused for alignment. One word into the first granule: an address + inside the area, so what is being tested is the 64-byte granule rule and + not the range. It must also consume no entry, which the five grants + below prove by all succeeding. */ + + result -> pass_align_status = + (ULONG) txm_module_manager_external_memory_enable( + instance, + (VOID *) (&__module_status_start__ + MODULE_STATUS_MARK_OFFSET), + MODULE_STATUS_LENGTH, + TXM_MODULE_ATTRIBUTE_READ_WRITE); + + /* One region per granule, every entry the port has, and never the gap. */ + + for (granule = 0UL; granule < MODULE_STATUS_GRANULES; granule++) + { + if (granule != MODULE_STATUS_UNGRANTED) + { + status = txm_module_manager_external_memory_enable( + instance, + (VOID *) (&__module_status_start__ + (granule * MODULE_STATUS_LENGTH)), + MODULE_STATUS_LENGTH, + TXM_MODULE_ATTRIBUTE_READ_WRITE); + + if (status == TX_SUCCESS) + { + result -> pass_shared_grants++; + } + else if (first_failure == TX_SUCCESS) + { + first_failure = status; + } + else + { + /* Already recorded; the first failure is the informative one. */ + } + } + } + + /* And one grant too many, which must be refused. Not cosmetic: the entry + written is TXM_MODULE_MPU_SHARED_INDEX plus the count, so a sixth grant + that got past the check would write one past the end of the region table + and into the instance fields that follow it. + + Aimed at the GAP, deliberately. It overlaps nothing, so a refusal costs + nothing -- and if the check ever failed, the gap would become a granted + region and the module would go on to survive writing it. The same defect + would then be reported twice, once as this status and once by name as + MODULE_PROGRESS_SURVIVED_GAP, rather than only as a status nobody reads. */ + + result -> pass_exhaust_status = + (ULONG) txm_module_manager_external_memory_enable( + instance, + (VOID *) (&__module_status_start__ + + (MODULE_STATUS_UNGRANTED * MODULE_STATUS_LENGTH)), + MODULE_STATUS_LENGTH, + TXM_MODULE_ATTRIBUTE_READ_WRITE); + + result -> pass_shared_count = instance -> txm_module_instance_shared_memory_count; + + return first_failure; +} + + +/**************************************************************************/ +/* One pass: load the blob from a given address, grant it the shared */ +/* granules it is entitled to, start it, wait for the fault it is written */ +/* to provoke, and stop it. */ /* */ /* The module is left loaded. Its data allocation is what moves the next */ /* pass's data base, and releasing it here would defeat half the test. */ @@ -334,16 +585,55 @@ static void run_one_pass(UINT index, CHAR *name, VOID *location, ULONG test) PASS_RESULT *result = &pass_results[index]; TXM_MODULE_INSTANCE *instance = &demo_module[index]; unsigned int waited; + ULONG granule; result -> pass_name = name; result -> pass_test = test; - /* Cleared before the module starts, so a fault counted here belongs to this - pass and not the previous one. */ + /* What the module should manage before the hardware stops it: its own data, + a kernel call, and the attempt -- plus, for the shared pass, the five + granules it wrote and read back on the way. Never a SURVIVED bit; that + one is the failure. + + The address it must fault on goes with it, because the three tests do not + share one. Two of them reach into the kernel's data; the shared pass + writes the mark word of the granule it was not granted, and that address + is computed here exactly as the module computes it. */ + + if (test == MODULE_TEST_SHARED_ABORT) + { + result -> pass_expect_progress = MODULE_PROGRESS_OWN_DATA + | MODULE_PROGRESS_KERNEL_CALL + | MODULE_PROGRESS_SHARED_WROTE + | MODULE_PROGRESS_ATTEMPTED_GAP; + + result -> pass_expect_fault = MODULE_GRANULE_ADDRESS(MODULE_STATUS_UNGRANTED) + + MODULE_STATUS_MARK_OFFSET; + } + else if (test == MODULE_TEST_PREFETCH_ABORT) + { + result -> pass_expect_progress = MODULE_PROGRESS_OWN_DATA + | MODULE_PROGRESS_KERNEL_CALL + | MODULE_PROGRESS_ATTEMPTED_JUMP; + + result -> pass_expect_fault = MODULE_FORBIDDEN_ADDRESS; + } + else + { + result -> pass_expect_progress = MODULE_PROGRESS_OWN_DATA + | MODULE_PROGRESS_KERNEL_CALL + | MODULE_PROGRESS_ATTEMPTED_STEAL; + + result -> pass_expect_fault = MODULE_FORBIDDEN_ADDRESS; + } + + /* Cleared before the module starts, so a fault or a progress bit counted + here belongs to this pass and not the previous one. */ fault_count = 0UL; fault_notify_thread = 0UL; fault_notify_instance = 0UL; + module_status_clear(); result -> pass_load_status = (ULONG) txm_module_manager_in_place_load(instance, name, location); @@ -352,6 +642,36 @@ static void run_one_pass(UINT index, CHAR *name, VOID *location, ULONG test) return; } + /* Read from the instance rather than computed here. Where the code ended up + is what the manager decided, and the whole point of the comparison at the + end is to check the module against the manager's own numbers. + + Recorded HERE, immediately after the load and before anything that can + fail, because the load is what decided them. Taken any later, a pass that + failed to be granted its shared regions would carry zeros into the overlap + comparison at the end of the run -- and zero against zero overlaps, so a + grant failure would be reported a second time as "its code and data + regions overlap", which is not true and points at the wrong thing. */ + + result -> pass_code_start = (ULONG) instance -> txm_module_instance_code_start; + result -> pass_code_end = (ULONG) instance -> txm_module_instance_code_end; + result -> pass_data_start = (ULONG) instance -> txm_module_instance_data_start; + result -> pass_data_end = (ULONG) instance -> txm_module_instance_data_end; + result -> pass_data_base = (ULONG) instance -> txm_module_instance_module_data_base_address; + + /* The shared granules the module reports through, and for the shared pass + every other entry as well. Granted while the module is LOADED and before + it is STARTED, which is the only window the manager accepts, and granted + per pass because each pass is its own instance with its own region table + -- an instance is memset by the load, so nothing carries over. */ + + result -> pass_share_status = (ULONG) grant_shared_regions(instance, result); + + if (result -> pass_share_status != (ULONG) TX_SUCCESS) + { + return; + } + /* Which violation this pass provokes. Written after the load, which is what filled the field in from the preamble, and before the start, which is what hands it to the module's start thread. */ @@ -365,16 +685,6 @@ static void run_one_pass(UINT index, CHAR *name, VOID *location, ULONG test) result -> pass_expect_thread = (ULONG) &(instance -> txm_module_instance_start_stop_thread); result -> pass_expect_instance = (ULONG) instance; - /* Read from the instance rather than computed here. Where the code ended up - is what the manager decided, and the whole point of the comparison at the - end is to check the module against the manager's own numbers. */ - - result -> pass_code_start = (ULONG) instance -> txm_module_instance_code_start; - result -> pass_code_end = (ULONG) instance -> txm_module_instance_code_end; - result -> pass_data_start = (ULONG) instance -> txm_module_instance_data_start; - result -> pass_data_end = (ULONG) instance -> txm_module_instance_data_end; - result -> pass_data_base = (ULONG) instance -> txm_module_instance_module_data_base_address; - result -> pass_start_status = (ULONG) txm_module_manager_start(instance); if (result -> pass_start_status != (ULONG) TX_SUCCESS) @@ -420,13 +730,27 @@ static void run_one_pass(UINT index, CHAR *name, VOID *location, ULONG test) result -> pass_code_location = (ULONG) _txm_module_manager_memory_fault_info.txm_module_manager_memory_fault_info_code_location; } - /* What the notify callback saw. Read after the wait above, so a callback - that ran is recorded and one that did not shows as zero. */ + /* What the module reached, and what the notify callback saw. Both read + after the wait above, so what ran is recorded and what did not shows as + zero. The status word is read while THIS pass is still the last thing to + have written it -- the same discipline pass_done() exists for on the + module's own copy, for the same reason. */ + result -> pass_progress = module_status_read(); result -> pass_faults = fault_count; result -> pass_notify_thread = fault_notify_thread; result -> pass_notify_instance = fault_notify_instance; + /* Every granule's mark, granted or not. The granted ones say the module + wrote and read back the granule it meant to, which is what proves five + distinct entries were programmed rather than one entry five times; the + gap's says whether it stayed untouched. */ + + for (granule = 0UL; granule < MODULE_STATUS_GRANULES; granule++) + { + result -> pass_marks[granule] = MODULE_SHARED_MARK(granule); + } + /* Stopped, not unloaded. The fault terminated the module's start thread but left the module STARTED, and unload refuses anything that is not LOADED or STOPPED -- so this is required, not tidiness. */ @@ -451,12 +775,17 @@ static void report_one_pass(const PASS_RESULT *result) { linflexd_puts("prefetch (IFSR/IFAR)\n"); } + else if (result -> pass_test == MODULE_TEST_SHARED_ABORT) + { + linflexd_puts("data (DFSR/DFAR), writing an ungranted shared granule\n"); + } else { linflexd_puts("data (DFSR/DFAR)\n"); } put_field(" load status = ", result -> pass_load_status); + put_field(" share status = ", result -> pass_share_status); put_field(" start status = ", result -> pass_start_status); put_field(" stop status = ", result -> pass_stop_status); put_field(" code region = ", result -> pass_code_start); @@ -465,6 +794,9 @@ static void report_one_pass(const PASS_RESULT *result) put_field(" .. to = ", result -> pass_data_end); put_field(" data base (r9) = ", result -> pass_data_base); put_field(" fault captured = ", result -> pass_captured); + put_field(" module progress = ", result -> pass_progress); + put_field(" should be = ", result -> pass_expect_progress); + put_field(" fault address = ", result -> pass_expect_fault); put_field(" notify callbacks = ", result -> pass_faults); put_field(" notified thread = ", result -> pass_notify_thread); put_field(" should be = ", result -> pass_expect_thread); @@ -486,6 +818,124 @@ static void report_one_pass(const PASS_RESULT *result) { put_field(" pc - code base = ", result -> pass_code_location - result -> pass_code_start); } + + /* The shared-region half, reported only by the pass that produces it. */ + + if (result -> pass_test == MODULE_TEST_SHARED_ABORT) + { + ULONG granule; + + put_field(" shared grants = ", result -> pass_shared_grants); + put_field(" should be = ", (unsigned long) MODULE_SHARED_GRANTS); + put_field(" entries held = ", result -> pass_shared_count); + put_field(" unaligned grant = ", result -> pass_align_status); + put_field(" should be = ", (unsigned long) TXM_MODULE_ALIGNMENT_ERROR); + put_field(" one grant too many = ", result -> pass_exhaust_status); + put_field(" should be = ", (unsigned long) TX_NO_MEMORY); + + for (granule = 0UL; granule < MODULE_STATUS_GRANULES; granule++) + { + if (granule == MODULE_STATUS_UNGRANTED) + { + put_field(" gap granule mark = ", result -> pass_marks[granule]); + put_field(" should be = ", 0UL); + } + else + { + put_field(" granule mark = ", result -> pass_marks[granule]); + put_field(" should be = ", MODULE_SHARED_SIGNATURE | granule); + } + } + } +} + + +/**************************************************************************/ +/* The shared-region verdict. Returns the number of failures it found, */ +/* and zero for any pass that does not exercise the shared regions. */ +/**************************************************************************/ + +static UINT judge_shared_regions(const PASS_RESULT *result) +{ + ULONG granule; + + if (result -> pass_test != MODULE_TEST_SHARED_ABORT) + { + return 0U; + } + + /* Every entry the port provides, granted. Fewer means an entry past the + first is not usable, which is the state this port was in before this pass + existed. */ + + if (result -> pass_shared_grants != (ULONG) MODULE_SHARED_GRANTS) + { + linflexd_puts("FAIL "); + linflexd_puts(result -> pass_name); + linflexd_puts(": not every shared entry could be granted\n"); + return 1U; + } + + if (result -> pass_shared_count != (ULONG) TXM_MODULE_MPU_SHARED_ENTRIES) + { + linflexd_puts("FAIL "); + linflexd_puts(result -> pass_name); + linflexd_puts(": the instance does not hold the entries it granted\n"); + return 1U; + } + + /* An unaligned grant must be refused BY NAME. Accepting it would not + fault: the low six bits of PRBAR are the shareability, permission and + execute-never fields, so an under-aligned base silently changes what the + region permits instead of where it is. */ + + if (result -> pass_align_status != (ULONG) TXM_MODULE_ALIGNMENT_ERROR) + { + linflexd_puts("FAIL "); + linflexd_puts(result -> pass_name); + linflexd_puts(": an unaligned shared grant was not refused as such\n"); + return 1U; + } + + if (result -> pass_exhaust_status != (ULONG) TX_NO_MEMORY) + { + linflexd_puts("FAIL "); + linflexd_puts(result -> pass_name); + linflexd_puts(": one grant too many was not refused\n"); + return 1U; + } + + /* And what the module actually managed to write where. Each granted + granule must hold its own mark -- a region programmed with the wrong base + accepts the store and puts it somewhere else -- and the gap must still + hold the zero the manager left in it. */ + + for (granule = 0UL; granule < MODULE_STATUS_GRANULES; granule++) + { + if (granule == MODULE_STATUS_UNGRANTED) + { + if (result -> pass_marks[granule] != 0UL) + { + linflexd_puts("FAIL "); + linflexd_puts(result -> pass_name); + linflexd_puts(": the ungranted granule was written -- a grant covered too much\n"); + return 1U; + } + } + else if (result -> pass_marks[granule] != (MODULE_SHARED_SIGNATURE | granule)) + { + linflexd_puts("FAIL "); + linflexd_puts(result -> pass_name); + linflexd_puts(": a granted granule does not hold its own mark\n"); + return 1U; + } + else + { + /* This granule is as it should be. */ + } + } + + return 0U; } @@ -555,6 +1005,24 @@ static void manager_entry(ULONG input) printed its way as far as the load, and stopped there for ever. The Armv8-M sample does all of this from a thread for the same reason. */ + /* The spellings of the shared status base, checked rather than trusted. + It is a constant agreed between two separately linked images, and a + disagreement would present as a module faulting somewhere unexpected -- + which is exactly what the pass verdicts are trying to distinguish. */ + + if ((unsigned long) &__module_status_start__ != MODULE_STATUS_ADDRESS) + { + linflexd_puts("FAIL the linker put the status area somewhere the module will not look\n"); + failures++; + } + + if ((unsigned long) (&__module_status_end__ - &__module_status_start__) + < MODULE_STATUS_AREA_LENGTH) + { + linflexd_puts("FAIL the status area is smaller than the granules granted over it\n"); + failures++; + } + linflexd_puts("M1 initialize\n"); status = txm_module_manager_initialize((VOID *) module_pool, MODULE_POOL_SIZE); @@ -656,6 +1124,19 @@ static void manager_entry(ULONG input) MODULE_TEST_PREFETCH_ABORT); (void) txm_module_manager_unload(&demo_module[2]); + + /* The shared-region pass. Granted every entry the port provides, one + granule each, and never the granule between two of them -- which it + then writes. This is the only pass that exercises a shared entry + past the first, the granule-alignment refusal, and what happens when + the entries run out; the three passes above grant one region each and + would look identical if four of the five entries did not work. */ + + linflexd_puts("M7 pass 4, shared regions\n"); + run_one_pass(3U, "relocated, shared-region gap", (VOID *) &__module_stage_start__, + MODULE_TEST_SHARED_ABORT); + + (void) txm_module_manager_unload(&demo_module[3]); } /* ------------------------------------------------------------------ @@ -699,6 +1180,52 @@ static void manager_entry(ULONG input) linflexd_puts(": its code and data regions overlap\n"); failures++; } + + /* The shared area is granted as regions too, and it sits in the same + 64 KB as the image, the pool and the staging area. A module grown + large enough, or a pool sized differently, and two of them would + meet. */ + + if ((result -> pass_code_start <= (MODULE_STATUS_ADDRESS + MODULE_STATUS_AREA_LENGTH - 1UL)) && + (MODULE_STATUS_ADDRESS <= result -> pass_code_end)) + { + linflexd_puts("FAIL "); + linflexd_puts(result -> pass_name); + linflexd_puts(": the shared status area overlaps its code\n"); + failures++; + } + + if ((result -> pass_data_start <= (MODULE_STATUS_ADDRESS + MODULE_STATUS_AREA_LENGTH - 1UL)) && + (MODULE_STATUS_ADDRESS <= result -> pass_data_end)) + { + linflexd_puts("FAIL "); + linflexd_puts(result -> pass_name); + linflexd_puts(": the shared status area overlaps its data\n"); + failures++; + } + + /* And what _txm_module_manager_alignment_adjust produced. It is called + by every load and has never been checked, because nothing it gets + wrong faults: it rounds a module's code and data sizes up to the + protection granule and declares the granule as their alignment, and + the loader allocates on that. Get it wrong and the region bases are + written into PRBAR with their low six bits landing on the shareability, + permission and execute-never fields -- so the module runs with + attributes nobody asked for rather than at the wrong address. + + The data region's end is checked as well as its base, because the data + region's length is the rounded size; the CODE region's end is not, it + is the true end of the image and is not rounded to anything. */ + + if (((result -> pass_code_start & (TXM_MODULE_MPU_ALIGNMENT - 1UL)) != 0UL) || + ((result -> pass_data_start & (TXM_MODULE_MPU_ALIGNMENT - 1UL)) != 0UL) || + (((result -> pass_data_end + 1UL) & (TXM_MODULE_MPU_ALIGNMENT - 1UL)) != 0UL)) + { + linflexd_puts("FAIL "); + linflexd_puts(result -> pass_name); + linflexd_puts(": its regions are not aligned to the protection granule\n"); + failures++; + } } /* Each pass on its own: it has to have loaded, started, faulted once, and @@ -709,11 +1236,25 @@ static void manager_entry(ULONG input) const PASS_RESULT *result = &pass_results[i]; if ((result -> pass_load_status != (ULONG) TX_SUCCESS) || + (result -> pass_share_status != (ULONG) TX_SUCCESS) || (result -> pass_start_status != (ULONG) TX_SUCCESS)) { linflexd_puts("FAIL "); linflexd_puts(result -> pass_name); - linflexd_puts(": did not load and start\n"); + linflexd_puts(": did not load, share and start\n"); + failures++; + } + else if ((result -> pass_progress & MODULE_PROGRESS_SURVIVED) != 0UL) + { + /* First, and named explicitly: the module must not have survived + what it was told to attempt. This is the one result that means + the protection did not hold, and it is checked before anything + else so that a run in which the MPU did nothing says so in those + words rather than through some downstream symptom. */ + + linflexd_puts("FAIL "); + linflexd_puts(result -> pass_name); + linflexd_puts(": the module SURVIVED its violation -- it is not protected\n"); failures++; } else if (result -> pass_captured == 0UL) @@ -723,6 +1264,18 @@ static void manager_entry(ULONG input) linflexd_puts(": reached outside its memory and was not stopped\n"); failures++; } + else if (result -> pass_progress != result -> pass_expect_progress) + { + /* What it did manage, from the shared granule. A module that + faulted without having read its own data or made a kernel call + faulted for the wrong reason, and the fault alone would not have + said so. */ + + linflexd_puts("FAIL "); + linflexd_puts(result -> pass_name); + linflexd_puts(": did not reach the point it was supposed to fault at\n"); + failures++; + } else if (result -> pass_fault_r9 != result -> pass_data_base) { /* r9 is the module's PIC base and the manager seeded it from the @@ -749,7 +1302,7 @@ static void manager_entry(ULONG input) } else if (((result -> pass_test == MODULE_TEST_PREFETCH_ABORT) ? result -> pass_ifar : result -> pass_dfar) - != MODULE_FORBIDDEN_ADDRESS) + != result -> pass_expect_fault) { /* The module got this address out of its own initialised data, through its rebased GOT. Any other value means it faulted @@ -796,6 +1349,13 @@ static void manager_entry(ULONG input) linflexd_puts(": notified about the wrong thread or module\n"); failures++; } + else if (judge_shared_regions(result) != 0U) + { + /* judge_shared_regions has already said which of the shared-region + results was wrong. */ + + failures++; + } else { linflexd_puts("PASS "); diff --git a/ports_module/cortex_r52/gnu/example_build/s32z280_evb/tools/diagnose_module.gdb b/ports_module/cortex_r52/gnu/example_build/s32z280_evb/tools/diagnose_module.gdb index ab106b4ad..44a2477dd 100644 --- a/ports_module/cortex_r52/gnu/example_build/s32z280_evb/tools/diagnose_module.gdb +++ b/ports_module/cortex_r52/gnu/example_build/s32z280_evb/tools/diagnose_module.gdb @@ -173,19 +173,33 @@ except gdb.error as e: print(" cannot read demo_module: %s" % e) print("") -print(" the module's own symbols, for comparison (absolute link):") -print(" module_progress = 0x317F0584") -print(" module_scratch = (just below it)") -print(" __module_image_start__ = 0x%08X" % u32("(unsigned int)&__module_image_start__")) +print(" where the module area's pieces ended up:") +_status = u32("(unsigned int)&__module_status_start__") +_image = u32("(unsigned int)&__module_image_start__") +print(" __module_status_start__ = 0x%08X" % _status) +print(" __module_image_start__ = 0x%08X" % _image) print(" __module_pool_start__ = 0x%08X" % u32("(unsigned int)&__module_pool_start__")) +print(" __module_stage_start__ = 0x%08X" % u32("(unsigned int)&__module_stage_start__")) print("") -print(" first words of the module image, to confirm the blob is really there:") +print(" first words of the module image, to confirm the blob is really there.") +print(" Read from the linker's symbol, not from a literal: the module moves") +print(" whenever anything above it in the area changes size, and a stale") +print(" address here prints whatever now lives there as though it were the blob.") for off in (0x00, 0x04, 0x14, 0x18): - a = 0x317F0000 + off - print(" 0x%08X = 0x%08X" % (a, u32("*(unsigned int *)0x%x" % a))) -print(" module_progress / scratch area as it stands now:") -for a in (0x317F0580, 0x317F0584, 0x317F0544, 0x317F0548): + a = _image + off print(" 0x%08X = 0x%08X" % (a, u32("*(unsigned int *)0x%x" % a))) +print("") +print(" the shared granules, which is where the module reports progress and") +print(" where granule %d must have stayed zero:" % 2) +for _g in range(6): + a = _status + _g * 0x40 + print(" granule %d at 0x%08X: word0 = 0x%08X word1 = 0x%08X" + % (_g, a, u32("*(unsigned int *)0x%x" % a), + u32("*(unsigned int *)0x%x" % (a + 4)))) +print("") +print(" module_progress itself lives in the data area the manager allocated for") +print(" the pass, at that pass's data base plus its offset in the module's ELF;") +print(" it has no fixed address since the module became relocatable.") end echo \n===== LIVE MPU REGIONS (read back from hardware) =====\n diff --git a/ports_module/cortex_r52/gnu/example_build/s32z280_evb/tools/run_module_demo.gdb b/ports_module/cortex_r52/gnu/example_build/s32z280_evb/tools/run_module_demo.gdb index 4d0c4b3fa..6f0a6331a 100644 --- a/ports_module/cortex_r52/gnu/example_build/s32z280_evb/tools/run_module_demo.gdb +++ b/ports_module/cortex_r52/gnu/example_build/s32z280_evb/tools/run_module_demo.gdb @@ -37,10 +37,20 @@ # bit 0x08 survived the forbidden read expect CLEAR # bit 0x10 about to BRANCH to it prefetch pass only # bit 0x20 survived the forbidden branch expect CLEAR +# bit 0x40 wrote and read back every granted +# shared granule shared pass only +# bit 0x80 about to write the UNGRANTED granule shared pass only +# bit 0x100 survived writing it expect CLEAR # -# so module_progress == 0x7 for a data-abort pass and 0x13 for the prefetch pass. +# so module_progress == 0x7 for a data-abort pass, 0x13 for the prefetch pass and +# 0xC3 for the shared pass. # -# THREE PASSES, because one run proves neither relocation nor both abort types +# The manager records the same word a second time, out of the first shared +# granule, and this script reads the module's own copy out of its data area. Two +# independent readings of one event: if they disagree, one of the two channels is +# lying and the run says which. +# +# FOUR PASSES, because one run proves neither relocation nor both abort types # # The module is position independent, so a single run at the address it was # linked for would prove nothing: the GOT rebase would map every address to @@ -55,6 +65,17 @@ # been unloaded, so it also shows that a module fault leaves the manager able to # load and run the next module. # +# Pass 4 is the shared regions. Passes 1 to 3 are each granted ONE shared +# region -- the granule they report progress through -- which exercises the first +# of the five shared entries the port provides and nothing about the other four. +# Pass 4 is granted all five, one 64-byte granule each, and is deliberately NOT +# granted the granule that sits between two of them; it writes every granule it +# was given, reads them all back, and then writes the gap, which must fault. A +# limit register masked the wrong way, or a base off by a granule, reaches into +# that gap from one side or the other. The same pass probes the two refusals -- +# an unaligned grant must come back TXM_MODULE_ALIGNMENT_ERROR (0xF0) and one +# grant past the entry count must come back TX_NO_MEMORY (0x10). +# # THE NOTIFY CALLBACK IS A CHECK NOW, not a note # # It used to be dead on this port: the shared fault handler terminates the @@ -151,7 +172,7 @@ PROGRESS_BY_PASS = [] _offset = os.environ.get("S32Z280_MODULE_PROGRESS_OFFSET") _offset = int(_offset, 0) if _offset else None -for _i in range(3): +for _i in range(4): gdb.execute("continue") if _offset is None: PROGRESS_BY_PASS.append(None) @@ -186,13 +207,23 @@ failures = 0 OWN_DATA, KERNEL_CALL = 0x01, 0x02 ATTEMPTED_STEAL, SURVIVED_STEAL = 0x04, 0x08 ATTEMPTED_JUMP, SURVIVED_JUMP = 0x10, 0x20 +SHARED_WROTE = 0x40 +ATTEMPTED_GAP, SURVIVED_GAP = 0x80, 0x100 FORBIDDEN = 0x31780000 + +# The shared area, mirrored from S32Z_MODULE_STATUS_* in platform.h. Five of the +# six granules are granted, one entry each; index 2 never is, and the marks go at +# word 1 because word 0 of granule 0 is the progress word. +STATUS_BASE, STATUS_GRANULE, STATUS_GRANULES = 0x317F0000, 0x40, 6 +STATUS_UNGRANTED, STATUS_MARK_OFFSET = 2, 4 +SHARED_SIGNATURE = 0x5A5A0000 +ALIGNMENT_ERROR, NO_MEMORY = 0xF0, 0x10 MODE = {0x10: "User", 0x13: "Supervisor", 0x1A: "Hyp", 0x1F: "System"} # Which violation each pass was told to commit, from the low byte of the module ID # the manager writes into the instance before starting it. -TEST_DATA_ABORT, TEST_PREFETCH_ABORT = 0x1, 0x2 -PASSES = 3 +TEST_DATA_ABORT, TEST_PREFETCH_ABORT, TEST_SHARED_ABORT = 0x1, 0x2, 0x3 +PASSES = 4 RELOCATION_PASSES = 2 # The nominal offset of module_progress inside the module's data segment. Both @@ -210,15 +241,21 @@ else: passes = [] for i in range(PASSES): p = {} - for field in ("pass_test", "pass_load_status", "pass_start_status", "pass_stop_status", + for field in ("pass_test", "pass_load_status", "pass_share_status", + "pass_start_status", "pass_stop_status", "pass_code_start", "pass_code_end", "pass_data_start", "pass_data_end", "pass_data_base", "pass_faults", "pass_captured", "pass_fault_r9", "pass_dfsr", "pass_dfar", "pass_ifsr", "pass_ifar", "pass_spsr", "pass_code_location", "pass_notify_thread", "pass_notify_instance", - "pass_expect_thread", "pass_expect_instance"): + "pass_expect_thread", "pass_expect_instance", + "pass_progress", "pass_expect_progress", "pass_expect_fault", + "pass_shared_grants", "pass_shared_count", + "pass_align_status", "pass_exhaust_status"): p[field] = ev("pass_results[%d].%s" % (i, field)) + p["marks"] = [ev("pass_results[%d].pass_marks[%d]" % (i, g)) + for g in range(STATUS_GRANULES)] try: p["name"] = gdb.parse_and_eval("pass_results[%d].pass_name" % i).string() except gdb.error: @@ -227,20 +264,28 @@ for i in range(PASSES): for i, p in enumerate(passes): prefetch = (p["pass_test"] == TEST_PREFETCH_ABORT) + shared = (p["pass_test"] == TEST_SHARED_ABORT) print("") print(" --- pass %d: %s ---" % (i + 1, p["name"])) - print(" expected abort = %s" - % ("prefetch, through IFSR/IFAR" if prefetch else "data, through DFSR/DFAR")) - print(" load / start / stop = 0x%08X / 0x%08X / 0x%08X" - % (p["pass_load_status"], p["pass_start_status"], p["pass_stop_status"])) + if prefetch: + print(" expected abort = prefetch, through IFSR/IFAR") + elif shared: + print(" expected abort = data, through DFSR/DFAR, writing an") + print(" ungranted shared granule") + else: + print(" expected abort = data, through DFSR/DFAR") + print(" load / share / start / stop = 0x%08X / 0x%08X / 0x%08X / 0x%08X" + % (p["pass_load_status"], p["pass_share_status"], + p["pass_start_status"], p["pass_stop_status"])) print(" code region = 0x%08X .. 0x%08X" % (p["pass_code_start"], p["pass_code_end"])) print(" data region = 0x%08X .. 0x%08X" % (p["pass_data_start"], p["pass_data_end"])) print(" data base (r9) = 0x%08X" % p["pass_data_base"]) - if p["pass_load_status"] != 0 or p["pass_start_status"] != 0: - print(" *** FAIL: did not load and start") + if (p["pass_load_status"] != 0 or p["pass_share_status"] != 0 + or p["pass_start_status"] != 0): + print(" *** FAIL: did not load, share and start") failures += 1 continue @@ -262,27 +307,46 @@ for i, p in enumerate(passes): % (progress, addr)) print(" still held that memory)") - # Every bit is checked, including the ones belonging to the violation this - # pass was NOT told to commit: a module that took both routes, or the - # wrong one, is not the module this pass asked for and the result would - # not mean what the register checks below claim. + # Every bit is checked, including the ones belonging to the violations + # this pass was NOT told to commit: a module that took more than one + # route, or the wrong one, is not the module this pass asked for and the + # result would not mean what the register checks below claim. + data = not (prefetch or shared) wanted = [(OWN_DATA, "wrote its own data", True), (KERNEL_CALL, "reached the kernel through SVC", True), - (ATTEMPTED_STEAL, "attempted the forbidden read", not prefetch), + (ATTEMPTED_STEAL, "attempted the forbidden read", data), (SURVIVED_STEAL, "SURVIVED the forbidden read", False), (ATTEMPTED_JUMP, "attempted the forbidden branch", prefetch), - (SURVIVED_JUMP, "SURVIVED the forbidden branch", False)] + (SURVIVED_JUMP, "SURVIVED the forbidden branch", False), + (SHARED_WROTE, "wrote and read back every granted granule", shared), + (ATTEMPTED_GAP, "attempted the ungranted granule", shared), + (SURVIVED_GAP, "SURVIVED the ungranted granule", False)] for bit, name, want_set in wanted: got = bool(progress & bit) ok = (got == want_set) if not ok: failures += 1 - print(" 0x%02X %-33s %-5s %s" + print(" 0x%03X %-42s %-5s %s" % (bit, name, "set" if got else "clear", "ok" if ok else "*** WRONG ***")) - if progress & (SURVIVED_STEAL | SURVIVED_JUMP): + if progress & (SURVIVED_STEAL | SURVIVED_JUMP | SURVIVED_GAP): print(" ISOLATION FAILURE: the module reached memory it was never granted.") + # The manager's own reading of the same word, out of the first shared + # granule. Two independent channels for one event: the module writes + # both, this script reads its private copy and the manager read the + # shared one, so a disagreement means one of the two paths is lying and + # neither result should be trusted until it is explained. + if p["pass_progress"] != progress: + print(" *** FAIL: the manager read progress 0x%08X out of the shared" + % p["pass_progress"]) + print(" granule, the module's own copy says 0x%08X" % progress) + failures += 1 + if p["pass_progress"] != p["pass_expect_progress"]: + print(" *** FAIL: progress 0x%08X, expected 0x%08X" + % (p["pass_progress"], p["pass_expect_progress"])) + failures += 1 + # "captured" and "notified" are different events: the abort vector records the # fault registers before anything else runs, and the notify callback is a # later consequence of the same fault. Both are required. Reporting them @@ -367,10 +431,16 @@ for i, p in enumerate(passes): # are sticky, so checking the pair that does not belong to this fault would # pass on a value some earlier fault left behind -- including the boot probes, # which take a prefetch abort of their own before ThreadX starts. + # + # The shared pass is the exception: the address it faults on is the mark word + # of the granule it was not granted, which it computes from a literal rather + # than out of .data -- so for that pass this is a check on the grant and not + # on the rebase. Which is why the expectation is carried per pass. reg_name, reg_value = ("IFAR", ifar) if prefetch else ("DFAR", dfar) - if reg_value != FORBIDDEN: + expect_fault = p["pass_expect_fault"] or FORBIDDEN + if reg_value != expect_fault: print(" *** FAIL: expected %s 0x%08X, the address the module was told" - % (reg_name, FORBIDDEN)) + % (reg_name, expect_fault)) print(" not to touch. A different one means .data was not") print(" copied or the GOT was not rebased.") failures += 1 @@ -382,6 +452,52 @@ for i, p in enumerate(passes): print(" *** FAIL: IFSR reports no instruction fault status") failures += 1 + # The shared-region results, for the one pass that produces them. Read out + # of the manager's own record rather than out of the MPU: what the hardware + # holds is checked by the module having faulted, and what is checked here is + # that the manager granted what it meant to and refused what it should. + if shared: + print(" shared grants = %d (expected %d)" + % (p["pass_shared_grants"], STATUS_GRANULES - 1)) + print(" entries held = %d" % p["pass_shared_count"]) + print(" unaligned grant = 0x%02X (expected 0x%02X, TXM_MODULE_ALIGNMENT_ERROR)" + % (p["pass_align_status"], ALIGNMENT_ERROR)) + print(" one grant too many = 0x%02X (expected 0x%02X, TX_NO_MEMORY)" + % (p["pass_exhaust_status"], NO_MEMORY)) + + if p["pass_shared_grants"] != STATUS_GRANULES - 1: + print(" *** FAIL: not every shared entry could be granted") + failures += 1 + if p["pass_shared_count"] != STATUS_GRANULES - 1: + print(" *** FAIL: the instance does not hold the entries it granted") + failures += 1 + if p["pass_align_status"] != ALIGNMENT_ERROR: + print(" *** FAIL: an unaligned shared grant was not refused as such.") + print(" Accepting one does not fault -- the low six bits of") + print(" PRBAR are permissions, so it changes what the region") + print(" allows instead of where it is.") + failures += 1 + if p["pass_exhaust_status"] != NO_MEMORY: + print(" *** FAIL: one grant too many was not refused. The entry index") + print(" is TXM_MODULE_MPU_SHARED_INDEX plus the count, so a") + print(" sixth grant writes past the end of the region table.") + failures += 1 + + # And what the module managed to write where. A granted granule holding + # something other than its own mark means that entry was programmed with + # the wrong base -- the store was accepted and landed elsewhere -- and a + # non-zero gap means a grant covered a granule nobody asked for. + for g in range(STATUS_GRANULES): + addr = STATUS_BASE + g * STATUS_GRANULE + STATUS_MARK_OFFSET + want = 0 if g == STATUS_UNGRANTED else (SHARED_SIGNATURE | g) + got = p["marks"][g] + tag = "ungranted" if g == STATUS_UNGRANTED else "granted " + ok = (got == want) + if not ok: + failures += 1 + print(" granule %d %s at 0x%08X = 0x%08X (expected 0x%08X) %s" + % (g, tag, addr, got, want, "ok" if ok else "*** WRONG ***")) + # --- the relocation result, which is the comparison between the passes -------- print("") print(" ===== both abort types =====") From 272309b7657502794851ebaec89d9252952b17f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20Desbiens?= Date: Thu, 20 Aug 2026 10:31:15 -0400 Subject: [PATCH 31/33] Applied the kernel warning set to the module port, and unified the mode names Mirrors what AR1 did at its M6: upstream's kernel warning set from test/tx/cmake/CMakeLists.txt -- -Werror -Wall -Wextra -pedantic -Wunused -Wuninitialized -Wmissing-declarations -Wconversion -Wpointer-arith -Wlogical-op -Waggregate-return -Wfloat-equal -- plus -Wmissing-prototypes, which is the flag that found AR1's Rule 8.4 findings, applied to every C file of the module port and of the board support both module-manager images compile. Twenty-five translation units in scope across the two boards. The nine module port ones had four findings, identical in both configurations; the sixteen board support ones were already clean, so AR1's board.h work and T4's cache.c and mpu.c changes hold. Rule 7.2 -- TXM_MODULE_MPU_ALIGNMENT was a plain 64, and every use of it is a mask over a ULONG address or size in the form ~(ALIGNMENT - 1). Spelled signed, that complement is -64 converted to 0xFFFFFFC0 on the way into the expression: three -Wsign-conversion reports across alignment_adjust and external_memory_enable. Now 64UL, which fixes all three at the definition rather than at each use, and keeps the essential type of the mask unsigned for whatever uses it next. Rule 2.7 -- _txm_module_manager_alignment_adjust never reads module_preamble, and cannot drop it: _txm_module_manager_internal_load calls it through a prototype every port shares. Referenced and discarded, with the reason and a pointer to the Cortex-R4 port, which does read it. Rule 8.4 -- demo_module_start is called only from txm_module_preamble.S, which names it with .extern and stores its offset in the START entry-point word, and bsp_main is called only from entry.S. Assembly callers supply no prototype, so both were definitions with external linkage and no visible declaration. demo_module_start is now declared above its definition in both sample modules; the two sample managers now include board.h, which already declares bsp_main for exactly this reason and which every other example in the board support package includes. Also picked one spelling for the processor-mode constants, which this port had two of. The base port already draws the distinction that matters: *_MODE_BITS is the bare CPSR mode field (SYS_MODE_BITS, IRQ_MODE_BITS, FIQ_MODE_BITS) and a plain *_MODE is a whole CPSR value with the interrupt masks already in it (SVC_MODE 0xD3, IRQ_MODE 0xD2). The module port used both names for the bare field, so SYS_MODE meant 0x1F in three files and 0xDF or 0x9F in a fourth. Every bare-field constant is now *_MODE_BITS, which is what tx_thread_schedule.S and txm_module_manager_fault_capture.S already used, and the values are unchanged and still 0x10 / 0x13 / 0x1F. Two dead SVC_MODE definitions went with it -- one in tx_thread_stack_build.S and one in tx_thread_context_restore.S, both left behind when this port moved kernel threads from SVC mode to System mode. Deviations this port has already taken, commented at their use sites in the samples and repeated here for the record: Rule 11.6, integer to pointer, for the shared status address, which is an agreement between two separately linked images and has no conforming spelling; and Rule 11.1, function pointer conversion, for the prefetch-abort test, whose whole purpose is a fetch from an address holding no function. Verified. All nine module port translation units and all sixteen board support ones compile clean under the set, in both board configurations. FVP 603 targets and S32Z280 596 targets, zero warnings either way. FVP ctest 6/6 in 8.63 s. And the pass emitted no code: fvp_module.elf, fvp_demo_module.elf, s32z280_module.elf and s32z280_demo_module.elf are all byte-identical to their 87ee71ee builds, so T5's silicon result stands on the same instructions without a hardware re-run. One finding is recorded rather than fixed, because fixing it is a functional change and this is not a functional commit. tx_port.h guards the CLZ implementation of TX_LOWEST_SET_BIT_CALCULATE with #if __TARGET_ARCH_ARM > 4. That is an Arm Compiler predefine; GCC defines __ARM_ARCH 8 and __ARM_FEATURE_CLZ 1 and does not define it at all. So under GCC the test is 0 > 4, the CLZ path is dead and tx_thread.h's portable loop is used instead, on a core that has the instruction. Confirmed against the compiler's own -dM output. It is upstream's, identical in the base port and in about a dozen other AArch32 ports, and it changes the scheduler's priority search, so it belongs in an upstream issue and a commit of its own. Assisted-by: Claude Code (Opus 5) --- .../fvp_baser_aemv8r/sample_threadx_module.c | 10 ++++++++++ .../sample_threadx_module_manager.c | 7 +++++++ .../s32z280_evb/sample_threadx_module.c | 10 ++++++++++ .../sample_threadx_module_manager.c | 7 +++++++ .../cortex_r52/gnu/inc/txm_module_port.h | 14 ++++++++++++-- .../src/tx_thread_context_restore.S | 9 +++++++-- .../module_manager/src/tx_thread_stack_build.S | 13 ++++++++++--- .../src/txm_module_manager_alignment_adjust.c | 11 +++++++++++ .../src/txm_module_manager_svc_handler.S | 18 +++++++++--------- .../txm_module_manager_thread_stack_build.S | 14 +++++++++----- 10 files changed, 92 insertions(+), 21 deletions(-) diff --git a/ports_module/cortex_r52/gnu/example_build/fvp_baser_aemv8r/sample_threadx_module.c b/ports_module/cortex_r52/gnu/example_build/fvp_baser_aemv8r/sample_threadx_module.c index f93a8d9a8..e2fd2508f 100644 --- a/ports_module/cortex_r52/gnu/example_build/fvp_baser_aemv8r/sample_threadx_module.c +++ b/ports_module/cortex_r52/gnu/example_build/fvp_baser_aemv8r/sample_threadx_module.c @@ -171,6 +171,16 @@ volatile ULONG module_scratch[16]; volatile ULONG module_forbidden_address = 0x003EFFC0UL; +/* Declared as well as defined because the only caller is assembly: + txm_module_preamble.S names it with .extern and stores its offset in the + START entry-point word. An assembly caller supplies no prototype, so + without this the definition has external linkage and no visible + declaration -- prohibited by MISRA C:2012 Rule 8.4 and reported by + -Wmissing-prototypes. Same reason the board support has board.h. */ + +void demo_module_start(ULONG id); + + void demo_module_start(ULONG id) { ULONG i; diff --git a/ports_module/cortex_r52/gnu/example_build/fvp_baser_aemv8r/sample_threadx_module_manager.c b/ports_module/cortex_r52/gnu/example_build/fvp_baser_aemv8r/sample_threadx_module_manager.c index 869ba72db..db3aeb17b 100644 --- a/ports_module/cortex_r52/gnu/example_build/fvp_baser_aemv8r/sample_threadx_module_manager.c +++ b/ports_module/cortex_r52/gnu/example_build/fvp_baser_aemv8r/sample_threadx_module_manager.c @@ -119,6 +119,13 @@ #include "mpu.h" #include "cache.h" +/* For bsp_main, which entry.S calls and therefore never declares. Every + other example in the board support package includes this header for the + same reason; MISRA C:2012 Rule 8.4 wants the declaration visible at the + definition, and -Wmissing-prototypes reports it when it is not. */ + +#include "board.h" + /* The fault information the abort vector captured. Declared here because the module manager expands it into the port's fault handler through the TXM_MODULE_MANAGER_FAULT_INFO macro rather than declaring it in a header, so an diff --git a/ports_module/cortex_r52/gnu/example_build/s32z280_evb/sample_threadx_module.c b/ports_module/cortex_r52/gnu/example_build/s32z280_evb/sample_threadx_module.c index 5a2afaba9..dbf0c8a82 100644 --- a/ports_module/cortex_r52/gnu/example_build/s32z280_evb/sample_threadx_module.c +++ b/ports_module/cortex_r52/gnu/example_build/s32z280_evb/sample_threadx_module.c @@ -171,6 +171,16 @@ volatile ULONG module_scratch[16]; volatile ULONG module_forbidden_address = 0x31780000UL; +/* Declared as well as defined because the only caller is assembly: + txm_module_preamble.S names it with .extern and stores its offset in the + START entry-point word. An assembly caller supplies no prototype, so + without this the definition has external linkage and no visible + declaration -- prohibited by MISRA C:2012 Rule 8.4 and reported by + -Wmissing-prototypes. Same reason the board support has board.h. */ + +void demo_module_start(ULONG id); + + void demo_module_start(ULONG id) { ULONG i; diff --git a/ports_module/cortex_r52/gnu/example_build/s32z280_evb/sample_threadx_module_manager.c b/ports_module/cortex_r52/gnu/example_build/s32z280_evb/sample_threadx_module_manager.c index 742959d85..f0c3949a9 100644 --- a/ports_module/cortex_r52/gnu/example_build/s32z280_evb/sample_threadx_module_manager.c +++ b/ports_module/cortex_r52/gnu/example_build/s32z280_evb/sample_threadx_module_manager.c @@ -138,6 +138,13 @@ #include "thread_mpu.h" #include "cache.h" +/* For bsp_main, which entry.S calls and therefore never declares. Every + other example in the board support package includes this header for the + same reason; MISRA C:2012 Rule 8.4 wants the declaration visible at the + definition, and -Wmissing-prototypes reports it when it is not. */ + +#include "board.h" + /* The fault information the abort vector captured. Declared here because the module manager expands it into the port's fault handler through the TXM_MODULE_MANAGER_FAULT_INFO macro rather than declaring it in a header, so an diff --git a/ports_module/cortex_r52/gnu/inc/txm_module_port.h b/ports_module/cortex_r52/gnu/inc/txm_module_port.h index 3ccc5bae0..f8b32fdf7 100644 --- a/ports_module/cortex_r52/gnu/inc/txm_module_port.h +++ b/ports_module/cortex_r52/gnu/inc/txm_module_port.h @@ -9,6 +9,8 @@ * SPDX-License-Identifier: MIT **************************************************************************/ +// Some portions generated by Claude Code (Opus 5). + /**************************************************************************/ /**************************************************************************/ @@ -223,8 +225,16 @@ DATA WBWA RA : 0b [Outer]1111 [Inner]1111 /* MPU regions must be 64-byte aligned on PMSAv8-R, where Armv8-M requires 32. Getting this wrong is silent: the low bits of PRBAR and PRLAR hold attributes, so an under-aligned base does not fault, it changes the shareability and - permissions of the region instead. */ -#define TXM_MODULE_MPU_ALIGNMENT 64 + permissions of the region instead. + + Unsigned, and that is not decoration. Every use of this macro is a mask over + an address or a size -- both ULONG -- and the useful form is the complement, + ~(TXM_MODULE_MPU_ALIGNMENT - 1). Spelled as a plain 64 that complement is a + signed -64 converted to 0xFFFFFFC0 on the way into the expression, which is + what MISRA C:2012 Rule 7.2 requires a U suffix to prevent and what + -Wsign-conversion reports. The value is identical either way here; the + suffix is what keeps the essential type of the mask unsigned. */ +#define TXM_MODULE_MPU_ALIGNMENT 64UL /* Mask that keeps only the address bits of PRBAR and PRLAR. The low six bits hold attributes on this core, so every base and limit written into the region diff --git a/ports_module/cortex_r52/gnu/module_manager/src/tx_thread_context_restore.S b/ports_module/cortex_r52/gnu/module_manager/src/tx_thread_context_restore.S index 4069b440e..90865906b 100644 --- a/ports_module/cortex_r52/gnu/module_manager/src/tx_thread_context_restore.S +++ b/ports_module/cortex_r52/gnu/module_manager/src/tx_thread_context_restore.S @@ -26,12 +26,17 @@ .arm +@ Whole CPSR values, mode field and interrupt masks together -- which is what a +@ plain _MODE name means throughout this port. The bare mode field is spelled +@ _MODE_BITS instead; see tx_thread_stack_build.S. The base port's SVC_MODE is +@ absent because this port restores kernel threads into System mode, so nothing +@ here needs it and a name that is never used is a name waiting to be picked by +@ mistake. + #ifdef TX_ENABLE_FIQ_SUPPORT -SVC_MODE = 0xD3 @ Disable IRQ/FIQ, SVC mode SYS_MODE = 0xDF @ Disable IRQ/FIQ, SYS mode IRQ_MODE = 0xD2 @ Disable IRQ/FIQ, IRQ mode #else -SVC_MODE = 0x93 @ Disable IRQ, SVC mode SYS_MODE = 0x9F @ Disable IRQ, SYS mode IRQ_MODE = 0x92 @ Disable IRQ, IRQ mode #endif diff --git a/ports_module/cortex_r52/gnu/module_manager/src/tx_thread_stack_build.S b/ports_module/cortex_r52/gnu/module_manager/src/tx_thread_stack_build.S index 88293e290..935b321ad 100644 --- a/ports_module/cortex_r52/gnu/module_manager/src/tx_thread_stack_build.S +++ b/ports_module/cortex_r52/gnu/module_manager/src/tx_thread_stack_build.S @@ -26,8 +26,15 @@ .arm -SVC_MODE = 0x13 @ SVC mode -SYS_MODE = 0x1F @ SYS mode, privileged +@ Bare CPSR mode field, spelled _MODE_BITS as the base port does in +@ tx_thread_irq_nesting_start.S and tx_thread_fiq_context_restore.S. In this +@ port a plain _MODE name means a whole CPSR value with the interrupt masks +@ already in it -- tx_thread_context_restore.S defines SYS_MODE as 0xDF or 0x9F +@ -- so the two must not share a name. The base port's SVC_MODE = 0x13 here was +@ the frame's mode before this port moved kernel threads to System mode; nothing +@ uses it now, so it is gone rather than left to be picked up by mistake. + +SYS_MODE_BITS = 0x1F @ SYS mode, privileged #ifdef TX_ENABLE_FIQ_SUPPORT CPSR_MASK = 0xDF @ Mask initial CPSR, IRQ & FIQ interrupts enabled #else @@ -166,7 +173,7 @@ _tx_thread_stack_build: @ leaving it as SVC would have put kernel threads on the SVC stack while the @ restore path looked for them on the SYS one. */ @ - ORR r3, r1, #SYS_MODE @ Build CPSR, SYS mode, interrupts enabled + ORR r3, r1, #SYS_MODE_BITS @ Build CPSR, SYS mode, interrupts enabled STR r3, [r2, #4] @ Store initial CPSR @ @ /* Setup stack pointer. */ diff --git a/ports_module/cortex_r52/gnu/module_manager/src/txm_module_manager_alignment_adjust.c b/ports_module/cortex_r52/gnu/module_manager/src/txm_module_manager_alignment_adjust.c index 48d1eca5c..6850fe558 100644 --- a/ports_module/cortex_r52/gnu/module_manager/src/txm_module_manager_alignment_adjust.c +++ b/ports_module/cortex_r52/gnu/module_manager/src/txm_module_manager_alignment_adjust.c @@ -9,6 +9,8 @@ * SPDX-License-Identifier: MIT **************************************************************************/ +// Some portions generated by Claude Code (Opus 5). + /**************************************************************************/ /**************************************************************************/ @@ -69,6 +71,15 @@ VOID _txm_module_manager_alignment_adjust(TXM_MODULE_PREAMBLE *module_preamble, ULONG *data_alignment) { + /* The preamble is not read here, and the signature is not ours to change: + _txm_module_manager_internal_load calls this through a fixed prototype + shared by every port. Referenced and discarded so that the parameter is + used, which is what MISRA C:2012 Rule 2.7 asks for and what + -Wunused-parameter reports. A port that did have to grow or reposition a + module -- the Cortex-R4 one, below -- reads it. */ + + (VOID)module_preamble; + /* Rounding to the granule is all this has to do, and that is a property of PMSAv8-R rather than a shortcut. The Cortex-R4 port's equivalent runs to 183 lines because PMSAv7 regions must be a power of two in size and diff --git a/ports_module/cortex_r52/gnu/module_manager/src/txm_module_manager_svc_handler.S b/ports_module/cortex_r52/gnu/module_manager/src/txm_module_manager_svc_handler.S index 90d553cea..0689ffbaa 100644 --- a/ports_module/cortex_r52/gnu/module_manager/src/txm_module_manager_svc_handler.S +++ b/ports_module/cortex_r52/gnu/module_manager/src/txm_module_manager_svc_handler.S @@ -66,9 +66,9 @@ @ Processor mode encodings and the CPSR mode field. .equ MODE_MASK, 0x1F - .equ USR_MODE, 0x10 - .equ SVC_MODE, 0x13 - .equ SYS_MODE, 0x1F + .equ USR_MODE_BITS, 0x10 + .equ SVC_MODE_BITS, 0x13 + .equ SYS_MODE_BITS, 0x1F .equ THUMB_MASK, 0x20 @ Offsets into TX_THREAD. Checked against offsetof at compile time in @@ -160,7 +160,7 @@ _tx_module_svc_enter: LDMFD sp!, {r0, r3} @ SPSR back off the stack BIC r0, r0, #MODE_MASK - ORR r0, r0, #SYS_MODE + ORR r0, r0, #SYS_MODE_BITS MSR SPSR_cxsf, r0 @ Swap stacks. The module's sp is saved so SVC 2 can restore it, and sp is set @@ -168,10 +168,10 @@ _tx_module_svc_enter: @ System mode shares User mode's banked sp, which is why the swap happens there. LDR r1, [r2, #THREAD_KSTACK_END] @ Top of the kernel stack - CPS #SYS_MODE + CPS #SYS_MODE_BITS MOV r3, sp @ The module's own sp MOV sp, r1 - CPS #SVC_MODE + CPS #SVC_MODE_BITS STR r3, [r2, #THREAD_MSTACK_PTR] @ Point ThreadX's stack checking at the kernel stack while it is in use. @@ -211,15 +211,15 @@ _tx_module_svc_exit: LDMFD sp!, {r0, r3} BIC r0, r0, #MODE_MASK - ORR r0, r0, #USR_MODE + ORR r0, r0, #USR_MODE_BITS MSR SPSR_cxsf, r0 @ Put the module's own stack back. LDR r1, [r2, #THREAD_MSTACK_PTR] - CPS #SYS_MODE + CPS #SYS_MODE_BITS MOV sp, r1 - CPS #SVC_MODE + CPS #SVC_MODE_BITS @ And its own bounds, for the same reason they were changed on the way in. diff --git a/ports_module/cortex_r52/gnu/module_manager/src/txm_module_manager_thread_stack_build.S b/ports_module/cortex_r52/gnu/module_manager/src/txm_module_manager_thread_stack_build.S index 9f8ebfbf0..7e323c2c1 100644 --- a/ports_module/cortex_r52/gnu/module_manager/src/txm_module_manager_thread_stack_build.S +++ b/ports_module/cortex_r52/gnu/module_manager/src/txm_module_manager_thread_stack_build.S @@ -27,9 +27,13 @@ .syntax unified .arm -SVC_MODE = 0x13 @ SVC mode -USR_MODE = 0x10 @ User mode, unprivileged -SYS_MODE = 0x1F @ System mode, privileged +@ Bare CPSR mode field. See tx_thread_stack_build.S for why the _MODE_BITS +@ suffix is not optional in this port: a plain _MODE name is a whole CPSR value +@ with the interrupt masks in it. SVC is not among them -- no thread in a module +@ port runs in SVC mode, which is reserved for the supervisor call handler. + +USR_MODE_BITS = 0x10 @ User mode, unprivileged +SYS_MODE_BITS = 0x1F @ System mode, privileged THUMB_MASK = 0x20 @ CPSR Thumb bit @ Offset of tx_thread_module_user_mode, checked against offsetof at compile time @@ -219,8 +223,8 @@ _txm_module_manager_thread_stack_build: @ LDR r3, [r0, #THREAD_MODULE_USER_MODE] @ Pickup the requested user mode TST r3, #1 - ORREQ r3, r1, #SYS_MODE @ Clear: privileged, System mode - ORRNE r3, r1, #USR_MODE @ Set: unprivileged, User mode + ORREQ r3, r1, #SYS_MODE_BITS @ Clear: privileged, System mode + ORRNE r3, r1, #USR_MODE_BITS @ Set: unprivileged, User mode STR r3, [r2, #4] @ Store initial CPSR @ @ /* Setup stack pointer. */ From a7bf6188d05f91dae2b5fdfd57b720dc9d08d0b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20Desbiens?= Date: Thu, 20 Aug 2026 13:21:37 -0400 Subject: [PATCH 32/33] Woke the CLZ priority search, which had never once run under GCC T7's advisory pass found this with -Wundef and recorded it rather than fixing it, because it changes emitted code and T7 was a quality-only commit. This is that commit. tx_port.h guarded its CLZ implementation of TX_LOWEST_SET_BIT_CALCULATE with #if __TARGET_ARCH_ARM > 4. That is an Arm Compiler 5 predefine. GCC predefines the ACLE macros -- __ARM_ARCH 8 and __ARM_FEATURE_CLZ 1 -- and does not define it at all, so the test read 0 > 4, the block was dropped, and tx_thread.h's portable loop ran on a core that has had the instruction since Armv5. Confirmed two ways on 20 Aug 2026: arm-none-eabi-gcc 14.3 -dM -E lists no __TARGET_ARCH_ARM, and the built scheduler objects contained zero CLZ instructions. This was not a dormant optimisation. Half the TX_LOWEST_SET_BIT_CALCULATE call sites in tx_thread_suspend.c and tx_thread_system_suspend.c sit OUTSIDE the TX_MAX_PRIORITIES > 32 guards, so the portable loop was running the scheduler's priority search in the default 32-priority configuration -- which is the one both R52 ports use. The guard now asks __ARM_FEATURE_CLZ, which is the ACLE answer to the question actually being asked: the compiler defines it exactly when the architecture has the instruction, so a core without CLZ is excluded by construction rather than by an architecture number. Arm Compiler 5's spelling is kept beside it, wrapped in defined() so the test no longer leans on an undefined identifier evaluating to zero. The __thumb__ guard stays, and it turns out to be load-bearing rather than inherited caution. __ARM_FEATURE_CLZ describes the architecture, not the instruction set: GCC defines it for -mthumb -march=armv5te, where Thumb-1 has no CLZ and this asm would fail to assemble. Measured, not assumed. On this core it is moot -- the R52 toolchain file builds -marm. Two changes to the macro body, both deliberate: Rule 10.1/10.3. Upstream isolates the lowest set bit with (ULONG) (-((LONG) m)) -- an unsigned map converted to signed and negated, which is undefined for the one input whose top bit is set. Respelled as (~(m)) + 1, which is the same value in well-defined unsigned arithmetic and is character-for-character what tx_thread.h's portable version uses, so the two implementations now visibly compute the same thing rather than merely agreeing. Rule 20.7. m and b are parenthesised in the expansion; upstream leaves them bare. And the asm is spelled __asm__, because the asm keyword is rejected under -std=c99 -- verified, it is an "'asm' undeclared" error -- and AGENTS.md requires C99 compatibility. Inline assembly itself is a documented Rule 1.2 deviation; there is no conforming way to reach the instruction. A precondition is written down that was previously only true by luck of the call sites: m must be non-zero, and the two implementations DISAGREE when it is not. CLZ(0) is 32, so this yields 31 - 32 while the portable loop yields 0. All twelve call sites in common/src were read, and every one reaches the macro only on a map already tested against zero, so the difference is unreachable today. demo_clz pins both answers so that changing which is true has to be a decision. New regression image, demo_clz.elf, because turning this on means hand-written inline assembly now decides which thread runs next: - all 32 single-bit maps return their own bit position; - all 32 positions with a deterministic spray of HIGHER bits still return that position, which is what distinguishes "lowest set bit" from "only set bit"; - ten hand-picked patterns including 0x80000000, the input upstream's signed negation had undefined behaviour for; - every map run twice, into a UINT and into a ULONG, because the kernel calls the macro with both and the closing 31 - b is where a width mistake hides; - the m == 0 divergence, pinned; - the map-is-consumed property, which callers rely on by never relying on it; - and #error guards so an image that reached the checks with the CLZ path disabled fails to build rather than testing the portable loop and reporting a pass. Verified, including negatively -- three deliberate breaks, each caught: drop the isolation step -> the 32 single-bit maps still PASS and the sprayed group catches it, which is exactly why that group exists; 31 - b becomes 32 - b -> caught by the single-bit group; restore the dead guard -> caught at compile time by the #error. FVP 613 targets, S32Z280 596 targets, zero warnings either way. FVP ctest 7/7 in 9.75 s. All 370 kernel translation units of both libthreadx and libthreadx_module compile clean under T7's strict warning set with the CLZ path live. CLZ present in fvp_module.elf, s32z280_module.elf and s32z280_demo.elf, so the module port's own copy of the header is covered by the module image rather than by inspection. Measured on tx_thread_system_suspend.o, the one object that carries it in the default configuration: .text 2188 -> 1316 bytes, 547 -> 329 instructions. A 40% smaller priority search on a code path the scheduler runs on every suspend and resume. Scope deliberately limited to the two R52 ports, which are the ones this branch can build and run. The same dead guard is in about two dozen other tx_port.h files across ports/, ports_arch/, ports_module/ and ports_smp/ -- every GNU port, and the armclang ones too, since armclang is Clang-based and predefines the ACLE macros rather than Arm Compiler 5's. Fixing ports with no build here would mean shipping unvalidated changes, so those belong in an upstream issue with this commit as the reference. The S32Z280 images DID change this time, unlike T7: the kernel now carries two CLZ instructions. The FVP runs the same instructions on the same architecture and is green across all seven images, and CLZ has no memory, MPU or cache interaction, so the risk is low -- but a silicon re-run is outstanding and is not claimed here. Assisted-by: Claude Code (Opus 5) --- .../fvp_baser_aemv8r/CMakeLists.txt | 37 +- .../example_build/fvp_baser_aemv8r/demo_clz.c | 359 ++++++++++++++++++ ports/cortex_r52/gnu/inc/tx_port.h | 71 +++- ports_module/cortex_r52/gnu/inc/tx_port.h | 71 +++- 4 files changed, 521 insertions(+), 17 deletions(-) create mode 100644 ports/cortex_r52/gnu/example_build/fvp_baser_aemv8r/demo_clz.c diff --git a/ports/cortex_r52/gnu/example_build/fvp_baser_aemv8r/CMakeLists.txt b/ports/cortex_r52/gnu/example_build/fvp_baser_aemv8r/CMakeLists.txt index 41510a620..b4eabeb56 100644 --- a/ports/cortex_r52/gnu/example_build/fvp_baser_aemv8r/CMakeLists.txt +++ b/ports/cortex_r52/gnu/example_build/fvp_baser_aemv8r/CMakeLists.txt @@ -131,6 +131,38 @@ target_link_options(demo_threadx.elf PRIVATE ${R52_LINK_QUIET_RWX} ) +# CLZ lowest-set-bit regression. tx_port.h replaces tx_thread.h's portable +# priority search with a CLZ instruction, and that macro decides which thread +# runs next on every suspend and resume -- so it gets an image of its own rather +# than being covered incidentally by demos that would merely hang if it broke. +# Needs the tick because the checks run in a thread; needs nothing else. +add_executable(demo_clz.elf EXCLUDE_FROM_ALL + ${FVP_DIR}/entry.S + ${R52_CONSOLE_SOURCES} + ${FVP_DIR}/gicv3.c + ${FVP_DIR}/timer.c + ${FVP_DIR}/irq_dispatch.c + ${FVP_DIR}/tx_initialize_low_level.S + ${FVP_DIR}/demo_clz.c +) + +target_compile_definitions(demo_clz.elf PRIVATE TX_R52_USE_THREADX_IRQ) + +target_link_libraries(demo_clz.elf PRIVATE threadx) + +target_include_directories(demo_clz.elf PRIVATE + ${FVP_DIR} + ${CMAKE_SOURCE_DIR}/common/inc + ${CMAKE_SOURCE_DIR}/ports/${THREADX_ARCH}/${THREADX_TOOLCHAIN}/inc +) + +target_link_options(demo_clz.elf PRIVATE + -T${FVP_DIR}/link.lds + -nostartfiles + -Wl,-Map=demo_clz.map + ${R52_LINK_QUIET_RWX} +) + # AR1/M5 -- lazy VFP context save and restore. Only meaningful when the # library was built with TX_R52_ENABLE_VFP and a floating-point ABI, so the # target exists only in that configuration. @@ -264,7 +296,8 @@ if(TX_R52_ENABLE_FIQ_NESTING) endif() # Every image built here, in the order they should be exercised. -set(R52_IMAGES boot_check.elf demo_m2.elf demo_m3.elf demo_threadx.elf demo_mpu.elf) +set(R52_IMAGES boot_check.elf demo_m2.elf demo_m3.elf demo_threadx.elf demo_mpu.elf + demo_clz.elf) # The linker script is passed with -T, which CMake does not treat as a # dependency, so editing it would not trigger a relink and stale images would @@ -613,6 +646,8 @@ if(FVP_BASER_AEMV8R) "Running AR1/M3 tick and preemption demo on FVP_BaseR_AEMv8R...") threadx_r52_add_fvp_run(demo_threadx.elf run-demo-threadx-r52 "Running the standard ThreadX demo on FVP_BaseR_AEMv8R...") + threadx_r52_add_fvp_run(demo_clz.elf run-demo-clz-r52 + "Running the CLZ lowest-set-bit regression on FVP_BaseR_AEMv8R...") if(TX_R52_BUILD_FVP_MODULE_EXAMPLE) threadx_r52_add_fvp_run(fvp_module.elf run-module-r52 "Running the AR2 module isolation example on FVP_BaseR_AEMv8R...") diff --git a/ports/cortex_r52/gnu/example_build/fvp_baser_aemv8r/demo_clz.c b/ports/cortex_r52/gnu/example_build/fvp_baser_aemv8r/demo_clz.c new file mode 100644 index 000000000..a156f4ec7 --- /dev/null +++ b/ports/cortex_r52/gnu/example_build/fvp_baser_aemv8r/demo_clz.c @@ -0,0 +1,359 @@ +/*************************************************************************** + * 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 + **************************************************************************/ + +/**************************************************************************/ +/* */ +/* DEMONSTRATION RELEASE */ +/* */ +/* demo_clz.c Cortex-R52/GNU */ +/* 6.5.2 */ +/* AUTHOR */ +/* */ +/* Frédéric Desbiens, Eclipse Foundation */ +/* */ +/* DESCRIPTION */ +/* */ +/* Regression test for this port's CLZ implementation of */ +/* TX_LOWEST_SET_BIT_CALCULATE, which replaces the portable loop in */ +/* tx_thread.h and which the scheduler's priority search runs on */ +/* every suspend and resume. */ +/* */ +/* Why this image exists. The macro was DEAD until 20 Aug 2026: */ +/* tx_port.h guarded it with __TARGET_ARCH_ARM > 4, an Arm Compiler 5 */ +/* predefine that GCC never defines, so the test read 0 > 4 and the */ +/* portable loop ran on a core that has had CLZ since Armv5. Turning */ +/* it on means hand-written inline assembly now decides which thread */ +/* runs next, so it gets a test that pins the answer for every input */ +/* class rather than a build that merely compiles. */ +/* */ +/* What is checked, against an obvious linear-scan reference rather */ +/* than against the implementation being tested: */ +/* */ +/* 1. Each of the 32 single-bit maps returns its own bit position. */ +/* 2. Each bit position with a deterministic spray of HIGHER bits */ +/* set still returns that position -- the macro must find the */ +/* LOWEST set bit, and a single-bit test cannot tell the */ +/* difference between "lowest" and "only". */ +/* 3. A table of hand-picked patterns, including the two the */ +/* respelled isolation step could plausibly break: 0x80000000, */ +/* the one input for which upstream's signed negation was */ +/* undefined, and 0xFFFFFFFF. */ +/* 4. Every group is run twice, once through a UINT result and once */ +/* through a ULONG one, because the kernel calls the macro with */ +/* both and the closing 31 - b is where a width mistake would */ +/* hide. */ +/* 5. The m == 0 divergence is PINNED, not fixed. CLZ(0) is 32, so */ +/* this implementation yields 31 - 32 while the portable loop */ +/* yields 0. Every one of the twelve call sites in common/src */ +/* reaches the macro only on a map already tested against zero, */ +/* so the difference is unreachable -- and a test that records */ +/* it is how it stays a decision instead of becoming a surprise. */ +/* */ +/* The checks run inside a ThreadX thread on purpose. By the time the */ +/* thread's entry is reached the scheduler has already executed the */ +/* macro on the way to dispatching it, so a macro broken badly enough */ +/* to misdirect the priority search shows up as a hang -- which the */ +/* runner scores as a failure -- before any check is evaluated. */ +/* */ +/**************************************************************************/ + +#include "tx_api.h" +#include "console.h" +#include "board.h" + +/* The port's definition must be the one in scope. tx_thread.h supplies a + portable fallback under #ifndef, so an image that reached this file with the + macro undefined would be testing that fallback and reporting a pass for it -- + the exact failure mode this whole image exists to rule out. */ + +#ifndef TX_LOWEST_SET_BIT_CALCULATE +#error "demo_clz: tx_port.h did not define TX_LOWEST_SET_BIT_CALCULATE, so this image would test the portable loop and call it a pass." +#endif + +/* And it must be defined for the reason we think it is. These are the two + conditions tx_port.h uses; asserting them here means a future edit that + silently drops the CLZ path cannot leave this image passing. */ + +#if !defined(__ARM_FEATURE_CLZ) +#error "demo_clz: __ARM_FEATURE_CLZ is not defined, so the CLZ path in tx_port.h is not the definition under test." +#endif + +#if defined(__thumb__) +#error "demo_clz: built for Thumb, where tx_port.h deliberately keeps the portable loop. Build this image -marm." +#endif + + +#define DEMO_STACK_SIZE 2048 + +static TX_THREAD check_thread; +static UCHAR check_stack[DEMO_STACK_SIZE]; + +/* Failure detail, kept as counters rather than printed per case: 32 passing + lines tell a reader nothing and would bury the one failing line. */ + +static UINT failures; + + +/**************************************************************************/ +/* reference_lowest_bit */ +/* */ +/* The ground truth, written to be read rather than to be fast. It is */ +/* deliberately NOT the portable macro from tx_thread.h: comparing an */ +/* implementation against a cleverer implementation of the same idea */ +/* tests that they share their mistakes. A linear scan has nowhere to */ +/* hide one. */ +/**************************************************************************/ + +static ULONG reference_lowest_bit(ULONG map) +{ + ULONG index; + ULONG result = 0xFFFFFFFFUL; /* No bit set. */ + + for (index = 0UL; index < 32UL; index++) + { + if ((map & (1UL << index)) != 0UL) + { + result = index; + break; + } + } + + return result; +} + + +/**************************************************************************/ +/* next_pattern */ +/* */ +/* A fixed linear congruential sequence. A regression test must produce */ +/* the same inputs on every run, so the spray of high bits in group 2 is */ +/* pseudo-random and seeded, never actually random. */ +/**************************************************************************/ + +static ULONG next_pattern(ULONG state) +{ + return (state * 1103515245UL) + 12345UL; +} + + +/**************************************************************************/ +/* check_one */ +/* */ +/* Runs one map through the macro twice, once landing in a UINT and once */ +/* in a ULONG, and compares both against the reference. The macro */ +/* CONSUMES its first argument -- both implementations rewrite the map */ +/* in place -- so each call gets its own copy, which is also the trap a */ +/* caller reusing the variable afterwards would fall into. */ +/**************************************************************************/ + +static void check_one(ULONG map, ULONG expected, const char *group_ptr) +{ + ULONG working; + UINT bit_as_uint; + ULONG bit_as_ulong; + + working = map; + TX_LOWEST_SET_BIT_CALCULATE(working, bit_as_uint) + + working = map; + TX_LOWEST_SET_BIT_CALCULATE(working, bit_as_ulong) + + if (((ULONG) bit_as_uint != expected) || (bit_as_ulong != expected)) + { + failures++; + + console_puts("[FAIL] "); + console_puts(group_ptr); + console_puts(" map="); + console_puthex(map); + console_puts(" expected="); + console_puthex(expected); + console_puts(" got UINT="); + console_puthex((unsigned long) bit_as_uint); + console_puts(" ULONG="); + console_puthex(bit_as_ulong); + console_puts("\n"); + } +} + + +/**************************************************************************/ +/* report */ +/**************************************************************************/ + +static void report(const char *label_ptr, UINT failures_before) +{ + console_puts(label_ptr); + console_puts((failures == failures_before) ? "PASS\n" : "FAIL\n"); +} + + +/**************************************************************************/ +/* thread_check_entry */ +/**************************************************************************/ + +static void thread_check_entry(ULONG thread_input) +{ + ULONG index; + ULONG spray; + ULONG state; + ULONG map; + ULONG working; + ULONG bit; + UINT before; + + /* Hand-picked patterns. 0x80000000 is the one input for which upstream's + (ULONG)(-((LONG) m)) isolation was undefined behaviour, and 0xFFFFFFFF is + the densest map the scheduler can present. */ + + static const ULONG patterns[] = + { + 0xFFFFFFFFUL, 0x80000000UL, 0xAAAAAAAAUL, 0x55555555UL, + 0xF0F0F000UL, 0x00000003UL, 0x7FFFFFFFUL, 0xC0000000UL, + 0x00010000UL, 0xFFFF0000UL + }; + + (void) thread_input; + + console_puts("\n=== ThreadX Cortex-R52 CLZ lowest-set-bit regression ===\n\n"); + + /* Group 1 -- every single-bit map. */ + + before = failures; + for (index = 0UL; index < 32UL; index++) + { + check_one(1UL << index, index, "single bit"); + } + report("[check] 32 single-bit maps ................ ", before); + + /* Group 2 -- every bit position with higher bits sprayed above it. This is + the group that distinguishes "lowest set bit" from "only set bit". */ + + before = failures; + state = 0x12345678UL; + for (index = 0UL; index < 32UL; index++) + { + state = next_pattern(state); + + /* Keep only bits strictly above index, then put index back. Shifting a + 32-bit value by 32 is undefined, so the top position is special-cased + to no spray at all -- there is nothing above bit 31 to set. */ + + if (index < 31UL) + { + spray = (state << (index + 1UL)) & 0xFFFFFFFFUL; + } + else + { + spray = 0UL; + } + + check_one(spray | (1UL << index), index, "lowest of many"); + } + report("[check] 32 sprayed maps, lowest bit wins .. ", before); + + /* Group 3 -- the hand-picked table. */ + + before = failures; + for (index = 0UL; index < (ULONG) (sizeof(patterns) / sizeof(patterns[0])); index++) + { + check_one(patterns[index], reference_lowest_bit(patterns[index]), "pattern"); + } + report("[check] 10 hand-picked patterns .......... ", before); + + /* Group 4 -- the pinned divergence at zero. Not a fix: a record. CLZ(0) + is 32 and the closing subtraction is unsigned, so the answer is 31 - 32 + taken modulo 2^32. The portable loop in tx_thread.h answers 0 for the + same input. Neither is reachable from the kernel, and this check exists + so that changing which one is true has to be deliberate. */ + + before = failures; + working = 0UL; + TX_LOWEST_SET_BIT_CALCULATE(working, bit) + + if (bit != 0xFFFFFFFFUL) + { + failures++; + console_puts("[FAIL] map=0 answered "); + console_puthex(bit); + console_puts(", documented answer for the CLZ implementation is 0xFFFFFFFF\n"); + } + report("[check] map = 0 answers as documented .... ", before); + + /* Group 5 -- the macro consumes its first argument, in both + implementations. Stated as a check because it is a property callers rely + on by never relying on it, and a "fix" that stopped rewriting the map + would quietly change what every call site's variable holds afterwards. */ + + before = failures; + map = 0x00000280UL; /* Lowest set bit is 7. */ + working = map; + TX_LOWEST_SET_BIT_CALCULATE(working, bit) + + if ((bit != 7UL) || (working != (1UL << 7))) + { + failures++; + console_puts("[FAIL] expected bit 7 and map reduced to 0x80, got bit "); + console_puthex(bit); + console_puts(" map "); + console_puthex(working); + console_puts("\n"); + } + report("[check] map is reduced to its lowest bit . ", before); + + console_puts("\n[info] 75 maps checked, each through a UINT and a ULONG result.\n"); + + if (failures == 0U) + { + console_puts("\nCLZ RESULT: ALL CHECKS PASSED\n"); + } + else + { + console_puts("\nCLZ RESULT: FAILED\n"); + } + + console_exit(failures); +} + + +/**************************************************************************/ +/* tx_application_define */ +/**************************************************************************/ + +void tx_application_define(void *first_unused_memory) +{ + (void) first_unused_memory; + + tx_thread_create(&check_thread, "clz check", thread_check_entry, 0UL, + check_stack, DEMO_STACK_SIZE, + 1U, 1U, TX_NO_TIME_SLICE, TX_AUTO_START); +} + + +/**************************************************************************/ +/* bsp_main -- entered at EL1 from entry.S. Does not return. */ +/**************************************************************************/ + +void bsp_main(void) +{ + tx_kernel_enter(); + + /* Not reached. */ + + for (;;) + { + /* Nothing. */ + } +} diff --git a/ports/cortex_r52/gnu/inc/tx_port.h b/ports/cortex_r52/gnu/inc/tx_port.h index 7feb2300a..173e485d7 100644 --- a/ports/cortex_r52/gnu/inc/tx_port.h +++ b/ports/cortex_r52/gnu/inc/tx_port.h @@ -255,17 +255,72 @@ typedef unsigned short USHORT; #define TX_TIMER_DELETE_EXTENSION(timer_ptr) -/* Determine if the ARM architecture has the CLZ instruction. This is available on - architectures v5 and above. If available, redefine the macro for calculating the - lowest bit set. */ - -#if __TARGET_ARCH_ARM > 4 +/* Determine whether this core has the CLZ instruction and this compiler will + admit to it, and if so replace the portable lowest-set-bit search with it. + + The guard is not upstream's. Upstream asks __TARGET_ARCH_ARM > 4, which is an + Arm Compiler 5 predefine. GCC does not define it -- it predefines the ACLE + macros __ARM_ARCH and __ARM_FEATURE_CLZ instead -- so under GCC the test reads + 0 > 4, this whole block is dropped and tx_thread.h's portable loop runs on a + core that has had the instruction since Armv5. Measured with + arm-none-eabi-gcc 14.3 on 20 Aug 2026: zero CLZ instructions in the built + scheduler objects. + + That was not a dormant path. Half the TX_LOWEST_SET_BIT_CALCULATE call sites + in tx_thread_suspend.c and tx_thread_system_suspend.c sit OUTSIDE the + TX_MAX_PRIORITIES > 32 guards, so the portable loop was running in the + scheduler's priority search in the default 32-priority configuration, which is + the one every R52 build uses. + + __ARM_FEATURE_CLZ is the ACLE answer to the question actually being asked, and + the compiler defines it exactly when the architecture has the instruction, so + a core without CLZ is excluded by construction rather than by an architecture + number. Arm Compiler 5's spelling is kept beside it, now wrapped in defined() + so the test no longer leans on an undefined identifier evaluating to zero -- + which is what -Wundef reports and how this was found. + + The __thumb__ guard stays, and it is load-bearing rather than inherited + caution: __ARM_FEATURE_CLZ describes the ARCHITECTURE, not the instruction + set. Checked on 20 Aug 2026 -- GCC defines it for -mthumb -march=armv5te, + where Thumb-1 has no CLZ at all and this asm would fail to assemble. A Thumb + build therefore keeps the portable loop on purpose. (On this core it is moot: + the R52 toolchain file builds -marm.) + + Two deliberate deviations, per AGENTS.md: + + - Rule 1.2, language extensions. Inline assembly is the entire point of the + macro; there is no conforming way to reach CLZ. Spelled __asm__ and not + asm, because the asm keyword is rejected under -std=c99 -- verified, it is + an "'asm' undeclared" error -- and AGENTS.md requires C99 compatibility. + + - Rule 10.1 / 10.3 on the isolation step, which is why it is respelled. + Upstream isolates the lowest set bit with (ULONG) (-((LONG) m)): that + converts an unsigned map to signed and negates it, which is undefined for + the one input whose top bit is set. (~(m)) + 1 is the same value in + well-defined unsigned arithmetic, and it is character-for-character what + tx_thread.h's portable version uses -- so the two implementations now + visibly compute the same thing instead of merely agreeing. + + Rule 20.7 is a straight fix rather than a deviation: upstream leaves m and b + unparenthesised in the expansion. + + PRECONDITION: m must be non-zero, and the two implementations DISAGREE when it + is not. CLZ(0) is 32, so this yields 31 - 32; the portable loop yields 0. + All twelve call sites in common/src reach the macro only on a map already + tested against zero -- every one checked on 20 Aug 2026 -- so the divergence is + unreachable today. It is written down because a new call site is exactly how + it would stop being unreachable, and demo_clz.c pins both answers so that + changing this has to be a decision. */ + +#if defined(__ARM_FEATURE_CLZ) || (defined(__TARGET_ARCH_ARM) && (__TARGET_ARCH_ARM > 4)) #ifndef __thumb__ -#define TX_LOWEST_SET_BIT_CALCULATE(m, b) m = m & ((ULONG) (-((LONG) m))); \ - asm volatile (" CLZ %0,%1 ": "=r" (b) : "r" (m) ); \ - b = 31 - b; +#define TX_LOWEST_SET_BIT_CALCULATE(m, b) \ + (m) = (m) & ((~(m)) + ((ULONG) 1)); \ + __asm__ volatile (" CLZ %0,%1 " : "=r" (b) : "r" (m)); \ + (b) = 31 - (b); + #endif #endif diff --git a/ports_module/cortex_r52/gnu/inc/tx_port.h b/ports_module/cortex_r52/gnu/inc/tx_port.h index fdc5a7dc0..d3013b1f0 100644 --- a/ports_module/cortex_r52/gnu/inc/tx_port.h +++ b/ports_module/cortex_r52/gnu/inc/tx_port.h @@ -304,17 +304,72 @@ typedef unsigned short USHORT; #define TX_TIMER_DELETE_EXTENSION(timer_ptr) -/* Determine if the ARM architecture has the CLZ instruction. This is available on - architectures v5 and above. If available, redefine the macro for calculating the - lowest bit set. */ - -#if __TARGET_ARCH_ARM > 4 +/* Determine whether this core has the CLZ instruction and this compiler will + admit to it, and if so replace the portable lowest-set-bit search with it. + + The guard is not upstream's. Upstream asks __TARGET_ARCH_ARM > 4, which is an + Arm Compiler 5 predefine. GCC does not define it -- it predefines the ACLE + macros __ARM_ARCH and __ARM_FEATURE_CLZ instead -- so under GCC the test reads + 0 > 4, this whole block is dropped and tx_thread.h's portable loop runs on a + core that has had the instruction since Armv5. Measured with + arm-none-eabi-gcc 14.3 on 20 Aug 2026: zero CLZ instructions in the built + scheduler objects. + + That was not a dormant path. Half the TX_LOWEST_SET_BIT_CALCULATE call sites + in tx_thread_suspend.c and tx_thread_system_suspend.c sit OUTSIDE the + TX_MAX_PRIORITIES > 32 guards, so the portable loop was running in the + scheduler's priority search in the default 32-priority configuration, which is + the one every R52 build uses. + + __ARM_FEATURE_CLZ is the ACLE answer to the question actually being asked, and + the compiler defines it exactly when the architecture has the instruction, so + a core without CLZ is excluded by construction rather than by an architecture + number. Arm Compiler 5's spelling is kept beside it, now wrapped in defined() + so the test no longer leans on an undefined identifier evaluating to zero -- + which is what -Wundef reports and how this was found. + + The __thumb__ guard stays, and it is load-bearing rather than inherited + caution: __ARM_FEATURE_CLZ describes the ARCHITECTURE, not the instruction + set. Checked on 20 Aug 2026 -- GCC defines it for -mthumb -march=armv5te, + where Thumb-1 has no CLZ at all and this asm would fail to assemble. A Thumb + build therefore keeps the portable loop on purpose. (On this core it is moot: + the R52 toolchain file builds -marm.) + + Two deliberate deviations, per AGENTS.md: + + - Rule 1.2, language extensions. Inline assembly is the entire point of the + macro; there is no conforming way to reach CLZ. Spelled __asm__ and not + asm, because the asm keyword is rejected under -std=c99 -- verified, it is + an "'asm' undeclared" error -- and AGENTS.md requires C99 compatibility. + + - Rule 10.1 / 10.3 on the isolation step, which is why it is respelled. + Upstream isolates the lowest set bit with (ULONG) (-((LONG) m)): that + converts an unsigned map to signed and negates it, which is undefined for + the one input whose top bit is set. (~(m)) + 1 is the same value in + well-defined unsigned arithmetic, and it is character-for-character what + tx_thread.h's portable version uses -- so the two implementations now + visibly compute the same thing instead of merely agreeing. + + Rule 20.7 is a straight fix rather than a deviation: upstream leaves m and b + unparenthesised in the expansion. + + PRECONDITION: m must be non-zero, and the two implementations DISAGREE when it + is not. CLZ(0) is 32, so this yields 31 - 32; the portable loop yields 0. + All twelve call sites in common/src reach the macro only on a map already + tested against zero -- every one checked on 20 Aug 2026 -- so the divergence is + unreachable today. It is written down because a new call site is exactly how + it would stop being unreachable, and demo_clz.c pins both answers so that + changing this has to be a decision. */ + +#if defined(__ARM_FEATURE_CLZ) || (defined(__TARGET_ARCH_ARM) && (__TARGET_ARCH_ARM > 4)) #ifndef __thumb__ -#define TX_LOWEST_SET_BIT_CALCULATE(m, b) m = m & ((ULONG) (-((LONG) m))); \ - asm volatile (" CLZ %0,%1 ": "=r" (b) : "r" (m) ); \ - b = 31 - b; +#define TX_LOWEST_SET_BIT_CALCULATE(m, b) \ + (m) = (m) & ((~(m)) + ((ULONG) 1)); \ + __asm__ volatile (" CLZ %0,%1 " : "=r" (b) : "r" (m)); \ + (b) = 31 - (b); + #endif #endif From ca4d6008d90def527fd43885b19ece69167dc2f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20Desbiens?= Date: Thu, 20 Aug 2026 13:25:27 -0400 Subject: [PATCH 33/33] Documented the CLZ search, and corrected the readme's AP claim Two readme fixes that the previous commit made due. The image list in section 3 enumerates every example, so adding demo_clz.elf without adding it here would have left the readme telling a reader to build five images out of six. The ninja line and the list both name it now, and section 6 says what the optimisation is, what it is worth (2188 to 1316 bytes of the priority search), that it applies in the default 32-priority configuration rather than only above 32, and why a Thumb build deliberately keeps the portable loop. The second one is not mine and is worth calling out. Section 7 told anyone reusing this code that PRBAR.AP is encoded "the reverse of the widely-published Armv8-R AArch64 macro set" and to re-calibrate before trusting it. That is false, and mpu.h has said so since the shift bug was found: AP[2] selects read-only and AP[1] grants EL0 access, exactly as published, and the four-region calibration that suggested otherwise was measuring a program_region() that shifted every PRBAR field one bit too far left. So the readme was contradicting the header beside it on the encoding of a permissions register, which is the worst place to be wrong -- a reader who "re-calibrated" against that advice would have inverted their own access permissions. Replaced with what mpu.h establishes, including the part that is genuinely worth carrying: region coverage is enforced even when the permissions are not what you asked for, so an unmapped address faults while a "read-only" region quietly accepts writes, and only provoking a real fault tells the two apart. That is the transferable lesson the old text was reaching for. No code change; FVP ctest still 7/7. Assisted-by: Claude Code (Opus 5) --- ports/cortex_r52/gnu/readme_threadx.txt | 42 ++++++++++++++++++++----- 1 file changed, 34 insertions(+), 8 deletions(-) diff --git a/ports/cortex_r52/gnu/readme_threadx.txt b/ports/cortex_r52/gnu/readme_threadx.txt index ccc2c8d60..c3b026d4a 100644 --- a/ports/cortex_r52/gnu/readme_threadx.txt +++ b/ports/cortex_r52/gnu/readme_threadx.txt @@ -48,7 +48,7 @@ baseline selects the soft ABI rather than removing the FPU. -DCMAKE_TOOLCHAIN_FILE=cmake/cortex_r52.cmake \ -DTX_R52_BUILD_FVP_EXAMPLE=ON . ninja -C build_r52 boot_check.elf demo_m2.elf demo_m3.elf \ - demo_threadx.elf demo_mpu.elf + demo_threadx.elf demo_mpu.elf demo_clz.elf ctest --test-dir build_r52 The images target the free Armv8-R AEM FVP (FVP_BaseR_AEMv8R): @@ -58,6 +58,7 @@ The images target the free Armv8-R AEM FVP (FVP_BaseR_AEMv8R): demo_m3.elf generic timer tick, GICv3 and preemption demo_threadx.elf the standard eight-thread demo plus verification demo_mpu.elf PMSAv8-R protection and cache enable + demo_clz.elf the CLZ lowest-set-bit priority search demo_m5.elf lazy VFP context save (needs TX_R52_ENABLE_VFP) Each image reports its own result and terminates the model through the @@ -132,6 +133,20 @@ and the thread stack pointer and run counter offsets, at compile time: a layout change becomes a build failure instead of silent corruption. +Also worth knowing: this port replaces tx_thread.h's portable lowest-set-bit +search with the CLZ instruction, which is what the scheduler uses to pick the +next thread to run. Upstream gates that on __TARGET_ARCH_ARM, an Arm Compiler +5 predefine that GCC does not define, so the optimisation had never once been +compiled in under GCC; the guard here asks __ARM_FEATURE_CLZ instead. It is +worth 40% of the priority search's code size (2188 to 1316 bytes in +tx_thread_system_suspend.o) and it applies in the default 32-priority +configuration, not only above 32. A Thumb build deliberately keeps the +portable loop, because __ARM_FEATURE_CLZ describes the architecture rather +than the instruction set and Thumb-1 has no CLZ. demo_clz.elf is the +regression test, and it fails to build rather than silently testing the +portable loop if the CLZ path is ever disabled again. + + 7. Memory Protection PMSAv8-R regions are described by a table rather than a sequence of @@ -142,13 +157,24 @@ disables the regions it does not use. Two cautions for anyone reusing this code on silicon: - - The PRBAR.AP encoding used here was calibrated against the hardware. - The low bit is read-only and the high bit grants EL0 access, which is - the reverse of the widely-published Armv8-R AArch64 macro set. Getting - it wrong produces regions that report as read-only and accept writes, - because region coverage is still enforced: an unmapped address faults - while a "read-only" region does not. Re-calibrate before trusting - these values on a different implementation. + - The PRBAR.AP encoding is the standard Armv8-R one, as published: + AP[2] selects read-only and AP[1] grants EL0 access. No calibration is + needed, and an earlier revision of this file said otherwise -- it + claimed the two bits were reversed, on the strength of a real + measurement with a wrong cause. program_region() had been shifting + every PRBAR field one bit too far left, so the AP value's low bit + landed in the true AP[2] and writes faulted exactly when that bit was + set, which looks precisely like a reversed encoding. The shift is + fixed; see the comment on the MPU_AP_* macros in mpu.h for the full + calibration table and what it actually proved. + + The reason it took a silicon run to notice: region coverage is enforced + even when permissions are not what you asked for, so an unmapped + address faults while a "read-only" region quietly accepts writes. A + configuration-only review cannot tell the two apart -- provoke a real + fault instead. Verified on S32Z280 silicon: a write to an RO region + faults and execution from an XN region takes a prefetch abort, the + latter never having worked under the old shift. - The code and data regions must not share a 64-byte granule. The linker script separates them with ".data ALIGN(64) :" on the output