summaryrefslogtreecommitdiff
path: root/test
diff options
context:
space:
mode:
Diffstat (limited to 'test')
-rw-r--r--test/freertos/cmake/CMakeLists.txt78
-rw-r--r--test/freertos/cmake/regression/CMakeLists.txt84
-rwxr-xr-xtest/freertos/cmake/run.sh135
-rw-r--r--test/freertos/fixtures/FreeRTOSConfig.h66
-rw-r--r--test/freertos/fixtures/tx_user.h35
-rw-r--r--test/freertos/readme.md86
-rw-r--r--test/freertos/regression/txfr_queue_create_test.c132
-rw-r--r--test/freertos/regression/txfr_sync_create_test.c129
-rw-r--r--test/freertos/regression/txfr_task_create_test.c145
-rw-r--r--test/freertos/regression/txfr_test_harness.c384
-rw-r--r--test/freertos/regression/txfr_test_harness.h103
-rw-r--r--test/posix/cmake/CMakeLists.txt83
-rw-r--r--test/posix/cmake/regression/CMakeLists.txt65
-rwxr-xr-xtest/posix/cmake/run.sh189
-rw-r--r--test/posix/regression/posix_mq_basic_test.c159
-rw-r--r--test/posix/regression/posix_mq_send_abort_test.c273
-rw-r--r--test/posix/regression/posix_pthread_basic_test.c192
-rw-r--r--test/posix/regression/posix_pthread_self_test.c177
-rw-r--r--test/smp/cmake/CMakeLists.txt16
-rwxr-xr-xtest/smp/cmake/coverage.sh146
-rw-r--r--test/smp/cmake/regression/CMakeLists.txt8
-rw-r--r--test/smp/cmake/threadx_smp/CMakeLists.txt1
-rw-r--r--test/smp/cmake/threadx_smp/common_smp/CMakeLists.txt1
-rw-r--r--test/smp/cmake/threadx_smp/ports_smp/linux/gnu/CMakeLists.txt1
-rw-r--r--test/smp/cmake/threadx_smp/ports_smp/win64/vs_2022/CMakeLists.txt1
-rw-r--r--test/smp/regression/testcontrol.c379
-rw-r--r--test/smp/regression/threadx_block_memory_basic_test.c4
-rw-r--r--test/smp/regression/threadx_block_memory_error_detection_test.c4
-rw-r--r--test/smp/regression/threadx_block_memory_information_test.c8
-rw-r--r--test/smp/regression/threadx_block_memory_suspension_timeout_test.c2
-rw-r--r--test/smp/regression/threadx_byte_memory_basic_test.c4
-rw-r--r--test/smp/regression/threadx_byte_memory_information_test.c6
-rw-r--r--test/smp/regression/threadx_byte_memory_long_search_test.c337
-rw-r--r--test/smp/regression/threadx_byte_memory_suspension_timeout_test.c2
-rw-r--r--test/smp/regression/threadx_event_flag_basic_test.c4
-rw-r--r--test/smp/regression/threadx_event_flag_information_test.c6
-rw-r--r--test/smp/regression/threadx_mutex_basic_test.c4
-rw-r--r--test/smp/regression/threadx_mutex_information_test.c6
-rw-r--r--test/smp/regression/threadx_object_delete_suspension_test.c481
-rw-r--r--test/smp/regression/threadx_queue_basic_one_word_test.c4
-rw-r--r--test/smp/regression/threadx_queue_information_test.c6
-rw-r--r--test/smp/regression/threadx_semaphore_basic_test.c4
-rw-r--r--test/smp/regression/threadx_semaphore_information_test.c6
-rw-r--r--test/smp/regression/threadx_smp_random_resume_suspend_exclusion_pt_test.c10
-rw-r--r--test/smp/regression/threadx_smp_relinquish_rebalance_test.c250
-rw-r--r--test/smp/regression/threadx_smp_thread_exit_callback_remote_test.c514
-rw-r--r--test/smp/regression/threadx_smp_time_slice_test.c2
-rw-r--r--test/smp/regression/threadx_thread_basic_execution_test.c4
-rw-r--r--test/smp/regression/threadx_thread_delayed_suspension_test.c39
-rw-r--r--test/smp/regression/threadx_thread_exit_callback_transition_test.c1375
-rw-r--r--test/smp/regression/threadx_thread_information_test.c6
-rw-r--r--test/smp/regression/threadx_thread_misaligned_stack_test.c156
-rw-r--r--test/smp/regression/threadx_thread_multiple_sleep_test.c2
-rw-r--r--test/smp/regression/threadx_thread_priority_change.c50
-rw-r--r--test/smp/regression/threadx_thread_stack_checking_test.c15
-rw-r--r--test/smp/regression/threadx_thread_stack_fill_value_test.c190
-rw-r--r--test/smp/regression/threadx_thread_wait_abort_and_isr_test.c89
-rw-r--r--test/smp/regression/threadx_timer_information_test.c6
-rw-r--r--test/smp/regression/threadx_timer_simple_test.c4
-rw-r--r--test/smp/regression/threadx_trace_basic_test.c4
-rw-r--r--test/smp/regression/threadx_trace_entry_update_test.c367
-rw-r--r--test/tx/cmake/CMakeLists.txt79
-rwxr-xr-xtest/tx/cmake/coverage.sh142
-rw-r--r--test/tx/cmake/module_manager/CMakeLists.txt254
-rw-r--r--test/tx/cmake/regression/CMakeLists.txt4
-rw-r--r--test/tx/cmake/riscv/CMakeLists.txt11
-rw-r--r--test/tx/cmake/riscv/bsp/entry.S6
-rw-r--r--test/tx/cmake/riscv/bsp/hwtimer.c84
-rw-r--r--test/tx/cmake/riscv/bsp/hwtimer.h4
-rw-r--r--test/tx/cmake/riscv/bsp/link.lds32
-rw-r--r--test/tx/cmake/riscv/bsp/plic.c25
-rw-r--r--test/tx/cmake/riscv/bsp/syscalls.c49
-rw-r--r--test/tx/cmake/riscv/bsp/trap.c3
-rw-r--r--test/tx/cmake/riscv/bsp/tx_initialize_low_level.S35
-rw-r--r--test/tx/cmake/riscv/regression/CMakeLists.txt12
-rw-r--r--test/tx/cmake/riscv/regression/threadx_riscv_new_thread_fpu_state_test.c278
-rwxr-xr-xtest/tx/cmake/riscv/run.sh33
-rw-r--r--test/tx/cmake/thread_stack_alignment/CMakeLists.txt89
-rw-r--r--test/tx/cmake/thread_transition/CMakeLists.txt131
-rw-r--r--test/tx/module_manager/threadx_module_manager_block_pool_parameters_test.c1452
-rw-r--r--test/tx/module_manager/threadx_module_manager_cortex_a7_mmu_test.c205
-rw-r--r--test/tx/module_manager/threadx_module_manager_cortex_a7_range_check_test.c443
-rw-r--r--test/tx/module_manager/threadx_module_manager_delete_ownership_test.c1031
-rw-r--r--test/tx/module_manager/threadx_module_manager_host_test_port.h80
-rw-r--r--test/tx/module_manager/threadx_module_manager_live_object_deallocation_test.c1262
-rw-r--r--test/tx/module_manager/threadx_module_manager_object_allocate_overflow_test.c830
-rw-r--r--test/tx/module_manager/threadx_module_manager_object_authentication_test.c1217
-rw-r--r--test/tx/module_manager/threadx_module_manager_object_name_range_test.c1608
-rw-r--r--test/tx/module_manager/threadx_module_manager_queue_message_range_test.c836
-rw-r--r--test/tx/module_manager/threadx_module_manager_thread_kernel_stack_test.c589
-rw-r--r--test/tx/regression/testcontrol.c12
-rw-r--r--test/tx/regression/threadx_block_memory_basic_test.c4
-rw-r--r--test/tx/regression/threadx_block_memory_error_detection_test.c4
-rw-r--r--test/tx/regression/threadx_block_memory_information_test.c8
-rw-r--r--test/tx/regression/threadx_byte_memory_basic_test.c4
-rw-r--r--test/tx/regression/threadx_byte_memory_information_test.c6
-rw-r--r--test/tx/regression/threadx_event_flag_basic_test.c4
-rw-r--r--test/tx/regression/threadx_event_flag_information_test.c6
-rw-r--r--test/tx/regression/threadx_initialize_kernel_setup_test.c2
-rw-r--r--test/tx/regression/threadx_mutex_basic_test.c4
-rw-r--r--test/tx/regression/threadx_mutex_information_test.c6
-rw-r--r--test/tx/regression/threadx_queue_basic_one_word_test.c4
-rw-r--r--test/tx/regression/threadx_queue_information_test.c6
-rw-r--r--test/tx/regression/threadx_semaphore_basic_test.c4
-rw-r--r--test/tx/regression/threadx_semaphore_information_test.c6
-rw-r--r--test/tx/regression/threadx_test_port.h7
-rw-r--r--test/tx/regression/threadx_thread_basic_execution_test.c6
-rw-r--r--test/tx/regression/threadx_thread_delayed_suspension_test.c39
-rw-r--r--test/tx/regression/threadx_thread_exit_callback_transition_test.c1375
-rw-r--r--test/tx/regression/threadx_thread_information_test.c6
-rw-r--r--test/tx/regression/threadx_thread_misaligned_stack_test.c150
-rw-r--r--test/tx/regression/threadx_thread_multi_level_preemption_threshold_test.c4
-rw-r--r--test/tx/regression/threadx_thread_priority_change.c27
-rw-r--r--test/tx/regression/threadx_thread_stack_checking_test.c15
-rw-r--r--test/tx/regression/threadx_thread_stack_fill_value_test.c190
-rw-r--r--test/tx/regression/threadx_thread_wait_abort_and_isr_test.c178
-rw-r--r--test/tx/regression/threadx_timer_information_test.c6
-rw-r--r--test/tx/regression/threadx_timer_simple_test.c4
-rw-r--r--test/tx/regression/threadx_trace_basic_test.c47
-rw-r--r--test/tx/regression/threadx_trace_entry_update_test.c383
-rw-r--r--test/tx/thread_stack_alignment/threadx_thread_stack_alignment_schedule_test.c154
-rw-r--r--test/tx/thread_stack_alignment/threadx_thread_stack_alignment_test.c228
-rw-r--r--test/tx/thread_transition/threadx_thread_transition_configuration_test.c607
-rw-r--r--test/tx/thread_transition/threadx_thread_transition_host_test_port.h120
124 files changed, 21337 insertions, 152 deletions
diff --git a/test/freertos/cmake/CMakeLists.txt b/test/freertos/cmake/CMakeLists.txt
new file mode 100644
index 00000000..a8c07d8d
--- /dev/null
+++ b/test/freertos/cmake/CMakeLists.txt
@@ -0,0 +1,78 @@
+##############################################################################
+# 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
+##############################################################################
+
+cmake_minimum_required(VERSION 3.13 FATAL_ERROR)
+cmake_policy(SET CMP0054 NEW)
+cmake_policy(SET CMP0057 NEW)
+
+project(threadx_freertos_test LANGUAGES C)
+
+set(CMAKE_C_STANDARD 99)
+set(CMAKE_C_STANDARD_REQUIRED ON)
+set(CMAKE_C_EXTENSIONS OFF)
+
+# The suite intercepts the ThreadX entry points with the linker's --wrap, which
+# is a GNU ld and lld feature. There is no MSVC equivalent, so the suite is
+# built on Linux only and the build stops early elsewhere rather than failing
+# later with confusing link errors.
+if(NOT CMAKE_C_COMPILER_ID MATCHES "GNU|Clang")
+ message(FATAL_ERROR
+ "The FreeRTOS compatibility layer tests require GNU ld or lld for "
+ "--wrap support and are supported on Linux only.")
+endif()
+
+set(BUILD_CONFIGURATIONS default_build)
+set(CMAKE_CONFIGURATION_TYPES
+ ${BUILD_CONFIGURATIONS}
+ CACHE STRING "list of supported configuration types" FORCE)
+set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS
+ ${CMAKE_CONFIGURATION_TYPES})
+list(GET CMAKE_CONFIGURATION_TYPES 0 BUILD_TYPE)
+if((NOT CMAKE_BUILD_TYPE) OR (NOT ("${CMAKE_BUILD_TYPE}" IN_LIST
+ CMAKE_CONFIGURATION_TYPES)))
+ set(CMAKE_BUILD_TYPE
+ "${BUILD_TYPE}"
+ CACHE STRING "Build Type of the project" FORCE)
+endif()
+
+message(STATUS "Build type: ${CMAKE_BUILD_TYPE}")
+message(STATUS "Using toolchain file: ${CMAKE_TOOLCHAIN_FILE}.")
+
+# Built 32 bit, as the ThreadX and SMP suites are. This is not incidental for
+# this suite: the Linux port defines ULONG as unsigned int on x86_64, while the
+# compatibility layer passes pointers through ULONG arguments, for instance the
+# task argument and the timer identifier. A 64 bit build therefore truncates
+# those pointers, which the compiler reports as -Wpointer-to-int-cast and which
+# would crash the timer callback wrapper.
+add_compile_options(-m32)
+add_link_options(-m32)
+
+# Applied to everything, including the ThreadX build pulled in below. The
+# strict warning set is applied to the test code alone, in the regression
+# CMakeLists, since neither ThreadX nor the layer under test builds clean
+# under -Wextra today and that is not this suite's to fix.
+add_compile_options(-ggdb -g3 -fdiagnostics-color)
+
+enable_testing()
+
+# The compatibility layer stores a back pointer in each thread control block,
+# so ThreadX has to be built with the matching user extension.
+set(TX_USER_FILE
+ ${CMAKE_CURRENT_LIST_DIR}/../fixtures/tx_user.h
+ CACHE FILEPATH "tx_user.h used for the FreeRTOS layer tests" FORCE)
+
+add_subdirectory(${CMAKE_CURRENT_LIST_DIR}/../../.. threadx)
+
+add_subdirectory(${CMAKE_CURRENT_LIST_DIR}/regression)
diff --git a/test/freertos/cmake/regression/CMakeLists.txt b/test/freertos/cmake/regression/CMakeLists.txt
new file mode 100644
index 00000000..21da2a6d
--- /dev/null
+++ b/test/freertos/cmake/regression/CMakeLists.txt
@@ -0,0 +1,84 @@
+##############################################################################
+# 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
+##############################################################################
+
+cmake_minimum_required(VERSION 3.13 FATAL_ERROR)
+cmake_policy(SET CMP0057 NEW)
+
+project(freertos_regression_test LANGUAGES C)
+
+set(SOURCE_DIR ${CMAKE_CURRENT_LIST_DIR}/../../regression)
+set(FIXTURE_DIR ${CMAKE_CURRENT_LIST_DIR}/../../fixtures)
+set(REPO_ROOT ${CMAKE_CURRENT_LIST_DIR}/../../../..)
+set(LAYER_SOURCE ${REPO_ROOT}/utility/rtos_compatibility_layers/FreeRTOS/tx_freertos.c)
+set(LAYER_INCLUDE_DIR ${REPO_ROOT}/utility/rtos_compatibility_layers/FreeRTOS)
+
+set(freertos_test_cases
+ ${SOURCE_DIR}/txfr_queue_create_test.c
+ ${SOURCE_DIR}/txfr_task_create_test.c
+ ${SOURCE_DIR}/txfr_sync_create_test.c)
+
+# tx_api.h maps the public API onto the error checking entry points, so those
+# are the symbols that exist at link time and the ones the harness wraps. A
+# name that does not resolve is silently ignored by the linker, so this list
+# must be kept in step with the __wrap_ functions in txfr_test_harness.c.
+set(wrapped_symbols
+ _txe_byte_allocate
+ _txe_byte_release
+ _txe_semaphore_create
+ _txe_semaphore_delete
+ _txe_mutex_create
+ _txe_mutex_delete
+ _txe_event_flags_create
+ _txe_event_flags_delete
+ _txe_timer_create
+ _txe_timer_delete
+ _txe_thread_create
+ _txe_thread_delete
+ _txe_thread_resume)
+
+set(wrap_link_options "")
+foreach(symbol ${wrapped_symbols})
+ list(APPEND wrap_link_options "-Wl,--wrap=${symbol}")
+endforeach()
+
+# The layer is compiled from source into each test rather than linked from the
+# freertos-threadx target, so that the test's FreeRTOSConfig.h applies.
+add_library(freertos_layer_under_test OBJECT ${LAYER_SOURCE})
+target_include_directories(freertos_layer_under_test
+ PUBLIC ${LAYER_INCLUDE_DIR} ${FIXTURE_DIR})
+target_link_libraries(freertos_layer_under_test PUBLIC azrtos::threadx)
+
+set(test_warning_options -Wall -Wextra -Werror)
+
+add_library(freertos_test_harness OBJECT ${SOURCE_DIR}/txfr_test_harness.c)
+target_include_directories(freertos_test_harness
+ PUBLIC ${SOURCE_DIR} ${LAYER_INCLUDE_DIR} ${FIXTURE_DIR})
+target_link_libraries(freertos_test_harness PUBLIC azrtos::threadx)
+target_compile_options(freertos_test_harness PRIVATE ${test_warning_options})
+
+foreach(test_case ${freertos_test_cases})
+ get_filename_component(test_name ${test_case} NAME_WE)
+
+ add_executable(${test_name} ${test_case}
+ $<TARGET_OBJECTS:freertos_test_harness>
+ $<TARGET_OBJECTS:freertos_layer_under_test>)
+ target_include_directories(${test_name}
+ PRIVATE ${SOURCE_DIR} ${LAYER_INCLUDE_DIR} ${FIXTURE_DIR})
+ target_link_libraries(${test_name} PRIVATE azrtos::threadx)
+ target_link_options(${test_name} PRIVATE ${wrap_link_options})
+ target_compile_options(${test_name} PRIVATE ${test_warning_options})
+
+ add_test(${CMAKE_BUILD_TYPE}::${test_name} ${test_name})
+endforeach()
diff --git a/test/freertos/cmake/run.sh b/test/freertos/cmake/run.sh
new file mode 100755
index 00000000..896a5c37
--- /dev/null
+++ b/test/freertos/cmake/run.sh
@@ -0,0 +1,135 @@
+#!/bin/bash
+##############################################################################
+# 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
+##############################################################################
+
+# Builds and runs the FreeRTOS compatibility layer regression suite. The
+# interface matches test/tx/cmake/run.sh so that the shared CI template can
+# drive it: "run.sh build all" then "run.sh test all".
+
+set -e
+
+function help() {
+ echo "Usage: $0 [build|test] [all|<build_configuration> <build_configuration>...]"
+ echo "Available build_configuration:"
+ for build in ${build_configurations[*]}; do
+ echo " $build"
+ done
+ exit 1
+}
+
+function validate() {
+ for build in ${build_configurations[*]}; do
+ if [ "$1" == "$build" ]; then
+ return
+ fi
+ done
+ help
+}
+
+function generate() {
+ build=$1
+ cmake -Bbuild/$build -GNinja -DCMAKE_TOOLCHAIN_FILE=$(dirname $(realpath $0))/../../../cmake/linux.cmake -DCMAKE_BUILD_TYPE=$build .
+}
+
+function build() {
+ # -k 0 keeps Ninja going after a target fails, so one broken target no
+ # longer decides whether the targets after it exist. ctest reports a
+ # missing binary as a failing test, which turned a single link error into
+ # a failure count that varied with build scheduling order.
+ cmake --build build/$1 -- -k 0
+}
+
+function test() {
+ # Guard the pushd: with the caller capturing this function's status, set -e
+ # no longer aborts here, so a missing build directory would otherwise let
+ # ctest run in the source tree and report "no tests" as success.
+ pushd build/$1 || return 1
+ [ -z "${CTEST_PARALLEL_LEVEL}" ] && parallel="-j$2"
+ if [ -z "${CTEST_REPEAT_FAIL}" ];
+ then
+ repeat_fail=2
+ else
+ repeat_fail=${CTEST_REPEAT_FAIL}
+ fi
+ # ctest's status is captured and returned past the popd and the summary
+ # steps, which all succeed. Without this the function would return 0 and a
+ # failing configuration would be reported as a pass.
+ local status=0
+ ctest $parallel --timeout 300 -O $1.txt -T test --no-compress-output --test-output-size-passed 4194304 --test-output-size-failed 4194304 --output-on-failure --repeat until-pass:${repeat_fail} --output-junit $1.xml || status=$?
+ popd
+ # Tolerated: this is a summary for humans, and a ctest that died early
+ # enough to leave no matching line must not mask its own status.
+ grep -E "^(\s*[0-9]+|Total)" build/$1/$1.txt >build/$1.txt || true
+ sed -i "s/\x1B\[[0-9;]*[JKmsu]//g" build/$1.txt
+ return $status
+}
+
+cd $(dirname $0)
+
+result=$(sed -n "/(BUILD_CONFIGURATIONS/,/)/p" CMakeLists.txt|sed ':label;N;s/\n/ /;b label'|grep -Pzo "[a-zA-Z0-9_]*build[a-zA-Z0-9_]*\s*"| tr -d '\0')
+IFS=' '
+read -ra build_configurations <<< "$result"
+
+if [ $# -lt 1 ]; then
+ help
+fi
+
+command=$1
+shift
+
+if [ "$#" == "0" ]; then
+ builds=${build_configurations[0]}
+elif [ "$*" == "all" ]; then
+ builds=${build_configurations[@]}
+else
+ for item in $*; do
+ validate $item
+ done
+ builds=$*
+fi
+
+if [ "$command" == "build" ]; then
+ for item in $builds; do
+ generate $item
+ echo ""
+ done
+
+ # A failing configuration must not stop the ones after it: under set -e
+ # the loop would abort and leave them unbuilt, which then reads as a wall
+ # of missing-binary test failures. The status is accumulated and returned.
+ build_status=0
+ for item in $builds; do
+ echo "Building $item"
+ build $item || build_status=$?
+ echo ""
+ done
+ [ $build_status -eq 0 ] || exit $build_status
+elif [ "$command" == "test" ]; then
+ cores=$(nproc)
+ if [ -z "${CTEST_PARALLEL_LEVEL}" ];
+ then
+ parallel_jobs=$(($cores + 2))
+ fi
+ # ctest's status is accumulated rather than allowed to abort the loop, so
+ # a failing configuration does not leave the ones after it untested.
+ exit_code=0
+ for item in $builds; do
+ echo "Testing $item"
+ test $item $parallel_jobs || exit_code=$?
+ done
+ exit $exit_code
+else
+ help
+fi
diff --git a/test/freertos/fixtures/FreeRTOSConfig.h b/test/freertos/fixtures/FreeRTOSConfig.h
new file mode 100644
index 00000000..017b7171
--- /dev/null
+++ b/test/freertos/fixtures/FreeRTOSConfig.h
@@ -0,0 +1,66 @@
+/***************************************************************************
+ * 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
+ **************************************************************************/
+
+/**************************************************************************/
+/** */
+/** ThreadX Component */
+/** */
+/** FreeRTOS compatibility layer regression test configuration */
+/** */
+/**************************************************************************/
+
+/* Configuration used to build the compatibility layer under test. It follows
+ the shipped template in utility/rtos_compatibility_layers/FreeRTOS/
+ config_template, with the choices a test build needs called out below. */
+
+#ifndef FREERTOS_CONFIG_H
+#define FREERTOS_CONFIG_H
+
+#define configTICK_RATE_HZ (1000u)
+#define configMAX_PRIORITIES (32u)
+#define configMINIMAL_STACK_SIZE (512u)
+
+/* Large enough that a handful of leaked descriptors cannot exhaust it. The
+ tests therefore never infer a leak from an allocation failure; they count
+ the byte pool operations directly. */
+#define configTOTAL_HEAP_SIZE (1024u * 128u)
+
+#define configUSE_16_BIT_TICKS 0
+#define configSTACK_DEPTH_TYPE uint32_t
+
+/* Task deletion is included so that the idle task, and with it the teardown
+ path the tests exercise, is built. */
+#define INCLUDE_vTaskDelete 1
+
+/* The tests drive the error paths deliberately, so neither assertion may halt
+ the run. A failed expectation is reported by the harness instead. */
+#define configASSERT(x)
+#define TX_FREERTOS_ASSERT_FAIL()
+
+/* The harness calls tx_freertos_init() from tx_application_define(), so the
+ automatic initialization path is not wanted here. */
+#define TX_FREERTOS_AUTO_INIT 0
+
+/* FreeRTOS.h selects portDISABLE_INTERRUPTS() by compiler rather than by
+ target, so a GNU build resolves it to the bare metal __disable_interrupts()
+ intrinsic, which does not exist when the layer is hosted on Linux. Both
+ macros are guarded with #ifndef, so the definitions below pre-empt that
+ choice with the ThreadX primitives the header's own fallback branch uses. */
+UINT _tx_thread_interrupt_disable(VOID);
+VOID _tx_thread_interrupt_restore(UINT previous_posture);
+#define portDISABLE_INTERRUPTS() _tx_thread_interrupt_disable()
+#define portENABLE_INTERRUPTS() _tx_thread_interrupt_restore(TX_INT_ENABLE)
+
+#endif /* #ifndef FREERTOS_CONFIG_H */
diff --git a/test/freertos/fixtures/tx_user.h b/test/freertos/fixtures/tx_user.h
new file mode 100644
index 00000000..834e711e
--- /dev/null
+++ b/test/freertos/fixtures/tx_user.h
@@ -0,0 +1,35 @@
+/***************************************************************************
+ * 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
+ **************************************************************************/
+
+/**************************************************************************/
+/** */
+/** ThreadX Component */
+/** */
+/** FreeRTOS compatibility layer regression test ThreadX settings */
+/** */
+/**************************************************************************/
+
+/* Every other ThreadX setting is left at its default; only the extension the
+ compatibility layer requires is defined here. */
+
+#ifndef TX_USER_H
+#define TX_USER_H
+
+/* The layer stores a back pointer to its own task structure in each thread
+ control block, as documented in the layer's readme.md. Without this the
+ layer does not compile. */
+#define TX_THREAD_USER_EXTENSION VOID *txfr_thread_ptr;
+
+#endif /* #ifndef TX_USER_H */
diff --git a/test/freertos/readme.md b/test/freertos/readme.md
new file mode 100644
index 00000000..0bb1b650
--- /dev/null
+++ b/test/freertos/readme.md
@@ -0,0 +1,86 @@
+# FreeRTOS compatibility layer regression tests
+
+Regression tests for `utility/rtos_compatibility_layers/FreeRTOS/tx_freertos.c`,
+built against the real ThreadX Linux port.
+
+## Running them
+
+```bash
+./scripts/build_freertos.sh
+./scripts/test_freertos.sh
+```
+
+Or directly, which is the same thing:
+
+```bash
+test/freertos/cmake/run.sh build all
+test/freertos/cmake/run.sh test all
+```
+
+CI runs both scripts through `.github/workflows/regression_test.yml`.
+
+## What they cover
+
+Every function in the layer that creates an object takes one or two byte pool
+allocations for its bookkeeping and then creates one or more ThreadX kernel
+objects. When one of those kernel objects cannot be created, the function
+returns `NULL`, or `pdFAIL` for `xTaskCreate()`, and the caller is left with no
+handle and therefore no way to call the matching delete function. Anything the
+layer fails to release on the way out is lost until the system restarts.
+
+That makes these error paths invisible from the outside: a leaking version and
+a correct version return exactly the same thing to the caller. The suite
+therefore counts the ThreadX primitives the layer reaches for, and checks that
+each error path gives back precisely what it took.
+
+| Test | Covers |
+| --- | --- |
+| `txfr_queue_create_test` | `xQueueCreate`, `xQueueCreateStatic`, `vQueueDelete` |
+| `txfr_task_create_test` | `xTaskCreate`, `xTaskCreateStatic` |
+| `txfr_sync_create_test` | semaphores, mutexes, event groups and timers |
+
+## How the fault injection works
+
+A test asks the harness to fail a chosen kernel creation call, then reads back
+how many allocations, releases, object creations and object deletions the layer
+performed. The interception is done with the linker's `--wrap` option, so
+`tx_freertos.c` is compiled exactly as it ships, with no test hooks in it.
+
+Two things are worth knowing before adding tests:
+
+- `tx_api.h` maps the public API onto the error checking entry points, so the
+ symbols that exist at link time are the `_txe_` variants, and those are what
+ the wrap list in `cmake/regression/CMakeLists.txt` names. A `--wrap` for a
+ name that does not resolve is silently ignored, so a typo there produces a
+ test that quietly never injects anything.
+- `txfr_malloc()` and `txfr_free()` cannot be wrapped, because they are defined
+ in `tx_freertos.c` and called from within it, so the compiler resolves those
+ calls internally. The byte pool counts stand in for them.
+
+Because `--wrap` is a GNU ld and lld feature with no MSVC equivalent, this
+suite is Linux only. The CMake configuration stops with a clear message rather
+than failing later with confusing link errors.
+
+## Fixtures
+
+`fixtures/FreeRTOSConfig.h` configures the layer for the tests. Two of its
+settings are not arbitrary:
+
+- `configASSERT()` and `TX_FREERTOS_ASSERT_FAIL()` are empty, since the tests
+ drive error paths deliberately and neither may halt the run.
+- `portDISABLE_INTERRUPTS()` and `portENABLE_INTERRUPTS()` are defined up front.
+ `FreeRTOS.h` picks those by compiler rather than by target, so a GNU build
+ otherwise resolves them to the bare metal `__disable_interrupts()` intrinsic,
+ which does not exist when the layer is hosted on Linux.
+
+`fixtures/tx_user.h` supplies `TX_THREAD_USER_EXTENSION`, which the layer
+requires, as documented in the layer's own `readme.md`.
+
+## Why the build is 32 bit
+
+The Linux port defines `ULONG` as `unsigned int` on x86_64, while the layer
+passes pointers through `ULONG` arguments, such as the task argument and the
+timer identifier. A 64 bit build truncates those pointers, which the compiler
+reports as `-Wpointer-to-int-cast` and which crashes the timer callback
+wrapper. The ThreadX and SMP suites build 32 bit for their own reasons; this
+suite has to.
diff --git a/test/freertos/regression/txfr_queue_create_test.c b/test/freertos/regression/txfr_queue_create_test.c
new file mode 100644
index 00000000..aa996070
--- /dev/null
+++ b/test/freertos/regression/txfr_queue_create_test.c
@@ -0,0 +1,132 @@
+/***************************************************************************
+ * 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
+ **************************************************************************/
+
+/**************************************************************************/
+/** */
+/** ThreadX Component */
+/** */
+/** FreeRTOS compatibility layer, queue creation resource accounting */
+/** */
+/**************************************************************************/
+
+/* xQueueCreate() takes two byte pool allocations, the descriptor and the
+ backing storage, and then creates two semaphores. Since a failure returns
+ NULL, the caller has no handle and cannot call vQueueDelete(), so anything
+ the layer fails to undo on the way out is lost for good. These checks pin
+ down the accounting on each of those paths.
+
+ The failure paths here are the ones reported in issue 570. */
+
+#include "txfr_test_harness.h"
+
+#include "queue.h"
+
+#define QUEUE_LENGTH 8u
+#define QUEUE_ITEM_SIZE 32u
+
+/* Storage for the static variant. */
+static StaticQueue_t queue_control_block;
+static uint8_t queue_storage[QUEUE_LENGTH * QUEUE_ITEM_SIZE];
+
+
+void txfr_test_body(void)
+{
+ QueueHandle_t queue;
+ TXFR_COUNTERS counters;
+ int i;
+ int cycles;
+
+ /* A successful creation takes both allocations and creates both
+ semaphores, and releases nothing. */
+ txfr_test_account_start(TXFR_INJECT_NONE, 0);
+ queue = xQueueCreate(QUEUE_LENGTH, QUEUE_ITEM_SIZE);
+ txfr_test_account_stop(&counters);
+ txfr_test_check("xQueueCreate returns a handle", queue != NULL);
+ txfr_test_check_counts("xQueueCreate accounting", &counters, 2, 0, 2, 0);
+
+ /* Deleting it gives both allocations back and deletes both semaphores. */
+ if(queue != NULL)
+ {
+ txfr_test_account_start(TXFR_INJECT_NONE, 0);
+ vQueueDelete(queue);
+ txfr_test_account_stop(&counters);
+ txfr_test_check_counts("vQueueDelete accounting", &counters, 0, 2, 0, 2);
+ }
+
+ /* The read semaphore fails. Nothing was created that needs deleting, but
+ both allocations must be released. */
+ txfr_test_account_start(TXFR_INJECT_SEMAPHORE_CREATE, 1);
+ queue = xQueueCreate(QUEUE_LENGTH, QUEUE_ITEM_SIZE);
+ txfr_test_account_stop(&counters);
+ txfr_test_check("xQueueCreate returns NULL on read_sem failure", queue == NULL);
+ txfr_test_check_counts("read_sem failure releases both allocations", &counters, 2, 2, 1, 0);
+
+ /* The write semaphore fails. The read semaphore already exists by then, so
+ it must be deleted as well as both allocations released. */
+ txfr_test_account_start(TXFR_INJECT_SEMAPHORE_CREATE, 2);
+ queue = xQueueCreate(QUEUE_LENGTH, QUEUE_ITEM_SIZE);
+ txfr_test_account_stop(&counters);
+ txfr_test_check("xQueueCreate returns NULL on write_sem failure", queue == NULL);
+ txfr_test_check_counts("write_sem failure unwinds read_sem and memory", &counters, 2, 2, 2, 1);
+
+ /* The static variant takes no allocations and creates both semaphores. */
+ txfr_test_account_start(TXFR_INJECT_NONE, 0);
+ queue = xQueueCreateStatic(QUEUE_LENGTH, QUEUE_ITEM_SIZE, queue_storage, &queue_control_block);
+ txfr_test_account_stop(&counters);
+ txfr_test_check("xQueueCreateStatic returns a handle", queue != NULL);
+ txfr_test_check_counts("xQueueCreateStatic accounting", &counters, 0, 0, 2, 0);
+
+ if(queue != NULL)
+ {
+ txfr_test_account_start(TXFR_INJECT_NONE, 0);
+ vQueueDelete(queue);
+ txfr_test_account_stop(&counters);
+ txfr_test_check_counts("vQueueDelete on static queue", &counters, 0, 0, 0, 2);
+ }
+
+ /* The static variant owns no memory, but it still creates two semaphores
+ and still hands back nothing on failure, so the caller cannot call
+ vQueueDelete(). A read semaphore left behind here would stay registered
+ in the kernel, pointing into the caller's StaticQueue_t. */
+ txfr_test_account_start(TXFR_INJECT_SEMAPHORE_CREATE, 1);
+ queue = xQueueCreateStatic(QUEUE_LENGTH, QUEUE_ITEM_SIZE, queue_storage, &queue_control_block);
+ txfr_test_account_stop(&counters);
+ txfr_test_check("xQueueCreateStatic returns NULL on read_sem failure", queue == NULL);
+ txfr_test_check_counts("static read_sem failure leaves nothing behind", &counters, 0, 0, 1, 0);
+
+ txfr_test_account_start(TXFR_INJECT_SEMAPHORE_CREATE, 2);
+ queue = xQueueCreateStatic(QUEUE_LENGTH, QUEUE_ITEM_SIZE, queue_storage, &queue_control_block);
+ txfr_test_account_stop(&counters);
+ txfr_test_check("xQueueCreateStatic returns NULL on write_sem failure", queue == NULL);
+ txfr_test_check_counts("static write_sem failure unwinds read_sem", &counters, 0, 0, 2, 1);
+
+ /* Repeated create and delete cycles must leave the pool able to serve the
+ same request. This would not catch a small leak on its own, which is why
+ the counts above are checked directly, but it does catch a descriptor
+ that is released to the wrong pool or released twice. */
+ cycles = 0;
+ for(i = 0; i < 64; i++)
+ {
+ queue = xQueueCreate(QUEUE_LENGTH, QUEUE_ITEM_SIZE);
+ if(queue == NULL)
+ {
+ break;
+ }
+
+ cycles++;
+ vQueueDelete(queue);
+ }
+ txfr_test_check("64 create and delete cycles succeed", cycles == 64);
+}
diff --git a/test/freertos/regression/txfr_sync_create_test.c b/test/freertos/regression/txfr_sync_create_test.c
new file mode 100644
index 00000000..14bd53d8
--- /dev/null
+++ b/test/freertos/regression/txfr_sync_create_test.c
@@ -0,0 +1,129 @@
+/***************************************************************************
+ * 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
+ **************************************************************************/
+
+/**************************************************************************/
+/** */
+/** ThreadX Component */
+/** */
+/** FreeRTOS compatibility layer, synchronisation object accounting */
+/** */
+/**************************************************************************/
+
+/* Semaphores, mutexes, event groups and timers each take a single descriptor
+ allocation and then create one kernel object. Every one of them must give
+ the descriptor back when that kernel object cannot be created. */
+
+#include "txfr_test_harness.h"
+
+#include "semphr.h"
+#include "event_groups.h"
+#include "timers.h"
+
+
+static void test_timer_callback(TimerHandle_t timer)
+{
+ (void)timer;
+}
+
+
+void txfr_test_body(void)
+{
+ SemaphoreHandle_t semaphore;
+ EventGroupHandle_t event_group;
+ TimerHandle_t timer;
+ TXFR_COUNTERS counters;
+
+ /* Counting semaphore. */
+ txfr_test_account_start(TXFR_INJECT_NONE, 0);
+ semaphore = xSemaphoreCreateCounting(4u, 0u);
+ txfr_test_account_stop(&counters);
+ txfr_test_check("xSemaphoreCreateCounting returns a handle", semaphore != NULL);
+ txfr_test_check_counts("xSemaphoreCreateCounting accounting", &counters, 1, 0, 1, 0);
+ if(semaphore != NULL)
+ {
+ vSemaphoreDelete(semaphore);
+ }
+
+ txfr_test_account_start(TXFR_INJECT_SEMAPHORE_CREATE, 1);
+ semaphore = xSemaphoreCreateCounting(4u, 0u);
+ txfr_test_account_stop(&counters);
+ txfr_test_check("xSemaphoreCreateCounting returns NULL on failure", semaphore == NULL);
+ txfr_test_check_counts("counting semaphore failure releases descriptor", &counters, 1, 1, 1, 0);
+
+ /* Binary semaphore, which shares the same descriptor path. */
+ txfr_test_account_start(TXFR_INJECT_SEMAPHORE_CREATE, 1);
+ semaphore = xSemaphoreCreateBinary();
+ txfr_test_account_stop(&counters);
+ txfr_test_check("xSemaphoreCreateBinary returns NULL on failure", semaphore == NULL);
+ txfr_test_check_counts("binary semaphore failure releases descriptor", &counters, 1, 1, 1, 0);
+
+ /* Mutex. */
+ txfr_test_account_start(TXFR_INJECT_NONE, 0);
+ semaphore = xSemaphoreCreateMutex();
+ txfr_test_account_stop(&counters);
+ txfr_test_check("xSemaphoreCreateMutex returns a handle", semaphore != NULL);
+ txfr_test_check_counts("xSemaphoreCreateMutex accounting", &counters, 1, 0, 1, 0);
+ if(semaphore != NULL)
+ {
+ vSemaphoreDelete(semaphore);
+ }
+
+ txfr_test_account_start(TXFR_INJECT_MUTEX_CREATE, 1);
+ semaphore = xSemaphoreCreateMutex();
+ txfr_test_account_stop(&counters);
+ txfr_test_check("xSemaphoreCreateMutex returns NULL on failure", semaphore == NULL);
+ txfr_test_check_counts("mutex failure releases descriptor", &counters, 1, 1, 1, 0);
+
+ /* Recursive mutex, which differs only in the inheritance argument. */
+ txfr_test_account_start(TXFR_INJECT_MUTEX_CREATE, 1);
+ semaphore = xSemaphoreCreateRecursiveMutex();
+ txfr_test_account_stop(&counters);
+ txfr_test_check("xSemaphoreCreateRecursiveMutex returns NULL on failure", semaphore == NULL);
+ txfr_test_check_counts("recursive mutex failure releases descriptor", &counters, 1, 1, 1, 0);
+
+ /* Event group. */
+ txfr_test_account_start(TXFR_INJECT_NONE, 0);
+ event_group = xEventGroupCreate();
+ txfr_test_account_stop(&counters);
+ txfr_test_check("xEventGroupCreate returns a handle", event_group != NULL);
+ txfr_test_check_counts("xEventGroupCreate accounting", &counters, 1, 0, 1, 0);
+ if(event_group != NULL)
+ {
+ vEventGroupDelete(event_group);
+ }
+
+ txfr_test_account_start(TXFR_INJECT_EVENT_FLAGS_CREATE, 1);
+ event_group = xEventGroupCreate();
+ txfr_test_account_stop(&counters);
+ txfr_test_check("xEventGroupCreate returns NULL on failure", event_group == NULL);
+ txfr_test_check_counts("event group failure releases descriptor", &counters, 1, 1, 1, 0);
+
+ /* Timer. */
+ txfr_test_account_start(TXFR_INJECT_NONE, 0);
+ timer = xTimerCreate("t", 10u, pdFALSE, NULL, test_timer_callback);
+ txfr_test_account_stop(&counters);
+ txfr_test_check("xTimerCreate returns a handle", timer != NULL);
+ txfr_test_check_counts("xTimerCreate accounting", &counters, 1, 0, 1, 0);
+ if(timer != NULL)
+ {
+ (void)xTimerDelete(timer, 0u);
+ }
+
+ txfr_test_account_start(TXFR_INJECT_TIMER_CREATE, 1);
+ timer = xTimerCreate("t", 10u, pdFALSE, NULL, test_timer_callback);
+ txfr_test_account_stop(&counters);
+ txfr_test_check("xTimerCreate returns NULL on failure", timer == NULL);
+ txfr_test_check_counts("timer failure releases descriptor", &counters, 1, 1, 1, 0);
+}
diff --git a/test/freertos/regression/txfr_task_create_test.c b/test/freertos/regression/txfr_task_create_test.c
new file mode 100644
index 00000000..2371bcc0
--- /dev/null
+++ b/test/freertos/regression/txfr_task_create_test.c
@@ -0,0 +1,145 @@
+/***************************************************************************
+ * 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
+ **************************************************************************/
+
+/**************************************************************************/
+/** */
+/** ThreadX Component */
+/** */
+/** FreeRTOS compatibility layer, task creation resource accounting */
+/** */
+/**************************************************************************/
+
+/* xTaskCreate() takes two byte pool allocations, the stack and the task
+ structure, then creates a notification semaphore and a thread. It is the
+ function the rest of the layer's error handling is modelled on, so these
+ checks also serve as the reference for what correct unwinding looks like. */
+
+#include "txfr_test_harness.h"
+
+#include "task.h"
+
+#define TASK_STACK_DEPTH (configMINIMAL_STACK_SIZE)
+#define TASK_PRIORITY (3u)
+
+static StaticTask_t task_control_block;
+static StackType_t task_stack[TASK_STACK_DEPTH];
+
+/* Each failure case gets its own buffers, for two reasons. vTaskDelete() only
+ queues a task for the idle task to reap, so the control block of a task
+ deleted above is still live while this test thread runs. And if one of these
+ paths ever regresses and leaves an object behind, a shared buffer would
+ carry that damage into the next case and report misleading counts there. */
+static StaticTask_t semaphore_failure_control_block;
+static StackType_t semaphore_failure_stack[TASK_STACK_DEPTH];
+static StaticTask_t thread_failure_control_block;
+static StackType_t thread_failure_stack[TASK_STACK_DEPTH];
+static StaticTask_t resume_failure_control_block;
+static StackType_t resume_failure_stack[TASK_STACK_DEPTH];
+
+
+static void test_task_entry(void *p_arg)
+{
+ (void)p_arg;
+
+ /* Never scheduled at this priority while the test thread runs, but a task
+ entry point must not return. */
+ for(;;)
+ {
+ vTaskDelay(portMAX_DELAY);
+ }
+}
+
+
+void txfr_test_body(void)
+{
+ BaseType_t result;
+ TaskHandle_t task;
+ TXFR_COUNTERS counters;
+
+ /* A successful creation takes both allocations, creates the notification
+ semaphore and the thread, and releases nothing. */
+ task = NULL;
+ txfr_test_account_start(TXFR_INJECT_NONE, 0);
+ result = xTaskCreate(test_task_entry, "ok", TASK_STACK_DEPTH, NULL, TASK_PRIORITY, &task);
+ txfr_test_account_stop(&counters);
+ txfr_test_check("xTaskCreate reports success", result == pdPASS);
+ txfr_test_check("xTaskCreate returns a handle", task != NULL);
+ txfr_test_check_counts("xTaskCreate accounting", &counters, 2, 0, 2, 0);
+
+ if(task != NULL)
+ {
+ vTaskDelete(task);
+ }
+
+ /* The notification semaphore fails before anything else is created, so
+ both allocations must come back and nothing needs deleting. */
+ task = NULL;
+ txfr_test_account_start(TXFR_INJECT_SEMAPHORE_CREATE, 1);
+ result = xTaskCreate(test_task_entry, "sem", TASK_STACK_DEPTH, NULL, TASK_PRIORITY, &task);
+ txfr_test_account_stop(&counters);
+ txfr_test_check("xTaskCreate fails when the semaphore fails", result != pdPASS);
+ txfr_test_check_counts("semaphore failure releases stack and task", &counters, 2, 2, 1, 0);
+
+ /* The thread creation fails with the semaphore already in place, so that
+ semaphore must be deleted and both allocations released. */
+ task = NULL;
+ txfr_test_account_start(TXFR_INJECT_THREAD_CREATE, 1);
+ result = xTaskCreate(test_task_entry, "thread", TASK_STACK_DEPTH, NULL, TASK_PRIORITY, &task);
+ txfr_test_account_stop(&counters);
+ txfr_test_check("xTaskCreate fails when the thread fails", result != pdPASS);
+ txfr_test_check_counts("thread failure unwinds the semaphore too", &counters, 2, 2, 2, 1);
+
+ /* The static variant takes no allocations, and creates the semaphore and
+ the thread from the caller's buffers. */
+ txfr_test_account_start(TXFR_INJECT_NONE, 0);
+ task = xTaskCreateStatic(test_task_entry, "static", TASK_STACK_DEPTH, NULL, TASK_PRIORITY,
+ task_stack, &task_control_block);
+ txfr_test_account_stop(&counters);
+ txfr_test_check("xTaskCreateStatic returns a handle", task != NULL);
+ txfr_test_check_counts("xTaskCreateStatic accounting", &counters, 0, 0, 2, 0);
+
+ if(task != NULL)
+ {
+ vTaskDelete(task);
+ }
+
+ /* As with the queues, the static variant owns no memory but still creates
+ kernel objects and still returns nothing on failure. A semaphore or a
+ thread left behind here stays registered in the kernel, pointing into
+ the caller's StaticTask_t. */
+ txfr_test_account_start(TXFR_INJECT_SEMAPHORE_CREATE, 1);
+ task = xTaskCreateStatic(test_task_entry, "s_sem", TASK_STACK_DEPTH, NULL, TASK_PRIORITY,
+ semaphore_failure_stack, &semaphore_failure_control_block);
+ txfr_test_account_stop(&counters);
+ txfr_test_check("xTaskCreateStatic returns NULL on semaphore failure", task == NULL);
+ txfr_test_check_counts("static semaphore failure leaves nothing behind", &counters, 0, 0, 1, 0);
+
+ txfr_test_account_start(TXFR_INJECT_THREAD_CREATE, 1);
+ task = xTaskCreateStatic(test_task_entry, "s_thr", TASK_STACK_DEPTH, NULL, TASK_PRIORITY,
+ thread_failure_stack, &thread_failure_control_block);
+ txfr_test_account_stop(&counters);
+ txfr_test_check("xTaskCreateStatic returns NULL on thread failure", task == NULL);
+ txfr_test_check_counts("static thread failure unwinds the semaphore", &counters, 0, 0, 2, 1);
+
+ /* The resume is the last step, so by then both kernel objects exist. The
+ thread has to be terminated before it can be deleted, which is the order
+ the idle task uses when it reaps a deleted task. */
+ txfr_test_account_start(TXFR_INJECT_THREAD_RESUME, 1);
+ task = xTaskCreateStatic(test_task_entry, "s_res", TASK_STACK_DEPTH, NULL, TASK_PRIORITY,
+ resume_failure_stack, &resume_failure_control_block);
+ txfr_test_account_stop(&counters);
+ txfr_test_check("xTaskCreateStatic returns NULL on resume failure", task == NULL);
+ txfr_test_check_counts("static resume failure unwinds thread and semaphore", &counters, 0, 0, 2, 2);
+}
diff --git a/test/freertos/regression/txfr_test_harness.c b/test/freertos/regression/txfr_test_harness.c
new file mode 100644
index 00000000..b5bd45de
--- /dev/null
+++ b/test/freertos/regression/txfr_test_harness.c
@@ -0,0 +1,384 @@
+/***************************************************************************
+ * 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
+ **************************************************************************/
+
+/**************************************************************************/
+/** */
+/** ThreadX Component */
+/** */
+/** FreeRTOS compatibility layer test harness */
+/** */
+/**************************************************************************/
+
+#include "txfr_test_harness.h"
+
+#include <stdio.h>
+#include <stdlib.h>
+
+/* Accounting state. Only the single test thread touches these. */
+static int txfr_accounting;
+static TXFR_INJECT_TARGET txfr_inject_target;
+static int txfr_inject_fail_on;
+static int txfr_inject_call_index;
+static TXFR_COUNTERS txfr_counters;
+static int txfr_failures;
+
+/* The real entry points, supplied by the linker in response to --wrap. */
+UINT __real__txe_byte_allocate(TX_BYTE_POOL *pool_ptr, VOID **memory_ptr, ULONG memory_size, ULONG wait_option);
+UINT __real__txe_byte_release(VOID *memory_ptr);
+UINT __real__txe_semaphore_create(TX_SEMAPHORE *semaphore_ptr, TX_NAME_CONST CHAR *name_ptr, ULONG initial_count, UINT semaphore_control_block_size);
+UINT __real__txe_semaphore_delete(TX_SEMAPHORE *semaphore_ptr);
+UINT __real__txe_mutex_create(TX_MUTEX *mutex_ptr, TX_NAME_CONST CHAR *name_ptr, UINT inherit, UINT mutex_control_block_size);
+UINT __real__txe_mutex_delete(TX_MUTEX *mutex_ptr);
+UINT __real__txe_event_flags_create(TX_EVENT_FLAGS_GROUP *group_ptr, TX_NAME_CONST CHAR *name_ptr, UINT event_control_block_size);
+UINT __real__txe_event_flags_delete(TX_EVENT_FLAGS_GROUP *group_ptr);
+UINT __real__txe_timer_create(TX_TIMER *timer_ptr, TX_NAME_CONST CHAR *name_ptr, VOID (*expiration_function)(ULONG input),
+ ULONG expiration_input, ULONG initial_ticks, ULONG reschedule_ticks,
+ UINT auto_activate, UINT timer_control_block_size);
+UINT __real__txe_timer_delete(TX_TIMER *timer_ptr);
+UINT __real__txe_thread_create(TX_THREAD *thread_ptr, TX_NAME_CONST CHAR *name_ptr, VOID (*entry_function)(ULONG entry_input),
+ ULONG entry_input, VOID *stack_start, ULONG stack_size, UINT priority,
+ UINT preempt_threshold, ULONG time_slice, UINT auto_start,
+ UINT thread_control_block_size);
+UINT __real__txe_thread_delete(TX_THREAD *thread_ptr);
+UINT __real__txe_thread_resume(TX_THREAD *thread_ptr);
+
+
+/* Determine whether the call now being made is the one the test asked to fail.
+ Only calls made while accounting is active are considered, which keeps the
+ objects created by tx_freertos_init() and by the harness out of the count. */
+static int txfr_should_fail(TXFR_INJECT_TARGET target)
+{
+ if((txfr_accounting == 0) || (txfr_inject_target != target))
+ {
+ return 0;
+ }
+
+ txfr_inject_call_index++;
+
+ return (txfr_inject_call_index == txfr_inject_fail_on) ? 1 : 0;
+}
+
+
+UINT __wrap__txe_byte_allocate(TX_BYTE_POOL *pool_ptr, VOID **memory_ptr, ULONG memory_size, ULONG wait_option)
+{
+ if(txfr_accounting != 0)
+ {
+ txfr_counters.byte_allocate++;
+ }
+
+ return __real__txe_byte_allocate(pool_ptr, memory_ptr, memory_size, wait_option);
+}
+
+
+UINT __wrap__txe_byte_release(VOID *memory_ptr)
+{
+ if(txfr_accounting != 0)
+ {
+ txfr_counters.byte_release++;
+ }
+
+ return __real__txe_byte_release(memory_ptr);
+}
+
+
+UINT __wrap__txe_semaphore_create(TX_SEMAPHORE *semaphore_ptr, TX_NAME_CONST CHAR *name_ptr, ULONG initial_count, UINT semaphore_control_block_size)
+{
+ if(txfr_should_fail(TXFR_INJECT_SEMAPHORE_CREATE) != 0)
+ {
+ txfr_counters.semaphore_create++;
+ return TX_SEMAPHORE_ERROR;
+ }
+
+ if(txfr_accounting != 0)
+ {
+ txfr_counters.semaphore_create++;
+ }
+
+ return __real__txe_semaphore_create(semaphore_ptr, name_ptr, initial_count, semaphore_control_block_size);
+}
+
+
+UINT __wrap__txe_semaphore_delete(TX_SEMAPHORE *semaphore_ptr)
+{
+ if(txfr_accounting != 0)
+ {
+ txfr_counters.semaphore_delete++;
+ }
+
+ return __real__txe_semaphore_delete(semaphore_ptr);
+}
+
+
+UINT __wrap__txe_mutex_create(TX_MUTEX *mutex_ptr, TX_NAME_CONST CHAR *name_ptr, UINT inherit, UINT mutex_control_block_size)
+{
+ if(txfr_should_fail(TXFR_INJECT_MUTEX_CREATE) != 0)
+ {
+ txfr_counters.mutex_create++;
+ return TX_MUTEX_ERROR;
+ }
+
+ if(txfr_accounting != 0)
+ {
+ txfr_counters.mutex_create++;
+ }
+
+ return __real__txe_mutex_create(mutex_ptr, name_ptr, inherit, mutex_control_block_size);
+}
+
+
+UINT __wrap__txe_mutex_delete(TX_MUTEX *mutex_ptr)
+{
+ if(txfr_accounting != 0)
+ {
+ txfr_counters.mutex_delete++;
+ }
+
+ return __real__txe_mutex_delete(mutex_ptr);
+}
+
+
+UINT __wrap__txe_event_flags_create(TX_EVENT_FLAGS_GROUP *group_ptr, TX_NAME_CONST CHAR *name_ptr, UINT event_control_block_size)
+{
+ if(txfr_should_fail(TXFR_INJECT_EVENT_FLAGS_CREATE) != 0)
+ {
+ txfr_counters.event_flags_create++;
+ return TX_GROUP_ERROR;
+ }
+
+ if(txfr_accounting != 0)
+ {
+ txfr_counters.event_flags_create++;
+ }
+
+ return __real__txe_event_flags_create(group_ptr, name_ptr, event_control_block_size);
+}
+
+
+UINT __wrap__txe_event_flags_delete(TX_EVENT_FLAGS_GROUP *group_ptr)
+{
+ if(txfr_accounting != 0)
+ {
+ txfr_counters.event_flags_delete++;
+ }
+
+ return __real__txe_event_flags_delete(group_ptr);
+}
+
+
+UINT __wrap__txe_timer_create(TX_TIMER *timer_ptr, TX_NAME_CONST CHAR *name_ptr, VOID (*expiration_function)(ULONG input),
+ ULONG expiration_input, ULONG initial_ticks, ULONG reschedule_ticks,
+ UINT auto_activate, UINT timer_control_block_size)
+{
+ if(txfr_should_fail(TXFR_INJECT_TIMER_CREATE) != 0)
+ {
+ txfr_counters.timer_create++;
+ return TX_TIMER_ERROR;
+ }
+
+ if(txfr_accounting != 0)
+ {
+ txfr_counters.timer_create++;
+ }
+
+ return __real__txe_timer_create(timer_ptr, name_ptr, expiration_function, expiration_input,
+ initial_ticks, reschedule_ticks, auto_activate, timer_control_block_size);
+}
+
+
+UINT __wrap__txe_timer_delete(TX_TIMER *timer_ptr)
+{
+ if(txfr_accounting != 0)
+ {
+ txfr_counters.timer_delete++;
+ }
+
+ return __real__txe_timer_delete(timer_ptr);
+}
+
+
+UINT __wrap__txe_thread_create(TX_THREAD *thread_ptr, TX_NAME_CONST CHAR *name_ptr, VOID (*entry_function)(ULONG entry_input),
+ ULONG entry_input, VOID *stack_start, ULONG stack_size, UINT priority,
+ UINT preempt_threshold, ULONG time_slice, UINT auto_start,
+ UINT thread_control_block_size)
+{
+ if(txfr_should_fail(TXFR_INJECT_THREAD_CREATE) != 0)
+ {
+ txfr_counters.thread_create++;
+ return TX_THREAD_ERROR;
+ }
+
+ if(txfr_accounting != 0)
+ {
+ txfr_counters.thread_create++;
+ }
+
+ return __real__txe_thread_create(thread_ptr, name_ptr, entry_function, entry_input, stack_start,
+ stack_size, priority, preempt_threshold, time_slice, auto_start,
+ thread_control_block_size);
+}
+
+
+UINT __wrap__txe_thread_delete(TX_THREAD *thread_ptr)
+{
+ if(txfr_accounting != 0)
+ {
+ txfr_counters.thread_delete++;
+ }
+
+ return __real__txe_thread_delete(thread_ptr);
+}
+
+
+UINT __wrap__txe_thread_resume(TX_THREAD *thread_ptr)
+{
+ if(txfr_should_fail(TXFR_INJECT_THREAD_RESUME) != 0)
+ {
+ txfr_counters.thread_resume++;
+ return TX_RESUME_ERROR;
+ }
+
+ if(txfr_accounting != 0)
+ {
+ txfr_counters.thread_resume++;
+ }
+
+ return __real__txe_thread_resume(thread_ptr);
+}
+
+
+void txfr_test_account_start(TXFR_INJECT_TARGET target, int fail_on_call)
+{
+ TX_MEMSET(&txfr_counters, 0, sizeof(txfr_counters));
+ txfr_inject_target = target;
+ txfr_inject_fail_on = fail_on_call;
+ txfr_inject_call_index = 0;
+ txfr_accounting = 1;
+}
+
+
+void txfr_test_account_stop(TXFR_COUNTERS *p_counters)
+{
+ txfr_accounting = 0;
+ txfr_inject_target = TXFR_INJECT_NONE;
+
+ if(p_counters != NULL)
+ {
+ *p_counters = txfr_counters;
+ }
+}
+
+
+void txfr_test_check(const char *label, int condition)
+{
+ printf("%-52s %s\n", label, (condition != 0) ? "PASS" : "FAIL");
+
+ if(condition == 0)
+ {
+ txfr_failures++;
+ }
+}
+
+
+void txfr_test_check_counts(const char *label, const TXFR_COUNTERS *p_counters,
+ int expect_allocate, int expect_release,
+ int expect_object_create, int expect_object_delete)
+{
+ int object_create;
+ int object_delete;
+ int ok;
+
+ /* Only one kind of kernel object is exercised per check, so the totals
+ identify it unambiguously. */
+ object_create = p_counters->semaphore_create + p_counters->mutex_create +
+ p_counters->event_flags_create + p_counters->timer_create +
+ p_counters->thread_create;
+ object_delete = p_counters->semaphore_delete + p_counters->mutex_delete +
+ p_counters->event_flags_delete + p_counters->timer_delete +
+ p_counters->thread_delete;
+
+ ok = ((p_counters->byte_allocate == expect_allocate) &&
+ (p_counters->byte_release == expect_release) &&
+ (object_create == expect_object_create) &&
+ (object_delete == expect_object_delete)) ? 1 : 0;
+
+ printf("%-52s %s\n", label, (ok != 0) ? "PASS" : "FAIL");
+
+ if(ok == 0)
+ {
+ printf("%-52s got alloc=%d release=%d create=%d delete=%d\n", "",
+ p_counters->byte_allocate, p_counters->byte_release, object_create, object_delete);
+ printf("%-52s expected alloc=%d release=%d create=%d delete=%d\n", "",
+ expect_allocate, expect_release, expect_object_create, expect_object_delete);
+ txfr_failures++;
+ }
+}
+
+
+int txfr_test_failures(void)
+{
+ return txfr_failures;
+}
+
+
+/* Test thread. The byte pool operations under test require thread context. */
+
+static TX_THREAD txfr_test_thread;
+static ULONG txfr_test_stack[4096];
+
+
+static void txfr_test_thread_entry(ULONG id)
+{
+ (void)id;
+
+ txfr_test_body();
+
+ printf("\n%s\n", (txfr_failures == 0) ? "ALL CHECKS PASSED" : "THERE WERE FAILED CHECKS");
+ fflush(stdout);
+
+ /* The kernel owns the calling thread, so returning here would simply idle.
+ Leave the process with a status ctest can read. */
+ exit((txfr_failures == 0) ? 0 : 1);
+}
+
+
+void tx_application_define(void *first_unused_memory)
+{
+ UINT ret;
+
+ (void)first_unused_memory;
+
+ ret = tx_freertos_init();
+ if(ret != TX_SUCCESS)
+ {
+ printf("tx_freertos_init() failed with %u\n", ret);
+ exit(2);
+ }
+
+ ret = tx_thread_create(&txfr_test_thread, "txfr_test", txfr_test_thread_entry, 0u,
+ txfr_test_stack, sizeof(txfr_test_stack), 16u, 16u, 0u, TX_AUTO_START);
+ if(ret != TX_SUCCESS)
+ {
+ printf("tx_thread_create() failed with %u\n", ret);
+ exit(2);
+ }
+}
+
+
+int main(void)
+{
+ tx_kernel_enter();
+
+ return 0;
+}
diff --git a/test/freertos/regression/txfr_test_harness.h b/test/freertos/regression/txfr_test_harness.h
new file mode 100644
index 00000000..ffb6db0e
--- /dev/null
+++ b/test/freertos/regression/txfr_test_harness.h
@@ -0,0 +1,103 @@
+/***************************************************************************
+ * 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
+ **************************************************************************/
+
+/**************************************************************************/
+/** */
+/** ThreadX Component */
+/** */
+/** FreeRTOS compatibility layer test harness */
+/** */
+/**************************************************************************/
+
+/* This harness lets a test force a ThreadX object creation call to fail and
+ then account for what the compatibility layer did with the resources it had
+ already taken. The creation entry points of the layer take a byte pool
+ allocation for the descriptor, sometimes a second one for backing storage,
+ and then create one or more kernel objects; an error path that forgets to
+ undo any of that is invisible to the caller, which only ever sees NULL.
+
+ Interception is done with the linker's --wrap option rather than with hooks
+ in the layer itself, so tx_freertos.c is compiled exactly as it ships. Note
+ that tx_api.h maps the public API onto the error checking entry points, so
+ the symbols that exist at link time, and therefore the ones wrapped, are the
+ _txe_ variants. The wrap options live in the test CMakeLists.
+
+ This mechanism is specific to GNU ld and lld, so the suite is Linux only. */
+
+#ifndef TXFR_TEST_HARNESS_H
+#define TXFR_TEST_HARNESS_H
+
+#include "FreeRTOS.h"
+
+/* Identifies the kernel creation call a test wants to fail. */
+typedef enum TXFR_INJECT_TARGET_ENUM
+{
+ TXFR_INJECT_NONE = 0,
+ TXFR_INJECT_SEMAPHORE_CREATE,
+ TXFR_INJECT_MUTEX_CREATE,
+ TXFR_INJECT_EVENT_FLAGS_CREATE,
+ TXFR_INJECT_TIMER_CREATE,
+ TXFR_INJECT_THREAD_CREATE,
+ TXFR_INJECT_THREAD_RESUME
+} TXFR_INJECT_TARGET;
+
+/* Counts of the ThreadX primitives the layer reached for while accounting was
+ active. The byte pool counts stand in for txfr_malloc() and txfr_free(),
+ which cannot be wrapped: they are defined in tx_freertos.c and called from
+ within it, so the compiler resolves those calls internally. */
+typedef struct TXFR_COUNTERS_STRUCT
+{
+ int byte_allocate;
+ int byte_release;
+ int semaphore_create;
+ int semaphore_delete;
+ int mutex_create;
+ int mutex_delete;
+ int event_flags_create;
+ int event_flags_delete;
+ int timer_create;
+ int timer_delete;
+ int thread_create;
+ int thread_delete;
+
+ /* Counted, but not part of the object totals checked below, since a
+ resume creates nothing. */
+ int thread_resume;
+} TXFR_COUNTERS;
+
+/* Start accounting. Pass TXFR_INJECT_NONE to only count, or a target together
+ with the 1 based index of the call to fail, counted from this call onward. */
+void txfr_test_account_start(TXFR_INJECT_TARGET target, int fail_on_call);
+
+/* Stop accounting and copy out what was counted. */
+void txfr_test_account_stop(TXFR_COUNTERS *p_counters);
+
+/* Record the outcome of one expectation. Prints a line per check and remembers
+ whether anything failed. */
+void txfr_test_check(const char *label, int condition);
+
+/* As above, with the observed and expected counts printed on a failure. */
+void txfr_test_check_counts(const char *label, const TXFR_COUNTERS *p_counters,
+ int expect_allocate, int expect_release,
+ int expect_object_create, int expect_object_delete);
+
+/* Number of failed checks so far. */
+int txfr_test_failures(void);
+
+/* Each test file defines this. It runs in thread context, since the byte pool
+ operations under test are not callable from initialization. */
+void txfr_test_body(void);
+
+#endif /* TXFR_TEST_HARNESS_H */
diff --git a/test/posix/cmake/CMakeLists.txt b/test/posix/cmake/CMakeLists.txt
new file mode 100644
index 00000000..830e69ba
--- /dev/null
+++ b/test/posix/cmake/CMakeLists.txt
@@ -0,0 +1,83 @@
+cmake_minimum_required(VERSION 3.13 FATAL_ERROR)
+cmake_policy(SET CMP0054 NEW)
+cmake_policy(SET CMP0057 NEW)
+
+# Regression tests for the POSIX compatibility layer.
+#
+# The layer cannot run on the hosted simulation ports: it supplies its own
+# pthread.h, errno.h, time.h and friends, which collide with the C library's,
+# and the linux port emulates threads with the very pthread calls the layer
+# exports. These tests therefore run on emulated targets, reusing the RISC-V
+# BSP and QEMU setup that the ThreadX RISC-V regression suite already uses.
+
+project(threadx_posix_test LANGUAGES C ASM)
+
+set(BUILD_CONFIGURATIONS default_build disable_notify_callbacks_build
+ stack_checking_build trace_build)
+set(CMAKE_CONFIGURATION_TYPES
+ ${BUILD_CONFIGURATIONS}
+ CACHE STRING "list of supported configuration types" FORCE)
+set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS
+ ${CMAKE_CONFIGURATION_TYPES})
+list(GET CMAKE_CONFIGURATION_TYPES 0 BUILD_TYPE)
+if((NOT CMAKE_BUILD_TYPE) OR (NOT ("${CMAKE_BUILD_TYPE}" IN_LIST
+ CMAKE_CONFIGURATION_TYPES)))
+ set(CMAKE_BUILD_TYPE
+ "${BUILD_TYPE}"
+ CACHE STRING "Build Type of the project" FORCE)
+endif()
+
+message(STATUS "Build type: ${CMAKE_BUILD_TYPE}")
+message(STATUS "Using toolchain file: ${CMAKE_TOOLCHAIN_FILE}.")
+message(STATUS "THREADX_ARCH: ${THREADX_ARCH}")
+
+set(default_build -DTX_QUEUE_MESSAGE_MAX_SIZE=32)
+set(disable_notify_callbacks_build -DTX_QUEUE_MESSAGE_MAX_SIZE=32 -DTX_DISABLE_NOTIFY_CALLBACKS)
+set(stack_checking_build -DTX_QUEUE_MESSAGE_MAX_SIZE=32 -DTX_ENABLE_STACK_CHECKING)
+set(trace_build -DTX_QUEUE_MESSAGE_MAX_SIZE=32 -DTX_ENABLE_EVENT_TRACE)
+
+# These tests are 32-bit only for now, and the reason is a gap in the layer
+# rather than a choice about coverage.
+#
+# The layer passes a message by putting the address of a private buffer into the
+# queue. ULONG is 32 bits on every ThreadX port, including the 64-bit ones, so
+# on a 64-bit target that address only fits if TX_64_BIT is defined: the layer
+# then splits it across two message words and sizes the message accordingly.
+# Without it, px_mq_send.c truncates the pointer to 32 bits and px_mq_receive.c
+# casts the truncated value back, which the compiler reports only as a warning.
+#
+# Defining TX_64_BIT is not a way out either: tx_api.h then uses the extension
+# pointer macros, which need tx_thread_extension_ptr in the thread control
+# block, and no 64-bit port outside ports_smp and ports/linux declares it.
+# Closing that gap belongs with the layer, not with its tests.
+if(THREADX_ARCH STREQUAL "risc-v64")
+ message(FATAL_ERROR
+ "The POSIX layer cannot run on a 64-bit port yet: it needs TX_64_BIT to "
+ "carry a 64-bit message pointer, and TX_64_BIT needs a port that declares "
+ "tx_thread_extension_ptr. Use the riscv32 toolchain file for these tests.")
+endif()
+
+add_compile_options(
+ -std=c99
+ -O0
+ -g3
+ -fdiagnostics-color
+ -Werror
+ -ffunction-sections
+ -fdata-sections
+ -DTX_REGRESSION_TEST
+ -DTEST_STACK_SIZE_PRINTF=4096
+ -DEXTERNAL_EXIT
+ ${${CMAKE_BUILD_TYPE}}
+)
+
+enable_testing()
+
+# ThreadX itself, plus the posix-threadx target the top-level file defines.
+add_subdirectory(${CMAKE_CURRENT_LIST_DIR}/../../.. threadx)
+
+# Reuse the RISC-V BSP from the ThreadX regression suite rather than keeping a
+# second copy of the board support in this tree.
+add_subdirectory(${CMAKE_CURRENT_LIST_DIR}/../../tx/cmake/riscv/bsp riscv_bsp)
+
+add_subdirectory(regression)
diff --git a/test/posix/cmake/regression/CMakeLists.txt b/test/posix/cmake/regression/CMakeLists.txt
new file mode 100644
index 00000000..8f433bcb
--- /dev/null
+++ b/test/posix/cmake/regression/CMakeLists.txt
@@ -0,0 +1,65 @@
+cmake_minimum_required(VERSION 3.13 FATAL_ERROR)
+cmake_policy(SET CMP0057 NEW)
+
+project(posix_regression_test LANGUAGES C ASM)
+
+set(SOURCE_DIR ${CMAKE_CURRENT_LIST_DIR}/../../regression)
+set(TX_TEST_DIR ${CMAKE_CURRENT_LIST_DIR}/../../../tx/regression)
+set(BSP_DIR ${CMAKE_CURRENT_LIST_DIR}/../../../tx/cmake/riscv/bsp)
+
+if(THREADX_ARCH STREQUAL "risc-v64")
+ set(QEMU_SYSTEM "qemu-system-riscv64")
+ set(XLEN_TAG "rv64")
+else()
+ set(QEMU_SYSTEM "qemu-system-riscv32")
+ set(XLEN_TAG "rv32")
+endif()
+
+set(posix_test_cases
+ ${SOURCE_DIR}/posix_mq_basic_test.c
+ ${SOURCE_DIR}/posix_mq_send_abort_test.c
+ ${SOURCE_DIR}/posix_pthread_basic_test.c
+ ${SOURCE_DIR}/posix_pthread_self_test.c
+)
+
+set(WEAK_DEFAULTS_SOURCE ${CMAKE_CURRENT_LIST_DIR}/../../../shared/regression/testcontrol_weak_defaults.c)
+
+# Weak defaults must be an OBJECT library so all objects are always linked into
+# each test executable. A STATIC library would have the linker skip them, since
+# weak definitions do not satisfy undefined strong references while scanning an
+# archive.
+add_library(posix_test_weak_defaults OBJECT ${WEAK_DEFAULTS_SOURCE})
+target_compile_definitions(posix_test_weak_defaults PRIVATE
+ CTEST BATCH_TEST EXTERNAL_EXIT TEST_STACK_SIZE_PRINTF=4096)
+target_compile_options(posix_test_weak_defaults PRIVATE -include stdlib.h)
+target_link_libraries(posix_test_weak_defaults PRIVATE azrtos::threadx)
+
+# testcontrol.c supplies main(), tx_application_define() and
+# test_control_return(), and is shared with the ThreadX regression suite.
+add_library(posix_test_utility ${TX_TEST_DIR}/testcontrol.c)
+target_link_libraries(posix_test_utility PUBLIC riscv_bsp azrtos::threadx)
+target_compile_definitions(posix_test_utility PUBLIC
+ CTEST BATCH_TEST EXTERNAL_EXIT TEST_STACK_SIZE_PRINTF=4096)
+target_compile_options(posix_test_utility PUBLIC -include stdlib.h)
+
+function(register_posix_test test_name)
+ target_link_options(${test_name} PRIVATE
+ -T ${BSP_DIR}/link.lds -nostartfiles
+ -Wl,--gc-sections)
+ add_test(
+ NAME ${CMAKE_BUILD_TYPE}::${XLEN_TAG}::${test_name}
+ COMMAND ${QEMU_SYSTEM} -nographic -smp 1 -bios none -m 128M
+ -machine virt -kernel $<TARGET_FILE:${test_name}>
+ )
+ set_tests_properties(${CMAKE_BUILD_TYPE}::${XLEN_TAG}::${test_name}
+ PROPERTIES TIMEOUT 120)
+endfunction()
+
+foreach(test_case ${posix_test_cases})
+ get_filename_component(test_name ${test_case} NAME_WE)
+ add_executable(${test_name} ${test_case} $<TARGET_OBJECTS:posix_test_weak_defaults>)
+ target_link_libraries(${test_name} PRIVATE
+ -Wl,--whole-archive riscv_bsp -Wl,--no-whole-archive
+ posix_test_utility posix-threadx)
+ register_posix_test(${test_name})
+endforeach()
diff --git a/test/posix/cmake/run.sh b/test/posix/cmake/run.sh
new file mode 100755
index 00000000..76536cf7
--- /dev/null
+++ b/test/posix/cmake/run.sh
@@ -0,0 +1,189 @@
+#!/bin/bash
+##############################################################################
+# 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
+##############################################################################
+
+
+set -e
+
+# POSIX compatibility layer regression test build/test runner.
+# Usage: run.sh <arch> <build|test> [all|<config> ...]
+# arch: riscv32
+#
+# Only riscv32 is available: the layer cannot carry a 64-bit message pointer
+# without TX_64_BIT, and TX_64_BIT needs a port that declares
+# tx_thread_extension_ptr. See the comment in ../CMakeLists.txt.
+
+function help() {
+ echo "Usage: $0 <arch> <command> [all|<build_configuration> ...]"
+ echo ""
+ echo " arch: riscv32"
+ echo " command: build | test"
+ echo ""
+ echo "Available build configurations:"
+ for build in ${build_configurations[*]}; do
+ echo " $build"
+ done
+ exit 1
+}
+
+function validate() {
+ for build in ${build_configurations[*]}; do
+ if [ "$1" == "$build" ]; then
+ return
+ fi
+ done
+ help
+}
+
+function generate() {
+ local arch=$1
+ local build=$2
+ local toolchain_file
+ local build_dir
+
+ toolchain_file="${REPO_ROOT}/cmake/riscv32_gnu.cmake"
+
+ build_dir="build/${arch}_${build}"
+ cmake -B"$build_dir" -GNinja \
+ -DCMAKE_TOOLCHAIN_FILE="$toolchain_file" \
+ -DCMAKE_BUILD_TYPE="$build" \
+ .
+}
+
+function build() {
+ local arch=$1
+ local build=$2
+ # -k 0 keeps Ninja going after a target fails, so one broken target no
+ # longer decides whether the targets after it exist. ctest reports a
+ # missing binary as a failing test, which turned a single link error into
+ # a failure count that varied with build scheduling order.
+ cmake --build "build/${arch}_${build}" -- -k 0
+}
+
+function run_test() {
+ local arch=$1
+ local build=$2
+ local parallel_jobs=$3
+
+ # Guard the pushd: with the caller capturing this function's status, set -e
+ # no longer aborts here, so a missing build directory would otherwise let
+ # ctest run in the source tree and report "no tests" as success.
+ pushd "build/${arch}_${build}" || return 1
+ [ -z "${CTEST_PARALLEL_LEVEL}" ] && parallel="-j${parallel_jobs}"
+ if [ -z "${CTEST_REPEAT_FAIL}" ]; then
+ repeat_fail=2
+ else
+ repeat_fail=${CTEST_REPEAT_FAIL}
+ fi
+ # ctest's status is captured and returned past the popd and the summary
+ # steps, which all succeed. Without this the function would return 0 and a
+ # failing configuration would be reported as a pass.
+ local status=0
+ ctest $parallel --timeout 1000 \
+ -O "${arch}_${build}.txt" \
+ -T test --no-compress-output \
+ --test-output-size-passed 4194304 \
+ --test-output-size-failed 4194304 \
+ --output-on-failure \
+ --repeat until-pass:${repeat_fail} \
+ --output-junit "${arch}_${build}.xml" || status=$?
+ popd
+ return $status
+}
+
+# Determine repo root and script directory
+SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
+REPO_ROOT="$(cd "${SCRIPT_DIR}/../../.." && pwd)"
+cd "$SCRIPT_DIR"
+
+# Parse build configurations from CMakeLists.txt
+result=$(sed -n "/(BUILD_CONFIGURATIONS/,/)/p" CMakeLists.txt | sed ':label;N;s/\n/ /;b label' | grep -Pzo "[a-zA-Z0-9_]*build[a-zA-Z0-9_]*\s*" | tr -d '\0')
+IFS=' '
+read -ra build_configurations <<< "$result"
+
+if [ $# -lt 2 ]; then
+ help
+fi
+
+arch=$1
+shift
+
+if [ "$arch" != "riscv32" ]; then
+ echo "Error: arch must be 'riscv32'"
+ help
+fi
+
+command=$1
+shift
+
+if [ "$#" == "0" ]; then
+ builds=${build_configurations[0]}
+elif [ "$*" == "all" ]; then
+ builds=${build_configurations[@]}
+else
+ for item in $*; do
+ validate "$item"
+ done
+ builds=$*
+fi
+
+if [ "$command" == "build" ]; then
+ for item in $builds; do
+ generate "$arch" "$item"
+ echo ""
+ done
+
+ # A failing configuration must not stop the ones after it: under set -e
+ # the loop would abort and leave them unbuilt, which then reads as a wall
+ # of missing-binary test failures. The status is accumulated and returned.
+ build_status=0
+ for item in $builds; do
+ echo "Building ${arch} ${item}"
+ build "$arch" "$item" || build_status=$?
+ echo ""
+ done
+ [ $build_status -eq 0 ] || exit $build_status
+elif [ "$command" == "test" ]; then
+ cores=$(nproc)
+ if [ -z "${CTEST_PARALLEL_LEVEL}" ]; then
+ build_counts=$(echo $builds | wc -w)
+ parallel_jobs=$(($cores / $build_counts))
+ parallel_jobs=$(($parallel_jobs + 2))
+ pids=""
+ for item in $builds; do
+ echo "Testing ${arch} ${item}"
+ run_test "$arch" "$item" "$parallel_jobs" &
+ pids+=" $!"
+ done
+ exit_code=0
+ for p in $pids; do
+ wait $p || exit_code=$?
+ done
+ exit $exit_code
+ else
+ # ctest's status is accumulated rather than allowed to abort the loop.
+ # set -e would otherwise stop at the first failing configuration and
+ # leave every configuration after it untested. This mirrors what the
+ # parallel branch above already does with wait.
+ exit_code=0
+ for item in $builds; do
+ echo "Testing ${arch} ${item}"
+ run_test "$arch" "$item" "$parallel_jobs" || exit_code=$?
+ done
+ exit $exit_code
+ fi
+else
+ help
+fi
diff --git a/test/posix/regression/posix_mq_basic_test.c b/test/posix/regression/posix_mq_basic_test.c
new file mode 100644
index 00000000..2ecb1ac8
--- /dev/null
+++ b/test/posix/regression/posix_mq_basic_test.c
@@ -0,0 +1,159 @@
+/***************************************************************************/
+/* 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 */
+/***************************************************************************/
+
+/* This test is designed to test basic POSIX message queue operation: open a
+ queue, send a message through it, receive the message back and confirm both
+ the contents and the priority survived the round trip. */
+
+#include <stdio.h>
+#include "tx_api.h"
+#include "pthread.h"
+
+#define TEST_MESSAGE_SIZE 40
+#define TEST_QUEUE_DEPTH 10
+#define TEST_PRIORITY 3
+
+/* Memory the POSIX layer carves its own objects out of. */
+static ULONG posix_region[192 * 1024 / sizeof(ULONG)];
+
+static pthread_t worker;
+static pthread_attr_t worker_attr;
+static mqd_t queue_des;
+static struct mq_attr queue_attr;
+
+extern void test_control_return(UINT status);
+
+static VOID *worker_entry(VOID *input);
+
+
+/* Define what the initial system looks like. */
+
+#ifdef CTEST
+void test_application_define(void *first_unused_memory)
+#else
+void posix_mq_basic_application_define(void *first_unused_memory)
+#endif
+{
+
+struct sched_param param;
+VOID *storage_ptr;
+
+ (void) first_unused_memory;
+
+ /* Bring the POSIX layer up. It returns the first address beyond its own
+ structures, which is where pthread stacks are taken from. */
+ storage_ptr = posix_initialize(posix_region);
+
+ queue_attr.mq_maxmsg = TEST_QUEUE_DEPTH;
+ queue_attr.mq_msgsize = TEST_MESSAGE_SIZE;
+
+ queue_des = mq_open("mq_basic", O_CREAT | O_RDWR, 0, &queue_attr);
+ if (queue_des == (mqd_t) ERROR)
+ {
+
+ printf("Running POSIX Message Queue Basic Test............................. ERROR #1\n");
+ test_control_return(1);
+ }
+
+ pthread_attr_init(&worker_attr);
+ param.sched_priority = 10;
+ pthread_attr_setschedparam(&worker_attr, &param);
+ pthread_attr_setstackaddr(&worker_attr, storage_ptr);
+
+ if (pthread_create(&worker, &worker_attr, worker_entry, NULL) != OK)
+ {
+
+ printf("Running POSIX Message Queue Basic Test............................. ERROR #2\n");
+ test_control_return(1);
+ }
+}
+
+
+static VOID *worker_entry(VOID *input)
+{
+
+CHAR sent[TEST_MESSAGE_SIZE];
+CHAR received[TEST_MESSAGE_SIZE];
+ULONG received_priority;
+ssize_t received_length;
+INT i;
+
+ (void) input;
+
+ printf("Running POSIX Message Queue Basic Test............................. ");
+
+ /* Build a recognisable payload rather than a constant one, so a message
+ that is dropped or truncated cannot pass by accident. */
+ for (i = 0; i < TEST_MESSAGE_SIZE; i++)
+ {
+
+ sent[i] = (CHAR) (i + 1);
+ received[i] = (CHAR) 0;
+ }
+
+ if (mq_send(queue_des, sent, TEST_MESSAGE_SIZE, TEST_PRIORITY) != OK)
+ {
+
+ printf("ERROR #3\n");
+ test_control_return(1);
+ }
+
+ received_priority = 0;
+ received_length = mq_receive(queue_des, received, TEST_MESSAGE_SIZE, &received_priority);
+
+ if (received_length != (ssize_t) TEST_MESSAGE_SIZE)
+ {
+
+ printf("ERROR #4\n");
+ test_control_return(1);
+ }
+
+ if (received_priority != (ULONG) TEST_PRIORITY)
+ {
+
+ printf("ERROR #5\n");
+ test_control_return(1);
+ }
+
+ for (i = 0; i < TEST_MESSAGE_SIZE; i++)
+ {
+
+ if (received[i] != sent[i])
+ {
+
+ printf("ERROR #6\n");
+ test_control_return(1);
+ }
+ }
+
+ if (mq_close(queue_des) != OK)
+ {
+
+ printf("ERROR #7\n");
+ test_control_return(1);
+ }
+
+ if (mq_unlink("mq_basic") != OK)
+ {
+
+ printf("ERROR #8\n");
+ test_control_return(1);
+ }
+
+ printf("SUCCESS!\n");
+ test_control_return(0);
+
+ return (NULL);
+}
diff --git a/test/posix/regression/posix_mq_send_abort_test.c b/test/posix/regression/posix_mq_send_abort_test.c
new file mode 100644
index 00000000..a4e7e7e6
--- /dev/null
+++ b/test/posix/regression/posix_mq_send_abort_test.c
@@ -0,0 +1,273 @@
+/***************************************************************************/
+/* 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 */
+/***************************************************************************/
+
+/* This test covers the message buffer mq_send() used to leak when the send
+ failed, reported as issue #568.
+
+ mq_send() allocates a private buffer from the queue's own byte pool, copies
+ the caller's message into it and passes the address through the queue. The
+ receiver owns the buffer from then on and releases it. When the send itself
+ fails the message never reaches a receiver, so mq_send() has to release the
+ buffer before returning or it is lost.
+
+ The failure is provoked the way an application would meet it: the queue is
+ filled, a sender blocks on it, and its wait is aborted. The queue survives,
+ so any buffer that is not released stays lost. Aborting repeatedly and
+ watching the queue's byte pool separates the two outcomes: with the buffer
+ released the pool returns to its original size, and without it the pool
+ shrinks by a message every time. */
+
+#include <stdio.h>
+#include "tx_api.h"
+#include "pthread.h"
+
+/* The queue's pool is sized (mq_maxmsg + 1) * (mq_msgsize + 11), with a floor
+ of 100 bytes, so it holds barely more than a full queue. Keep the messages
+ small and the queue shallow: that floor then leaves room for a few leaked
+ buffers to accumulate and be measured, instead of exhausting the pool on the
+ first one and hanging the sender inside posix_internal_error(). */
+#define TEST_MESSAGE_SIZE 8
+#define TEST_QUEUE_DEPTH 1
+#define TEST_ABORT_COUNT 2
+
+static ULONG posix_region[192 * 1024 / sizeof(ULONG)];
+
+static pthread_t sender;
+static pthread_attr_t sender_attr;
+static pthread_t controller;
+static pthread_attr_t controller_attr;
+
+static mqd_t queue_des;
+static struct mq_attr queue_attr;
+
+/* Written by the sender, read by the controller. */
+static TX_THREAD * volatile sender_thread_ptr;
+static volatile INT send_failures;
+static volatile INT stop_sending;
+
+extern void test_control_return(UINT status);
+
+static VOID *sender_entry(VOID *input);
+static VOID *controller_entry(VOID *input);
+
+
+/* Report how many bytes the queue's message pool has free. */
+static ULONG pool_available(void)
+{
+
+ULONG available;
+
+ available = 0;
+ tx_byte_pool_info_get(&(queue_des -> f_data -> vq_message_area),
+ TX_NULL, &available, TX_NULL, TX_NULL, TX_NULL, TX_NULL);
+
+ return (available);
+}
+
+
+/* Define what the initial system looks like. */
+
+#ifdef CTEST
+void test_application_define(void *first_unused_memory)
+#else
+void posix_mq_send_abort_application_define(void *first_unused_memory)
+#endif
+{
+
+struct sched_param param;
+VOID *storage_ptr;
+
+ (void) first_unused_memory;
+
+ storage_ptr = posix_initialize(posix_region);
+
+ queue_attr.mq_maxmsg = TEST_QUEUE_DEPTH;
+ queue_attr.mq_msgsize = TEST_MESSAGE_SIZE;
+
+ queue_des = mq_open("mq_abort", O_CREAT | O_RDWR, 0, &queue_attr);
+ if (queue_des == (mqd_t) ERROR)
+ {
+
+ printf("Running POSIX Message Queue Send Abort Test....................... ERROR #1\n");
+ test_control_return(1);
+ }
+
+ /* The controller drives the test and runs above the sender, so it keeps
+ control while the sender is being unblocked. */
+ pthread_attr_init(&controller_attr);
+ param.sched_priority = 15;
+ pthread_attr_setschedparam(&controller_attr, &param);
+ pthread_attr_setstackaddr(&controller_attr, storage_ptr);
+ storage_ptr = (ULONG *) storage_ptr + (4096 / sizeof(ULONG));
+
+ pthread_attr_init(&sender_attr);
+ param.sched_priority = 10;
+ pthread_attr_setschedparam(&sender_attr, &param);
+ pthread_attr_setstackaddr(&sender_attr, storage_ptr);
+
+ if (pthread_create(&controller, &controller_attr, controller_entry, NULL) != OK)
+ {
+
+ printf("Running POSIX Message Queue Send Abort Test....................... ERROR #2\n");
+ test_control_return(1);
+ }
+
+ if (pthread_create(&sender, &sender_attr, sender_entry, NULL) != OK)
+ {
+
+ printf("Running POSIX Message Queue Send Abort Test....................... ERROR #3\n");
+ test_control_return(1);
+ }
+}
+
+
+/* Sends until told to stop. Every send blocks, because the controller keeps
+ the queue full, and every one is released by an aborted wait. */
+static VOID *sender_entry(VOID *input)
+{
+
+CHAR message[TEST_MESSAGE_SIZE];
+INT i;
+
+ (void) input;
+
+ for (i = 0; i < TEST_MESSAGE_SIZE; i++)
+ {
+
+ message[i] = (CHAR) i;
+ }
+
+ /* Publish our own thread pointer so the controller can abort our wait
+ without casting a pthread ID back to a ThreadX thread. */
+ sender_thread_ptr = tx_thread_identify();
+
+ while (stop_sending == 0)
+ {
+
+ if (mq_send(queue_des, message, TEST_MESSAGE_SIZE, 1) != OK)
+ {
+
+ send_failures++;
+ }
+ }
+
+ return (NULL);
+}
+
+
+static VOID *controller_entry(VOID *input)
+{
+
+CHAR message[TEST_MESSAGE_SIZE];
+ULONG baseline;
+ULONG after;
+ULONG lost;
+INT i;
+
+ (void) input;
+
+ printf("Running POSIX Message Queue Send Abort Test....................... ");
+
+ for (i = 0; i < TEST_MESSAGE_SIZE; i++)
+ {
+
+ message[i] = (CHAR) i;
+ }
+
+ /* Fill the queue so that any further send has to wait. */
+ if (mq_send(queue_des, message, TEST_MESSAGE_SIZE, 1) != OK)
+ {
+
+ printf("ERROR #4\n");
+ test_control_return(1);
+ }
+
+ /* Let the sender reach its first blocking send, then take the baseline.
+ The sender is holding one allocation at this point, which is the state
+ every later measurement is compared against. */
+ tx_thread_sleep(2);
+
+ if (sender_thread_ptr == TX_NULL)
+ {
+
+ printf("ERROR #5\n");
+ test_control_return(1);
+ }
+
+ baseline = pool_available();
+
+ for (i = 0; i < TEST_ABORT_COUNT; i++)
+ {
+
+ /* Release the sender from its wait. Its send fails, and the buffer it
+ allocated has to come back to the pool. */
+ if (tx_thread_wait_abort(sender_thread_ptr) != TX_SUCCESS)
+ {
+
+ printf("ERROR #6\n");
+ test_control_return(1);
+ }
+
+ /* Give the sender time to return from mq_send() and block again. */
+ tx_thread_sleep(2);
+ }
+
+ if (send_failures != TEST_ABORT_COUNT)
+ {
+
+ printf("ERROR #7\n");
+ test_control_return(1);
+ }
+
+ /* The sender is blocked again holding exactly one allocation, the same as
+ at the baseline, so the pool should read the same. */
+ after = pool_available();
+
+ if (after > baseline)
+ {
+
+ printf("ERROR #8\n");
+ test_control_return(1);
+ }
+
+ lost = baseline - after;
+
+ /* Anything approaching a message means buffers are not coming back. A
+ leak would cost TEST_ABORT_COUNT messages, so this separates the two
+ cases comfortably without depending on byte pool overhead. */
+ if (lost >= (ULONG) TEST_MESSAGE_SIZE)
+ {
+
+ printf("ERROR #9\n");
+ test_control_return(1);
+ }
+
+ /* Shut the sender down and confirm the queue still works. */
+ stop_sending = 1;
+ tx_thread_wait_abort(sender_thread_ptr);
+ tx_thread_sleep(2);
+
+ if (mq_receive(queue_des, message, TEST_MESSAGE_SIZE, TX_NULL) != (ssize_t) TEST_MESSAGE_SIZE)
+ {
+
+ printf("ERROR #10\n");
+ test_control_return(1);
+ }
+
+ printf("SUCCESS!\n");
+ test_control_return(0);
+
+ return (NULL);
+}
diff --git a/test/posix/regression/posix_pthread_basic_test.c b/test/posix/regression/posix_pthread_basic_test.c
new file mode 100644
index 00000000..afb8d4f3
--- /dev/null
+++ b/test/posix/regression/posix_pthread_basic_test.c
@@ -0,0 +1,192 @@
+/***************************************************************************/
+/* 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 */
+/***************************************************************************/
+
+/* This test covers basic pthread and synchronisation operation: a pthread runs
+ and reports its own identity, a mutex serialises access to shared state, a
+ semaphore signals between two pthreads, and a join collects the worker's exit
+ value. */
+
+#include <stdio.h>
+#include "tx_api.h"
+/* The layer ships no semaphore.h; sem_t and the sem_* calls arrive through
+ pthread.h, which pulls in tx_posix.h. */
+#include "pthread.h"
+
+#define TEST_STACK_BYTES 4096
+
+static ULONG posix_region[192 * 1024 / sizeof(ULONG)];
+
+static pthread_t worker;
+static pthread_attr_t worker_attr;
+static pthread_t driver;
+static pthread_attr_t driver_attr;
+
+static pthread_mutex_t guard;
+static sem_t handoff;
+
+static volatile INT shared_counter;
+static pthread_t worker_self_id;
+
+static INT worker_result = 77;
+
+extern void test_control_return(UINT status);
+
+static VOID *worker_entry(VOID *input);
+static VOID *driver_entry(VOID *input);
+
+
+/* Define what the initial system looks like. */
+
+#ifdef CTEST
+void test_application_define(void *first_unused_memory)
+#else
+void posix_pthread_basic_application_define(void *first_unused_memory)
+#endif
+{
+
+struct sched_param param;
+VOID *storage_ptr;
+
+ (void) first_unused_memory;
+
+ storage_ptr = posix_initialize(posix_region);
+
+ if (pthread_mutex_init(&guard, NULL) != OK)
+ {
+
+ printf("Running POSIX Pthread Basic Test.................................. ERROR #1\n");
+ test_control_return(1);
+ }
+
+ if (sem_init(&handoff, 0, 0) != OK)
+ {
+
+ printf("Running POSIX Pthread Basic Test.................................. ERROR #2\n");
+ test_control_return(1);
+ }
+
+ /* The driver runs below the worker so that the worker reaches its first
+ blocking point before the driver inspects anything. */
+ pthread_attr_init(&worker_attr);
+ param.sched_priority = 15;
+ pthread_attr_setschedparam(&worker_attr, &param);
+ pthread_attr_setstackaddr(&worker_attr, storage_ptr);
+ storage_ptr = (ULONG *) storage_ptr + (TEST_STACK_BYTES / sizeof(ULONG));
+
+ pthread_attr_init(&driver_attr);
+ param.sched_priority = 10;
+ pthread_attr_setschedparam(&driver_attr, &param);
+ pthread_attr_setstackaddr(&driver_attr, storage_ptr);
+
+ if (pthread_create(&worker, &worker_attr, worker_entry, NULL) != OK)
+ {
+
+ printf("Running POSIX Pthread Basic Test.................................. ERROR #3\n");
+ test_control_return(1);
+ }
+
+ if (pthread_create(&driver, &driver_attr, driver_entry, NULL) != OK)
+ {
+
+ printf("Running POSIX Pthread Basic Test.................................. ERROR #4\n");
+ test_control_return(1);
+ }
+}
+
+
+static VOID *worker_entry(VOID *input)
+{
+
+INT i;
+
+ (void) input;
+
+ /* Record what this pthread believes its own ID to be, for the driver to
+ compare against the ID pthread_create() handed back. */
+ worker_self_id = pthread_self();
+
+ for (i = 0; i < 100; i++)
+ {
+
+ pthread_mutex_lock(&guard);
+ shared_counter++;
+ pthread_mutex_unlock(&guard);
+ }
+
+ /* Hand control to the driver. */
+ sem_post(&handoff);
+
+ return ((VOID *) &worker_result);
+}
+
+
+static VOID *driver_entry(VOID *input)
+{
+
+VOID *exit_value;
+
+ (void) input;
+
+ printf("Running POSIX Pthread Basic Test.................................. ");
+
+ /* Wait for the worker to finish its counting. */
+ if (sem_wait(&handoff) != OK)
+ {
+
+ printf("ERROR #5\n");
+ test_control_return(1);
+ }
+
+ if (shared_counter != 100)
+ {
+
+ printf("ERROR #6\n");
+ test_control_return(1);
+ }
+
+ if (worker_self_id != worker)
+ {
+
+ printf("ERROR #7\n");
+ test_control_return(1);
+ }
+
+ exit_value = NULL;
+ if (pthread_join(worker, &exit_value) != OK)
+ {
+
+ printf("ERROR #8\n");
+ test_control_return(1);
+ }
+
+ if (exit_value != (VOID *) &worker_result)
+ {
+
+ printf("ERROR #9\n");
+ test_control_return(1);
+ }
+
+ if (pthread_mutex_destroy(&guard) != OK)
+ {
+
+ printf("ERROR #10\n");
+ test_control_return(1);
+ }
+
+ printf("SUCCESS!\n");
+ test_control_return(0);
+
+ return (NULL);
+}
diff --git a/test/posix/regression/posix_pthread_self_test.c b/test/posix/regression/posix_pthread_self_test.c
new file mode 100644
index 00000000..7cbb6450
--- /dev/null
+++ b/test/posix/regression/posix_pthread_self_test.c
@@ -0,0 +1,177 @@
+/***************************************************************************/
+/* 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 */
+/***************************************************************************/
+
+/* This test covers pthread_self() being called from a thread that is not a
+ pthread.
+
+ Nothing stops an application mixing tx_thread_create() with the POSIX layer,
+ and a ThreadX thread created that way has no POSIX control block.
+ posix_thread2tcb() returns NULL for it, and pthread_self() used to read the
+ pthread ID through that null pointer, then read the signal fields of a
+ POSIX_TCB out of a plain TX_THREAD that never had any.
+
+ The test calls pthread_self() from both kinds of thread: a real pthread,
+ which must get the ID pthread_create() handed back, and a plain ThreadX
+ thread, which must get zero rather than a fault or a value read out of
+ whatever follows its control block. */
+
+#include <stdio.h>
+#include "tx_api.h"
+#include "pthread.h"
+
+#define TEST_STACK_BYTES 4096
+
+static ULONG posix_region[192 * 1024 / sizeof(ULONG)];
+
+static pthread_t worker;
+static pthread_attr_t worker_attr;
+
+/* A plain ThreadX thread, deliberately not created through pthread_create(). */
+static TX_THREAD plain_thread;
+
+static volatile INT worker_done;
+static volatile INT plain_done;
+static pthread_t worker_reported_id;
+static pthread_t plain_reported_id;
+
+extern void test_control_return(UINT status);
+
+static VOID *worker_entry(VOID *input);
+static VOID plain_entry(ULONG input);
+static VOID checker_entry(ULONG input);
+
+static TX_THREAD checker_thread;
+
+
+/* Define what the initial system looks like. */
+
+#ifdef CTEST
+void test_application_define(void *first_unused_memory)
+#else
+void posix_pthread_self_application_define(void *first_unused_memory)
+#endif
+{
+
+struct sched_param param;
+VOID *storage_ptr;
+CHAR *pointer;
+
+ pointer = (CHAR *) first_unused_memory;
+
+ storage_ptr = posix_initialize(posix_region);
+
+ /* A genuine pthread. */
+ pthread_attr_init(&worker_attr);
+ param.sched_priority = 15;
+ pthread_attr_setschedparam(&worker_attr, &param);
+ pthread_attr_setstackaddr(&worker_attr, storage_ptr);
+
+ if (pthread_create(&worker, &worker_attr, worker_entry, NULL) != OK)
+ {
+
+ printf("Running POSIX Pthread Self Test.................................. ERROR #1\n");
+ test_control_return(1);
+ }
+
+ /* A ThreadX thread that the POSIX layer knows nothing about. */
+ if (tx_thread_create(&plain_thread, "plain threadx thread", plain_entry, 0,
+ pointer, TEST_STACK_BYTES,
+ 20, 20, TX_NO_TIME_SLICE, TX_AUTO_START) != TX_SUCCESS)
+ {
+
+ printf("Running POSIX Pthread Self Test.................................. ERROR #2\n");
+ test_control_return(1);
+ }
+ pointer = pointer + TEST_STACK_BYTES;
+
+ /* Runs last and inspects what the other two reported. */
+ if (tx_thread_create(&checker_thread, "checker", checker_entry, 0,
+ pointer, TEST_STACK_BYTES,
+ 25, 25, TX_NO_TIME_SLICE, TX_AUTO_START) != TX_SUCCESS)
+ {
+
+ printf("Running POSIX Pthread Self Test.................................. ERROR #3\n");
+ test_control_return(1);
+ }
+}
+
+
+static VOID *worker_entry(VOID *input)
+{
+
+ (void) input;
+
+ worker_reported_id = pthread_self();
+ worker_done = 1;
+
+ return (NULL);
+}
+
+
+/* The case that used to fault: a ThreadX thread asking for its pthread ID. */
+static VOID plain_entry(ULONG input)
+{
+
+ (void) input;
+
+ plain_reported_id = pthread_self();
+ plain_done = 1;
+}
+
+
+static VOID checker_entry(ULONG input)
+{
+
+ (void) input;
+
+ printf("Running POSIX Pthread Self Test.................................. ");
+
+ /* Both threads run above this one, so they have finished by now. */
+ if ((worker_done == 0) || (plain_done == 0))
+ {
+
+ printf("ERROR #4\n");
+ test_control_return(1);
+ }
+
+ /* A pthread must report the ID pthread_create() produced. */
+ if (worker_reported_id != worker)
+ {
+
+ printf("ERROR #5\n");
+ test_control_return(1);
+ }
+
+ /* A pthread ID is the address of its control block, so a real one is never
+ zero. */
+ if (worker_reported_id == (pthread_t) 0)
+ {
+
+ printf("ERROR #6\n");
+ test_control_return(1);
+ }
+
+ /* The plain ThreadX thread has no pthread ID, and must be told so rather
+ than handed something read out of memory that follows its TX_THREAD. */
+ if (plain_reported_id != (pthread_t) 0)
+ {
+
+ printf("ERROR #7\n");
+ test_control_return(1);
+ }
+
+ printf("SUCCESS!\n");
+ test_control_return(0);
+}
diff --git a/test/smp/cmake/CMakeLists.txt b/test/smp/cmake/CMakeLists.txt
index e2ca8627..3a0d9638 100644
--- a/test/smp/cmake/CMakeLists.txt
+++ b/test/smp/cmake/CMakeLists.txt
@@ -64,7 +64,21 @@ add_subdirectory(regression)
add_subdirectory(samples)
# Coverage
-if(CMAKE_BUILD_TYPE MATCHES ".*_coverage")
+#
+# The gate here used to be the build type alone, and only one of the five
+# configurations -- default_build_coverage -- has a name matching "*_coverage".
+# So four configurations built and ran every test and their coverage was thrown
+# away. That is not redundancy discarded: each configuration selects a different
+# set of TX_ feature macros, so the code the other four compile is *absent from
+# the denominator* rather than reported as uncovered. A figure taken from one of
+# them is the executable-line count of one configuration, not of the kernel.
+#
+# TX_COVERAGE instruments a build regardless of its name. It defaults to OFF and
+# the build-type match is kept, so configuring a single configuration by hand
+# behaves exactly as it did before; the regression run turns it on for all five
+# and merges the results.
+option(TX_COVERAGE "Instrument this build for coverage regardless of build type" OFF)
+if(TX_COVERAGE OR CMAKE_BUILD_TYPE MATCHES ".*_coverage")
if(NOT MSVC)
target_compile_options(threadx_smp PRIVATE -fprofile-arcs -ftest-coverage)
target_link_options(threadx_smp PRIVATE -fprofile-arcs -ftest-coverage)
diff --git a/test/smp/cmake/coverage.sh b/test/smp/cmake/coverage.sh
index d35284f5..88de4f04 100755
--- a/test/smp/cmake/coverage.sh
+++ b/test/smp/cmake/coverage.sh
@@ -14,7 +14,145 @@
set -e
cd $(dirname $0)
-threadx_smp=$(realpath ../../../common_smp/src)
-mkdir -p coverage_report/$1
-gcovr --object-directory=build/$1/threadx_smp/CMakeFiles/threadx_smp.dir/$threadx_smp -r build/$1 -f ../../../common_smp/src --xml-pretty --output coverage_report/$1.xml
-gcovr --object-directory=build/$1/threadx_smp/CMakeFiles/threadx_smp.dir/$threadx_smp -r build/$1 -f ../../../common_smp/src --html --html-details --output coverage_report/$1/index.html
+
+# gcov reads a data format tied to the compiler that produced it, so gcov has to match
+# the gcc that built the objects. Since cmake/linux.cmake began honouring CC, taking
+# whatever gcov happens to be first on PATH is no longer safe: building with gcc-14 and
+# reading with gcov-13 gives "version 'B42*', prefer 'B33*'" from gcov, and gcovr turns
+# that into "GCOV returncode was 3" and exits 64. The tests pass and then the coverage
+# step fails with a Python traceback, which reads like a coverage bug rather than a
+# toolchain mismatch.
+#
+# So derive gcov from CC rather than asking the caller to remember both. GCOV still
+# overrides, for a toolchain that does not follow the gcc/gcov naming.
+: "${CC:=gcc}"
+if [ -z "${GCOV:-}" ]; then
+ cc_base=$(basename "$CC")
+ case "$cc_base" in
+ gcc*) GCOV="gcov${cc_base#gcc}" ;;
+ *) GCOV="gcov" ;;
+ esac
+fi
+
+if ! command -v "$GCOV" >/dev/null 2>&1; then
+ echo "coverage.sh: '$GCOV' not found, derived from CC='$CC'." >&2
+ echo "Install it, or set GCOV to the gcov matching that compiler." >&2
+ exit 1
+fi
+
+# The repository root, needed by every mode below. It has to be absolute: see the
+# note on the per-configuration paths further down.
+repo_root=$(cd ../../.. && pwd)
+filter=$repo_root/common_smp/src
+
+# --merge unions the per-configuration reports into the one number that means
+# something. Each configuration writes an intermediate JSON beside its XML, and
+# this pass adds them all together.
+#
+# Reporting five separate percentages instead would invite a reader to average
+# them, and an average is not a coverage figure -- a line covered only by
+# trace_build is covered, and only the union says so.
+#
+# Do not be alarmed when the merged percentage is lower than the single
+# configuration this used to report. That is the point: the denominator now
+# includes code the old report never counted at all, because it was compiled out
+# of the only instrumented build.
+if [ "$1" = "--merge" ]; then
+ shopt -s nullglob
+ tracefiles=(coverage_report/per_configuration/*.json)
+ shopt -u nullglob
+ if [ ${#tracefiles[@]} -eq 0 ]; then
+ echo "coverage.sh --merge: no JSON in coverage_report/per_configuration/." >&2
+ echo "Run the suites with TX_COVERAGE=ON first." >&2
+ exit 1
+ fi
+
+ add_args=()
+ for t in "${tracefiles[@]}"; do
+ add_args+=(--add-tracefile "$t")
+ done
+
+ mkdir -p coverage_report/merged
+ gcovr -r "$repo_root" "${add_args[@]}" --xml-pretty --output coverage_report/merged.xml
+ gcovr -r "$repo_root" "${add_args[@]}" --html --html-details --output coverage_report/merged/index.html
+
+ if ! grep -q "<class " coverage_report/merged.xml; then
+ echo "coverage.sh --merge: the merged report contains no files." >&2
+ exit 1
+ fi
+
+ # Named, not just counted. coverage_report/ is not cleaned between runs, so a
+ # tracefile left by an earlier run of a different set of configurations would
+ # otherwise be merged in without anything saying so.
+ echo "coverage.sh --merge: SMP, ${#tracefiles[@]} configuration(s):"
+ for t in "${tracefiles[@]}"; do
+ echo " $(basename "$t" .json)"
+ done
+ exit 0
+fi
+
+# gcovr is given three paths below, and each of them has to be absolute, for a
+# different reason.
+#
+# -r is the repository root rather than the build directory, so the report names
+# files the way the repository does -- "common_smp/src/tx_block_allocate.c"
+# instead of "/home/runner/work/threadx/threadx/common_smp/src/tx_block_allocate.c".
+# With -r inside build/, gcovr cannot express the sources relative to it, because
+# they are outside it, and falls back to absolute paths. Those paths then differ
+# on every machine and disagree with the <source> element written beside them in
+# the same file, so anything that maps coverage back to the repository -- PR
+# annotations, Codecov, SonarQube -- cannot follow them.
+#
+# Both -r and -f must be absolute. "-r ../../.. -f common_smp/src" produces a
+# report containing zero files and exits 0, which is the worst failure mode
+# available here: a green run carrying an empty report. Measured, not assumed.
+repo_root=$(cd ../../.. && pwd)
+
+# This is what actually scopes the report to one build configuration, and it is
+# the positional search path -- not --object-directory, which used to be here
+# and was doing nothing at all. That flag tells gcovr how to get from a gcda
+# file back to the compiler's working directory; it does not restrict which gcda
+# files are found. Pointed at an empty directory it still produced the full
+# report, because gcovr searches -r as well.
+#
+# That matters more now than it did before. While -r was build/$1 it happened to
+# constrain the search to this configuration by accident. -r is the repository
+# root now, and every configuration's gcda lies somewhere under it, so without
+# an explicit search path the report would silently merge all five.
+#
+# The odd shape of this path is CMake's, not ours: common_smp/src sits outside
+# this directory's source tree, so CMake mangles each source file's absolute
+# path into the object path, minus the leading slash. Hence the concatenation
+# rather than a join -- $repo_root already begins with one.
+objdir=$PWD/build/$1/threadx_smp/CMakeFiles/threadx_smp.dir$repo_root/common_smp/src
+
+# The Linux port's own sources are deliberately outside -f. gcno files exist for
+# two directories -- common_smp/src and ports_smp/linux/gnu/src -- and only the
+# first is reported. The kernel is what this suite is here to cover; the
+# architecture ports are validated functionally rather than structurally, and
+# linux/gnu is a development host port that nothing ships on. Written down
+# because a filter argument on its own is not a decision the next reader can see.
+
+# Per-configuration output is kept in a subdirectory of its own, and the merged
+# report sits alongside it at the top. That is not tidiness: the Pages deploy
+# uploads coverage_report wholesale and merges the ThreadX and SMP artifacts
+# into one tree, and every configuration directory has the same name in both
+# suites. Left at the top level, default_build_coverage/ from one suite would
+# overwrite the other's on the published site. Nested here, the top level still
+# holds exactly the one suite directory the deploy expects.
+mkdir -p coverage_report/per_configuration/$1
+gcovr --gcov-executable "$GCOV" -r "$repo_root" -f "$filter" "$objdir" \
+ --json coverage_report/per_configuration/$1.json \
+ --xml-pretty --output coverage_report/per_configuration/$1.xml
+gcovr --gcov-executable "$GCOV" -r "$repo_root" -f "$filter" "$objdir" --html --html-details --output coverage_report/per_configuration/$1/index.html
+
+# An empty report is not an error as far as gcovr is concerned: it warns and
+# exits 0. Worse, it advertises line-rate="1.0" alongside lines-valid="0", so
+# every downstream consumer reads "no data at all" as "100% covered". A coverage
+# threshold cannot catch that, because an empty report passes any threshold. So
+# the assertion belongs here, next to the paths that would cause it.
+if ! grep -q "<class " coverage_report/per_configuration/$1.xml; then
+ echo "coverage.sh: the report for '$1' contains no files." >&2
+ echo "Expected gcda files under $objdir." >&2
+ exit 1
+fi
diff --git a/test/smp/cmake/regression/CMakeLists.txt b/test/smp/cmake/regression/CMakeLists.txt
index 908ac5c7..cdf0c585 100644
--- a/test/smp/cmake/regression/CMakeLists.txt
+++ b/test/smp/cmake/regression/CMakeLists.txt
@@ -25,6 +25,7 @@ set(regression_test_cases
${SOURCE_DIR}/threadx_block_memory_thread_terminate_test.c
${SOURCE_DIR}/threadx_byte_memory_basic_test.c
${SOURCE_DIR}/threadx_byte_memory_information_test.c
+ ${SOURCE_DIR}/threadx_byte_memory_long_search_test.c
${SOURCE_DIR}/threadx_byte_memory_prioritize_test.c
${SOURCE_DIR}/threadx_byte_memory_suspension_test.c
${SOURCE_DIR}/threadx_byte_memory_suspension_timeout_test.c
@@ -52,6 +53,7 @@ set(regression_test_cases
${SOURCE_DIR}/threadx_mutex_proritize_test.c
${SOURCE_DIR}/threadx_mutex_suspension_timeout_test.c
${SOURCE_DIR}/threadx_mutex_thread_terminate_test.c
+ ${SOURCE_DIR}/threadx_object_delete_suspension_test.c
${SOURCE_DIR}/threadx_queue_basic_eight_word_test.c
${SOURCE_DIR}/threadx_queue_basic_four_word_test.c
${SOURCE_DIR}/threadx_queue_basic_one_word_test.c
@@ -84,6 +86,7 @@ set(regression_test_cases
${SOURCE_DIR}/threadx_smp_random_resume_suspend_exclusion_test.c
${SOURCE_DIR}/threadx_smp_random_resume_suspend_test.c
${SOURCE_DIR}/threadx_smp_rebalance_exclusion_test.c
+ ${SOURCE_DIR}/threadx_smp_relinquish_rebalance_test.c
${SOURCE_DIR}/threadx_smp_relinquish_test.c
${SOURCE_DIR}/threadx_smp_resume_suspend_ascending_order_test.c
${SOURCE_DIR}/threadx_smp_resume_suspend_descending_order_test.c
@@ -95,6 +98,7 @@ set(regression_test_cases
${SOURCE_DIR}/threadx_thread_create_preemption_threshold_test.c
${SOURCE_DIR}/threadx_thread_delayed_suspension_test.c
${SOURCE_DIR}/threadx_thread_information_test.c
+ ${SOURCE_DIR}/threadx_thread_misaligned_stack_test.c
${SOURCE_DIR}/threadx_thread_multi_level_preemption_threshold_test.c
${SOURCE_DIR}/threadx_thread_multiple_non_current_test.c
${SOURCE_DIR}/threadx_thread_multiple_sleep_test.c
@@ -111,7 +115,10 @@ set(regression_test_cases
${SOURCE_DIR}/threadx_thread_sleep_for_100ticks_test.c
${SOURCE_DIR}/threadx_thread_sleep_terminate_test.c
${SOURCE_DIR}/threadx_thread_stack_checking_test.c
+ ${SOURCE_DIR}/threadx_thread_stack_fill_value_test.c
${SOURCE_DIR}/threadx_thread_terminate_delete_test.c
+ ${SOURCE_DIR}/threadx_thread_exit_callback_transition_test.c
+ ${SOURCE_DIR}/threadx_smp_thread_exit_callback_remote_test.c
${SOURCE_DIR}/threadx_thread_time_slice_change_test.c
${SOURCE_DIR}/threadx_thread_wait_abort_and_isr_test.c
${SOURCE_DIR}/threadx_thread_wait_abort_test.c
@@ -124,6 +131,7 @@ set(regression_test_cases
${SOURCE_DIR}/threadx_timer_multiple_test.c
${SOURCE_DIR}/threadx_timer_simple_test.c
${SOURCE_DIR}/threadx_trace_basic_test.c
+ ${SOURCE_DIR}/threadx_trace_entry_update_test.c
${SOURCE_DIR}/threadx_initialize_kernel_setup_test.c)
add_custom_command(
diff --git a/test/smp/cmake/threadx_smp/CMakeLists.txt b/test/smp/cmake/threadx_smp/CMakeLists.txt
index 568fece4..a2e17636 100644
--- a/test/smp/cmake/threadx_smp/CMakeLists.txt
+++ b/test/smp/cmake/threadx_smp/CMakeLists.txt
@@ -45,6 +45,7 @@ else()
endif()
configure_file(${TX_USER_FILE} ${CUSTOM_INC_DIR}/tx_user.h COPYONLY)
target_include_directories(${PROJECT_NAME}
+ SYSTEM
PUBLIC
${CUSTOM_INC_DIR}
)
diff --git a/test/smp/cmake/threadx_smp/common_smp/CMakeLists.txt b/test/smp/cmake/threadx_smp/common_smp/CMakeLists.txt
index 646b200b..770379af 100644
--- a/test/smp/cmake/threadx_smp/common_smp/CMakeLists.txt
+++ b/test/smp/cmake/threadx_smp/common_smp/CMakeLists.txt
@@ -214,6 +214,7 @@ target_sources(${PROJECT_NAME}
# Add the Common/inc directory to the project include list
target_include_directories(${PROJECT_NAME}
+ SYSTEM
PUBLIC
${CURRENT_DIR}/inc
)
diff --git a/test/smp/cmake/threadx_smp/ports_smp/linux/gnu/CMakeLists.txt b/test/smp/cmake/threadx_smp/ports_smp/linux/gnu/CMakeLists.txt
index 9c51522c..64d97255 100644
--- a/test/smp/cmake/threadx_smp/ports_smp/linux/gnu/CMakeLists.txt
+++ b/test/smp/cmake/threadx_smp/ports_smp/linux/gnu/CMakeLists.txt
@@ -25,6 +25,7 @@ target_sources(${PROJECT_NAME}
)
target_include_directories(${PROJECT_NAME}
+ SYSTEM
PUBLIC
${CURRENT_DIR}/inc
)
diff --git a/test/smp/cmake/threadx_smp/ports_smp/win64/vs_2022/CMakeLists.txt b/test/smp/cmake/threadx_smp/ports_smp/win64/vs_2022/CMakeLists.txt
index 6b219bb2..8635647a 100644
--- a/test/smp/cmake/threadx_smp/ports_smp/win64/vs_2022/CMakeLists.txt
+++ b/test/smp/cmake/threadx_smp/ports_smp/win64/vs_2022/CMakeLists.txt
@@ -24,6 +24,7 @@ target_sources(${PROJECT_NAME}
)
target_include_directories(${PROJECT_NAME}
+ SYSTEM
PUBLIC
${CURRENT_DIR}/inc
)
diff --git a/test/smp/regression/testcontrol.c b/test/smp/regression/testcontrol.c
index 1c3b2ccf..c964dc03 100644
--- a/test/smp/regression/testcontrol.c
+++ b/test/smp/regression/testcontrol.c
@@ -10,7 +10,7 @@
/***************************************************************************/
/* This is the test control routine of the ThreadX kernel. All tests are dispatched from this routine. */
-// Some portions generated by Codex (gpt 5.5).
+// Portions of this file were generated with AI assistance.
#define TX_THREAD_SMP_SOURCE_CODE
@@ -25,6 +25,19 @@
#include "tx_block_pool.h"
#include "tx_byte_pool.h"
#include "tx_event_flags.h"
+#include <stdlib.h>
+#include <string.h>
+#ifdef _WIN32
+#include <io.h>
+#include <process.h>
+#include <stdint.h>
+#else
+#include <unistd.h>
+#include <errno.h>
+#include <signal.h>
+#include <time.h>
+#include <pthread.h>
+#endif
#define TEST_STACK_SIZE 6144
@@ -58,6 +71,7 @@ TX_BLOCK_POOL init_block_pool;
ULONG init_block_pool_area[50];
TX_EVENT_FLAGS_GROUP init_event_flags;
TX_TIMER init_timer;
+TX_TIMER init_timer_change;
TX_THREAD init_test_thread;
TX_THREAD second_test_thread;
#ifndef TX_TIMER_PROCESS_IN_ISR
@@ -233,6 +247,8 @@ void threadx_thread_simple_sleep_non_clear_application_define(void *);
void threadx_thread_sleep_for_100ticks_application_define(void *);
void threadx_thread_multiple_sleep_application_define(void *);
void threadx_thread_terminate_delete_application_define(void *);
+void threadx_thread_exit_callback_transition_application_define(void *);
+void threadx_smp_thread_exit_callback_remote_application_define(void *);
void threadx_thread_preemption_change_application_define(void *);
void threadx_thread_priority_change_application_define(void *);
void threadx_thread_time_slice_change_application_define(void *);
@@ -244,6 +260,7 @@ void threadx_thread_create_preemption_threshold_application_define(void *);
void threadx_thread_information_application_define(void *);
void threadx_thread_reset_application_define(void *);
void threadx_thread_stack_checking_application_define(void *);
+void threadx_thread_stack_fill_value_application_define(void *);
void threadx_time_get_set_application_define(void *);
@@ -377,6 +394,8 @@ TEST_ENTRY test_control_tests[] =
threadx_thread_sleep_for_100ticks_application_define,
threadx_thread_multiple_sleep_application_define,
threadx_thread_terminate_delete_application_define,
+ threadx_thread_exit_callback_transition_application_define,
+ threadx_smp_thread_exit_callback_remote_application_define,
threadx_thread_priority_change_application_define,
@@ -390,6 +409,7 @@ TEST_ENTRY test_control_tests[] =
threadx_thread_information_application_define,
threadx_thread_reset_application_define,
threadx_thread_stack_checking_application_define,
+ threadx_thread_stack_fill_value_application_define,
threadx_time_get_set_application_define,
@@ -681,6 +701,11 @@ TX_THREAD *thread_ptr;
test_timer_create_init = tx_timer_create(&init_timer, "init timer", init_timer_entry, 0x5678,
100, 200, TX_AUTO_ACTIVATE);
+ /* Test timer change from initialization. */
+ test_timer_create_init += tx_timer_create(&init_timer_change, "init timer change", init_timer_entry, 0x5678,
+ 100, 200, TX_NO_ACTIVATE);
+ test_timer_create_init += tx_timer_change(&init_timer_change, 50, 100);
+
/* Test calling tx_thread_relinquish to see if the error checking throws it out. */
tx_thread_relinquish();
#endif
@@ -1335,6 +1360,314 @@ TX_THREAD *thread_ptr;
}
+/* ---------------------------------------------------------------------------
+ Teardown diagnostics.
+
+ When this suite hangs, CI reports nothing but a ctest timeout. The last
+ flush of stdout happens on entry to test_control_return(), so a test that
+ wedges after that point leaves the rest of its output sitting in stdout's
+ block buffer, which is discarded when ctest kills the process. The log
+ stops at the test's own result line and says nothing about where the
+ process stopped.
+
+ Two things close that gap. A stage variable records how far teardown has
+ progressed, and a watchdog thread, armed on entry to test_control_return()
+ and stood down once the next test is about to be dispatched, reports the
+ last stage reached along with the scheduler state, then exits non-zero.
+
+ The watchdog covers teardown only, never the test body. Several tests here
+ wait on a probabilistic interrupt window and have no bounded runtime:
+ threadx_thread_wait_abort_and_isr_test has been observed taking anywhere
+ from 0.34 to 439 seconds while passing. Teardown is a fixed amount of work
+ that takes milliseconds, so bounding it cannot turn a slow pass into a
+ failure.
+
+ The report is written directly to the standard-error descriptor rather
+ than with printf() deliberately. A wedged thread may hold the stdio lock,
+ and a watchdog that blocked on that lock would reproduce the silent timeout
+ it exists to replace.
+
+ TX_TEST_TEARDOWN_TIMEOUT overrides the bound, in seconds; zero disables the
+ watchdog. TX_TEST_TEARDOWN_TRACE echoes every stage as it is reached and
+ puts stdout in line-buffered mode so the surrounding output survives too.
+ --------------------------------------------------------------------------- */
+
+#define TEST_TEARDOWN_TIMEOUT_DEFAULT 60 /* Seconds allowed for teardown. */
+#define TEST_TEARDOWN_POLL_MS 250 /* Watchdog poll interval. */
+#define TEST_TEARDOWN_WALK_LIMIT 100000 /* Cap on the unbounded walk in cleanup. */
+
+static const char * volatile test_teardown_stage_name = "test body";
+static volatile UINT test_teardown_armed = TX_FALSE;
+static UINT test_teardown_timeout = TEST_TEARDOWN_TIMEOUT_DEFAULT;
+static UINT test_teardown_trace = TX_FALSE;
+
+#ifdef _WIN32
+#define TEST_TEARDOWN_DEFERRED_PREEMPT(thread_ptr) ((thread_ptr) -> tx_thread_win32_deferred_preempt)
+#define TEST_TEARDOWN_SUSPENSION_TYPE(thread_ptr) ((thread_ptr) -> tx_thread_win32_suspension_type)
+#else
+#define TEST_TEARDOWN_DEFERRED_PREEMPT(thread_ptr) ((thread_ptr) -> tx_thread_linux_deferred_preempt)
+#define TEST_TEARDOWN_SUSPENSION_TYPE(thread_ptr) ((thread_ptr) -> tx_thread_linux_suspension_type)
+#endif
+
+
+/* Write diagnostics directly to standard error without taking the stdio lock. */
+static void test_teardown_write(const char *buffer, size_t length)
+{
+
+#ifdef _WIN32
+ (void) _write(2, buffer, (unsigned int) length);
+#else
+ (void) write(2, buffer, length);
+#endif
+}
+
+
+/* Record how far teardown has progressed, and echo it when tracing is on. */
+static void test_teardown_stage(const char *stage)
+{
+
+ test_teardown_stage_name = stage;
+
+ if (test_teardown_trace != TX_FALSE)
+ {
+ test_teardown_write("[teardown] ", 11u);
+ test_teardown_write(stage, strlen(stage));
+ test_teardown_write("\n", 1u);
+ }
+}
+
+
+/* Name a thread for the report, without dereferencing a null pointer. */
+static const char *test_teardown_thread_name(TX_THREAD *thread_ptr)
+{
+
+ if (thread_ptr == TX_NULL)
+ return "(none)";
+
+ if (thread_ptr -> tx_thread_name == TX_NULL)
+ return "(unnamed)";
+
+ return thread_ptr -> tx_thread_name;
+}
+
+
+/* Write out the state that matters for a teardown hang. */
+static void test_teardown_report(const char *reason)
+{
+
+char buffer[512];
+int length;
+UINT core;
+UINT count;
+TX_THREAD *thread_ptr;
+
+
+ length = snprintf(buffer, sizeof(buffer),
+ "\n**** TEARDOWN WATCHDOG: %s ****\n"
+ " last stage reached: %s\n"
+ " _tx_thread_preempt_disable: %u\n"
+ " _tx_thread_created_count: %lu\n",
+ reason, test_teardown_stage_name,
+ (UINT) _tx_thread_preempt_disable,
+ (ULONG) _tx_thread_created_count);
+ if (length > 0)
+ test_teardown_write(buffer, (size_t) length);
+
+ for (core = 0; core < ((UINT) TX_THREAD_SMP_MAX_CORES); core++)
+ {
+
+ length = snprintf(buffer, sizeof(buffer),
+ " core %u: system_state=%lu time_slice=%lu current=%s execute=%s\n",
+ core, (ULONG) _tx_thread_system_state[core],
+ (ULONG) _tx_timer_time_slice[core],
+ test_teardown_thread_name(_tx_thread_current_ptr[core]),
+ test_teardown_thread_name(_tx_thread_execute_ptr[core]));
+ if (length > 0)
+ test_teardown_write(buffer, (size_t) length);
+ }
+
+ /* Walk the created list, stopping at the head, and after a fixed number of
+ entries, so a corrupt list cannot trap us in here. */
+ thread_ptr = _tx_thread_created_ptr;
+ for (count = 0; (thread_ptr != TX_NULL) && (count < ((UINT) 64)); count++)
+ {
+
+ length = snprintf(buffer, sizeof(buffer),
+ " thread %-24s state=%u priority=%u threshold=%u inherit=%u "
+ "core_mapped=%u core_control=%lu deferred_preempt=%u suspension=%u\n",
+ test_teardown_thread_name(thread_ptr),
+ thread_ptr -> tx_thread_state,
+ thread_ptr -> tx_thread_priority,
+ thread_ptr -> tx_thread_preempt_threshold,
+ thread_ptr -> tx_thread_inherit_priority,
+ thread_ptr -> tx_thread_smp_core_mapped,
+ (ULONG) thread_ptr -> tx_thread_smp_core_control,
+ TEST_TEARDOWN_DEFERRED_PREEMPT(thread_ptr),
+ TEST_TEARDOWN_SUSPENSION_TYPE(thread_ptr));
+ if (length > 0)
+ test_teardown_write(buffer, (size_t) length);
+
+ thread_ptr = thread_ptr -> tx_thread_created_next;
+ if (thread_ptr == _tx_thread_created_ptr)
+ break;
+ }
+}
+
+
+/* Report and leave. Used by the watchdog and by the bounded walk in cleanup. */
+static void test_teardown_abort(const char *reason)
+{
+
+ test_teardown_report(reason);
+ _exit(99);
+}
+
+
+/* Watch an armed teardown, and only an armed one. */
+#ifdef _WIN32
+static unsigned __stdcall test_teardown_watchdog(void *input)
+{
+
+ULONG waited_ms;
+ULONG limit_ms;
+
+
+ (void) input;
+ limit_ms = ((ULONG) test_teardown_timeout) * ((ULONG) 1000);
+ waited_ms = ((ULONG) 0);
+
+ while (1)
+ {
+
+ Sleep((DWORD) TEST_TEARDOWN_POLL_MS);
+
+ /* Nothing to watch while teardown is stood down. */
+ if (test_teardown_armed == TX_FALSE)
+ {
+ waited_ms = ((ULONG) 0);
+ }
+ else
+ {
+ waited_ms = waited_ms + ((ULONG) TEST_TEARDOWN_POLL_MS);
+ if (waited_ms >= limit_ms)
+ {
+ test_teardown_abort("teardown did not complete");
+ }
+ }
+ }
+
+ return 0u;
+}
+#else
+static void *test_teardown_watchdog(void *input)
+{
+
+sigset_t mask;
+struct timespec interval;
+ULONG waited_ms;
+ULONG limit_ms;
+
+
+ /* The port drives thread suspend and resume with SIGUSR1 and SIGUSR2.
+ Block both here so this thread never takes one. */
+ sigemptyset(&mask);
+ sigaddset(&mask, SIGUSR1);
+ sigaddset(&mask, SIGUSR2);
+ pthread_sigmask(SIG_BLOCK, &mask, TX_NULL);
+
+ limit_ms = ((ULONG) test_teardown_timeout) * ((ULONG) 1000);
+ waited_ms = ((ULONG) 0);
+
+ while (1)
+ {
+
+ interval.tv_sec = 0;
+ interval.tv_nsec = ((long) TEST_TEARDOWN_POLL_MS) * 1000000L;
+ while (nanosleep(&interval, &interval) != 0)
+ {
+ if (errno != EINTR)
+ break;
+ }
+
+ /* Nothing to watch while teardown is stood down. */
+ if (test_teardown_armed == TX_FALSE)
+ {
+ waited_ms = ((ULONG) 0);
+ continue;
+ }
+
+ waited_ms = waited_ms + ((ULONG) TEST_TEARDOWN_POLL_MS);
+ if (waited_ms >= limit_ms)
+ test_teardown_abort("teardown did not complete");
+ }
+
+ return input;
+}
+#endif
+
+
+/* Start the watchdog, stood down. Called before any test runs, so that no
+ thread is created inside the window under investigation. */
+static void test_teardown_watchdog_start(void)
+{
+
+#ifdef _WIN32
+uintptr_t watchdog_id;
+#else
+pthread_t watchdog_id;
+#endif
+char *value;
+
+
+ value = getenv("TX_TEST_TEARDOWN_TIMEOUT");
+ if (value != TX_NULL)
+ test_teardown_timeout = (UINT) atoi(value);
+
+ if (getenv("TX_TEST_TEARDOWN_TRACE") != TX_NULL)
+ {
+
+ /* Tracing wants the stages in the log next to the output around them,
+ which block buffering would otherwise discard on a kill. */
+ test_teardown_trace = TX_TRUE;
+#ifdef _WIN32
+ setvbuf(stdout, TX_NULL, _IONBF, 0u);
+#else
+ setvbuf(stdout, TX_NULL, _IOLBF, 0);
+#endif
+ }
+
+ /* A timeout of zero turns the watchdog off. */
+ if (test_teardown_timeout == ((UINT) 0))
+ return;
+
+#ifdef _WIN32
+ watchdog_id = _beginthreadex(TX_NULL, 0u, test_teardown_watchdog, TX_NULL, 0u, TX_NULL);
+ if (watchdog_id != ((uintptr_t) 0))
+ {
+ (void) CloseHandle((HANDLE) watchdog_id);
+ }
+#else
+ if (pthread_create(&watchdog_id, TX_NULL, test_teardown_watchdog, TX_NULL) == 0)
+ pthread_detach(watchdog_id);
+#endif
+}
+
+
+static void test_teardown_arm(const char *stage)
+{
+
+ test_teardown_stage(stage);
+ test_teardown_armed = TX_TRUE;
+}
+
+
+static void test_teardown_stand_down(void)
+{
+
+ test_teardown_armed = TX_FALSE;
+ test_teardown_stage("test body");
+}
+
/* Define the test control thread. This thread is responsible for dispatching all of the
tests in the ThreadX test suite. */
@@ -1346,6 +1679,9 @@ UINT i;
/* Raise the priority of the control thread to 0. */
tx_thread_priority_change(&test_control_thread, 0, &i);
+
+ /* Bring up the teardown watchdog, stood down for now. */
+ test_teardown_watchdog_start();
#ifdef CTEST
test_control_cleanup();
#endif
@@ -1364,6 +1700,10 @@ UINT i;
while (test_control_tests[i].test_entry != TX_NULL)
{
+ /* Teardown of the previous test is done. The watchdog covers teardown
+ only, so stand it down before dispatching the next test. */
+ test_teardown_stand_down();
+
/* Clear the ISR dispatch. */
test_isr_dispatch = TX_NULL;
@@ -1381,12 +1721,15 @@ UINT i;
tx_thread_suspend(&test_control_thread);
/* Test finished, cleanup in preparation for the next test. */
+ test_teardown_stage("control thread resumed after test");
test_control_cleanup();
}
/* Finished with all tests, print results and return! */
+ test_teardown_stage("all tests dispatched, printing summary");
printf("**** Testing Complete ****\n");
printf("**** Test Summary: Tests Passed: %lu Tests Failed: %lu System Errors: %lu\n", test_control_successful_tests, test_control_failed_tests, test_control_system_errors);
+ test_teardown_stage("calling exit()");
#ifndef EXTERNAL_EXIT
exit(test_control_failed_tests + test_control_system_errors);
#else
@@ -1404,6 +1747,10 @@ UINT old_posture = TX_INT_ENABLE;
fflush(stdout);
+ /* This is the last flush before the test either finishes or wedges, so
+ everything from here on is invisible in the log. Arm the watchdog. */
+ test_teardown_arm("test_control_return entered");
+
/* Save the status in a global. */
test_control_return_status = status;
@@ -1456,7 +1803,9 @@ UINT old_posture = TX_INT_ENABLE;
#endif
/* Resume the control thread to fully exit the test. */
+ test_teardown_stage("test_control_return: resuming control thread");
tx_thread_resume(&test_control_thread);
+ test_teardown_stage("test_control_return: control thread resume returned");
}
@@ -1465,8 +1814,11 @@ void test_control_cleanup(void)
TX_MUTEX *mutex_ptr;
TX_THREAD *thread_ptr;
+UINT walk_guard;
+ test_teardown_stage("cleanup: deleting threads");
+
/* Delete all threads, except for timer thread, and test control thread.
This ensures application-owned objects are no longer referenced before
the object cleanup loops below attempt to delete them. */
@@ -1497,9 +1849,18 @@ TX_THREAD *thread_ptr;
break;
/* Move to the thread not protected. */
+ walk_guard = ((UINT) 0);
while ((thread_ptr == &_tx_timer_thread) || (thread_ptr == &test_control_thread))
{
+ /* This walk has no natural bound. Should the created count and the
+ created list ever disagree, it spins here for good, which is one
+ of the shapes the CI timeout could be taking. Say so and stop
+ instead of hanging. */
+ walk_guard = walk_guard + ((UINT) 1);
+ if (walk_guard > ((UINT) TEST_TEARDOWN_WALK_LIMIT))
+ test_teardown_abort("cleanup thread walk did not terminate");
+
/* Yes, move to the next thread. */
thread_ptr = thread_ptr -> tx_thread_created_next;
}
@@ -1512,6 +1873,8 @@ TX_THREAD *thread_ptr;
tx_thread_delete(thread_ptr);
}
+ test_teardown_stage("cleanup: deleting queues");
+
/* Delete all queues. */
while(_tx_queue_created_ptr)
{
@@ -1520,6 +1883,8 @@ TX_THREAD *thread_ptr;
tx_queue_delete(_tx_queue_created_ptr);
}
+ test_teardown_stage("cleanup: deleting semaphores");
+
/* Delete all semaphores. */
while(_tx_semaphore_created_ptr)
{
@@ -1528,6 +1893,8 @@ TX_THREAD *thread_ptr;
tx_semaphore_delete(_tx_semaphore_created_ptr);
}
+ test_teardown_stage("cleanup: deleting event flag groups");
+
/* Delete all event flag groups. */
while(_tx_event_flags_created_ptr)
{
@@ -1536,6 +1903,8 @@ TX_THREAD *thread_ptr;
tx_event_flags_delete(_tx_event_flags_created_ptr);
}
+ test_teardown_stage("cleanup: deleting byte pools");
+
/* Delete all byte pools. */
while(_tx_byte_pool_created_ptr)
{
@@ -1544,6 +1913,8 @@ TX_THREAD *thread_ptr;
tx_byte_pool_delete(_tx_byte_pool_created_ptr);
}
+ test_teardown_stage("cleanup: deleting block pools");
+
/* Delete all block pools. */
while(_tx_block_pool_created_ptr)
{
@@ -1552,6 +1923,8 @@ TX_THREAD *thread_ptr;
tx_block_pool_delete(_tx_block_pool_created_ptr);
}
+ test_teardown_stage("cleanup: deleting timers");
+
/* Delete all timers. */
while(_tx_timer_created_ptr)
{
@@ -1563,6 +1936,8 @@ TX_THREAD *thread_ptr;
tx_timer_delete(_tx_timer_created_ptr);
}
+ test_teardown_stage("cleanup: deleting mutexes");
+
/* Delete all mutexes (except for system mutex). */
while(_tx_mutex_created_ptr)
{
@@ -1589,6 +1964,8 @@ TX_THREAD *thread_ptr;
tx_mutex_delete(mutex_ptr);
}
+ test_teardown_stage("cleanup: complete");
+
/* At this point, only the test control thread and the system timer thread and/or mutex should still be
in the system. */
}
diff --git a/test/smp/regression/threadx_block_memory_basic_test.c b/test/smp/regression/threadx_block_memory_basic_test.c
index 28edee3f..27990f88 100644
--- a/test/smp/regression/threadx_block_memory_basic_test.c
+++ b/test/smp/regression/threadx_block_memory_basic_test.c
@@ -9,6 +9,8 @@
/* SPDX-License-Identifier: MIT */
/***************************************************************************/
+/* Portions of this file were generated with AI assistance. */
+
/* This test is designed to test simple memory block pool creation, deletion, and
allocates and releases. */
@@ -72,7 +74,7 @@ void test_control_return(UINT status);
/* Prototype direct call to block pool core service. */
-UINT _tx_block_pool_create(TX_BLOCK_POOL *pool_ptr, CHAR *name_ptr, ULONG block_size,
+UINT _tx_block_pool_create(TX_BLOCK_POOL *pool_ptr, TX_NAME_CONST CHAR *name_ptr, ULONG block_size,
VOID *pool_start, ULONG pool_size);
diff --git a/test/smp/regression/threadx_block_memory_error_detection_test.c b/test/smp/regression/threadx_block_memory_error_detection_test.c
index 41148120..b0303b76 100644
--- a/test/smp/regression/threadx_block_memory_error_detection_test.c
+++ b/test/smp/regression/threadx_block_memory_error_detection_test.c
@@ -9,6 +9,8 @@
/* SPDX-License-Identifier: MIT */
/***************************************************************************/
+/* Portions of this file were generated with AI assistance. */
+
/* This test is designed to test error detection for simple memory block operations. */
#include <stdio.h>
@@ -29,7 +31,7 @@ static CHAR *pointer;
static void thread_0_entry(ULONG thread_input);
-UINT _txe_block_pool_create(TX_BLOCK_POOL *pool_ptr, CHAR *name_ptr, ULONG block_size,
+UINT _txe_block_pool_create(TX_BLOCK_POOL *pool_ptr, TX_NAME_CONST CHAR *name_ptr, ULONG block_size,
VOID *pool_start, ULONG pool_size, UINT pool_control_block_size);
diff --git a/test/smp/regression/threadx_block_memory_information_test.c b/test/smp/regression/threadx_block_memory_information_test.c
index 82d57f40..0c04cb7e 100644
--- a/test/smp/regression/threadx_block_memory_information_test.c
+++ b/test/smp/regression/threadx_block_memory_information_test.c
@@ -9,6 +9,8 @@
/* SPDX-License-Identifier: MIT */
/***************************************************************************/
+/* Portions of this file were generated with AI assistance. */
+
/* This test is designed to test block memory information services. */
#include <stdio.h>
@@ -68,7 +70,7 @@ static void thread_5_entry(ULONG thread_input);
static void thread_6_entry(ULONG thread_input);
/* Direct core function to bypass the error checking shell. */
-UINT _tx_block_pool_info_get(TX_BLOCK_POOL *pool_ptr, CHAR **name, ULONG *available_blocks,
+UINT _tx_block_pool_info_get(TX_BLOCK_POOL *pool_ptr, TX_NAME_CONST CHAR **name, ULONG *available_blocks,
ULONG *total_blocks, TX_THREAD **first_suspended,
ULONG *suspended_count, TX_BLOCK_POOL **next_pool);
@@ -227,7 +229,7 @@ static void thread_0_entry(ULONG thread_input)
UINT status;
VOID *pointer;
-CHAR *name;
+TX_NAME_CONST CHAR *name;
ULONG available;
ULONG total_blocks;
TX_THREAD *first_suspended;
@@ -369,7 +371,7 @@ ULONG timeouts;
status += tx_block_pool_info_get(&block_pool_0, &name, &available, &total_blocks, &first_suspended, &suspended_count, &next_pool);
/* Check for an error condition. */
- if ((status) || (available != block_pool_0.tx_block_pool_available) || (total_blocks != block_pool_0.tx_block_pool_total) ||
+ if ((status) || (name != block_pool_0.tx_block_pool_name) || (available != block_pool_0.tx_block_pool_available) || (total_blocks != block_pool_0.tx_block_pool_total) ||
(first_suspended != &thread_4) || (suspended_count != block_pool_0.tx_block_pool_suspended_count) || (next_pool != &block_pool_0))
{
diff --git a/test/smp/regression/threadx_block_memory_suspension_timeout_test.c b/test/smp/regression/threadx_block_memory_suspension_timeout_test.c
index 43477285..1477cff7 100644
--- a/test/smp/regression/threadx_block_memory_suspension_timeout_test.c
+++ b/test/smp/regression/threadx_block_memory_suspension_timeout_test.c
@@ -11,7 +11,7 @@
/* This test is designed to test timeouts on suspension on memory block pools. */
-// Some portions generated by Codex (gpt 5.4).
+// Portions of this file were generated with AI assistance.
#include <stdio.h>
#include "tx_api.h"
diff --git a/test/smp/regression/threadx_byte_memory_basic_test.c b/test/smp/regression/threadx_byte_memory_basic_test.c
index 45a2518d..d7c1232e 100644
--- a/test/smp/regression/threadx_byte_memory_basic_test.c
+++ b/test/smp/regression/threadx_byte_memory_basic_test.c
@@ -9,6 +9,8 @@
/* SPDX-License-Identifier: MIT */
/***************************************************************************/
+/* Portions of this file were generated with AI assistance. */
+
/* This test is designed to test simple memory byte pool creation, deletion, and
allocates and releases. */
@@ -42,7 +44,7 @@ extern UINT test_byte_pool_create_init;
extern VOID (*test_isr_dispatch)(void);
-UINT _txe_byte_pool_create(TX_BYTE_POOL *pool_ptr, CHAR *name_ptr, VOID *pool_start,
+UINT _txe_byte_pool_create(TX_BYTE_POOL *pool_ptr, TX_NAME_CONST CHAR *name_ptr, VOID *pool_start,
ULONG pool_size, UINT pool_control_block_size);
diff --git a/test/smp/regression/threadx_byte_memory_information_test.c b/test/smp/regression/threadx_byte_memory_information_test.c
index f1624a65..f65efac3 100644
--- a/test/smp/regression/threadx_byte_memory_information_test.c
+++ b/test/smp/regression/threadx_byte_memory_information_test.c
@@ -9,6 +9,8 @@
/* SPDX-License-Identifier: MIT */
/***************************************************************************/
+/* Portions of this file were generated with AI assistance. */
+
/* This test is designed to test byte memory information. */
#include <stdio.h>
@@ -226,7 +228,7 @@ static void thread_0_entry(ULONG thread_input)
UINT status;
VOID *pointer;
-CHAR *name;
+TX_NAME_CONST CHAR *name;
ULONG available;
ULONG fragments;
TX_THREAD *first_suspended;
@@ -369,7 +371,7 @@ ULONG timeouts;
status += tx_byte_pool_info_get(&byte_pool_0, &name, &available, &fragments, &first_suspended, &suspended_count, &next_pool);
/* Check the status. */
- if ((status != TX_SUCCESS) || (available != byte_pool_0.tx_byte_pool_available) || (fragments != byte_pool_0.tx_byte_pool_fragments) ||
+ if ((status != TX_SUCCESS) || (name != byte_pool_0.tx_byte_pool_name) || (available != byte_pool_0.tx_byte_pool_available) || (fragments != byte_pool_0.tx_byte_pool_fragments) ||
(first_suspended != &thread_4) || (suspended_count != byte_pool_0.tx_byte_pool_suspended_count) || (next_pool != &byte_pool_0))
{
diff --git a/test/smp/regression/threadx_byte_memory_long_search_test.c b/test/smp/regression/threadx_byte_memory_long_search_test.c
new file mode 100644
index 00000000..8f076335
--- /dev/null
+++ b/test/smp/regression/threadx_byte_memory_long_search_test.c
@@ -0,0 +1,337 @@
+/***************************************************************************
+ * 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
+ **************************************************************************/
+
+/* This test makes _tx_byte_pool_search walk a long chain of fragments, from
+ more than one core at a time.
+
+ The SMP version of that function carries two things its ThreadX counterpart
+ does not, and neither was covered.
+
+ The first is the block-search limit. Under TX_BYTE_POOL_MULTIPLE_BLOCK_SEARCH
+ -- which this port's tx_user.h sets to 20 -- the search holds protection
+ across at most twenty blocks and then deliberately drops and retakes it, so
+ that a long search cannot lock the other cores out for the whole walk.
+ Nothing in the suite ever made one search examine twenty blocks: every byte
+ pool it builds has a handful of fragments at most. That is what this test
+ fixes.
+
+ Having dropped protection, the search then checks whether some other thread
+ claimed the pool while it was let go, and if so spins for
+ TX_BYTE_POOL_DELAY_VALUE iterations before taking ownership back and
+ restarting the walk. That path this test does not reach, and the reason is a
+ property of the port rather than of the test: the Linux SMP port serialises
+ every core on one pthread mutex, and the window here is an unlock
+ immediately followed by a lock on that mutex. A thread spinning for the
+ protection on another core has to win that handoff, and glibc hands an
+ uncontended mutex straight back to the thread that just released it.
+ Measured: 180,003 windows over three cores, and the pool changed hands in
+ none of them. So tx_byte_pool_search.c lines 283 to 301 stay uncovered on
+ this port, and no number of iterations here will change that -- 250 is used
+ below rather than the tens of thousands that were tried, because the twenty-
+ block threshold above is crossed by the very first search and the rest buys
+ nothing.
+
+ The pool is therefore filled with small allocations until it refuses another
+ one, and every second allocation is then released. That leaves upwards of
+ twenty free fragments, each too small to satisfy the request and none of
+ them adjacent to another, so the search can neither stop early nor merge its
+ way out of the walk. Every request is expected to fail with TX_NO_MEMORY;
+ the failure is the point, because a search that fails is a search that
+ walked the whole pool.
+
+ The layout is asserted rather than assumed. If a change to the pool
+ arithmetic ever made the request satisfiable, or left fewer fragments than
+ the twenty-block threshold needs, this test would still pass while quietly
+ covering nothing -- so it checks the fragment count and checks that the
+ request really does fail before the threads start.
+
+ The three worker threads are deliberately not confined to a core, so that
+ the walk really is driven from more than one core at a time. */
+
+#include <stdio.h>
+#include "tx_api.h"
+#include "threadx_test_port.h"
+
+
+void test_control_return(UINT status);
+
+
+/* Small chunks, every second one given back. The pool is filled until it
+ refuses another chunk rather than a fixed number of times, so that the
+ layout does not depend on this test agreeing with the allocator about the
+ per-block overhead -- get that wrong by one word and the pool is left with a
+ free tail large enough to satisfy the request below, and the search stops at
+ the first fragment instead of walking. */
+
+#define CHUNK_BYTES ((ULONG) 8)
+#define CHUNK_LIMIT ((UINT) 96)
+#define MINIMUM_CHUNKS ((UINT) 40)
+
+/* Larger than any one free fragment and smaller than the pool's theoretical
+ capacity, which is what makes the search walk rather than refuse at the
+ door: _tx_byte_pool_search returns immediately when the request is at least
+ the theoretical total, without examining a single block. */
+
+#define REQUEST_BYTES ((ULONG) 64)
+
+/* TX_BYTE_POOL_MULTIPLE_BLOCK_SEARCH is 20 on this port. The walk has to be
+ longer than that for the protection to be dropped at all. */
+
+#define MINIMUM_FRAGMENTS ((ULONG) 24)
+
+/* Each worker searches this many times. One search is enough for the coverage;
+ the repetition is here so that the pool is genuinely walked from several
+ cores at once rather than by one thread at a time, which is the condition
+ the code under test was written for. */
+
+#define SEARCHES_PER_THREAD ((ULONG) 250)
+
+#define WORKERS ((UINT) 3)
+
+
+static TX_THREAD thread_0;
+static TX_THREAD worker[WORKERS];
+
+static TX_BYTE_POOL pool_0;
+
+static UCHAR pool_area[TX_TEST_BYTE_POOL_CAPACITY_BYTES(CHUNK_BYTES, 48)];
+
+static void *chunk[CHUNK_LIMIT];
+static UINT chunk_count;
+
+static volatile ULONG workers_done = 0;
+static volatile ULONG search_count[WORKERS];
+
+static UINT error = 0;
+
+
+static void thread_0_entry(ULONG thread_input);
+static void worker_entry(ULONG thread_input);
+
+
+/* Define what the initial system looks like. */
+
+#ifdef CTEST
+void test_application_define(void *first_unused_memory)
+#else
+void threadx_byte_memory_long_search_application_define(void *first_unused_memory)
+#endif
+{
+
+UINT status;
+CHAR *pointer;
+UINT i;
+TX_NAME_CONST CHAR *name;
+ULONG available;
+ULONG fragments;
+void *probe_ptr;
+
+
+ /* Setup a pointer. */
+ pointer = (CHAR *) first_unused_memory;
+
+ status = tx_byte_pool_create(&pool_0, "long search pool", pool_area, sizeof(pool_area));
+
+ if (status != TX_SUCCESS)
+ {
+
+ printf("Running Byte Memory Long Search Test................................ ERROR #1\n");
+ test_control_return(1);
+ return;
+ }
+
+ /* Fill the pool completely with small chunks, so that no free tail is left
+ behind that could satisfy the request. */
+ chunk_count = ((UINT) 0);
+ while (chunk_count < CHUNK_LIMIT)
+ {
+
+ status = tx_byte_allocate(&pool_0, &chunk[chunk_count], CHUNK_BYTES, TX_NO_WAIT);
+
+ if (status != TX_SUCCESS)
+ {
+
+ break;
+ }
+
+ chunk_count++;
+ }
+
+ if ((chunk_count < MINIMUM_CHUNKS) || (chunk_count == CHUNK_LIMIT))
+ {
+
+ printf("Running Byte Memory Long Search Test................................ ERROR #2\n");
+ test_control_return(1);
+ return;
+ }
+
+ /* Give every second one back. Alternating rather than consecutive, so that
+ no free fragment has a free neighbour and the search cannot merge two of
+ them into a block big enough to satisfy the request. */
+ for (i = 1; i < chunk_count; i = i + ((UINT) 2))
+ {
+
+ status = tx_byte_release(chunk[i]);
+
+ if (status != TX_SUCCESS)
+ {
+
+ printf("Running Byte Memory Long Search Test................................ ERROR #3\n");
+ test_control_return(1);
+ return;
+ }
+
+ chunk[i] = TX_NULL;
+ }
+
+ /* The walk has to be long enough to cross the twenty-block threshold. */
+ status = tx_byte_pool_info_get(&pool_0, &name, &available, &fragments, TX_NULL, TX_NULL, TX_NULL);
+
+ if ((status != TX_SUCCESS) || (fragments < MINIMUM_FRAGMENTS))
+ {
+
+ printf("Running Byte Memory Long Search Test................................ ERROR #4\n");
+ test_control_return(1);
+ return;
+ }
+
+ /* And the request has to fail, or the search stops at the first fragment
+ that fits and the walk never happens. */
+ status = tx_byte_allocate(&pool_0, &probe_ptr, REQUEST_BYTES, TX_NO_WAIT);
+
+ if (status != TX_NO_MEMORY)
+ {
+
+ printf("Running Byte Memory Long Search Test................................ ERROR #5\n");
+ test_control_return(1);
+ return;
+ }
+
+ status = tx_thread_create(&thread_0, "thread 0", thread_0_entry, 0,
+ pointer, TEST_STACK_SIZE_PRINTF,
+ 15, 15, TX_NO_TIME_SLICE, TX_AUTO_START);
+ pointer = pointer + TEST_STACK_SIZE_PRINTF;
+
+ if (status != TX_SUCCESS)
+ {
+
+ printf("Running Byte Memory Long Search Test................................ ERROR #6\n");
+ test_control_return(1);
+ return;
+ }
+
+ /* The workers share one priority so that the scheduler is free to put them
+ on different cores, which is the whole point. */
+ for (i = 0; i < WORKERS; i++)
+ {
+
+ search_count[i] = ((ULONG) 0);
+
+ status = tx_thread_create(&worker[i], "worker", worker_entry, (ULONG) i,
+ pointer, TEST_STACK_SIZE_PRINTF,
+ 16, 16, TX_NO_TIME_SLICE, TX_AUTO_START);
+ pointer = pointer + TEST_STACK_SIZE_PRINTF;
+
+ if (status != TX_SUCCESS)
+ {
+
+ printf("Running Byte Memory Long Search Test................................ ERROR #7\n");
+ test_control_return(1);
+ }
+ }
+}
+
+
+static void thread_0_entry(ULONG thread_input)
+{
+
+UINT status;
+UINT i;
+TX_NAME_CONST CHAR *name;
+ULONG available;
+ULONG fragments;
+
+
+ /* Inform user. */
+ printf("Running Byte Memory Long Search Test................................ ");
+
+ /* Wait for the workers. They do not sleep, so this thread has to. */
+ while (workers_done != ((ULONG) WORKERS))
+ {
+
+ tx_thread_sleep(1);
+ }
+
+ for (i = 0; i < WORKERS; i++)
+ {
+
+ if (search_count[i] != SEARCHES_PER_THREAD)
+ {
+
+ error++;
+ }
+ }
+
+ /* Every search failed, so nothing was taken and nothing was merged: the
+ pool must look exactly as it did when the workers started. */
+ status = tx_byte_pool_info_get(&pool_0, &name, &available, &fragments, TX_NULL, TX_NULL, TX_NULL);
+
+ if ((status != TX_SUCCESS) || (fragments < MINIMUM_FRAGMENTS))
+ {
+
+ error++;
+ }
+
+ if (error)
+ {
+
+ printf("ERROR #8\n");
+ test_control_return(1);
+ }
+ else
+ {
+
+ /* Successful test. */
+ printf("SUCCESS!\n");
+ test_control_return(0);
+ }
+}
+
+
+static void worker_entry(ULONG thread_input)
+{
+
+UINT status;
+ULONG i;
+void *memory_ptr;
+
+
+ for (i = 0; i < SEARCHES_PER_THREAD; i++)
+ {
+
+ status = tx_byte_allocate(&pool_0, &memory_ptr, REQUEST_BYTES, TX_NO_WAIT);
+
+ if (status != TX_NO_MEMORY)
+ {
+
+ error++;
+ break;
+ }
+
+ search_count[thread_input]++;
+ }
+
+ workers_done++;
+}
diff --git a/test/smp/regression/threadx_byte_memory_suspension_timeout_test.c b/test/smp/regression/threadx_byte_memory_suspension_timeout_test.c
index c92f2be0..aaf1ba51 100644
--- a/test/smp/regression/threadx_byte_memory_suspension_timeout_test.c
+++ b/test/smp/regression/threadx_byte_memory_suspension_timeout_test.c
@@ -11,7 +11,7 @@
/* This test is designed to test suspension timeout on a memory byte pool. */
-// Some portions generated by Codex (gpt 5.4).
+// Portions of this file were generated with AI assistance.
#include <stdio.h>
#include "tx_api.h"
diff --git a/test/smp/regression/threadx_event_flag_basic_test.c b/test/smp/regression/threadx_event_flag_basic_test.c
index 8ab990f7..fed424a6 100644
--- a/test/smp/regression/threadx_event_flag_basic_test.c
+++ b/test/smp/regression/threadx_event_flag_basic_test.c
@@ -9,6 +9,8 @@
/* SPDX-License-Identifier: MIT */
/***************************************************************************/
+/* Portions of this file were generated with AI assistance. */
+
/* This test is designed to test simple event flag group creation, deletion, gets and
sets. */
@@ -57,7 +59,7 @@ static TX_EVENT_FLAGS_GROUP group_3;
static void thread_0_entry(ULONG thread_input);
static void thread_1_entry(ULONG thread_input);
-UINT _txe_event_flags_create(TX_EVENT_FLAGS_GROUP *group_ptr, CHAR *name_ptr, UINT event_control_block_size);
+UINT _txe_event_flags_create(TX_EVENT_FLAGS_GROUP *group_ptr, TX_NAME_CONST CHAR *name_ptr, UINT event_control_block_size);
/* Prototype for test control return. */
diff --git a/test/smp/regression/threadx_event_flag_information_test.c b/test/smp/regression/threadx_event_flag_information_test.c
index 45e547f9..1c7cfd2d 100644
--- a/test/smp/regression/threadx_event_flag_information_test.c
+++ b/test/smp/regression/threadx_event_flag_information_test.c
@@ -9,6 +9,8 @@
/* SPDX-License-Identifier: MIT */
/***************************************************************************/
+/* Portions of this file were generated with AI assistance. */
+
/* This test is designed to test the event flag group information gathering services. */
#include <stdio.h>
@@ -134,7 +136,7 @@ static void thread_0_entry(ULONG thread_input)
UINT status;
ULONG actual_events;
-CHAR *name;
+TX_NAME_CONST CHAR *name;
ULONG current_flags;
TX_THREAD *first_suspended;
ULONG suspended_count;
@@ -356,7 +358,7 @@ ULONG timeouts;
status += tx_event_flags_info_get(&group_0, &name, &current_flags, &first_suspended, &suspended_count, &next_group);
/* Check the status. */
- if ((status != TX_SUCCESS) || (current_flags != group_0.tx_event_flags_group_current) || (first_suspended != TX_NULL) || (suspended_count != 0) || (next_group != &group_1))
+ if ((status != TX_SUCCESS) || (name != group_0.tx_event_flags_group_name) || (current_flags != group_0.tx_event_flags_group_current) || (first_suspended != TX_NULL) || (suspended_count != 0) || (next_group != &group_1))
{
/* Event flag error. */
diff --git a/test/smp/regression/threadx_mutex_basic_test.c b/test/smp/regression/threadx_mutex_basic_test.c
index bb81eb9a..5a652843 100644
--- a/test/smp/regression/threadx_mutex_basic_test.c
+++ b/test/smp/regression/threadx_mutex_basic_test.c
@@ -9,6 +9,8 @@
/* SPDX-License-Identifier: MIT */
/***************************************************************************/
+/* Portions of this file were generated with AI assistance. */
+
/* This test is designed to test the mutex create/delete and immediate
return gets and puts. */
@@ -69,7 +71,7 @@ static void thread_2_entry(ULONG thread_input);
static void thread_3_entry(ULONG thread_input);
static void thread_4_entry(ULONG thread_input);
-UINT _txe_mutex_create(TX_MUTEX *mutex_ptr, CHAR *name_ptr, UINT inherit, UINT mutex_control_block_size);
+UINT _txe_mutex_create(TX_MUTEX *mutex_ptr, TX_NAME_CONST CHAR *name_ptr, UINT inherit, UINT mutex_control_block_size);
/* Prototype for test control return. */
diff --git a/test/smp/regression/threadx_mutex_information_test.c b/test/smp/regression/threadx_mutex_information_test.c
index 3829122c..fea78c04 100644
--- a/test/smp/regression/threadx_mutex_information_test.c
+++ b/test/smp/regression/threadx_mutex_information_test.c
@@ -9,6 +9,8 @@
/* SPDX-License-Identifier: MIT */
/***************************************************************************/
+/* Portions of this file were generated with AI assistance. */
+
/* This test is designed to test the mutex information services. */
#include <stdio.h>
@@ -134,7 +136,7 @@ static void thread_0_entry(ULONG thread_input)
{
UINT status;
-CHAR *name;
+TX_NAME_CONST CHAR *name;
ULONG count;
TX_THREAD *owner;
TX_THREAD *first_suspended;
@@ -300,7 +302,7 @@ ULONG inheritances;
status += tx_mutex_info_get(&mutex_2, &name, &count, &owner, &first_suspended, &suspended_count, &next_mutex);
/* Check status. */
- if ((status != TX_SUCCESS) || (count != mutex_2.tx_mutex_ownership_count) || (owner != mutex_2.tx_mutex_owner) ||
+ if ((status != TX_SUCCESS) || (name != mutex_2.tx_mutex_name) || (count != mutex_2.tx_mutex_ownership_count) || (owner != mutex_2.tx_mutex_owner) ||
(first_suspended != mutex_2.tx_mutex_suspension_list) || (suspended_count != mutex_2.tx_mutex_suspended_count) || (next_mutex != mutex_2.tx_mutex_created_next))
{
diff --git a/test/smp/regression/threadx_object_delete_suspension_test.c b/test/smp/regression/threadx_object_delete_suspension_test.c
new file mode 100644
index 00000000..20633f08
--- /dev/null
+++ b/test/smp/regression/threadx_object_delete_suspension_test.c
@@ -0,0 +1,481 @@
+/***************************************************************************
+ * 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
+ **************************************************************************/
+
+/* This test deletes a block pool, a byte pool, an event flags group and a
+ queue while two threads are suspended on each of them, and checks that every
+ suspended thread is released with TX_DELETED.
+
+ Each of those four delete routines carries the same loop, and in this suite
+ the whole loop body was uncovered -- thirty-six lines across the four files,
+ the largest single gap in the SMP coverage report:
+
+ while (suspended_count != TX_NO_SUSPENSIONS)
+ {
+ suspended_count--;
+ TX_DISABLE
+ thread_ptr -> tx_thread_suspend_cleanup = TX_NULL;
+ thread_ptr -> tx_thread_suspend_status = TX_DELETED;
+ next_thread = thread_ptr -> tx_thread_suspended_next;
+ ...
+ _tx_thread_system_resume(thread_ptr);
+ thread_ptr = next_thread;
+ }
+
+ The reason it was uncovered is worth writing down, because it is not that
+ the suite never deletes these objects -- it deletes all four after every
+ single test. test_control_cleanup() in the ThreadX suite deletes the
+ application's objects first and its threads last, so a test that ends with a
+ thread parked on a queue has that thread walked out of the queue by
+ tx_queue_delete. The SMP suite's cleanup deletes the threads first, on
+ purpose: it was changed so that no application-owned object is still
+ referenced when the object loops run, which is what stopped a class of
+ teardown hang. Correct, and the side effect is that every one of those
+ deletes now runs against an empty suspension list.
+
+ So the coverage has to come from a test that means to delete a busy object
+ rather than from teardown that happens to. threadx_semaphore_delete_test
+ already does exactly this for semaphores, which is why tx_semaphore_delete.c
+ is the one member of the family that was fully covered; this is the same
+ idea for the other four.
+
+ Two waiters per object rather than one, so the loop runs twice and its back
+ edge is taken as well as its body.
+
+ The threads are not confined to a core. On this port they start on separate
+ cores and suspend in whatever order they get scheduled, so the control
+ thread waits on the object's own suspended count through tx_*_info_get
+ rather than on an ordering it cannot guarantee. The wait is bounded: a
+ suspension that never arrives fails the test instead of hanging it. */
+
+#include <stdio.h>
+#include "tx_api.h"
+#include "threadx_test_port.h"
+
+
+void test_control_return(UINT status);
+
+
+/* The four objects, deleted in this order. */
+
+#define STAGE_BLOCK_POOL ((UINT) 0)
+#define STAGE_BYTE_POOL ((UINT) 1)
+#define STAGE_EVENT_FLAGS ((UINT) 2)
+#define STAGE_QUEUE ((UINT) 3)
+#define STAGE_COUNT ((UINT) 4)
+
+/* Number of threads that suspend on each object. */
+
+#define WAITERS ((ULONG) 2)
+
+/* How many ticks the control thread will wait for a suspension before giving
+ up. Generous, because it is only ever reached when the test is failing --
+ and finite, because the alternative is a run that hangs instead of one that
+ reports. */
+
+#define WAIT_LIMIT_TICKS ((UINT) 200)
+
+
+static TX_THREAD thread_0;
+static TX_THREAD thread_1;
+static TX_THREAD thread_2;
+
+static TX_BLOCK_POOL block_pool_0;
+static TX_BYTE_POOL byte_pool_0;
+static TX_EVENT_FLAGS_GROUP group_0;
+static TX_QUEUE queue_0;
+
+/* Eight blocks of 16 bytes, all of them taken before any waiter runs. */
+
+static UCHAR block_pool_area[TX_TEST_BLOCK_POOL_BYTES(16, 8)];
+static UCHAR byte_pool_area[TX_TEST_BYTE_POOL_BYTES(256)];
+static ULONG queue_area[16];
+
+/* Counts of waiters that saw TX_DELETED, one entry per stage. */
+
+static volatile ULONG deleted_count[STAGE_COUNT];
+
+static UINT error = 0;
+
+
+static void thread_0_entry(ULONG thread_input);
+static void waiter_entry(ULONG thread_input);
+
+
+/* Define what the initial system looks like. */
+
+#ifdef CTEST
+void test_application_define(void *first_unused_memory)
+#else
+void threadx_object_delete_suspension_application_define(void *first_unused_memory)
+#endif
+{
+
+UINT status;
+CHAR *pointer;
+void *block_ptr;
+void *bytes_ptr;
+UINT i;
+
+
+ /* Setup a pointer. */
+ pointer = (CHAR *) first_unused_memory;
+
+ for (i = 0; i < STAGE_COUNT; i++)
+ {
+
+ deleted_count[i] = ((ULONG) 0);
+ }
+
+ /* Create the block pool and take every block, so that a waiter has nothing
+ to be given and suspends instead. */
+ status = tx_block_pool_create(&block_pool_0, "delete suspension block pool",
+ 16, block_pool_area, sizeof(block_pool_area));
+
+ if (status != TX_SUCCESS)
+ {
+
+ printf("Running Object Delete Suspension Test............................... ERROR #1\n");
+ test_control_return(1);
+ }
+
+ while (tx_block_allocate(&block_pool_0, &block_ptr, TX_NO_WAIT) == TX_SUCCESS)
+ {
+
+ /* Nothing to do with the block; the point is that the pool is empty. */
+ }
+
+ /* Same idea for the byte pool: take enough of it that the 128-byte requests
+ below cannot be met. */
+ status = tx_byte_pool_create(&byte_pool_0, "delete suspension byte pool",
+ byte_pool_area, sizeof(byte_pool_area));
+
+ if (status != TX_SUCCESS)
+ {
+
+ printf("Running Object Delete Suspension Test............................... ERROR #2\n");
+ test_control_return(1);
+ }
+
+ status = tx_byte_allocate(&byte_pool_0, &bytes_ptr, 200, TX_NO_WAIT);
+
+ if (status != TX_SUCCESS)
+ {
+
+ printf("Running Object Delete Suspension Test............................... ERROR #3\n");
+ test_control_return(1);
+ }
+
+ /* An event flags group with nothing set. The waiters ask for a flag that is
+ never set by anybody. */
+ status = tx_event_flags_create(&group_0, "delete suspension group");
+
+ if (status != TX_SUCCESS)
+ {
+
+ printf("Running Object Delete Suspension Test............................... ERROR #4\n");
+ test_control_return(1);
+ }
+
+ /* An empty queue. The waiters receive from it and nobody ever sends. */
+ status = tx_queue_create(&queue_0, "delete suspension queue", TX_1_ULONG,
+ queue_area, sizeof(queue_area));
+
+ if (status != TX_SUCCESS)
+ {
+
+ printf("Running Object Delete Suspension Test............................... ERROR #5\n");
+ test_control_return(1);
+ }
+
+ /* The control thread runs at a higher priority than the waiters, but that
+ decides nothing here -- it waits on the suspended counts. */
+ status = tx_thread_create(&thread_0, "thread 0", thread_0_entry, 0,
+ pointer, TEST_STACK_SIZE_PRINTF,
+ 15, 15, TX_NO_TIME_SLICE, TX_AUTO_START);
+ pointer = pointer + TEST_STACK_SIZE_PRINTF;
+
+ if (status != TX_SUCCESS)
+ {
+
+ printf("Running Object Delete Suspension Test............................... ERROR #6\n");
+ test_control_return(1);
+ }
+
+ status = tx_thread_create(&thread_1, "thread 1", waiter_entry, 1,
+ pointer, TEST_STACK_SIZE_PRINTF,
+ 16, 16, TX_NO_TIME_SLICE, TX_AUTO_START);
+ pointer = pointer + TEST_STACK_SIZE_PRINTF;
+
+ if (status != TX_SUCCESS)
+ {
+
+ printf("Running Object Delete Suspension Test............................... ERROR #7\n");
+ test_control_return(1);
+ }
+
+ status = tx_thread_create(&thread_2, "thread 2", waiter_entry, 2,
+ pointer, TEST_STACK_SIZE_PRINTF,
+ 16, 16, TX_NO_TIME_SLICE, TX_AUTO_START);
+ pointer = pointer + TEST_STACK_SIZE_PRINTF;
+
+ if (status != TX_SUCCESS)
+ {
+
+ printf("Running Object Delete Suspension Test............................... ERROR #8\n");
+ test_control_return(1);
+ }
+}
+
+
+/* Read the number of threads suspended on the object belonging to a stage.
+ Returns TX_TRUE when the count could be read, so that a failing info_get is
+ reported rather than mistaken for "nobody has suspended yet". */
+
+static UINT suspended_on(UINT stage, ULONG *count)
+{
+
+UINT status;
+
+
+ /* Every destination but the suspended count is TX_NULL: each info_get
+ tests its destinations individually and skips the ones it is not given,
+ so there is nothing to declare here to throw away. */
+ switch (stage)
+ {
+
+ case STAGE_BLOCK_POOL:
+
+ status = tx_block_pool_info_get(&block_pool_0, TX_NULL, TX_NULL, TX_NULL,
+ TX_NULL, count, TX_NULL);
+ break;
+
+ case STAGE_BYTE_POOL:
+
+ status = tx_byte_pool_info_get(&byte_pool_0, TX_NULL, TX_NULL, TX_NULL,
+ TX_NULL, count, TX_NULL);
+ break;
+
+ case STAGE_EVENT_FLAGS:
+
+ status = tx_event_flags_info_get(&group_0, TX_NULL, TX_NULL,
+ TX_NULL, count, TX_NULL);
+ break;
+
+ default:
+
+ status = tx_queue_info_get(&queue_0, TX_NULL, TX_NULL, TX_NULL,
+ TX_NULL, count, TX_NULL);
+ break;
+ }
+
+ return((status == TX_SUCCESS) ? ((UINT) TX_TRUE) : ((UINT) TX_FALSE));
+}
+
+
+/* Sleep until every waiter has suspended on this stage's object, or until the
+ budget runs out. Returns TX_TRUE if the waiters arrived. */
+
+static UINT wait_for_waiters(UINT stage)
+{
+
+UINT ticks;
+ULONG count;
+
+
+ count = ((ULONG) 0);
+ for (ticks = 0; ticks < WAIT_LIMIT_TICKS; ticks++)
+ {
+
+ if (suspended_on(stage, &count) == ((UINT) TX_FALSE))
+ {
+
+ return((UINT) TX_FALSE);
+ }
+
+ if (count == WAITERS)
+ {
+
+ return((UINT) TX_TRUE);
+ }
+
+ tx_thread_sleep(1);
+ }
+
+ return((UINT) TX_FALSE);
+}
+
+
+/* Sleep until every waiter has reported TX_DELETED for this stage, or until the
+ budget runs out. */
+
+static UINT wait_for_reports(UINT stage)
+{
+
+UINT ticks;
+
+
+ for (ticks = 0; ticks < WAIT_LIMIT_TICKS; ticks++)
+ {
+
+ if (deleted_count[stage] == WAITERS)
+ {
+
+ return((UINT) TX_TRUE);
+ }
+
+ tx_thread_sleep(1);
+ }
+
+ return((UINT) TX_FALSE);
+}
+
+
+static void thread_0_entry(ULONG thread_input)
+{
+
+UINT status;
+UINT stage;
+
+
+ /* Inform user. */
+ printf("Running Object Delete Suspension Test............................... ");
+
+ for (stage = 0; stage < STAGE_COUNT; stage++)
+ {
+
+ /* Both waiters have to be on the list before the delete, or the loop
+ under test walks a shorter list than this test means it to. */
+ if (wait_for_waiters(stage) == ((UINT) TX_FALSE))
+ {
+
+ printf("ERROR #9 (stage %u)\n", stage);
+ test_control_return(1);
+ }
+
+ switch (stage)
+ {
+
+ case STAGE_BLOCK_POOL:
+
+ status = tx_block_pool_delete(&block_pool_0);
+ break;
+
+ case STAGE_BYTE_POOL:
+
+ status = tx_byte_pool_delete(&byte_pool_0);
+ break;
+
+ case STAGE_EVENT_FLAGS:
+
+ status = tx_event_flags_delete(&group_0);
+ break;
+
+ default:
+
+ status = tx_queue_delete(&queue_0);
+ break;
+ }
+
+ if (status != TX_SUCCESS)
+ {
+
+ printf("ERROR #10 (stage %u)\n", stage);
+ test_control_return(1);
+ }
+
+ /* Every waiter must come out of the delete with TX_DELETED, which is
+ the status the loop under test writes into each suspended thread. */
+ if (wait_for_reports(stage) == ((UINT) TX_FALSE))
+ {
+
+ printf("ERROR #11 (stage %u)\n", stage);
+ test_control_return(1);
+ }
+ }
+
+ if (error)
+ {
+
+ printf("ERROR #12\n");
+ test_control_return(1);
+ }
+ else
+ {
+
+ /* Successful test. */
+ printf("SUCCESS!\n");
+ test_control_return(0);
+ }
+}
+
+
+static void waiter_entry(ULONG thread_input)
+{
+
+UINT status;
+UINT stage;
+void *block_ptr;
+void *bytes_ptr;
+ULONG actual_flags;
+ULONG message;
+
+
+ for (stage = 0; stage < STAGE_COUNT; stage++)
+ {
+
+ switch (stage)
+ {
+
+ case STAGE_BLOCK_POOL:
+
+ /* The pool was emptied before any thread ran, so this suspends. */
+ status = tx_block_allocate(&block_pool_0, &block_ptr, TX_WAIT_FOREVER);
+ break;
+
+ case STAGE_BYTE_POOL:
+
+ /* More than is left after the allocation made at define time. */
+ status = tx_byte_allocate(&byte_pool_0, &bytes_ptr, 128, TX_WAIT_FOREVER);
+ break;
+
+ case STAGE_EVENT_FLAGS:
+
+ /* Nobody ever sets this flag. */
+ status = tx_event_flags_get(&group_0, ((ULONG) 0x1), TX_AND_CLEAR,
+ &actual_flags, TX_WAIT_FOREVER);
+ break;
+
+ default:
+
+ /* Nobody ever sends to this queue. */
+ status = tx_queue_receive(&queue_0, &message, TX_WAIT_FOREVER);
+ break;
+ }
+
+ if (status == TX_DELETED)
+ {
+
+ deleted_count[stage]++;
+ }
+ else
+ {
+
+ /* Anything else means the object was not deleted out from under
+ this thread, and the control thread will say so. */
+ error++;
+ return;
+ }
+ }
+}
diff --git a/test/smp/regression/threadx_queue_basic_one_word_test.c b/test/smp/regression/threadx_queue_basic_one_word_test.c
index 1abb6986..58142471 100644
--- a/test/smp/regression/threadx_queue_basic_one_word_test.c
+++ b/test/smp/regression/threadx_queue_basic_one_word_test.c
@@ -9,6 +9,8 @@
/* SPDX-License-Identifier: MIT */
/***************************************************************************/
+/* Portions of this file were generated with AI assistance. */
+
/* This test is designed to test immediate response queue services including create
and delete. This test is for queue sizes of 1 ULONG. Two queues are used one with
a capacity of 1 message and another with a capacity of 3 messages. */
@@ -51,7 +53,7 @@ static void thread_0_entry(ULONG thread_input);
static void thread_1_entry(ULONG thread_input);
-UINT _txe_queue_create(TX_QUEUE *queue_ptr, CHAR *name_ptr, UINT message_size,
+UINT _txe_queue_create(TX_QUEUE *queue_ptr, TX_NAME_CONST CHAR *name_ptr, UINT message_size,
VOID *queue_start, ULONG queue_size, UINT queue_control_block_size);
diff --git a/test/smp/regression/threadx_queue_information_test.c b/test/smp/regression/threadx_queue_information_test.c
index f884bd0d..8929c465 100644
--- a/test/smp/regression/threadx_queue_information_test.c
+++ b/test/smp/regression/threadx_queue_information_test.c
@@ -9,6 +9,8 @@
/* SPDX-License-Identifier: MIT */
/***************************************************************************/
+/* Portions of this file were generated with AI assistance. */
+
/* This test is designed to test the queue information services. */
#include <stdio.h>
@@ -102,7 +104,7 @@ UINT status;
ULONG source_message = 0x12345678UL;
ULONG dest_message;
ULONG expected_message;
-CHAR *name;
+TX_NAME_CONST CHAR *name;
ULONG enqueued;
ULONG available_storage;
TX_THREAD *first_suspended;
@@ -409,7 +411,7 @@ ULONG timeouts;
status += tx_queue_info_get(&queue_0, &name, &enqueued, &available_storage, &first_suspended, &suspended_count, &next_queue);
/* Check for errors. */
- if ((status != TX_SUCCESS) || (enqueued != queue_0.tx_queue_enqueued) || (available_storage != queue_0.tx_queue_available_storage) ||
+ if ((status != TX_SUCCESS) || (name != queue_0.tx_queue_name) || (enqueued != queue_0.tx_queue_enqueued) || (available_storage != queue_0.tx_queue_available_storage) ||
(first_suspended != queue_0.tx_queue_suspension_list) || (suspended_count != queue_0.tx_queue_suspended_count) ||
(next_queue != queue_0.tx_queue_created_next))
{
diff --git a/test/smp/regression/threadx_semaphore_basic_test.c b/test/smp/regression/threadx_semaphore_basic_test.c
index 3209b586..a1a3d340 100644
--- a/test/smp/regression/threadx_semaphore_basic_test.c
+++ b/test/smp/regression/threadx_semaphore_basic_test.c
@@ -9,6 +9,8 @@
/* SPDX-License-Identifier: MIT */
/***************************************************************************/
+/* Portions of this file were generated with AI assistance. */
+
/* This test is designed to test the semaphore create/delete and immediate return gets and puts. */
#include <stdio.h>
@@ -62,7 +64,7 @@ static TX_SEMAPHORE semaphore_3;
static void thread_0_entry(ULONG thread_input);
static void thread_1_entry(ULONG thread_input);
-UINT _txe_semaphore_create(TX_SEMAPHORE *semaphore_ptr, CHAR *name_ptr, ULONG initial_count, UINT semaphore_control_block_size);
+UINT _txe_semaphore_create(TX_SEMAPHORE *semaphore_ptr, TX_NAME_CONST CHAR *name_ptr, ULONG initial_count, UINT semaphore_control_block_size);
/* Prototype for test control return. */
diff --git a/test/smp/regression/threadx_semaphore_information_test.c b/test/smp/regression/threadx_semaphore_information_test.c
index ede276e9..c0bb3c72 100644
--- a/test/smp/regression/threadx_semaphore_information_test.c
+++ b/test/smp/regression/threadx_semaphore_information_test.c
@@ -9,6 +9,8 @@
/* SPDX-License-Identifier: MIT */
/***************************************************************************/
+/* Portions of this file were generated with AI assistance. */
+
/* This test is designed to test the semaphore information services. */
#include <stdio.h>
@@ -100,7 +102,7 @@ static void thread_0_entry(ULONG thread_input)
{
UINT status;
-CHAR *name;
+TX_NAME_CONST CHAR *name;
ULONG current_value;
TX_THREAD *first_suspended;
ULONG suspended_count;
@@ -211,7 +213,7 @@ ULONG timeouts;
status += tx_semaphore_info_get(&semaphore_0, &name, &current_value, &first_suspended, &suspended_count, &next_semaphore);
/* Check status. */
- if ((status != TX_SUCCESS) || (current_value != semaphore_0.tx_semaphore_count) ||
+ if ((status != TX_SUCCESS) || (name != semaphore_0.tx_semaphore_name) || (current_value != semaphore_0.tx_semaphore_count) ||
(first_suspended != semaphore_0.tx_semaphore_suspension_list) || (suspended_count != semaphore_0.tx_semaphore_suspended_count) ||
(next_semaphore != semaphore_0.tx_semaphore_created_next))
{
diff --git a/test/smp/regression/threadx_smp_random_resume_suspend_exclusion_pt_test.c b/test/smp/regression/threadx_smp_random_resume_suspend_exclusion_pt_test.c
index a9ea93ad..d5fe0436 100644
--- a/test/smp/regression/threadx_smp_random_resume_suspend_exclusion_pt_test.c
+++ b/test/smp/regression/threadx_smp_random_resume_suspend_exclusion_pt_test.c
@@ -10,7 +10,7 @@
/***************************************************************************/
/* Define the ThreadX SMP random resume/suspend/exclude/pt test. */
-// Some portions generated by Codex (gpt 5.4).
+// Portions of this file were generated with AI assistance.
#include <stdio.h>
#include "tx_api.h"
@@ -2312,12 +2312,14 @@ UINT status;
exclusions = (ULONG) (rand()%15);
tx_thread_smp_core_exclude(thread_ptr, exclusions);
- /* See if we should setup a preemption-threshold. */
- if ((thread_ptr -> tx_thread_priority > 40) &&
+ /* See if we should setup a preemption-threshold. The thresholds are
+ derived from TX_MAX_PRIORITIES so that the randomised phase sets one
+ whatever the port's priority count is. */
+ if ((thread_ptr -> tx_thread_priority > (TX_MAX_PRIORITIES/2)) &&
(exclusions & 1))
{
/* Change preemption-threshold to enable it. */
- tx_thread_preemption_change(thread_ptr, thread_ptr -> tx_thread_priority - 20, &original_threshold);
+ tx_thread_preemption_change(thread_ptr, thread_ptr -> tx_thread_priority - (TX_MAX_PRIORITIES/4), &original_threshold);
}
/* Save the thread pointer. */
diff --git a/test/smp/regression/threadx_smp_relinquish_rebalance_test.c b/test/smp/regression/threadx_smp_relinquish_rebalance_test.c
new file mode 100644
index 00000000..68a887f9
--- /dev/null
+++ b/test/smp/regression/threadx_smp_relinquish_rebalance_test.c
@@ -0,0 +1,250 @@
+/***************************************************************************
+ * 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
+ **************************************************************************/
+
+/* Define the ThreadX SMP relinquish rebalance test. This test fills every core with
+ threads of one priority that relinquish in a loop, and places two more threads of the
+ same priority behind them in the ready list: one that no core may run, and one that any
+ core may run. Walking the ready list, a relinquish reaches the unrunnable thread first
+ and cannot schedule it, so the execute list has to be rebalanced for the runnable thread
+ to reach a core. The runnable thread never running is the failure this test detects. */
+
+#include <stdio.h>
+#include "tx_api.h"
+
+
+/* Fill the four cores, so a thread behind these in the ready list only reaches a core
+ through the execute list being rebalanced. */
+
+#define SPIN_THREADS 4
+
+
+/* The spinning threads keep their cores whatever else happens, so they carry the deadline.
+ One second is three orders of magnitude more than the handful of relinquishes a healthy
+ system needs, and the tick advances independently of which threads are scheduled. */
+
+#define DEADLINE_TICKS 100
+
+
+static TX_THREAD thread_control;
+static TX_THREAD thread_spin[SPIN_THREADS];
+static TX_THREAD thread_blocked;
+static TX_THREAD thread_starved;
+
+static ULONG spin_counter[SPIN_THREADS];
+static ULONG starved_counter;
+static ULONG blocked_counter;
+static ULONG reported;
+
+
+/* Define thread prototypes. */
+
+static void thread_control_entry(ULONG thread_input);
+static void thread_spin_entry(ULONG thread_input);
+static void thread_blocked_entry(ULONG thread_input);
+static void thread_starved_entry(ULONG thread_input);
+
+
+/* Prototype for test control return. */
+
+void test_control_return(UINT status);
+
+
+/* Define what the initial system looks like. */
+
+#ifdef CTEST
+void test_application_define(void *first_unused_memory)
+#else
+void threadx_smp_relinquish_rebalance_test(void *first_unused_memory)
+#endif
+{
+
+UINT status;
+CHAR *pointer;
+UINT i;
+
+
+ /* Put first available memory address into a character pointer. */
+ pointer = (CHAR *) first_unused_memory;
+
+ /* Create the control thread, which only resumes the others and then completes. */
+ status = tx_thread_create(&thread_control, "control thread", thread_control_entry, 0,
+ pointer, TEST_STACK_SIZE_PRINTF,
+ 0, 0, TX_NO_TIME_SLICE, TX_AUTO_START);
+ pointer = pointer + TEST_STACK_SIZE_PRINTF;
+ status += tx_thread_smp_core_exclude(&thread_control, 0xE); /* Core 0 only! */
+
+ /* Check status. */
+ if (status != TX_SUCCESS)
+ {
+
+ printf("Running SMP Relinquish Rebalance Test............................... ERROR #1\n");
+ test_control_return(1);
+ }
+
+ /* Create one spinning thread per core. */
+ for (i = 0; i < SPIN_THREADS; i++)
+ {
+
+ status = tx_thread_create(&thread_spin[i], "spin thread", thread_spin_entry, i,
+ pointer, TEST_STACK_SIZE_PRINTF,
+ 20, 20, TX_NO_TIME_SLICE, TX_DONT_START);
+ pointer = pointer + TEST_STACK_SIZE_PRINTF;
+
+ /* Check status. */
+ if (status != TX_SUCCESS)
+ {
+
+ printf("Running SMP Relinquish Rebalance Test............................... ERROR #2\n");
+ test_control_return(1);
+ }
+ }
+
+ /* Create the thread no core may run. Excluding every core leaves it ready and
+ permanently unmapped, which is what makes it a stable obstacle in the ready list
+ rather than a thread that eventually gets a turn. */
+ status = tx_thread_create(&thread_blocked, "blocked thread", thread_blocked_entry, 0,
+ pointer, TEST_STACK_SIZE_PRINTF,
+ 20, 20, TX_NO_TIME_SLICE, TX_DONT_START);
+ pointer = pointer + TEST_STACK_SIZE_PRINTF;
+ status += tx_thread_smp_core_exclude(&thread_blocked, 0xF); /* No core at all! */
+
+ /* Check status. */
+ if (status != TX_SUCCESS)
+ {
+
+ printf("Running SMP Relinquish Rebalance Test............................... ERROR #3\n");
+ test_control_return(1);
+ }
+
+ /* Create the thread that any core may run. It is resumed last so that it sits behind
+ the blocked thread in the ready list at this priority. */
+ status = tx_thread_create(&thread_starved, "starved thread", thread_starved_entry, 0,
+ pointer, TEST_STACK_SIZE_PRINTF,
+ 20, 20, TX_NO_TIME_SLICE, TX_DONT_START);
+ pointer = pointer + TEST_STACK_SIZE_PRINTF;
+
+ /* Check status. */
+ if (status != TX_SUCCESS)
+ {
+
+ printf("Running SMP Relinquish Rebalance Test............................... ERROR #4\n");
+ test_control_return(1);
+ }
+}
+
+
+/* Resume the others in ready list order and complete, so core 0 is released. */
+
+static void thread_control_entry(ULONG thread_input)
+{
+
+UINT status;
+UINT i;
+
+
+ status = TX_SUCCESS;
+
+ /* Fill the cores first. */
+ for (i = 0; i < SPIN_THREADS; i++)
+ {
+
+ status += tx_thread_resume(&thread_spin[i]);
+ }
+
+ /* Then the obstacle, then the thread behind it. */
+ status += tx_thread_resume(&thread_blocked);
+ status += tx_thread_resume(&thread_starved);
+
+ /* Check status. */
+ if (status != TX_SUCCESS)
+ {
+
+ printf("Running SMP Relinquish Rebalance Test............................... ERROR #5\n");
+ test_control_return(1);
+ }
+}
+
+
+/* Hold a core and relinquish. These threads keep running whatever the scheduler does with
+ the rest, so they are the ones that can enforce a deadline on the starved thread. */
+
+static void thread_spin_entry(ULONG thread_input)
+{
+
+ULONG start;
+
+
+ start = tx_time_get();
+
+ while (starved_counter == 0)
+ {
+
+ spin_counter[thread_input]++;
+
+ /* The tick advances whether or not any thread is being scheduled, so this bounds
+ the test even when nothing is. */
+ if ((tx_time_get() - start) > DEADLINE_TICKS)
+ {
+
+ if (reported == 0)
+ {
+
+ reported = 1;
+ printf("Running SMP Relinquish Rebalance Test............................... ERROR #6\n");
+ test_control_return(1);
+ }
+
+ return;
+ }
+
+ tx_thread_relinquish();
+ }
+}
+
+
+/* Never runs: every core is excluded for this thread. */
+
+static void thread_blocked_entry(ULONG thread_input)
+{
+
+ blocked_counter++;
+}
+
+
+/* Reaching a core at all is the pass condition. */
+
+static void thread_starved_entry(ULONG thread_input)
+{
+
+ starved_counter++;
+
+ if (reported == 0)
+ {
+
+ reported = 1;
+
+ /* The blocked thread must not have run, or the obstacle was not an obstacle and
+ the test proved nothing. */
+ if (blocked_counter != 0)
+ {
+
+ printf("Running SMP Relinquish Rebalance Test............................... ERROR #7\n");
+ test_control_return(1);
+ }
+
+ printf("Running SMP Relinquish Rebalance Test............................... SUCCESS!\n");
+ test_control_return(0);
+ }
+}
diff --git a/test/smp/regression/threadx_smp_thread_exit_callback_remote_test.c b/test/smp/regression/threadx_smp_thread_exit_callback_remote_test.c
new file mode 100644
index 00000000..488c2822
--- /dev/null
+++ b/test/smp/regression/threadx_smp_thread_exit_callback_remote_test.c
@@ -0,0 +1,514 @@
+/***************************************************************************
+ * 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
+ **************************************************************************/
+
+/* Define the ThreadX SMP thread exit callback remote-core termination test.
+
+ This is the SMP-specific half of the exit-callback transition coverage. The
+ deterministic cases are in threadx_thread_exit_callback_transition_test,
+ which confines every thread it drives to core 0 precisely so that they
+ cannot be made flaky by concurrency; this test supplies the case that needs
+ it.
+
+ The victim is excluded from every core but core 1 and spins there without
+ relinquishing, so it is genuinely executing on another core when the
+ controller -- excluded to core 0 -- terminates it. _tx_thread_terminate
+ publishes TX_TERMINATED, releases the global SMP protection at TX_RESTORE,
+ and calls the victim's exit notification callback in the controller's
+ context on core 0. On an unfixed kernel a callback-side reset of the victim
+ succeeds at that point, and _tx_thread_reset both memsets the victim's stack
+ to the fill pattern and rebuilds its initial frame -- over the stack a live
+ core is running on.
+
+ What is measured:
+
+ - that the victim really was mapped to and executing on another core when
+ termination began, which is the precondition; if it cannot be
+ established within a bounded wait the test says so and drops only the
+ assertions that depend on it, rather than reporting a pass it did not
+ earn or spinning until something changes,
+ - that the callback-side delete and reset are refused, from the wrapper
+ and from the core service alike,
+ - that the victim's stack is untouched, checked with a sentinel written
+ into the unused low end of the stack, which a reset's fill would erase,
+ - that after the service returns the victim is in no core's execute slot
+ and on no ready list.
+
+ Every wait is bounded and every list walk is bounded, so neither a failing
+ kernel nor an unlucky schedule can turn this into a hang. */
+
+#include <stdio.h>
+#include "tx_api.h"
+#include "tx_thread.h"
+
+
+#define CONTROLLER_PRIORITY 10
+#define VICTIM_PRIORITY 20
+
+/* The core the victim is pinned to, and the exclusion maps that pin it there
+ and the controller to core 0. */
+
+#define VICTIM_CORE 1
+#define EXCLUDE_ALL_BUT_CORE_0 0xE
+#define EXCLUDE_ALL_BUT_CORE_1 0xD
+
+/* Bound on the wait for the victim to be mapped to its core. */
+
+#define REMOTE_WAIT_TICKS 200
+
+/* Sentinel written into the unused low end of the victim's stack. A reset
+ memsets the whole stack to the fill pattern before rebuilding the frame, so
+ the sentinel is gone if a reset ran. Its offset keeps it clear of both the
+ frame a downward-growing stack builds at the top and the stack-checking
+ pattern the kernel inspects at the very bottom. */
+
+#define SENTINEL_OFFSET 64
+#define SENTINEL_WORDS 8
+#define SENTINEL_VALUE 0xA5A5A5A5UL
+
+#define READY_RING_LIMIT 32
+
+#define PROBE_NOT_ATTEMPTED 0xFFFFFFFFUL
+
+#define FAILED_TAG_LIMIT 16
+
+
+static TX_THREAD controller;
+static TX_THREAD victim;
+
+static CHAR *victim_stack;
+
+static volatile ULONG victim_spins = 0;
+static volatile UINT victim_run = TX_FALSE;
+
+static UINT probe_state;
+static UINT probe_suspending;
+static UINT probe_core_mapped;
+static UINT probe_delete_status = PROBE_NOT_ATTEMPTED;
+static UINT probe_core_delete_status = PROBE_NOT_ATTEMPTED;
+static UINT probe_reset_status = PROBE_NOT_ATTEMPTED;
+static UINT probe_core_reset_status = PROBE_NOT_ATTEMPTED;
+static UINT probe_ran = TX_FALSE;
+static UINT remote_confirmed = TX_FALSE;
+static UINT sentinel_intact_after = TX_FALSE;
+
+static unsigned long expectations_checked = 0;
+static unsigned long expectations_failed = 0;
+static UINT first_failure = 0;
+static UINT failed_tags[FAILED_TAG_LIMIT];
+static UINT failed_tag_count = 0;
+
+
+/* Define thread prototypes. */
+
+static void controller_entry(ULONG thread_input);
+static void victim_entry(ULONG thread_input);
+
+
+/* Prototype for test control return. */
+
+void test_control_return(UINT status);
+
+
+static void expect(UINT condition, UINT tag)
+{
+
+ expectations_checked++;
+
+ if (condition != TX_TRUE)
+ {
+
+ expectations_failed++;
+
+ if (first_failure == 0)
+ {
+ first_failure = tag;
+ }
+
+ if (failed_tag_count < FAILED_TAG_LIMIT)
+ {
+ failed_tags[failed_tag_count] = tag;
+ failed_tag_count++;
+ }
+ }
+}
+
+
+static void expect_status(UINT actual, UINT wanted, UINT tag)
+{
+
+ if (actual == wanted)
+ {
+ expect(TX_TRUE, tag);
+ }
+ else
+ {
+ expect(TX_FALSE, tag);
+ }
+}
+
+
+static void write_sentinel(void)
+{
+
+ULONG *words;
+UINT index;
+
+
+ words = (ULONG *) (victim_stack + SENTINEL_OFFSET);
+
+ for (index = 0; index < SENTINEL_WORDS; index++)
+ {
+ words[index] = SENTINEL_VALUE;
+ }
+}
+
+
+static UINT sentinel_intact(void)
+{
+
+ULONG *words;
+UINT index;
+UINT intact;
+
+
+ words = (ULONG *) (victim_stack + SENTINEL_OFFSET);
+ intact = TX_TRUE;
+
+ for (index = 0; index < SENTINEL_WORDS; index++)
+ {
+ if (words[index] != SENTINEL_VALUE)
+ {
+ intact = TX_FALSE;
+ }
+ }
+
+ return(intact);
+}
+
+
+/* Report whether thread_ptr appears in any core's execute slot or on any ready
+ list. Both walks are bounded. */
+
+static UINT fully_detached(TX_THREAD *thread_ptr)
+{
+
+TX_INTERRUPT_SAVE_AREA
+
+TX_THREAD *walk_ptr;
+TX_THREAD *head_ptr;
+UINT priority;
+UINT steps;
+UINT core;
+UINT detached;
+
+
+ detached = TX_TRUE;
+
+ TX_DISABLE
+
+ for (core = 0; core < ((UINT) TX_THREAD_SMP_MAX_CORES); core++)
+ {
+ if (_tx_thread_execute_ptr[core] == thread_ptr)
+ {
+ detached = TX_FALSE;
+ }
+ }
+
+ for (priority = 0; priority < ((UINT) TX_MAX_PRIORITIES); priority++)
+ {
+
+ head_ptr = _tx_thread_priority_list[priority];
+ walk_ptr = head_ptr;
+ steps = 0;
+
+ while ((walk_ptr != TX_NULL) && (steps < READY_RING_LIMIT))
+ {
+
+ if (walk_ptr == thread_ptr)
+ {
+ detached = TX_FALSE;
+ }
+
+ walk_ptr = walk_ptr -> tx_thread_ready_next;
+ steps++;
+
+ if (walk_ptr == head_ptr)
+ {
+ walk_ptr = TX_NULL;
+ }
+ }
+ }
+
+ TX_RESTORE
+
+ return(detached);
+}
+
+
+static void entry_exit_notify(TX_THREAD *thread_ptr, UINT type)
+{
+
+ if (thread_ptr != &victim)
+ {
+ return;
+ }
+
+ if (type != TX_THREAD_EXIT)
+ {
+ return;
+ }
+
+ probe_ran = TX_TRUE;
+ probe_state = thread_ptr -> tx_thread_state;
+ probe_suspending = thread_ptr -> tx_thread_suspending;
+ probe_core_mapped = thread_ptr -> tx_thread_smp_core_mapped;
+
+ /* The delete first, then the reset. Each core service is called only if
+ its wrapper already refused, so that a kernel which allows the operation
+ is not asked to perform it twice. */
+
+ probe_delete_status = tx_thread_delete(thread_ptr);
+
+ if (probe_delete_status != TX_SUCCESS)
+ {
+
+ probe_core_delete_status = _tx_thread_delete(thread_ptr);
+
+ if (probe_core_delete_status != TX_SUCCESS)
+ {
+
+ probe_reset_status = tx_thread_reset(thread_ptr);
+
+ if (probe_reset_status != TX_SUCCESS)
+ {
+ probe_core_reset_status = _tx_thread_reset(thread_ptr);
+ }
+ }
+ }
+}
+
+
+/* Define what the initial system looks like. */
+
+#ifdef CTEST
+void test_application_define(void *first_unused_memory)
+#else
+void threadx_smp_thread_exit_callback_remote_application_define(void *first_unused_memory)
+#endif
+{
+
+UINT status;
+CHAR *pointer;
+
+
+ pointer = (CHAR *) first_unused_memory;
+
+ status = tx_thread_create(&controller, "controller", controller_entry, 0,
+ pointer, TEST_STACK_SIZE_PRINTF,
+ CONTROLLER_PRIORITY, CONTROLLER_PRIORITY,
+ TX_NO_TIME_SLICE, TX_AUTO_START);
+ pointer = pointer + TEST_STACK_SIZE_PRINTF;
+
+ victim_stack = pointer;
+ pointer = pointer + TEST_STACK_SIZE_PRINTF;
+
+ status += tx_thread_smp_core_exclude(&controller, EXCLUDE_ALL_BUT_CORE_0);
+
+ if (status != TX_SUCCESS)
+ {
+
+ printf("Running SMP Thread Exit Callback Remote Test............... ERROR #1\n");
+ test_control_return(1);
+ }
+}
+
+
+static void controller_entry(ULONG thread_input)
+{
+
+UINT status;
+UINT notify_status;
+UINT index;
+ULONG waited;
+ULONG spins_before;
+
+
+ (void) thread_input;
+
+ printf("Running SMP Thread Exit Callback Remote Test............... ");
+
+ victim_run = TX_TRUE;
+
+ status = tx_thread_create(&victim, "victim", victim_entry, 0,
+ victim_stack, TEST_STACK_SIZE_PRINTF,
+ VICTIM_PRIORITY, VICTIM_PRIORITY,
+ TX_NO_TIME_SLICE, TX_DONT_START);
+
+ status += tx_thread_smp_core_exclude(&victim, EXCLUDE_ALL_BUT_CORE_1);
+
+ notify_status = tx_thread_entry_exit_notify(&victim, entry_exit_notify);
+
+#ifndef TX_DISABLE_NOTIFY_CALLBACKS
+
+ if (notify_status != TX_SUCCESS)
+ {
+ status = notify_status;
+ }
+#else
+
+ if (notify_status != TX_FEATURE_NOT_ENABLED)
+ {
+ status = notify_status;
+ }
+#endif
+
+ expect_status(status, TX_SUCCESS, 10);
+
+ /* The sentinel goes in after the create, because the create fills the whole
+ stack with the pattern the stack-checking configurations inspect. */
+ write_sentinel();
+ expect(sentinel_intact(), 11);
+
+ if (expectations_failed == 0)
+ {
+ expect_status(tx_thread_resume(&victim), TX_SUCCESS, 12);
+ }
+
+ /* Wait, bounded, for the victim to be both mapped to its core and selected
+ to execute there, and for it to have actually spun. Those three
+ together are the precondition this test exists for. */
+
+ waited = 0;
+
+ while ((waited < REMOTE_WAIT_TICKS) && (remote_confirmed == TX_FALSE))
+ {
+
+ if ((_tx_thread_execute_ptr[VICTIM_CORE] == &victim) &&
+ (victim.tx_thread_smp_core_mapped == ((UINT) VICTIM_CORE)) &&
+ (victim_spins > 0))
+ {
+ remote_confirmed = TX_TRUE;
+ }
+ else
+ {
+ tx_thread_sleep(1);
+ waited++;
+ }
+ }
+
+ /* Confirm it is still spinning, so that the termination below really does
+ start against a running remote thread rather than a stalled one. */
+
+ spins_before = victim_spins;
+
+ if (remote_confirmed == TX_TRUE)
+ {
+ tx_thread_sleep(2);
+ expect(((victim_spins > spins_before) ? TX_TRUE : TX_FALSE), 13);
+ }
+ else
+ {
+
+ /* Say so rather than assert it. The refusal assertions below do not
+ depend on the victim being remote, so they still run; the ones that
+ do are dropped, and this line records that the environment did not
+ provide the precondition. */
+ printf("(remote precondition not established in %u ticks; remote-only checks skipped) ",
+ (UINT) REMOTE_WAIT_TICKS);
+ }
+
+ if (expectations_failed == 0)
+ {
+
+ expect_status(tx_thread_terminate(&victim), TX_SUCCESS, 20);
+
+#ifndef TX_DISABLE_NOTIFY_CALLBACKS
+
+ expect(probe_ran, 21);
+ expect_status(probe_state, TX_TERMINATED, 22);
+
+ /* The transition marker is set across the callback, which is what the
+ two refusals below rest on. */
+ expect_status(probe_suspending, TX_TRUE, 23);
+
+ expect_status(probe_delete_status, TX_DELETE_ERROR, 24);
+ expect_status(probe_core_delete_status, TX_DELETE_ERROR, 25);
+ expect_status(probe_reset_status, TX_NOT_DONE, 26);
+ expect_status(probe_core_reset_status, TX_NOT_DONE, 27);
+
+ if (remote_confirmed == TX_TRUE)
+ {
+
+ /* The callback saw the victim still mapped to the remote core. */
+ expect_status(probe_core_mapped, (UINT) VICTIM_CORE, 28);
+ }
+#endif
+
+ /* The reset was refused, so nothing memset the stack a live core was
+ running on. */
+ sentinel_intact_after = sentinel_intact();
+ expect_status(sentinel_intact_after, TX_TRUE, 29);
+
+ /* And the termination completed: the marker is clear, and the victim is
+ in no core's execute slot and on no ready list. */
+ expect_status(victim.tx_thread_state, TX_TERMINATED, 30);
+ expect_status(victim.tx_thread_suspending, TX_FALSE, 31);
+ expect(fully_detached(&victim), 32);
+ }
+
+ /* The victim's loop exits on this, for the case where it is still between
+ the terminate and its core acting on the preemption. */
+ victim_run = TX_FALSE;
+
+ if (expectations_failed == 0)
+ {
+ expect_status(tx_thread_delete(&victim), TX_SUCCESS, 40);
+ }
+
+ if (expectations_failed != 0)
+ {
+
+ printf("ERROR #%u (%lu of %lu expectations failed:",
+ first_failure, expectations_failed, expectations_checked);
+
+ for (index = 0; index < failed_tag_count; index++)
+ {
+ printf(" %u", failed_tags[index]);
+ }
+
+ printf(")\n");
+ fflush(stdout);
+ test_control_return(1);
+ }
+ else
+ {
+ printf("SUCCESS!\n");
+ test_control_return(0);
+ }
+}
+
+
+static void victim_entry(ULONG thread_input)
+{
+
+ (void) thread_input;
+
+ /* Spin without relinquishing, so this thread occupies its core rather than
+ yielding it. */
+ while (victim_run == TX_TRUE)
+ {
+ victim_spins++;
+ }
+}
diff --git a/test/smp/regression/threadx_smp_time_slice_test.c b/test/smp/regression/threadx_smp_time_slice_test.c
index 370f5e37..c5ab0615 100644
--- a/test/smp/regression/threadx_smp_time_slice_test.c
+++ b/test/smp/regression/threadx_smp_time_slice_test.c
@@ -11,7 +11,7 @@
/* Define the ThreadX SMP time-slice test. */
-// Some portions generated by Codex (gpt 5.4).
+// Portions of this file were generated with AI assistance.
#include <stdio.h>
#include "tx_api.h"
diff --git a/test/smp/regression/threadx_thread_basic_execution_test.c b/test/smp/regression/threadx_thread_basic_execution_test.c
index d90d0769..b0acc842 100644
--- a/test/smp/regression/threadx_thread_basic_execution_test.c
+++ b/test/smp/regression/threadx_thread_basic_execution_test.c
@@ -9,6 +9,8 @@
/* SPDX-License-Identifier: MIT */
/***************************************************************************/
+/* Portions of this file were generated with AI assistance. */
+
/* This test is designed to see if one thread can be created and executed.
It thread_0_entry is hit, then the thread was successfully scheduled.
On success, thread_0_counter gets incremented. */
@@ -69,7 +71,7 @@ static unsigned long isr_executed = 0;
static void thread_0_entry(ULONG task_input);
-UINT _txe_thread_create(TX_THREAD *thread_ptr, CHAR *name_ptr,
+UINT _txe_thread_create(TX_THREAD *thread_ptr, TX_NAME_CONST CHAR *name_ptr,
VOID (*entry_function)(ULONG), ULONG entry_input,
VOID *stack_start, ULONG stack_size,
UINT priority, UINT preempt_threshold,
diff --git a/test/smp/regression/threadx_thread_delayed_suspension_test.c b/test/smp/regression/threadx_thread_delayed_suspension_test.c
index fe0a82f8..17a89d03 100644
--- a/test/smp/regression/threadx_thread_delayed_suspension_test.c
+++ b/test/smp/regression/threadx_thread_delayed_suspension_test.c
@@ -12,6 +12,7 @@
/* This test checks out the delayed suspension clear from tx_thread_resume. */
#include <stdio.h>
+#include <time.h>
#include "tx_api.h"
#include "tx_thread.h"
#include "tx_timer.h"
@@ -235,6 +236,12 @@ static void thread_0_entry(ULONG thread_input)
UINT status;
+#ifndef TX_NOT_INTERRUPTABLE
+#define DELAYED_SUSPENSION_SECOND_BUDGET ((ULONG) 120)
+
+time_t start_wall;
+#endif
+
/* Inform user. */
printf("Running Thread Delayed Suspension Clearing Test..................... ");
@@ -277,8 +284,23 @@ UINT status;
/* Resume the test thread. */
tx_thread_resume(&thread_2);
- /* Wait until we see the delayed suspension set flag. */
- while(delayed_suspend_set == 0)
+ /* Wait until we see the delayed suspension set flag.
+
+ The flag is set by the interrupt above, and only when that interrupt lands
+ while thread 2 is part way through suspending. That is a narrow window,
+ and waiting for it without a bound leaves the test no way out when it is
+ not reached. How narrow it is depends on the build: in the ThreadX suite
+ the same loop finishes in between a tenth of a second and three seconds in
+ four configurations, and took 490 seconds in trace_build, where it was the
+ most expensive thing in the suite by a wide margin.
+
+ The budget is in wall clock seconds rather than ticks. A tick arrives only
+ when the port's timer thread runs, so the tick clock falls behind real
+ time under load or instrumentation, which is exactly the configuration
+ that needs bounding here. */
+ start_wall = time(TX_NULL);
+ while ((delayed_suspend_set == 0) &&
+ (((ULONG) (time(TX_NULL) - start_wall)) <= DELAYED_SUSPENSION_SECOND_BUDGET))
{
/* Abort the suspension for thread 2. */
tx_thread_wait_abort(&thread_2);
@@ -291,7 +313,18 @@ UINT status;
tx_thread_relinquish();
/* At this point, check for an error. */
- if (thread_2_counter != thread_2_counter_capture)
+ if (delayed_suspend_set == 0)
+ {
+
+ /* The window was never reached, so thread_2_counter_capture was never
+ taken and the comparison below would be against a value that means
+ nothing. Not reaching the window is a gap in what this run covered
+ rather than a fault in the code under test, so say so and skip the
+ check that depends on it. */
+ printf("(delayed suspension window not reached in %lu seconds) ",
+ (ULONG) (time(TX_NULL) - start_wall));
+ }
+ else if (thread_2_counter != thread_2_counter_capture)
{
/* Delayed suspension error... thread kept running! */
diff --git a/test/smp/regression/threadx_thread_exit_callback_transition_test.c b/test/smp/regression/threadx_thread_exit_callback_transition_test.c
new file mode 100644
index 00000000..c931085c
--- /dev/null
+++ b/test/smp/regression/threadx_thread_exit_callback_transition_test.c
@@ -0,0 +1,1375 @@
+/***************************************************************************
+ * 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
+ **************************************************************************/
+
+/* This test covers the completion and termination transitions that a thread's
+ TX_THREAD_EXIT notification callback runs inside.
+
+ _tx_thread_shell_entry and _tx_thread_terminate both publish the terminal
+ state -- TX_COMPLETED or TX_TERMINATED -- and then call the thread's exit
+ notification callback, before the thread has been detached from the ready
+ list and before either service has finished with the pointer it holds to
+ the control block. That terminal state is the same state _tx_thread_delete
+ and _tx_thread_reset accept as authorization to invalidate or rebuild the
+ control block, so a callback used to be able to delete the object -- and
+ then lawfully recreate it over the same memory -- while the scheduler was
+ still linked to the old incarnation, or to reset a terminated thread and
+ resume it back onto the ready list after termination had reported success.
+
+ The tx_thread_suspending flag marks the transition for exactly its
+ duration, and delete and reset now refuse a target whose flag is still set.
+ The refusal is transient: TX_DELETE_ERROR and TX_NOT_DONE are errors those
+ two services already document, and the same call succeeds once the
+ transition has finished.
+
+ What the test measures, in every scenario:
+
+ 1. what the callback observes -- the published state, the transition
+ flag, and the status of each lifecycle service it can reach,
+ 2. that the scheduler's ready lists, priority map and execute pointer
+ stay consistent with the control blocks they point at, taken as one
+ snapshot under interrupt lockout so that the reading is not itself a
+ race,
+ 3. that the documented behaviour still works afterwards: the same delete
+ and reset succeed once the transition has completed, so nothing has
+ become permanently undeletable.
+
+ The consequences are demonstrated rather than described, and deliberately
+ from the terminator's context rather than the completing thread's:
+
+ - scenario A has the callback recreate the control block if its delete
+ unexpectedly succeeded, at a lower priority, so that the outcome is an
+ assertable disagreement -- the old priority list still heading at a
+ control block whose own priority field names a different list -- rather
+ than a fault;
+ - scenario B has the callback reset and then resume the target, so that a
+ kernel which allows the reset is caught reporting a successful
+ termination for a thread that is runnable again.
+
+ Both run while the victim is a ready thread that is not the current thread.
+ That ordering is not cosmetic. On the Linux, Win32 and Win64 host
+ simulation ports TX_THREAD_DELETE_PORT_COMPLETION cancels and joins the
+ host thread backing the deleted thread, so a callback-side delete of the
+ *completing* thread destroys the host thread the callback is running on and
+ wedges the simulator instead of reaching an assertion. The completion
+ scenarios therefore come last, and the scenario gate below stops the run
+ before them if anything earlier has already failed.
+
+ Every list walk is bounded, so a corrupted ring costs an assertion and not
+ a hang. Expectations are counted rather than short-circuited within a
+ scenario, so a failing kernel reports how much it failed by; progression
+ between scenarios is gated on there being no failures yet, so a kernel that
+ has already corrupted the scheduler is not driven further into it. */
+
+#include <stdio.h>
+#include "tx_api.h"
+#include "tx_thread.h"
+
+
+/* Priorities. The controller sits above every thread it drives. The victim
+ shares its priority with two peers, so that both the multi-member and the
+ single-member ready-ring unlink paths are reached. RECREATE_PRIORITY is
+ below VICTIM_PRIORITY on purpose: see the note in the callback. */
+
+#define CONTROLLER_PRIORITY 10
+#define VICTIM_PRIORITY 20
+#define RECREATE_PRIORITY 21
+
+/* Upper bound on a ready-ring walk. The suite never has this many threads at
+ one priority, so exceeding it means the ring is broken rather than long. */
+
+#define READY_RING_LIMIT 32
+
+/* Sentinel for a probe slot the callback did not reach. */
+
+#define PROBE_NOT_ATTEMPTED 0xFFFFFFFFUL
+
+
+static unsigned long controller_counter = 0;
+static unsigned long victim_counter = 0;
+static unsigned long peer_a_counter = 0;
+static unsigned long peer_b_counter = 0;
+static unsigned long replacement_counter = 0;
+static unsigned long callback_entry_counter = 0;
+static unsigned long callback_exit_counter = 0;
+
+static TX_THREAD controller;
+static TX_THREAD victim;
+static TX_THREAD peer_a;
+static TX_THREAD peer_b;
+static TX_SEMAPHORE victim_semaphore;
+static TX_MUTEX victim_mutex;
+
+static CHAR *victim_stack;
+static CHAR *replacement_stack;
+
+/* What the victim's entry function should do before it returns. */
+
+static volatile UINT victim_hold = TX_FALSE;
+static volatile UINT victim_self_terminate = TX_FALSE;
+static volatile UINT victim_take_mutex = TX_FALSE;
+static volatile UINT victim_wait_semaphore = TX_FALSE;
+static volatile UINT peers_run = TX_FALSE;
+
+/* What the exit callback should attempt the next time it runs. Independent
+ flags rather than one mode, because a delete that unexpectedly succeeds
+ destroys the object and would mask the reset measurement behind it. */
+
+static volatile UINT probe_delete_enabled = TX_FALSE;
+static volatile UINT probe_reset_enabled = TX_FALSE;
+static volatile UINT probe_misc_enabled = TX_FALSE;
+static volatile UINT probe_reuse_enabled = TX_FALSE;
+
+/* What the callback saw the last time it probed. */
+
+static UINT probe_state;
+static UINT probe_suspending;
+static UINT probe_delete_status;
+static UINT probe_core_delete_status;
+static UINT probe_reset_status;
+static UINT probe_core_reset_status;
+static UINT probe_resume_status;
+static UINT probe_suspend_status;
+static UINT probe_terminate_status;
+static UINT probe_create_status;
+static UINT probe_consistent;
+static UINT probe_ran;
+static UINT delete_succeeded;
+
+/* Ordering witness for the notification pair. Entry and exit counts cannot
+ simply be compared: a victim created with TX_DONT_START never reports an
+ entry, and an incarnation that is reset and terminated again reports a
+ second exit with no second entry. What must hold is that an exit for a
+ thread that ran is preceded by that thread's entry. */
+
+static UINT last_notify_type = PROBE_NOT_ATTEMPTED;
+static UINT exit_saw_entry_first = TX_FALSE;
+
+/* Expectation accounting. */
+
+#define FAILED_TAG_LIMIT 32
+
+static unsigned long expectations_checked = 0;
+static unsigned long expectations_failed = 0;
+static UINT first_failure = 0;
+static UINT failed_tags[FAILED_TAG_LIMIT];
+static UINT failed_tag_count = 0;
+
+
+/* Define task prototypes. */
+
+static void controller_entry(ULONG thread_input);
+static void victim_entry(ULONG thread_input);
+static void peer_entry(ULONG thread_input);
+static void replacement_entry(ULONG thread_input);
+
+
+/* Prototype for test control return. */
+
+void test_control_return(UINT status);
+
+
+/* Record one expectation. tag identifies it in the failure report; the first
+ failing tag is the number the suite prints, so tags are allocated in the
+ order the expectations are written. */
+
+static void expect(UINT condition, UINT tag)
+{
+
+ expectations_checked++;
+
+ if (condition != TX_TRUE)
+ {
+
+ expectations_failed++;
+
+ if (first_failure == 0)
+ {
+ first_failure = tag;
+ }
+
+ /* Keep the tags, so that a failing kernel reports which expectations
+ it failed rather than only the first and a total. */
+ if (failed_tag_count < FAILED_TAG_LIMIT)
+ {
+ failed_tags[failed_tag_count] = tag;
+ failed_tag_count++;
+ }
+ }
+}
+
+
+static void expect_status(UINT actual, UINT wanted, UINT tag)
+{
+
+ if (actual == wanted)
+ {
+ expect(TX_TRUE, tag);
+ }
+ else
+ {
+ expect(TX_FALSE, tag);
+ }
+}
+
+
+static void expect_pointer(void *actual, void *wanted, UINT tag)
+{
+
+ if (actual == wanted)
+ {
+ expect(TX_TRUE, tag);
+ }
+ else
+ {
+ expect(TX_FALSE, tag);
+ }
+}
+
+
+static void expect_count(unsigned long actual, unsigned long wanted, UINT tag)
+{
+
+ if (actual == wanted)
+ {
+ expect(TX_TRUE, tag);
+ }
+ else
+ {
+ expect(TX_FALSE, tag);
+ }
+}
+
+
+/* Walk one ready ring and report whether thread_ptr is a member of it. The
+ walk is bounded, so a ring that does not close is reported as a
+ non-membership rather than spun on for ever. */
+
+static UINT ready_ring_contains(UINT priority, TX_THREAD *thread_ptr)
+{
+
+TX_THREAD *walk_ptr;
+TX_THREAD *head_ptr;
+UINT steps;
+UINT found;
+
+
+ found = TX_FALSE;
+ head_ptr = _tx_thread_priority_list[priority];
+ walk_ptr = head_ptr;
+ steps = 0;
+
+ while ((walk_ptr != TX_NULL) && (steps < READY_RING_LIMIT) && (found == TX_FALSE))
+ {
+
+ if (walk_ptr == thread_ptr)
+ {
+ found = TX_TRUE;
+ }
+
+ walk_ptr = walk_ptr -> tx_thread_ready_next;
+ steps++;
+
+ /* The ring has closed. */
+ if (walk_ptr == head_ptr)
+ {
+ walk_ptr = TX_NULL;
+ }
+ }
+
+ return(found);
+}
+
+
+/* Take one snapshot of the scheduler's ready structures and check that they
+ agree with the control blocks they point at.
+
+ This is the assertion the pre-fix behaviour fails. A callback that deletes
+ the victim and recreates it at another priority leaves the old priority list
+ heading at that control block while the block's own priority field names the
+ new list, and leaves the old priority's map bit set with nothing behind it.
+ Neither disagreement needs a fault to be visible.
+
+ The whole snapshot is taken under interrupt lockout -- which is the global
+ SMP protection on an SMP port -- so that reading the scheduler's state is
+ not itself a race against another core.
+
+ transitioning names the one thread that is allowed to be on a ready list
+ without being ready, which is precisely the thread whose completion or
+ termination transition is in progress: the terminal state is published
+ before the detachment, so inside the callback the victim is legitimately a
+ TX_COMPLETED or TX_TERMINATED member of its ready ring. That single
+ exemption is from the state clause only. Every other clause, and in
+ particular the clause that requires a ready-list member to agree that this
+ is its priority -- the one a callback-side delete and recreate breaks --
+ still applies to it. */
+
+static UINT scheduler_consistent_except(TX_THREAD *transitioning)
+{
+
+TX_INTERRUPT_SAVE_AREA
+
+TX_THREAD *head_ptr;
+TX_THREAD *walk_ptr;
+TX_THREAD *execute_ptr;
+UINT priority;
+UINT steps;
+UINT consistent;
+#ifdef TX_THREAD_SMP_MAX_CORES
+UINT core;
+#endif
+
+
+ consistent = TX_TRUE;
+
+ TX_DISABLE
+
+ for (priority = 0; priority < ((UINT) TX_MAX_PRIORITIES); priority++)
+ {
+
+ head_ptr = _tx_thread_priority_list[priority];
+
+#if TX_MAX_PRIORITIES <= 32
+
+ /* A priority with a ready list must have its map bit set, and a
+ priority with no ready list must not. */
+ if (head_ptr == TX_NULL)
+ {
+ if ((_tx_thread_priority_maps[0] & (((ULONG) 1) << priority)) != ((ULONG) 0))
+ {
+ consistent = TX_FALSE;
+ }
+ }
+ else
+ {
+ if ((_tx_thread_priority_maps[0] & (((ULONG) 1) << priority)) == ((ULONG) 0))
+ {
+ consistent = TX_FALSE;
+ }
+ }
+#endif
+
+ walk_ptr = head_ptr;
+ steps = 0;
+
+ while ((walk_ptr != TX_NULL) && (steps < READY_RING_LIMIT))
+ {
+
+ /* Every thread on a ready list must be a live thread, must be
+ ready, and must agree that this is its priority. */
+ if (walk_ptr -> tx_thread_id != TX_THREAD_ID)
+ {
+ consistent = TX_FALSE;
+ }
+
+ if ((walk_ptr -> tx_thread_state != TX_READY) && (walk_ptr != transitioning))
+ {
+ consistent = TX_FALSE;
+ }
+
+ if (walk_ptr -> tx_thread_priority != priority)
+ {
+ consistent = TX_FALSE;
+ }
+
+ /* The ring must be doubly linked. */
+ if (walk_ptr -> tx_thread_ready_next -> tx_thread_ready_previous != walk_ptr)
+ {
+ consistent = TX_FALSE;
+ }
+
+ walk_ptr = walk_ptr -> tx_thread_ready_next;
+ steps++;
+
+ if (walk_ptr == head_ptr)
+ {
+ walk_ptr = TX_NULL;
+ }
+ }
+
+ /* A ring that did not close inside the bound is broken. */
+ if (walk_ptr != TX_NULL)
+ {
+ consistent = TX_FALSE;
+ }
+ }
+
+ /* Whatever the scheduler has selected to execute must be a member of the
+ ready list that its own priority field names. */
+#ifdef TX_THREAD_SMP_MAX_CORES
+
+ for (core = 0; core < ((UINT) TX_THREAD_SMP_MAX_CORES); core++)
+ {
+
+ execute_ptr = _tx_thread_execute_ptr[core];
+
+ if (execute_ptr != TX_NULL)
+ {
+ if (ready_ring_contains(execute_ptr -> tx_thread_priority, execute_ptr) == TX_FALSE)
+ {
+ consistent = TX_FALSE;
+ }
+ }
+ }
+#else
+
+ execute_ptr = _tx_thread_execute_ptr;
+
+ if (execute_ptr != TX_NULL)
+ {
+ if (ready_ring_contains(execute_ptr -> tx_thread_priority, execute_ptr) == TX_FALSE)
+ {
+ consistent = TX_FALSE;
+ }
+ }
+#endif
+
+ TX_RESTORE
+
+ return(consistent);
+}
+
+
+static UINT scheduler_consistent(void)
+{
+ return(scheduler_consistent_except(TX_NULL));
+}
+
+
+/* Report whether thread_ptr is absent from every ready list and from every
+ execute slot, which is what a completed or terminated thread must be once
+ its transition has finished. */
+
+static UINT fully_detached(TX_THREAD *thread_ptr)
+{
+
+TX_INTERRUPT_SAVE_AREA
+
+UINT priority;
+UINT detached;
+#ifdef TX_THREAD_SMP_MAX_CORES
+UINT core;
+#endif
+
+
+ detached = TX_TRUE;
+
+ TX_DISABLE
+
+ for (priority = 0; priority < ((UINT) TX_MAX_PRIORITIES); priority++)
+ {
+ if (ready_ring_contains(priority, thread_ptr) == TX_TRUE)
+ {
+ detached = TX_FALSE;
+ }
+ }
+
+#ifdef TX_THREAD_SMP_MAX_CORES
+
+ for (core = 0; core < ((UINT) TX_THREAD_SMP_MAX_CORES); core++)
+ {
+ if (_tx_thread_execute_ptr[core] == thread_ptr)
+ {
+ detached = TX_FALSE;
+ }
+ }
+#else
+
+ if (_tx_thread_execute_ptr == thread_ptr)
+ {
+ detached = TX_FALSE;
+ }
+#endif
+
+ TX_RESTORE
+
+ return(detached);
+}
+
+
+static void arm_probe(UINT probe_delete, UINT probe_reset, UINT probe_misc, UINT probe_reuse)
+{
+
+ probe_state = PROBE_NOT_ATTEMPTED;
+ probe_suspending = PROBE_NOT_ATTEMPTED;
+ probe_delete_status = PROBE_NOT_ATTEMPTED;
+ probe_core_delete_status = PROBE_NOT_ATTEMPTED;
+ probe_reset_status = PROBE_NOT_ATTEMPTED;
+ probe_core_reset_status = PROBE_NOT_ATTEMPTED;
+ probe_resume_status = PROBE_NOT_ATTEMPTED;
+ probe_suspend_status = PROBE_NOT_ATTEMPTED;
+ probe_terminate_status = PROBE_NOT_ATTEMPTED;
+ probe_create_status = PROBE_NOT_ATTEMPTED;
+ probe_consistent = PROBE_NOT_ATTEMPTED;
+ probe_ran = TX_FALSE;
+ delete_succeeded = TX_FALSE;
+
+ probe_delete_enabled = probe_delete;
+ probe_reset_enabled = probe_reset;
+ probe_misc_enabled = probe_misc;
+ probe_reuse_enabled = probe_reuse;
+}
+
+
+/* The exit notification callback. Everything it learns is recorded for the
+ controller to check; it prints nothing and it never suspends, so it obeys
+ the one restriction the manual places on a notification callback. */
+
+static void entry_exit_notify(TX_THREAD *thread_ptr, UINT type)
+{
+
+ /* Only the victim's notifications are of interest. Its control block is
+ reused across scenarios, so the pointer is the identity. */
+ if (thread_ptr != &victim)
+ {
+ return;
+ }
+
+ if (type == TX_THREAD_ENTRY)
+ {
+ callback_entry_counter++;
+ last_notify_type = TX_THREAD_ENTRY;
+ return;
+ }
+
+ if (type != TX_THREAD_EXIT)
+ {
+ return;
+ }
+
+ callback_exit_counter++;
+
+ if (last_notify_type == TX_THREAD_ENTRY)
+ {
+ exit_saw_entry_first = TX_TRUE;
+ }
+
+ last_notify_type = TX_THREAD_EXIT;
+
+ if ((probe_delete_enabled == TX_FALSE) && (probe_reset_enabled == TX_FALSE) &&
+ (probe_misc_enabled == TX_FALSE))
+ {
+ return;
+ }
+
+ probe_ran = TX_TRUE;
+
+ /* What the kernel has published about the thread at this point. */
+ probe_state = thread_ptr -> tx_thread_state;
+ probe_suspending = thread_ptr -> tx_thread_suspending;
+
+ /* None of the services below has a suspension option, which is the only
+ thing the manual forbids a callback from using, so every one of them is
+ reachable from here.
+
+ Where a service is expected to refuse, the core service is called after
+ the _txe_ wrapper as well. The wrapper is what an ordinary build
+ reaches; the core service is what a TX_DISABLE_ERROR_CHECKING build
+ reaches directly, and the refusal has to live there so that disabling
+ error checking cannot disable it. The core call is made only when the
+ wrapper already refused, so that a kernel which allows the operation is
+ not asked to perform it twice. */
+
+ if (probe_delete_enabled == TX_TRUE)
+ {
+
+ probe_delete_status = tx_thread_delete(thread_ptr);
+
+ if (probe_delete_status == TX_SUCCESS)
+ {
+ delete_succeeded = TX_TRUE;
+ }
+ else
+ {
+
+ probe_core_delete_status = _tx_thread_delete(thread_ptr);
+
+ if (probe_core_delete_status == TX_SUCCESS)
+ {
+ delete_succeeded = TX_TRUE;
+ }
+ }
+
+ /* Recreate the control block only if the delete unexpectedly
+ succeeded. On a fixed kernel this never runs. On an unfixed one it
+ is the lawful next step after a successful delete, and it is what
+ turns the window into an observable inconsistency: tx_thread_create
+ zeroes the whole control block, and the auto-start then links it
+ onto RECREATE_PRIORITY's ready list while the old incarnation is
+ still the head of VICTIM_PRIORITY's.
+
+ RECREATE_PRIORITY is the lower of the two so that the stale head is
+ the one the scheduler selects from, and so that the ring at the new
+ priority has a single member. Recreating at the same priority would
+ instead have the auto-start write through the control block's
+ freshly zeroed ready_previous pointer, which is a fault rather than
+ an assertion. */
+ if ((delete_succeeded == TX_TRUE) && (probe_reuse_enabled == TX_TRUE))
+ {
+ probe_create_status = tx_thread_create(thread_ptr, "replacement",
+ replacement_entry, 0,
+ replacement_stack,
+ TEST_STACK_SIZE_PRINTF,
+ RECREATE_PRIORITY, RECREATE_PRIORITY,
+ TX_NO_TIME_SLICE, TX_AUTO_START);
+ }
+ }
+
+ if ((probe_reset_enabled == TX_TRUE) && (delete_succeeded == TX_FALSE))
+ {
+
+ probe_reset_status = tx_thread_reset(thread_ptr);
+
+ if (probe_reset_status != TX_SUCCESS)
+ {
+ probe_core_reset_status = _tx_thread_reset(thread_ptr);
+ }
+
+ /* Follow the reset with the resume that the reset would have unlocked.
+ On a fixed kernel the state is still terminal and the resume is
+ refused. On an unfixed one the reset has moved the state to
+ TX_SUSPENDED, the resume finds a suspending thread whose state is no
+ longer terminal, clears the flag and puts the thread back on the
+ ready list -- and the outer service then skips the removal and
+ reports success for a termination that did not happen. */
+ probe_resume_status = tx_thread_resume(thread_ptr);
+ }
+
+ if ((probe_misc_enabled == TX_TRUE) && (delete_succeeded == TX_FALSE))
+ {
+ probe_suspend_status = tx_thread_suspend(thread_ptr);
+ probe_terminate_status = tx_thread_terminate(thread_ptr);
+ }
+
+ probe_consistent = scheduler_consistent_except(thread_ptr);
+}
+
+
+/* Define what the initial system looks like. */
+
+#ifdef CTEST
+void test_application_define(void *first_unused_memory)
+#else
+void threadx_thread_exit_callback_transition_application_define(void *first_unused_memory)
+#endif
+{
+
+UINT status;
+CHAR *pointer;
+
+
+ /* Put first available memory address into a character pointer. */
+ pointer = (CHAR *) first_unused_memory;
+
+ status = tx_thread_create(&controller, "controller", controller_entry, 0,
+ pointer, TEST_STACK_SIZE_PRINTF,
+ CONTROLLER_PRIORITY, CONTROLLER_PRIORITY,
+ TX_NO_TIME_SLICE, TX_AUTO_START);
+ pointer = pointer + TEST_STACK_SIZE_PRINTF;
+
+ status += tx_thread_create(&peer_a, "peer a", peer_entry, 1,
+ pointer, TEST_STACK_SIZE_PRINTF,
+ VICTIM_PRIORITY, VICTIM_PRIORITY,
+ TX_NO_TIME_SLICE, TX_DONT_START);
+ pointer = pointer + TEST_STACK_SIZE_PRINTF;
+
+ status += tx_thread_create(&peer_b, "peer b", peer_entry, 2,
+ pointer, TEST_STACK_SIZE_PRINTF,
+ VICTIM_PRIORITY, VICTIM_PRIORITY,
+ TX_NO_TIME_SLICE, TX_DONT_START);
+ pointer = pointer + TEST_STACK_SIZE_PRINTF;
+
+ /* The victim's control block is created and destroyed repeatedly, so its
+ two stacks are set aside here rather than per scenario. */
+ victim_stack = pointer;
+ pointer = pointer + TEST_STACK_SIZE_PRINTF;
+ replacement_stack = pointer;
+ pointer = pointer + TEST_STACK_SIZE_PRINTF;
+
+ status += tx_semaphore_create(&victim_semaphore, "victim semaphore", 0);
+ status += tx_mutex_create(&victim_mutex, "victim mutex", TX_INHERIT);
+
+ if (status != TX_SUCCESS)
+ {
+
+ printf("Running Thread Exit Callback Transition Test............... ERROR #1\n");
+ test_control_return(1);
+ }
+
+#ifdef TX_THREAD_SMP_MAX_CORES
+
+ /* Confine every thread this test drives to core 0. The measurements below
+ are of global scheduler structures at chosen moments, and letting the
+ peers and the victim run on other cores would make them a race against
+ the test rather than a property of it. A victim executing on a
+ different core when termination begins is deliberately covered by a
+ separate SMP test, so that the deterministic cases here cannot be made
+ flaky by it. */
+ status = tx_thread_smp_core_exclude(&controller, 0xE);
+ status += tx_thread_smp_core_exclude(&peer_a, 0xE);
+ status += tx_thread_smp_core_exclude(&peer_b, 0xE);
+
+ if (status != TX_SUCCESS)
+ {
+
+ printf("Running Thread Exit Callback Transition Test............... ERROR #2\n");
+ test_control_return(1);
+ }
+#endif
+}
+
+
+/* Create the victim over its own control block, with the exit callback
+ registered. The notify status is folded into the result according to what
+ the configuration supports. */
+
+static UINT create_victim(UINT priority, UINT auto_start)
+{
+
+UINT status;
+UINT notify_status;
+
+
+ status = tx_thread_create(&victim, "victim", victim_entry, 0,
+ victim_stack, TEST_STACK_SIZE_PRINTF,
+ priority, priority, TX_NO_TIME_SLICE, auto_start);
+
+ notify_status = tx_thread_entry_exit_notify(&victim, entry_exit_notify);
+
+#ifndef TX_DISABLE_NOTIFY_CALLBACKS
+
+ if (notify_status != TX_SUCCESS)
+ {
+ status = notify_status;
+ }
+#else
+
+ /* Notification is compiled out, so the registration is expected to be
+ refused and the callback never runs. */
+ if (notify_status != TX_FEATURE_NOT_ENABLED)
+ {
+ status = notify_status;
+ }
+#endif
+
+#ifdef TX_THREAD_SMP_MAX_CORES
+
+ if (status == TX_SUCCESS)
+ {
+ status = tx_thread_smp_core_exclude(&victim, 0xE);
+ }
+#endif
+
+ return(status);
+}
+
+
+/* The gate between scenarios. A kernel that has already failed an expectation
+ has, in these scenarios, already left the scheduler inconsistent, and
+ driving it further would report nothing more while risking a wedge instead
+ of a verdict. */
+
+static UINT ok_so_far(void)
+{
+
+UINT result;
+
+
+ result = TX_FALSE;
+
+ if (expectations_failed == 0)
+ {
+ result = TX_TRUE;
+ }
+
+ return(result);
+}
+
+
+static void controller_entry(ULONG thread_input)
+{
+
+ULONG expected_exits;
+ULONG expected_entries;
+UINT index;
+
+
+ (void) thread_input;
+
+ controller_counter++;
+ expected_exits = 0;
+ expected_entries = 0;
+
+ /* Inform user. */
+ printf("Running Thread Exit Callback Transition Test............... ");
+
+ /* The scheduler must be self-consistent before anything is measured, so
+ that a later failure is attributable to this test. */
+ expect(scheduler_consistent(), 10);
+
+ /* ------------------------------------------------------------------- */
+ /* Scenario A: terminate a ready target that is not the current thread, */
+ /* with two peers ready at the same priority. The callback attempts */
+ /* the delete, and recreates the control block if it succeeded. */
+ /* ------------------------------------------------------------------- */
+
+ if (ok_so_far() == TX_TRUE)
+ {
+
+ peers_run = TX_TRUE;
+ victim_hold = TX_TRUE;
+
+ expect_status(tx_thread_resume(&peer_a), TX_SUCCESS, 11);
+ expect_status(tx_thread_resume(&peer_b), TX_SUCCESS, 12);
+
+ arm_probe(TX_TRUE, TX_FALSE, TX_FALSE, TX_TRUE);
+
+ expect_status(create_victim(VICTIM_PRIORITY, TX_AUTO_START), TX_SUCCESS, 13);
+ }
+
+ if (ok_so_far() == TX_TRUE)
+ {
+
+ /* Let the victim and both peers become ready and start spinning, so
+ that the victim's unlink is from a multi-member ring. */
+ tx_thread_sleep(3);
+
+ expect_status(victim.tx_thread_state, TX_READY, 14);
+ expect(ready_ring_contains(VICTIM_PRIORITY, &victim), 15);
+ expect(ready_ring_contains(VICTIM_PRIORITY, &peer_a), 16);
+ expect(ready_ring_contains(VICTIM_PRIORITY, &peer_b), 17);
+ }
+
+ if (ok_so_far() == TX_TRUE)
+ {
+
+ expect_status(tx_thread_terminate(&victim), TX_SUCCESS, 18);
+
+#ifndef TX_DISABLE_NOTIFY_CALLBACKS
+
+ expect(probe_ran, 19);
+
+ /* The callback runs after the terminal state has been published, and
+ inside the transition, which is what the flag records. */
+ expect_status(probe_state, TX_TERMINATED, 20);
+ expect_status(probe_suspending, TX_TRUE, 21);
+
+ /* Both the wrapper and the core service must refuse. */
+ expect_status(probe_delete_status, TX_DELETE_ERROR, 22);
+ expect_status(probe_core_delete_status, TX_DELETE_ERROR, 23);
+
+ /* So the recreate never happened and nothing is inconsistent. */
+ expect_status(delete_succeeded, TX_FALSE, 24);
+ expect_status(probe_create_status, PROBE_NOT_ATTEMPTED, 25);
+ expect_status(probe_consistent, TX_TRUE, 26);
+ expect_count(replacement_counter, 0, 27);
+#endif
+
+ /* After the service returns the termination is complete: the state is
+ terminal, the marker is clear, the target is on no ready list and in
+ no execute slot, and the peers are undisturbed. */
+ expect_status(victim.tx_thread_state, TX_TERMINATED, 28);
+ expect_status(victim.tx_thread_suspending, TX_FALSE, 29);
+ expect(fully_detached(&victim), 30);
+ expect(ready_ring_contains(VICTIM_PRIORITY, &peer_a), 31);
+ expect(ready_ring_contains(VICTIM_PRIORITY, &peer_b), 32);
+ expect(scheduler_consistent(), 33);
+ }
+
+ if (ok_so_far() == TX_TRUE)
+ {
+
+ arm_probe(TX_FALSE, TX_FALSE, TX_FALSE, TX_FALSE);
+
+ /* The documented behaviour is available again. */
+ expect_status(tx_thread_delete(&victim), TX_SUCCESS, 34);
+ }
+
+ /* ------------------------------------------------------------------- */
+ /* Scenario B: the same termination, but the callback attempts a reset */
+ /* and then the resume that a successful reset would have unlocked. */
+ /* */
+ /* This is the leg where the transition check is the only thing */
+ /* standing between the callback and a reset of a thread that is still */
+ /* on the ready list. Without it the reset succeeds, the resume */
+ /* clears the suspending flag and restores TX_READY, the outer service */
+ /* then finds the flag clear and skips the removal, and */
+ /* tx_thread_terminate returns TX_SUCCESS for a runnable thread. */
+ /* ------------------------------------------------------------------- */
+
+ if (ok_so_far() == TX_TRUE)
+ {
+
+ arm_probe(TX_FALSE, TX_TRUE, TX_FALSE, TX_FALSE);
+
+ expect_status(create_victim(VICTIM_PRIORITY, TX_AUTO_START), TX_SUCCESS, 40);
+ }
+
+ if (ok_so_far() == TX_TRUE)
+ {
+
+ tx_thread_sleep(3);
+
+ expect_status(victim.tx_thread_state, TX_READY, 41);
+ expect_status(tx_thread_terminate(&victim), TX_SUCCESS, 42);
+
+#ifndef TX_DISABLE_NOTIFY_CALLBACKS
+
+ expect(probe_ran, 43);
+ expect_status(probe_state, TX_TERMINATED, 44);
+ expect_status(probe_suspending, TX_TRUE, 45);
+
+ /* The discriminating leg. The target is not the current thread and
+ its published state is terminal, so both of the conditions the
+ service tested before this fix are satisfied. */
+ expect_status(probe_reset_status, TX_NOT_DONE, 46);
+ expect_status(probe_core_reset_status, TX_NOT_DONE, 47);
+
+ /* With the reset refused, the state is still terminal, so the resume
+ cannot reach the branch that would void the termination. */
+ expect_status(probe_resume_status, TX_RESUME_ERROR, 48);
+ expect_status(probe_consistent, TX_TRUE, 49);
+#endif
+
+ /* And the termination stands. A kernel that let the callback reset
+ and resume the target arrives here with the victim ready to run. */
+ expect_status(victim.tx_thread_state, TX_TERMINATED, 50);
+ expect_status(victim.tx_thread_suspending, TX_FALSE, 51);
+ expect(fully_detached(&victim), 52);
+ expect(scheduler_consistent(), 53);
+ }
+
+ if (ok_so_far() == TX_TRUE)
+ {
+
+ arm_probe(TX_FALSE, TX_FALSE, TX_FALSE, TX_FALSE);
+
+ expect_status(tx_thread_reset(&victim), TX_SUCCESS, 54);
+ expect_status(tx_thread_terminate(&victim), TX_SUCCESS, 55);
+ expect_status(tx_thread_delete(&victim), TX_SUCCESS, 56);
+ }
+
+ /* Retire the peers, so that the ring drops to a single member and then to
+ none, and check consistency across both unlinks. */
+
+ if (ok_so_far() == TX_TRUE)
+ {
+
+ peers_run = TX_FALSE;
+ tx_thread_sleep(3);
+
+ expect_status(peer_a.tx_thread_state, TX_COMPLETED, 57);
+ expect_status(peer_b.tx_thread_state, TX_COMPLETED, 58);
+ expect(scheduler_consistent(), 59);
+ expect_status(tx_thread_delete(&peer_a), TX_SUCCESS, 60);
+ expect_status(tx_thread_delete(&peer_b), TX_SUCCESS, 61);
+ }
+
+ /* ------------------------------------------------------------------- */
+ /* Scenario C: terminate a target suspended on an object, so that the */
+ /* non-ready branch of _tx_thread_terminate runs -- suspension cleanup, */
+ /* the terminated extension, the callback, and mutex release, in that */
+ /* order. That branch used to clear the marker before the callback */
+ /* rather than after the last dereference of the target. */
+ /* ------------------------------------------------------------------- */
+
+ if (ok_so_far() == TX_TRUE)
+ {
+
+ arm_probe(TX_TRUE, TX_TRUE, TX_FALSE, TX_FALSE);
+ victim_hold = TX_FALSE;
+ victim_take_mutex = TX_TRUE;
+ victim_wait_semaphore = TX_TRUE;
+
+ expect_status(create_victim(VICTIM_PRIORITY, TX_AUTO_START), TX_SUCCESS, 70);
+ }
+
+ if (ok_so_far() == TX_TRUE)
+ {
+
+ /* Let the victim take the mutex and suspend on the semaphore. */
+ tx_thread_sleep(3);
+
+ expect_status(victim.tx_thread_state, TX_SEMAPHORE_SUSP, 71);
+ expect_pointer(victim_mutex.tx_mutex_owner, &victim, 72);
+ }
+
+ if (ok_so_far() == TX_TRUE)
+ {
+
+ expect_status(tx_thread_terminate(&victim), TX_SUCCESS, 73);
+
+#ifndef TX_DISABLE_NOTIFY_CALLBACKS
+
+ expect(probe_ran, 74);
+ expect_status(probe_state, TX_TERMINATED, 75);
+
+ /* The discriminating leg for this branch. The marker used to be
+ cleared before this point, which left the callback free to delete or
+ reset a control block that the service still had mutex-release
+ processing to do against. */
+ expect_status(probe_suspending, TX_TRUE, 76);
+ expect_status(probe_delete_status, TX_DELETE_ERROR, 77);
+ expect_status(probe_core_delete_status, TX_DELETE_ERROR, 78);
+ expect_status(probe_reset_status, TX_NOT_DONE, 79);
+ expect_status(probe_core_reset_status, TX_NOT_DONE, 80);
+ expect_status(probe_resume_status, TX_RESUME_ERROR, 81);
+ expect_status(delete_succeeded, TX_FALSE, 82);
+ expect_status(probe_consistent, TX_TRUE, 83);
+#endif
+
+ /* The marker is cleared at the safe point, after the mutex release,
+ and the mutex the terminated thread owned has been given up. The
+ value of the flag during the release itself is not observable from
+ application code; what is observable is that there is exactly one
+ clear site and that it sits downstream of the release. */
+ expect_status(victim.tx_thread_suspending, TX_FALSE, 84);
+ expect_pointer(victim_mutex.tx_mutex_owner, TX_NULL, 85);
+ expect_count((unsigned long) victim_semaphore.tx_semaphore_suspended_count, 0, 86);
+ expect(fully_detached(&victim), 87);
+ expect(scheduler_consistent(), 88);
+ }
+
+ if (ok_so_far() == TX_TRUE)
+ {
+
+ arm_probe(TX_FALSE, TX_FALSE, TX_FALSE, TX_FALSE);
+
+ expect_status(tx_thread_delete(&victim), TX_SUCCESS, 89);
+ }
+
+ /* ------------------------------------------------------------------- */
+ /* Scenario D: natural completion, victim alone at its priority. */
+ /* */
+ /* The callback here runs on the completing thread itself, so a */
+ /* callback-side delete that succeeds destroys the host thread on the */
+ /* simulation ports. This scenario is therefore placed after the ones */
+ /* that reach an assertion, and the gate above stops the run before it */
+ /* if any of them has already failed. */
+ /* ------------------------------------------------------------------- */
+
+ if (ok_so_far() == TX_TRUE)
+ {
+
+ arm_probe(TX_TRUE, TX_TRUE, TX_TRUE, TX_FALSE);
+ victim_take_mutex = TX_FALSE;
+ victim_wait_semaphore = TX_FALSE;
+
+ expect_status(create_victim(VICTIM_PRIORITY, TX_AUTO_START), TX_SUCCESS, 100);
+ }
+
+ if (ok_so_far() == TX_TRUE)
+ {
+
+ /* Let the victim run to completion, so that its callback runs inside
+ the completion transition. */
+ tx_thread_sleep(3);
+
+#ifndef TX_DISABLE_NOTIFY_CALLBACKS
+
+ expect(probe_ran, 101);
+ expect_status(probe_state, TX_COMPLETED, 102);
+ expect_status(probe_suspending, TX_TRUE, 103);
+ expect_status(probe_delete_status, TX_DELETE_ERROR, 104);
+ expect_status(probe_core_delete_status, TX_DELETE_ERROR, 105);
+
+ /* Reset of the running thread is refused for two independent reasons
+ here -- it is the current thread, and its transition is in progress
+ -- so this leg is a guard rather than a discriminator. Scenario B
+ is where the transition check is the only thing refusing a reset. */
+ expect_status(probe_reset_status, TX_NOT_DONE, 106);
+ expect_status(probe_core_reset_status, TX_NOT_DONE, 107);
+
+ /* A completed thread is neither resumable nor suspendable, and
+ terminating one is a documented no-op. */
+ expect_status(probe_resume_status, TX_RESUME_ERROR, 108);
+ expect_status(probe_suspend_status, TX_SUSPEND_ERROR, 109);
+ expect_status(probe_terminate_status, TX_SUCCESS, 110);
+ expect_status(delete_succeeded, TX_FALSE, 111);
+ expect_status(probe_consistent, TX_TRUE, 112);
+#endif
+
+ /* The transition has finished, so the calls the callback was refused
+ must succeed. The refusal is transient and nothing has become
+ permanently undeletable. */
+ expect_status(victim.tx_thread_state, TX_COMPLETED, 113);
+ expect_status(victim.tx_thread_suspending, TX_FALSE, 114);
+ expect(fully_detached(&victim), 115);
+ expect(scheduler_consistent(), 116);
+ expect(((victim_counter > 0) ? TX_TRUE : TX_FALSE), 117);
+ }
+
+ if (ok_so_far() == TX_TRUE)
+ {
+
+ arm_probe(TX_FALSE, TX_FALSE, TX_FALSE, TX_FALSE);
+
+ /* Reset, so that the documented reset of a completed thread is covered
+ on a target whose transition has finished, and so that the
+ reset/terminate/delete order is exercised on this branch too. */
+ expect_status(tx_thread_reset(&victim), TX_SUCCESS, 118);
+ expect_status(victim.tx_thread_state, TX_SUSPENDED, 119);
+ expect_status(tx_thread_delete(&victim), TX_DELETE_ERROR, 120);
+ expect_status(tx_thread_terminate(&victim), TX_SUCCESS, 121);
+ expect_status(victim.tx_thread_suspending, TX_FALSE, 122);
+ expect_status(tx_thread_delete(&victim), TX_SUCCESS, 123);
+ }
+
+ /* A safe post-completion recreate at a different priority, which is the
+ lifecycle the callback was refused, performed at the point it is
+ legitimate. It exercises the priority map and the list heads for a
+ recreate that moves priority as well as for one that does not. */
+
+ if (ok_so_far() == TX_TRUE)
+ {
+
+ expect_status(create_victim(RECREATE_PRIORITY, TX_AUTO_START), TX_SUCCESS, 124);
+
+ tx_thread_sleep(3);
+
+ expect_status(victim.tx_thread_state, TX_COMPLETED, 125);
+ expect(scheduler_consistent(), 126);
+ expect_status(tx_thread_delete(&victim), TX_SUCCESS, 127);
+ }
+
+ /* ------------------------------------------------------------------- */
+ /* Scenario E: self termination. The callback fires once, in the */
+ /* victim's own context, the lifecycle mutation is refused there too, */
+ /* and another thread can complete the lifecycle afterwards. */
+ /* ------------------------------------------------------------------- */
+
+ if (ok_so_far() == TX_TRUE)
+ {
+
+ victim_self_terminate = TX_TRUE;
+ expected_exits = callback_exit_counter;
+
+ arm_probe(TX_TRUE, TX_TRUE, TX_FALSE, TX_FALSE);
+
+ expect_status(create_victim(VICTIM_PRIORITY, TX_AUTO_START), TX_SUCCESS, 130);
+ }
+
+ if (ok_so_far() == TX_TRUE)
+ {
+
+ tx_thread_sleep(3);
+
+#ifndef TX_DISABLE_NOTIFY_CALLBACKS
+
+ /* Exactly one exit notification, even though the thread terminated
+ itself from inside its own entry function. */
+ expect_count(callback_exit_counter, expected_exits + 1, 131);
+ expect_status(probe_state, TX_TERMINATED, 132);
+ expect_status(probe_suspending, TX_TRUE, 133);
+ expect_status(probe_delete_status, TX_DELETE_ERROR, 134);
+ expect_status(probe_core_delete_status, TX_DELETE_ERROR, 135);
+ expect_status(probe_reset_status, TX_NOT_DONE, 136);
+ expect_status(delete_succeeded, TX_FALSE, 137);
+ expect_status(probe_consistent, TX_TRUE, 138);
+#else
+
+ expect_count(callback_exit_counter, expected_exits, 139);
+#endif
+
+ expect_status(victim.tx_thread_state, TX_TERMINATED, 140);
+ expect_status(victim.tx_thread_suspending, TX_FALSE, 141);
+ expect(fully_detached(&victim), 142);
+ }
+
+ if (ok_so_far() == TX_TRUE)
+ {
+
+ arm_probe(TX_FALSE, TX_FALSE, TX_FALSE, TX_FALSE);
+
+ expect_status(tx_thread_reset(&victim), TX_SUCCESS, 143);
+ expect_status(tx_thread_terminate(&victim), TX_SUCCESS, 144);
+ expect_status(tx_thread_delete(&victim), TX_SUCCESS, 145);
+ expect(scheduler_consistent(), 146);
+ }
+
+ /* ------------------------------------------------------------------- */
+ /* Scenario F: a benign callback is unaffected. The notification */
+ /* ordering and counts are what they were. */
+ /* ------------------------------------------------------------------- */
+
+ if (ok_so_far() == TX_TRUE)
+ {
+
+ victim_self_terminate = TX_FALSE;
+ expected_exits = callback_exit_counter;
+ expected_entries = callback_entry_counter;
+ exit_saw_entry_first = TX_FALSE;
+ last_notify_type = PROBE_NOT_ATTEMPTED;
+
+ arm_probe(TX_FALSE, TX_FALSE, TX_FALSE, TX_FALSE);
+
+ expect_status(create_victim(VICTIM_PRIORITY, TX_AUTO_START), TX_SUCCESS, 150);
+ }
+
+ if (ok_so_far() == TX_TRUE)
+ {
+
+ tx_thread_sleep(3);
+
+#ifndef TX_DISABLE_NOTIFY_CALLBACKS
+
+ /* This incarnation ran and completed, so it reported exactly one entry
+ and one exit, and the entry came first. The transition marker has
+ changed neither how often notifications are delivered nor in what
+ order. */
+ expect_count(callback_exit_counter, expected_exits + 1, 151);
+ expect_count(callback_entry_counter, expected_entries + 1, 152);
+ expect_status(exit_saw_entry_first, TX_TRUE, 153);
+#else
+
+ expect_count(callback_exit_counter, expected_exits, 154);
+ expect_count(callback_entry_counter, expected_entries, 155);
+#endif
+
+ expect_status(tx_thread_delete(&victim), TX_SUCCESS, 156);
+ expect(scheduler_consistent(), 157);
+ }
+
+ /* ------------------------------------------------------------------- */
+ /* Scenario G: state and boundary cases. A thread that never entered a */
+ /* terminal state, and one that is already fully detached, behave as */
+ /* documented -- the new refusal applies only while the marker is set. */
+ /* ------------------------------------------------------------------- */
+
+ if (ok_so_far() == TX_TRUE)
+ {
+
+ expect_status(create_victim(VICTIM_PRIORITY, TX_DONT_START), TX_SUCCESS, 170);
+
+ /* Suspended, never run: neither state is terminal, so both services
+ are refused for the reason they always were, and the marker is not
+ set on a thread that is not transitioning. */
+ expect_status(tx_thread_delete(&victim), TX_DELETE_ERROR, 171);
+ expect_status(tx_thread_reset(&victim), TX_NOT_DONE, 172);
+ expect_status(victim.tx_thread_suspending, TX_FALSE, 173);
+
+ /* Terminated from a suspended state: deletable immediately, and
+ repeatedly resettable and re-terminable in between. */
+ expect_status(tx_thread_terminate(&victim), TX_SUCCESS, 174);
+ expect_status(victim.tx_thread_suspending, TX_FALSE, 175);
+ expect_status(tx_thread_reset(&victim), TX_SUCCESS, 176);
+ expect_status(tx_thread_terminate(&victim), TX_SUCCESS, 177);
+ expect_status(tx_thread_terminate(&victim), TX_SUCCESS, 178);
+ expect_status(tx_thread_delete(&victim), TX_SUCCESS, 179);
+ expect(scheduler_consistent(), 180);
+ }
+
+ if (expectations_failed != 0)
+ {
+
+ printf("ERROR #%u (%lu of %lu expectations failed:",
+ first_failure, expectations_failed, expectations_checked);
+
+ for (index = 0; index < failed_tag_count; index++)
+ {
+ printf(" %u", failed_tags[index]);
+ }
+
+ if (expectations_failed > ((unsigned long) failed_tag_count))
+ {
+ printf(" ...");
+ }
+
+ printf(")\n");
+ fflush(stdout);
+ test_control_return(1);
+ }
+ else
+ {
+ printf("SUCCESS!\n");
+ test_control_return(0);
+ }
+}
+
+
+static void victim_entry(ULONG thread_input)
+{
+
+ (void) thread_input;
+
+ victim_counter++;
+
+ if (victim_take_mutex == TX_TRUE)
+ {
+ tx_mutex_get(&victim_mutex, TX_NO_WAIT);
+ }
+
+ if (victim_wait_semaphore == TX_TRUE)
+ {
+
+ /* Suspend on an object, so that a termination takes the non-ready
+ branch and runs the suspension cleanup routine. */
+ tx_semaphore_get(&victim_semaphore, TX_WAIT_FOREVER);
+ }
+
+ if (victim_self_terminate == TX_TRUE)
+ {
+
+ /* This does not return: the termination detaches this thread and the
+ preemption check at the end of the service switches away from it. */
+ tx_thread_terminate(tx_thread_identify());
+ }
+
+ while (victim_hold == TX_TRUE)
+ {
+
+ /* Stay ready, so that a terminate issued by another thread finds this
+ thread on the ready list. */
+ tx_thread_relinquish();
+ }
+
+ /* Returning enters the completion transition. */
+}
+
+
+static void peer_entry(ULONG thread_input)
+{
+
+ if (thread_input == 1)
+ {
+ peer_a_counter++;
+ }
+ else
+ {
+ peer_b_counter++;
+ }
+
+ while (peers_run == TX_TRUE)
+ {
+
+ /* Stay ready at the victim's priority, so that the victim's ready ring
+ has more than one member. */
+ tx_thread_relinquish();
+ }
+}
+
+
+static void replacement_entry(ULONG thread_input)
+{
+
+ (void) thread_input;
+
+ replacement_counter++;
+
+ /* Only reached on a kernel that let the callback recreate the control
+ block. Suspend rather than return, so that the run does not depend on a
+ second completion transition over a control block already known to be
+ inconsistent. */
+ tx_thread_suspend(tx_thread_identify());
+}
diff --git a/test/smp/regression/threadx_thread_information_test.c b/test/smp/regression/threadx_thread_information_test.c
index b5d06e71..fef3473f 100644
--- a/test/smp/regression/threadx_thread_information_test.c
+++ b/test/smp/regression/threadx_thread_information_test.c
@@ -9,6 +9,8 @@
/* SPDX-License-Identifier: MIT */
/***************************************************************************/
+/* Portions of this file were generated with AI assistance. */
+
/* This test is for the thread information services. */
#include <stdio.h>
@@ -63,7 +65,7 @@ static void thread_0_entry(ULONG thread_input)
{
UINT status;
-CHAR *name;
+TX_NAME_CONST CHAR *name;
UINT state;
ULONG run_count;
UINT priority;
@@ -96,7 +98,7 @@ ULONG idle_returns;
status += tx_thread_info_get(&thread_0, &name, &state, &run_count, &priority, &preemption_threshold, &time_slice, &next_thread, &suspended_thread);
/* Check for error status. */
- if ((status != TX_SUCCESS) || (state != TX_READY) || (run_count != thread_0.tx_thread_run_count) || (priority != 16) || (preemption_threshold != 16) ||
+ if ((status != TX_SUCCESS) || (name != thread_0.tx_thread_name) || (state != TX_READY) || (run_count != thread_0.tx_thread_run_count) || (priority != 16) || (preemption_threshold != 16) ||
(time_slice != 0) || (next_thread != thread_0.tx_thread_created_next) || (suspended_thread != thread_0.tx_thread_suspended_next))
{
diff --git a/test/smp/regression/threadx_thread_misaligned_stack_test.c b/test/smp/regression/threadx_thread_misaligned_stack_test.c
new file mode 100644
index 00000000..fa72aab5
--- /dev/null
+++ b/test/smp/regression/threadx_thread_misaligned_stack_test.c
@@ -0,0 +1,156 @@
+/***************************************************************************
+ * 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
+ **************************************************************************/
+
+/* This test creates a thread whose stack does not begin on a ULONG boundary.
+ It is the SMP counterpart of the ThreadX suite's test of the same name.
+
+ tx_thread_create rounds the starting address up to the next ULONG and then
+ takes a ULONG off the size, so that the rounding cannot push the end of the
+ stack past the memory the caller supplied:
+
+ if (new_stack_start != updated_stack_start)
+ {
+ stack_size = stack_size - (sizeof(ULONG));
+ }
+
+ Every other test in the suite hands tx_thread_create an aligned stack, so
+ that subtraction was never executed -- the only uncovered line in
+ tx_thread_create.c across all five build configurations. A misaligned stack
+ is legitimate: the API takes a VOID * and the alignment fix-up exists
+ precisely to accept one.
+
+ The line is compiled only under TX_ENABLE_STACK_CHECKING, so it is absent
+ from three of the five configurations' reports rather than uncovered in
+ them.
+
+ The thread is then run to completion rather than merely created, because the
+ point of the adjustment is that the resulting stack is still usable and still
+ inside the caller's buffer. */
+
+#include <stdio.h>
+#include "tx_api.h"
+
+
+void test_control_return(UINT status);
+
+
+static TX_THREAD thread_0;
+static TX_THREAD thread_1;
+
+/* Thread 1's stack is carved out of this by hand so that its start can be put
+ deliberately off a ULONG boundary. The extra ULONG is what the misaligned
+ start is offset into, so that the stack still ends inside the array after
+ tx_thread_create has rounded the start up. */
+
+static UCHAR misaligned_area[TEST_STACK_SIZE_PRINTF + sizeof(ULONG)];
+
+static ULONG thread_1_counter = 0;
+
+
+static void thread_0_entry(ULONG thread_input);
+static void thread_1_entry(ULONG thread_input);
+
+
+/* Define what the initial system looks like. */
+
+#ifdef CTEST
+void test_application_define(void *first_unused_memory)
+#else
+void threadx_thread_misaligned_stack_application_define(void *first_unused_memory)
+#endif
+{
+
+UINT status;
+CHAR *pointer;
+CHAR *misaligned_stack;
+
+
+ /* Setup a pointer. */
+ pointer = (CHAR *) first_unused_memory;
+
+ status = tx_thread_create(&thread_0, "thread 0", thread_0_entry, 0,
+ pointer, TEST_STACK_SIZE_PRINTF,
+ 16, 16, TX_NO_TIME_SLICE, TX_AUTO_START);
+ pointer = pointer + TEST_STACK_SIZE_PRINTF;
+
+ if (status != TX_SUCCESS)
+ {
+
+ printf("Running Thread Misaligned Stack Test................................ ERROR #1\n");
+ test_control_return(1);
+ }
+
+ /* Push the start one byte past a ULONG boundary. The array itself is
+ aligned, so adding one is enough to guarantee the fix-up runs whatever
+ the alignment of the array turned out to be. */
+ misaligned_stack = ((CHAR *) misaligned_area) + 1;
+
+ status = tx_thread_create(&thread_1, "thread 1", thread_1_entry, 0,
+ misaligned_stack, TEST_STACK_SIZE_PRINTF,
+ 15, 15, TX_NO_TIME_SLICE, TX_AUTO_START);
+
+ if (status != TX_SUCCESS)
+ {
+
+ printf("Running Thread Misaligned Stack Test................................ ERROR #2\n");
+ test_control_return(1);
+ }
+}
+
+
+static void thread_0_entry(ULONG thread_input)
+{
+
+ /* On this port the two threads start on separate cores, so this one does
+ not have to wait for a relinquish to run. It sleeps well past the single
+ tick thread 1 sleeps for, so the checks below are made against a thread
+ that has actually finished either way. */
+ tx_thread_sleep(5);
+
+ /* Both increments must have happened: one before thread 1 slept and one
+ after it woke, which is the half that needed the stack to survive a
+ context switch. */
+ if (thread_1_counter != ((ULONG) 2))
+ {
+
+ printf("Running Thread Misaligned Stack Test................................ ERROR #3\n");
+ test_control_return(1);
+ }
+
+ /* The thread that ran on the misaligned stack must have run to completion,
+ which is what says the adjusted stack was usable. */
+ if (thread_1.tx_thread_state != TX_COMPLETED)
+ {
+
+ printf("Running Thread Misaligned Stack Test................................ ERROR #4\n");
+ test_control_return(1);
+ }
+
+ printf("Running Thread Misaligned Stack Test................................ SUCCESS!\n");
+ test_control_return(0);
+}
+
+
+static void thread_1_entry(ULONG thread_input)
+{
+
+ /* Use the stack either side of a context switch rather than returning
+ straight away. */
+ thread_1_counter++;
+
+ tx_thread_sleep(1);
+
+ thread_1_counter++;
+}
diff --git a/test/smp/regression/threadx_thread_multiple_sleep_test.c b/test/smp/regression/threadx_thread_multiple_sleep_test.c
index 4dfe5bdc..778495a7 100644
--- a/test/smp/regression/threadx_thread_multiple_sleep_test.c
+++ b/test/smp/regression/threadx_thread_multiple_sleep_test.c
@@ -11,7 +11,7 @@
/* This test is designed to test multiple threads sleeping for 33 ticks. */
-// Some portions generated by Codex (gpt 5.4).
+// Portions of this file were generated with AI assistance.
#include <stdio.h>
#include "tx_api.h"
diff --git a/test/smp/regression/threadx_thread_priority_change.c b/test/smp/regression/threadx_thread_priority_change.c
index e68eadf5..d22f3d72 100644
--- a/test/smp/regression/threadx_thread_priority_change.c
+++ b/test/smp/regression/threadx_thread_priority_change.c
@@ -465,11 +465,28 @@ TX_THREAD *temp_thread;
/* Restore interrupts. */
TX_RESTORE
- /* Hit branch in _tx_thread_smp_simple_priority_change such that the
- new priority is less than the inheritance priority. */
- thread_0.tx_thread_inherit_priority = 0;
+ /* Hit the branch in _tx_thread_smp_simple_priority_change where the new
+ priority is below the inheritance priority. That needs an inheritance
+ priority above the new one, so 0 does not reach it: with 0 the other
+ branch runs, and that branch records the inheritance priority as the
+ thread's priority while still linking the thread at the new priority.
+ Thread 0 came out of here claiming priority 0 while living in the
+ priority 16 list, which is what has been hanging this test in CI.
+ Priority 0 ties with the control thread, so resuming the control thread
+ raised no preemption, and once thread 0 completed, core 0 was left owned
+ by a completed thread with the control thread ready and unscheduled.
+
+ This routine is internal and expects protection to be in force, so hold
+ it here rather than calling in the open. */
+ TX_DISABLE
+ thread_0.tx_thread_inherit_priority = 20;
_tx_thread_smp_simple_priority_change(&thread_0, 16);
+ /* Put the inheritance priority back to the value that means this thread is
+ not inheriting one, so nothing downstream reasons about a phantom. */
+ thread_0.tx_thread_inherit_priority = TX_MAX_PRIORITIES;
+ TX_RESTORE
+
/* Successful test. */
printf("SUCCESS!\n");
test_control_return(0);
@@ -513,6 +530,15 @@ static void thread_3_entry(ULONG thread_input)
UINT old_priority;
UINT loop;
+/* The interrupt below only fires on a narrow window: thread 3 at priority 6,
+ ready, and not yet at the head of its priority list. Waiting for it without a
+ bound made this test hang indefinitely when the window was never hit, which
+ showed up in CI as a timeout carrying no information about what went wrong.
+ The TX_NOT_INTERRUPTABLE path already stopped after a fixed amount of work;
+ this gives the interruptable path the same protection. */
+UINT attempts = 0;
+#define MAX_PRIORITY_CHANGE_ATTEMPTS 100000
+
/* Resume threads 4. */
tx_thread_resume(&thread_4);
@@ -537,7 +563,23 @@ UINT loop;
if (thread_4_counter)
thread_1_counter++;
- } while (test_isr_dispatch);
+ attempts++;
+
+ } while (test_isr_dispatch && (attempts < MAX_PRIORITY_CHANGE_ATTEMPTS));
+
+ /* Stop the interrupt regardless, so a window that was never hit does not
+ leave the handler installed for the rest of the test. */
+ test_isr_dispatch = TX_NULL;
+
+ if (attempts >= MAX_PRIORITY_CHANGE_ATTEMPTS)
+ {
+
+ /* The window was not reached. That is a gap in what this test covered on
+ this run rather than a fault in the code under test, so say so instead
+ of failing, and leave the counters alone so the checks that follow
+ still mean what they did before. */
+ printf("(priority change window not reached in %u attempts) ", attempts);
+ }
}
diff --git a/test/smp/regression/threadx_thread_stack_checking_test.c b/test/smp/regression/threadx_thread_stack_checking_test.c
index 9db10d0a..2ef9a2df 100644
--- a/test/smp/regression/threadx_thread_stack_checking_test.c
+++ b/test/smp/regression/threadx_thread_stack_checking_test.c
@@ -9,6 +9,8 @@
/* SPDX-License-Identifier: MIT */
/***************************************************************************/
+// Portions of this file were generated with AI assistance.
+
/* This test is for the thread stack checking services. */
#include <stdio.h>
@@ -200,6 +202,19 @@ TX_THREAD fake_thread;
fake_thread.tx_thread_stack_highest_ptr = TX_NULL;
_tx_thread_stack_analyze(&fake_thread);
+ /* Call the stack analyze with a highest stack pointer below the start of the stack, which is
+ what a stack overflow or a corrupted control block looks like. This used to hang. */
+ fake_thread.tx_thread_id = ((ULONG) 0x54485244);
+ fake_thread.tx_thread_stack_start = (void *) 0x2000;
+ fake_thread.tx_thread_stack_highest_ptr = (void *) 0x1000;
+ _tx_thread_stack_analyze(&fake_thread);
+
+ /* Call the stack analyze with a highest stack pointer equal to the start of the stack. */
+ fake_thread.tx_thread_id = ((ULONG) 0x54485244);
+ fake_thread.tx_thread_stack_start = (void *) 0x2000;
+ fake_thread.tx_thread_stack_highest_ptr = (void *) 0x2000;
+ _tx_thread_stack_analyze(&fake_thread);
+
/* Clear the pattern in thread 2's stack. */
TX_MEMSET(thread_2_stack_start, (CHAR) 0x11, TEST_STACK_SIZE_PRINTF);
diff --git a/test/smp/regression/threadx_thread_stack_fill_value_test.c b/test/smp/regression/threadx_thread_stack_fill_value_test.c
new file mode 100644
index 00000000..fa6eda22
--- /dev/null
+++ b/test/smp/regression/threadx_thread_stack_fill_value_test.c
@@ -0,0 +1,190 @@
+/***************************************************************************/
+/* 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 */
+/***************************************************************************/
+
+// Portions of this file were generated with AI assistance.
+
+/* This test checks that the stack fill value recorded in a thread control block is the
+ value that was actually written into that thread's stack. The two used to disagree
+ when TX_ENABLE_RANDOM_NUMBER_STACK_FILLING was enabled, because the control block was
+ cleared after the random value had been chosen and the stack had been filled with it,
+ which left the stack checking and stack analyze routines looking for a pattern that is
+ not the one present in the stack. */
+
+#include <stdio.h>
+#include "tx_api.h"
+
+
+/* Number of probe threads created. A random fill value can legitimately come out as zero,
+ which hides the defect for that one thread, so several threads are created and at least
+ one of them is required to have produced a non-zero pattern. */
+
+#define TEST_PROBE_THREADS 16
+
+
+static unsigned long thread_0_counter = 0;
+static TX_THREAD thread_0;
+
+static TX_THREAD probe_thread;
+static ULONG probe_stack[TX_MINIMUM_STACK / sizeof(ULONG)];
+
+
+/* Define task prototypes. */
+
+static void thread_0_entry(ULONG task_input);
+
+
+/* Prototype for test control return. */
+
+void test_control_return(UINT status);
+
+
+/* Define what the initial system looks like. */
+
+#ifdef CTEST
+void test_application_define(void *first_unused_memory)
+#else
+void threadx_thread_stack_fill_value_application_define(void *first_unused_memory)
+#endif
+{
+
+UINT status;
+CHAR *pointer;
+
+ pointer = (CHAR *) first_unused_memory;
+
+ /* Put system definition stuff in here, e.g. thread creates and other assorted
+ create information. */
+
+ status = tx_thread_create(&thread_0, "thread 0", thread_0_entry, 0,
+ pointer, TEST_STACK_SIZE_PRINTF,
+ 16, 16, TX_NO_TIME_SLICE, TX_AUTO_START);
+
+ /* Check for status. */
+ if (status != TX_SUCCESS)
+ {
+
+ printf("Running Thread Stack Fill Value Test................................ ERROR #1\n");
+ test_control_return(1);
+ }
+}
+
+
+/* Define the test threads. */
+
+static void thread_0_entry(ULONG thread_input)
+{
+
+UINT status;
+UINT i;
+ULONG stack_pattern;
+UINT non_zero_seen = TX_FALSE;
+
+
+ /* Increment thread 0 counter. */
+ thread_0_counter++;
+
+ /* Inform user of success getting to this test. */
+ printf("Running Thread Stack Fill Value Test................................ ");
+
+ for (i = 0; i < TEST_PROBE_THREADS; i++)
+ {
+
+ /* Create a thread that is never started, so that its stack still holds nothing but
+ the fill pattern and the initial stack frame built at the top of the stack. */
+ status = tx_thread_create(&probe_thread, "probe thread", thread_0_entry, 0,
+ probe_stack, sizeof(probe_stack),
+ 17, 17, TX_NO_TIME_SLICE, TX_DONT_START);
+
+ /* Check for status. */
+ if (status != TX_SUCCESS)
+ {
+
+ printf("ERROR #2\n");
+ test_control_return(1);
+ }
+
+ /* Pick up the pattern the create service wrote into the unused part of the stack. */
+ stack_pattern = *(((ULONG *) probe_thread.tx_thread_stack_start) + 4);
+
+#ifndef TX_DISABLE_STACK_FILLING
+#if defined(TX_ENABLE_RANDOM_NUMBER_STACK_FILLING) && defined(TX_ENABLE_STACK_CHECKING)
+
+ /* The control block must hold the value that was written into the stack. */
+ if (probe_thread.tx_thread_stack_fill_value != stack_pattern)
+ {
+
+ printf("ERROR #3\n");
+ test_control_return(1);
+ }
+
+ /* The random value is duplicated into all four bytes of the fill value. */
+ if ((stack_pattern & 0xFFUL) !=
+ ((stack_pattern >> 24) & 0xFFUL))
+ {
+
+ printf("ERROR #4\n");
+ test_control_return(1);
+ }
+
+ if (stack_pattern != ((ULONG) 0))
+ {
+
+ non_zero_seen = TX_TRUE;
+ }
+#else
+
+ /* Without random filling, the pattern is the fixed one. */
+ if (stack_pattern != ((ULONG) 0xEFEFEFEFUL))
+ {
+
+ printf("ERROR #5\n");
+ test_control_return(1);
+ }
+
+ non_zero_seen = TX_TRUE;
+#endif
+#else
+
+ /* Stack filling is disabled, so there is nothing to compare. */
+ non_zero_seen = TX_TRUE;
+#endif
+
+ /* Terminate and delete the probe thread so that the next pass starts from a clean
+ slate. A thread created with TX_DONT_START is suspended rather than completed,
+ so it must be terminated before it can be deleted. */
+ status = tx_thread_terminate(&probe_thread);
+
+ if (status == TX_SUCCESS)
+ {
+
+ status = tx_thread_delete(&probe_thread);
+ }
+
+ /* Check for status. */
+ if (status != TX_SUCCESS)
+ {
+
+ printf("ERROR #6\n");
+ test_control_return(1);
+ }
+ }
+
+ /* A run in which every fill value came out as zero would prove nothing. */
+ if (non_zero_seen != TX_TRUE)
+ {
+
+ printf("ERROR #7\n");
+ test_control_return(1);
+ }
+
+ /* Success! */
+ printf("SUCCESS!\n");
+ test_control_return(0);
+}
diff --git a/test/smp/regression/threadx_thread_wait_abort_and_isr_test.c b/test/smp/regression/threadx_thread_wait_abort_and_isr_test.c
index 75faf050..c66abff2 100644
--- a/test/smp/regression/threadx_thread_wait_abort_and_isr_test.c
+++ b/test/smp/regression/threadx_thread_wait_abort_and_isr_test.c
@@ -12,6 +12,7 @@
/* This test is designed to test for simultaneous thread suspension lifting AND thread wait abort calls. */
#include <stdio.h>
+#include <time.h>
#include "tx_api.h"
#include "tx_timer.h"
@@ -178,6 +179,54 @@ static void thread_0_entry(ULONG thread_input)
UINT status;
+/* The window this test waits for is probabilistic, and the handler above says
+ as much: it can settle into a resonance in which the condition is never met.
+ Waiting for it without a bound means such a run never ends, and this test has
+ been the most expensive thing in the suite: run one at a time in CI it took
+ between 148 and 726 seconds per configuration, 88 percent of the whole
+ ThreadX suite, against 273 seconds for the other ninety five tests together.
+
+ The budget is in wall clock seconds rather than ticks on purpose. The
+ simulated tick clock is not a proxy for elapsed time here: a tick arrives only
+ when the port's timer thread gets to run, so under load, or under coverage
+ instrumentation, ticks fall behind and never catch up. A budget of 20000
+ ticks, nominally 200 seconds, failed to stop a run that took 726 seconds,
+ because fewer than 20000 ticks had passed. time() does not drift that way.
+
+ How many windows to ask for is set by what a run can actually reach. Four CI
+ runs of the same tree measured this loop in two modes: one where a window
+ arrives in milliseconds and every window asked for costs under a second in
+ total, and one where a single window costs around forty seconds. Seven of
+ twenty configuration-runs landed in the slow mode and ran out of budget,
+ reaching 0, 3, 3, 3, 4, 7 and 7 of the ten then asked for, every one of them
+ still reporting a pass. Asking for three keeps the count reachable in both
+ modes. The later hits repeat what the first ones establish, so the coverage
+ given up is small, and what is recorded is honest. This copy reaches its
+ twenty windows in under half a second in all five of its configurations, so
+ it keeps that count; the non-SMP copy, where the slow mode was measured, asks
+ for three.
+
+ Reaching the window no times at all is different in kind, which is what the
+ ceiling below is for. The check after the loop compares semaphore bookkeeping
+ that a window has to have touched to mean anything, so a pass with a count of
+ zero claims coverage the run did not have. One of those twenty runs did
+ exactly that, and said so only in an artifact nobody reads. A run that has not
+ reached the window once keeps trying up to the ceiling, and fails if it still
+ has not.
+
+ The budget is 180 seconds rather than 120 because of what the slow mode costs
+ per window. The seven truncated runs reached their windows at between 17 and
+ 40 seconds each, so three of them can need 120 seconds, which is exactly what
+ the old budget allowed and would have truncated at two. 180 leaves margin at
+ the worst rate measured, and bounds five configurations at 15 minutes against
+ a 60 minute step timeout. Locally, where a window costs 2 to 5 seconds, three
+ of them take 5 to 14 seconds and the budget is never approached. */
+#define WAIT_ABORT_WINDOWS_WANTED ((ULONG) 20)
+#define WAIT_ABORT_SECOND_BUDGET ((ULONG) 180)
+#define WAIT_ABORT_ZERO_WINDOW_CEILING ((ULONG) 300)
+
+time_t start_wall;
+
/* Setup ISR for this test. */
test_isr_dispatch = isr_entry;
@@ -186,7 +235,8 @@ UINT status;
printf("Running Thread Wait Abort and ISR Resume Test....................... ");
/* Loop to exploit the probability window inside tx_thread_wait_abort. */
- while (condition_count < 20)
+ start_wall = time(TX_NULL);
+ while (condition_count < WAIT_ABORT_WINDOWS_WANTED)
{
/* Suspend on the semaphore that is going to be set via the ISR. */
@@ -227,11 +277,48 @@ UINT status;
#endif
}
+
+ /* Out of budget? A run that has not reached the window even once has
+ verified nothing yet, so it gets the higher ceiling before giving up. */
+#ifdef TX_NOT_INTERRUPTABLE
+ if (((ULONG) (time(TX_NULL) - start_wall)) > WAIT_ABORT_SECOND_BUDGET)
+ break;
+#else
+ if (condition_count == 0)
+ {
+ if (((ULONG) (time(TX_NULL) - start_wall)) > WAIT_ABORT_ZERO_WINDOW_CEILING)
+ break;
+ }
+ else if (((ULONG) (time(TX_NULL) - start_wall)) > WAIT_ABORT_SECOND_BUDGET)
+ break;
+#endif
}
/* Clear ISR dispatch. */
test_isr_dispatch = TX_NULL;
+ /* Say what this run reached, on every run and not only a short one. A count
+ printed only on shortfall cannot be told apart from a count nobody
+ recorded, and this line is what the CI artifacts carry. Falling short is a
+ gap in what was exercised rather than a fault in the code under test, so
+ the check below still runs and still means what it did. */
+ printf("(reached %lu of %lu windows in %lu seconds) ",
+ (ULONG) condition_count, WAIT_ABORT_WINDOWS_WANTED,
+ (ULONG) (time(TX_NULL) - start_wall));
+
+#ifndef TX_NOT_INTERRUPTABLE
+
+ /* Reached it no times? Then the check below compares bookkeeping no window
+ ever touched, and a pass would report coverage this run did not have. */
+ if (condition_count == 0)
+ {
+
+ /* Test error! */
+ printf("ERROR #8\n");
+ test_control_return(4);
+ }
+#endif
+
#ifdef TX_NOT_INTERRUPTABLE
/* At this point, check to see if we got all the semaphores! */
if ((thread_0_counter != (semaphore_put_counter - semaphore_0.tx_semaphore_count)) ||
diff --git a/test/smp/regression/threadx_timer_information_test.c b/test/smp/regression/threadx_timer_information_test.c
index ae3bb414..cbb88239 100644
--- a/test/smp/regression/threadx_timer_information_test.c
+++ b/test/smp/regression/threadx_timer_information_test.c
@@ -9,6 +9,8 @@
/* SPDX-License-Identifier: MIT */
/***************************************************************************/
+/* Portions of this file were generated with AI assistance. */
+
/* This test is designed to test timer information services. */
#include <stdio.h>
@@ -104,7 +106,7 @@ static void thread_0_entry(ULONG thread_input)
UINT status;
UINT interrupt_status;
-CHAR *name;
+TX_NAME_CONST CHAR *name;
UINT active;
ULONG remaining_ticks;
ULONG reschedule_ticks;
@@ -231,7 +233,7 @@ TX_TIMER_INTERNAL **list_head;
status += tx_timer_info_get(&timer_0, &name, &active, &remaining_ticks, &reschedule_ticks, &next_timer);
/* Check for successful completion. */
- if ((status != TX_SUCCESS) || (active != TX_TRUE) || (remaining_ticks != 100) || (reschedule_ticks != 200) || (next_timer != &timer_1))
+ if ((status != TX_SUCCESS) || (name != timer_0.tx_timer_name) || (active != TX_TRUE) || (remaining_ticks != 100) || (reschedule_ticks != 200) || (next_timer != &timer_1))
{
/* Application timer error. */
diff --git a/test/smp/regression/threadx_timer_simple_test.c b/test/smp/regression/threadx_timer_simple_test.c
index e2272bd2..5abd1d3c 100644
--- a/test/smp/regression/threadx_timer_simple_test.c
+++ b/test/smp/regression/threadx_timer_simple_test.c
@@ -9,6 +9,8 @@
/* SPDX-License-Identifier: MIT */
/***************************************************************************/
+/* Portions of this file were generated with AI assistance. */
+
/* This test is designed to test a simple application timer services, including create,
activate, deactivate, change, and delete. */
@@ -63,7 +65,7 @@ static void thread_1_entry(ULONG thread_input);
static void timer_0_expiration(ULONG timer_input);
static void timer_1_expiration(ULONG timer_input);
-UINT _txe_timer_create(TX_TIMER *timer_ptr, CHAR *name_ptr,
+UINT _txe_timer_create(TX_TIMER *timer_ptr, TX_NAME_CONST CHAR *name_ptr,
VOID (*expiration_function)(ULONG), ULONG expiration_input,
ULONG initial_ticks, ULONG reschedule_ticks, UINT auto_activate, UINT timer_control_block_size);
diff --git a/test/smp/regression/threadx_trace_basic_test.c b/test/smp/regression/threadx_trace_basic_test.c
index 51fa6256..10b927bb 100644
--- a/test/smp/regression/threadx_trace_basic_test.c
+++ b/test/smp/regression/threadx_trace_basic_test.c
@@ -9,6 +9,8 @@
/* SPDX-License-Identifier: MIT */
/***************************************************************************/
+/* Portions of this file were generated with AI assistance. */
+
/* This test is designed to test trace functionality in ThreadX. */
#include <stdio.h>
@@ -30,7 +32,7 @@
extern VOID (*test_isr_dispatch)(void);
extern UINT _tx_trace_interrupt_control(UINT new_posture);
-extern VOID _tx_trace_object_register(UCHAR object_type, VOID *object_ptr, CHAR *object_name, ULONG parameter_1, ULONG parameter_2);
+extern VOID _tx_trace_object_register(UCHAR object_type, VOID *object_ptr, TX_NAME_CONST CHAR *object_name, ULONG parameter_1, ULONG parameter_2);
extern VOID _tx_trace_object_unregister(VOID *object_ptr);
static unsigned long thread_0_counter = 0;
diff --git a/test/smp/regression/threadx_trace_entry_update_test.c b/test/smp/regression/threadx_trace_entry_update_test.c
new file mode 100644
index 00000000..17c36600
--- /dev/null
+++ b/test/smp/regression/threadx_trace_entry_update_test.c
@@ -0,0 +1,367 @@
+/***************************************************************************
+ * 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
+ **************************************************************************/
+
+/* This is the SMP counterpart of the ThreadX suite's trace entry update test.
+ It drives the trace entry update paths -- the blocks guarded by
+ TX_ENABLE_EVENT_TRACE that go back and patch a trace entry after the call
+ they belong to has finished its work.
+
+ Those blocks all have the same shape:
+
+ if (entry_ptr != TX_NULL)
+ {
+ if (time_stamp == entry_ptr -> tx_trace_buffer_entry_time_stamp)
+ {
+ entry_ptr -> tx_trace_buffer_entry_information_field_N = ...;
+ }
+ }
+
+ entry_ptr comes from _tx_trace_buffer_current_ptr, which stays TX_NULL until
+ tx_trace_enable() is called at run time. Compiling with TX_ENABLE_EVENT_TRACE
+ is therefore not enough to reach them, and the trace_build configuration on
+ its own never does: the only other test that enables tracing --
+ threadx_trace_basic_test -- exercises the enable API itself and never calls
+ tx_block_allocate or tx_byte_allocate at all. So the update blocks in
+ tx_block_allocate.c, tx_byte_allocate.c and tx_thread_system_suspend.c were
+ absent from the coverage report's covered set while appearing in its
+ denominator.
+
+ Both allocators are driven twice, because they carry two of these blocks
+ each and only one is on the path that succeeds immediately: the second sits
+ after a suspension, where the allocation completes in the releasing thread's
+ context. Hence the second thread, which exists only to release what
+ thread 0 is waiting on.
+
+ The one thing this version has to do that the ThreadX version does not is
+ pin both threads to a single core. On four cores the two threads run at the
+ same time, and thread 1 would give the block back before thread 0 had
+ suspended waiting for it -- the suspension the second update block sits
+ behind would never happen. Excluding both from cores 1 to 3 restores the
+ strict alternation the ThreadX suite gets for free.
+
+ The test is built in all five configurations, and four of them do not define
+ TX_ENABLE_EVENT_TRACE. There tx_trace_enable reports TX_FEATURE_NOT_ENABLED,
+ the update blocks do not exist, and the allocation sequence below is still a
+ valid exercise of both allocators' suspension paths -- so the test is
+ meaningful in every configuration rather than skipped in four. */
+
+#include <stdio.h>
+#include "tx_api.h"
+#include "threadx_test_port.h"
+#define TX_SOURCE_CODE
+#include "tx_trace.h"
+
+
+void test_control_return(UINT status);
+
+
+static TX_THREAD thread_0;
+static TX_THREAD thread_1;
+
+static TX_BLOCK_POOL block_pool_0;
+static TX_BYTE_POOL byte_pool_0;
+static TX_SEMAPHORE semaphore_0;
+
+/* Every thread this test creates is confined to core 0, so that the two of
+ them take turns the way they would on a uniprocessor. Bits 1, 2 and 3 set
+ means cores 1, 2 and 3 are excluded. */
+
+#define TRACE_ENTRY_UPDATE_CORE_0_ONLY ((ULONG) 0xE)
+
+/* The trace buffer is sized generously on purpose. The inner test of each
+ update block compares the saved time stamp against the one in the entry, and
+ a buffer small enough to wrap between the call and its completion would
+ overwrite the entry and take the false branch every time -- the block would
+ be reached and its body still never run. */
+
+static UCHAR trace_buffer[16384];
+
+/* Four blocks of 20 bytes. */
+
+static UCHAR block_pool_area[TX_TEST_BLOCK_POOL_BYTES(20, 4)];
+static UCHAR byte_pool_area[TX_TEST_BYTE_POOL_BYTES(512)];
+
+/* Allocated by thread 0 and released by thread 1, which is what lets thread 0's
+ suspended allocations complete. */
+
+static void *held_block;
+static void *held_bytes;
+
+static UINT error = 0;
+
+
+static void thread_0_entry(ULONG thread_input);
+static void thread_1_entry(ULONG thread_input);
+
+
+/* Define what the initial system looks like. */
+
+#ifdef CTEST
+void test_application_define(void *first_unused_memory)
+#else
+void threadx_trace_entry_update_application_define(void *first_unused_memory)
+#endif
+{
+
+UINT status;
+CHAR *pointer;
+
+
+ /* Setup a pointer. */
+ pointer = (CHAR *) first_unused_memory;
+
+ /* Create the pools before tracing is enabled, so that the events this test
+ cares about are the allocations rather than the creates. */
+ status = tx_block_pool_create(&block_pool_0, "trace entry update block pool",
+ 20, block_pool_area, sizeof(block_pool_area));
+
+ if (status != TX_SUCCESS)
+ {
+
+ printf("Running Trace Entry Update Test..................................... ERROR #1\n");
+ test_control_return(1);
+ }
+
+ status = tx_byte_pool_create(&byte_pool_0, "trace entry update byte pool",
+ byte_pool_area, sizeof(byte_pool_area));
+
+ if (status != TX_SUCCESS)
+ {
+
+ printf("Running Trace Entry Update Test..................................... ERROR #2\n");
+ test_control_return(1);
+ }
+
+ /* Turn tracing on. This is the whole point of the test: without it
+ _tx_trace_buffer_current_ptr stays TX_NULL and every update block below
+ takes its guarding false branch. */
+ _tx_trace_initialize();
+ status = tx_trace_enable(trace_buffer, sizeof(trace_buffer), 16);
+
+#ifdef TX_ENABLE_EVENT_TRACE
+
+ if (status != TX_SUCCESS)
+ {
+
+ printf("Running Trace Entry Update Test..................................... ERROR #3\n");
+ test_control_return(1);
+ }
+#else
+
+ /* Four of the five build configurations reach this branch. The rest of the
+ test still runs; it just is not exercising any update block. */
+ if (status != TX_FEATURE_NOT_ENABLED)
+ {
+
+ printf("Running Trace Entry Update Test..................................... ERROR #4\n");
+ test_control_return(1);
+ }
+#endif
+
+ /* Register an object with no name at all, now that tracing is on. The name
+ copy in _tx_trace_object_register guards against a TX_NULL name and
+ breaks out of the copy loop, and nothing else in the suite takes that
+ branch with tracing enabled -- threadx_trace_basic_test makes the
+ equivalent call only under #ifndef TX_ENABLE_EVENT_TRACE, against the
+ no-op stub.
+
+ A semaphore rather than a thread, deliberately: for a thread object the
+ register function dereferences the pointer it is handed to read the
+ thread's priority, so that type needs a real TX_THREAD behind it. */
+ status = tx_semaphore_create(&semaphore_0, TX_NULL, 1);
+
+ if (status != TX_SUCCESS)
+ {
+
+ printf("Running Trace Entry Update Test..................................... ERROR #5\n");
+ test_control_return(1);
+ }
+
+ /* Thread 0 drives the test and must run first, so it takes the higher
+ priority -- the lower number. Both threads are created stopped so that
+ the core exclusion is in place before either of them is schedulable. */
+ status = tx_thread_create(&thread_0, "thread 0", thread_0_entry, 0,
+ pointer, TEST_STACK_SIZE_PRINTF,
+ 15, 15, TX_NO_TIME_SLICE, TX_DONT_START);
+ pointer = pointer + TEST_STACK_SIZE_PRINTF;
+ status += tx_thread_smp_core_exclude(&thread_0, TRACE_ENTRY_UPDATE_CORE_0_ONLY);
+
+ if (status != TX_SUCCESS)
+ {
+
+ printf("Running Trace Entry Update Test..................................... ERROR #6\n");
+ test_control_return(1);
+ }
+
+ /* Thread 1 releases what thread 0 waits on. It runs only while thread 0 is
+ suspended, which is exactly when it is needed -- and only because it
+ shares thread 0's single core. */
+ status = tx_thread_create(&thread_1, "thread 1", thread_1_entry, 0,
+ pointer, TEST_STACK_SIZE_PRINTF,
+ 16, 16, TX_NO_TIME_SLICE, TX_DONT_START);
+ pointer = pointer + TEST_STACK_SIZE_PRINTF;
+ status += tx_thread_smp_core_exclude(&thread_1, TRACE_ENTRY_UPDATE_CORE_0_ONLY);
+
+ if (status != TX_SUCCESS)
+ {
+
+ printf("Running Trace Entry Update Test..................................... ERROR #7\n");
+ test_control_return(1);
+ }
+
+ /* Start them now that both exclusions are set. Priority decides the order,
+ not this one. */
+ status = tx_thread_resume(&thread_1);
+ status += tx_thread_resume(&thread_0);
+
+ if (status != TX_SUCCESS)
+ {
+
+ printf("Running Trace Entry Update Test..................................... ERROR #8\n");
+ test_control_return(1);
+ }
+}
+
+
+static void thread_0_entry(ULONG thread_input)
+{
+
+UINT status;
+void *block_ptr;
+void *byte_ptr;
+UINT i;
+
+
+ /* Inform user. */
+ printf("Running Trace Entry Update Test..................................... ");
+
+ /* Empty the block pool. The first of these takes the immediate-success path
+ through tx_block_allocate, which carries the first update block. */
+ held_block = TX_NULL;
+ for (i = 0; i < 4; i++)
+ {
+
+ status = tx_block_allocate(&block_pool_0, &block_ptr, TX_NO_WAIT);
+
+ if (status != TX_SUCCESS)
+ {
+
+ error++;
+ break;
+ }
+
+ /* Keep the first one for thread 1 to give back. */
+ if (held_block == TX_NULL)
+ {
+
+ held_block = block_ptr;
+ }
+ }
+
+ /* The pool is empty now, so this suspends. It completes in thread 1's
+ context when the block comes back, which is the second update block in
+ tx_block_allocate -- and the suspend it goes through carries the update
+ block in tx_thread_system_suspend that names the next thread to run on
+ this core. */
+ status = tx_block_allocate(&block_pool_0, &block_ptr, TX_WAIT_FOREVER);
+
+ if (status != TX_SUCCESS)
+ {
+
+ printf("ERROR #9\n");
+ test_control_return(1);
+ }
+
+ /* Now the same shape for the byte pool. Take most of it first, on the path
+ that succeeds immediately. */
+ status = tx_byte_allocate(&byte_pool_0, &held_bytes, 300, TX_NO_WAIT);
+
+ if (status != TX_SUCCESS)
+ {
+
+ printf("ERROR #10\n");
+ test_control_return(1);
+ }
+
+ /* Not enough left, so this one suspends and is satisfied by thread 1's
+ release. */
+ status = tx_byte_allocate(&byte_pool_0, &byte_ptr, 300, TX_WAIT_FOREVER);
+
+ if (status != TX_SUCCESS)
+ {
+
+ printf("ERROR #11\n");
+ test_control_return(1);
+ }
+
+ /* Sleep, so that thread 1 gets to run out its entry function and suspend
+ itself. That leaves nothing ready to execute anywhere, which is a second
+ update block in tx_thread_system_suspend -- the one on the branch taken
+ when the priority map is empty. The two suspensions above never reach it,
+ because thread 1 was always ready to take over. */
+ status = tx_thread_sleep(5);
+
+ if (status != TX_SUCCESS)
+ {
+
+ printf("ERROR #12\n");
+ test_control_return(1);
+ }
+
+ if (error)
+ {
+
+ printf("ERROR #13\n");
+ test_control_return(1);
+ }
+ else
+ {
+
+ /* Successful test. */
+ printf("SUCCESS!\n");
+ test_control_return(0);
+ }
+}
+
+
+static void thread_1_entry(ULONG thread_input)
+{
+
+UINT status;
+
+
+ /* Reached only when thread 0 has suspended on the empty block pool. */
+ status = tx_block_release(held_block);
+
+ if (status != TX_SUCCESS)
+ {
+
+ error++;
+ }
+
+ /* And again when thread 0 has suspended on the byte pool. Thread 0 preempts
+ on the release above, runs until it suspends a second time, and control
+ comes back here. */
+ status = tx_byte_release(held_bytes);
+
+ if (status != TX_SUCCESS)
+ {
+
+ error++;
+ }
+
+ /* Thread 0 reports the result; this thread has nothing left to do. */
+ tx_thread_suspend(&thread_1);
+}
diff --git a/test/tx/cmake/CMakeLists.txt b/test/tx/cmake/CMakeLists.txt
index 97b3615b..7ed620d9 100644
--- a/test/tx/cmake/CMakeLists.txt
+++ b/test/tx/cmake/CMakeLists.txt
@@ -1,3 +1,15 @@
+##############################################################################
+# 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
+##############################################################################
+
+# Portions of this file were generated with AI assistance.
+
cmake_minimum_required(VERSION 3.13 FATAL_ERROR)
cmake_policy(SET CMP0054 NEW)
cmake_policy(SET CMP0057 NEW)
@@ -14,7 +26,8 @@ set(CMAKE_C_EXTENSIONS OFF)
# Set build configurations
set(BUILD_CONFIGURATIONS default_build_coverage disable_notify_callbacks_build
- stack_checking_build stack_checking_rand_fill_build trace_build)
+ stack_checking_build stack_checking_rand_fill_build trace_build
+ misra_build misra_trace_build)
set(CMAKE_CONFIGURATION_TYPES
${BUILD_CONFIGURATIONS}
CACHE STRING "list of supported configuration types" FORCE)
@@ -36,6 +49,21 @@ set(stack_checking_build TX_QUEUE_MESSAGE_MAX_SIZE=32 TX_ENABLE_STACK_CHECKING)
set(stack_checking_rand_fill_build TX_QUEUE_MESSAGE_MAX_SIZE=32 TX_ENABLE_STACK_CHECKING TX_ENABLE_RANDOM_NUMBER_STACK_FILLING)
set(trace_build TX_QUEUE_MESSAGE_MAX_SIZE=32 TX_ENABLE_EVENT_TRACE)
+# TX_MISRA_ENABLE routes the kernel's pointer conversions, its memset and its
+# stack check through the shim in common/src/tx_misra.c, which every
+# configuration above compiles out entirely. It is also the source the MISRA
+# analysis runs against, so without this the build that is analysed and the build
+# that is tested are different builds of the same tree.
+#
+# The second adds event tracing, because the shim has a region behind
+# TX_ENABLE_EVENT_TRACE that neither macro alone reaches.
+#
+# Both defects found here were invisible for the same reason: #741 broke under
+# TX_MISRA_ENABLE and #745 under both macros together, and neither combination
+# was built anywhere.
+set(misra_build TX_QUEUE_MESSAGE_MAX_SIZE=32 TX_MISRA_ENABLE)
+set(misra_trace_build TX_QUEUE_MESSAGE_MAX_SIZE=32 TX_MISRA_ENABLE TX_ENABLE_EVENT_TRACE)
+
add_compile_definitions(
TX_REGRESSION_TEST
TEST_STACK_SIZE_PRINTF=4096
@@ -61,8 +89,47 @@ add_subdirectory(${CMAKE_CURRENT_LIST_DIR}/../../.. threadx)
add_subdirectory(regression)
add_subdirectory(samples)
+# The two configurations this change has to reach, TX_NOT_INTERRUPTABLE and
+# TX_DISABLE_ERROR_CHECKING, are not among the five above and cannot be reached
+# from the regression suite, which links one library per configuration. This
+# directory compiles the sources concerned directly. It is listed last rather than
+# beside regression on purpose: every branch in the current fix set adds an
+# add_subdirectory line immediately after regression, and putting a second one
+# there turns a set of clean merges into a set of one-line conflicts.
+# These two compile a hand-picked set of common/src sources directly instead of
+# linking the threadx library, which is what lets them reach feature macros the
+# library-per-configuration model cannot. That does not survive TX_MISRA_ENABLE:
+# the kernel's TX_MEMSET and its conversions become calls into tx_misra.c, and
+# pulling the shim into these targets pulls its own callees after it. Neither
+# exists to test the shim, and the regression suite covers it, so the MISRA
+# configurations skip them.
+if(NOT ("TX_MISRA_ENABLE" IN_LIST ${CMAKE_BUILD_TYPE}))
+ add_subdirectory(thread_transition)
+endif()
+
+# Host tests for the Module Manager. Listed after thread_transition for the reason
+# given above it: additions here are appended rather than inserted, so that branches
+# adding a directory of their own merge cleanly.
+if(NOT ("TX_MISRA_ENABLE" IN_LIST ${CMAKE_BUILD_TYPE}))
+ add_subdirectory(module_manager)
+endif()
+
# Coverage
-if(CMAKE_BUILD_TYPE MATCHES ".*_coverage")
+#
+# The gate here used to be the build type alone, and only one of the five
+# configurations -- default_build_coverage -- has a name matching "*_coverage".
+# So four configurations built and ran every test and their coverage was thrown
+# away. That is not redundancy discarded: each configuration selects a different
+# set of TX_ feature macros, so the code the other four compile is *absent from
+# the denominator* rather than reported as uncovered. A figure taken from one of
+# them is the executable-line count of one configuration, not of the kernel.
+#
+# TX_COVERAGE instruments a build regardless of its name. It defaults to OFF and
+# the build-type match is kept, so configuring a single configuration by hand
+# behaves exactly as it did before; the regression run turns it on for all five
+# and merges the results.
+option(TX_COVERAGE "Instrument this build for coverage regardless of build type" OFF)
+if(TX_COVERAGE OR CMAKE_BUILD_TYPE MATCHES ".*_coverage")
if(NOT MSVC)
target_compile_options(threadx PRIVATE -fprofile-arcs -ftest-coverage)
target_link_options(threadx PRIVATE -fprofile-arcs -ftest-coverage)
@@ -92,3 +159,11 @@ else()
-Waggregate-return
-Wfloat-equal)
endif()
+
+# Preserve native32 host coverage and also exercise the Linux pointer-width
+# mismatch with a native64 kernel, including MISRA plus stack checking.
+if(CMAKE_SYSTEM_NAME STREQUAL "Linux" AND THREADX_ARCH STREQUAL "linux" AND
+ THREADX_TOOLCHAIN STREQUAL "gnu" AND
+ CMAKE_SYSTEM_PROCESSOR MATCHES "^(x86_64|amd64|AMD64)$")
+ add_subdirectory(thread_stack_alignment)
+endif()
diff --git a/test/tx/cmake/coverage.sh b/test/tx/cmake/coverage.sh
index 6dda78b3..32d466b7 100755
--- a/test/tx/cmake/coverage.sh
+++ b/test/tx/cmake/coverage.sh
@@ -14,6 +14,142 @@
set -e
cd $(dirname $0)
-mkdir -p coverage_report/$1
-gcovr --object-directory=build/$1/threadx/CMakeFiles/threadx.dir/common/src -r build/$1 -f ../../../common/src --xml-pretty --output coverage_report/$1.xml
-gcovr --object-directory=build/$1/threadx/CMakeFiles/threadx.dir/common/src -r build/$1 -f ../../../common/src --html --html-details --output coverage_report/$1/index.html
+
+# gcov reads a data format tied to the compiler that produced it, so gcov has to match
+# the gcc that built the objects. Since cmake/linux.cmake began honouring CC, taking
+# whatever gcov happens to be first on PATH is no longer safe: building with gcc-14 and
+# reading with gcov-13 gives "version 'B42*', prefer 'B33*'" from gcov, and gcovr turns
+# that into "GCOV returncode was 3" and exits 64. The tests pass and then the coverage
+# step fails with a Python traceback, which reads like a coverage bug rather than a
+# toolchain mismatch.
+#
+# So derive gcov from CC rather than asking the caller to remember both. GCOV still
+# overrides, for a toolchain that does not follow the gcc/gcov naming.
+: "${CC:=gcc}"
+if [ -z "${GCOV:-}" ]; then
+ cc_base=$(basename "$CC")
+ case "$cc_base" in
+ gcc*) GCOV="gcov${cc_base#gcc}" ;;
+ *) GCOV="gcov" ;;
+ esac
+fi
+
+if ! command -v "$GCOV" >/dev/null 2>&1; then
+ echo "coverage.sh: '$GCOV' not found, derived from CC='$CC'." >&2
+ echo "Install it, or set GCOV to the gcov matching that compiler." >&2
+ exit 1
+fi
+
+# The repository root, needed by every mode below. It has to be absolute: see the
+# note on the per-configuration paths further down.
+repo_root=$(cd ../../.. && pwd)
+filter=$repo_root/common/src
+
+# --merge unions the per-configuration reports into the one number that means
+# something. Each configuration writes an intermediate JSON beside its XML, and
+# this pass adds them all together.
+#
+# Reporting five separate percentages instead would invite a reader to average
+# them, and an average is not a coverage figure -- a line covered only by
+# trace_build is covered, and only the union says so.
+#
+# Do not be alarmed when the merged percentage is lower than the single
+# configuration this used to report. That is the point: the denominator now
+# includes code the old report never counted at all, because it was compiled out
+# of the only instrumented build.
+if [ "$1" = "--merge" ]; then
+ shopt -s nullglob
+ tracefiles=(coverage_report/per_configuration/*.json)
+ shopt -u nullglob
+ if [ ${#tracefiles[@]} -eq 0 ]; then
+ echo "coverage.sh --merge: no JSON in coverage_report/per_configuration/." >&2
+ echo "Run the suites with TX_COVERAGE=ON first." >&2
+ exit 1
+ fi
+
+ add_args=()
+ for t in "${tracefiles[@]}"; do
+ add_args+=(--add-tracefile "$t")
+ done
+
+ mkdir -p coverage_report/merged
+ gcovr -r "$repo_root" "${add_args[@]}" --xml-pretty --output coverage_report/merged.xml
+ gcovr -r "$repo_root" "${add_args[@]}" --html --html-details --output coverage_report/merged/index.html
+
+ if ! grep -q "<class " coverage_report/merged.xml; then
+ echo "coverage.sh --merge: the merged report contains no files." >&2
+ exit 1
+ fi
+
+ # Named, not just counted. coverage_report/ is not cleaned between runs, so a
+ # tracefile left by an earlier run of a different set of configurations would
+ # otherwise be merged in without anything saying so.
+ echo "coverage.sh --merge: ThreadX, ${#tracefiles[@]} configuration(s):"
+ for t in "${tracefiles[@]}"; do
+ echo " $(basename "$t" .json)"
+ done
+ exit 0
+fi
+
+# gcovr is given three paths below, and each of them has to be absolute, for a
+# different reason.
+#
+# -r is the repository root rather than the build directory, so the report names
+# files the way the repository does -- "common/src/tx_block_allocate.c" instead
+# of "/home/runner/work/threadx/threadx/common/src/tx_block_allocate.c". With -r
+# inside build/, gcovr cannot express the sources relative to it, because they
+# are outside it, and falls back to absolute paths. Those paths then differ on
+# every machine and disagree with the <source> element written beside them in
+# the same file, so anything that maps coverage back to the repository -- PR
+# annotations, Codecov, SonarQube -- cannot follow them.
+#
+# Both -r and -f must be absolute. "-r ../../.. -f common/src" produces a report
+# containing zero files and exits 0, which is the worst failure mode available
+# here: a green run carrying an empty report. Measured, not assumed. repo_root
+# and filter are set above, before the merge mode, because it needs them too.
+
+# This is what actually scopes the report to one build configuration, and it is
+# the positional search path -- not --object-directory, which used to be here
+# and was doing nothing at all. That flag tells gcovr how to get from a gcda
+# file back to the compiler's working directory; it does not restrict which gcda
+# files are found. Pointed at an empty directory it still produced the full
+# 177-file report, because gcovr searches -r as well.
+#
+# That matters more now than it did before. While -r was build/$1 it happened to
+# constrain the search to this configuration by accident. -r is the repository
+# root now, and every configuration's gcda lies somewhere under it, so without
+# an explicit search path the report would silently merge all five. Measured on
+# gcovr 8.6: with this search path, an empty directory yields an empty report
+# and the real one yields 177 files.
+objdir=$PWD/build/$1/threadx/CMakeFiles/threadx.dir/common/src
+
+# The Linux port's own sources are deliberately outside -f. gcno files exist for
+# two directories -- common/src and ports/linux/gnu/src -- and only the first is
+# reported. The kernel is what this suite is here to cover; the architecture
+# ports are validated functionally rather than structurally, and linux/gnu is a
+# development host port that nothing ships on. Written down because a filter
+# argument on its own is not a decision the next reader can see.
+
+# Per-configuration output is kept in a subdirectory of its own, and the merged
+# report sits alongside it at the top. That is not tidiness: the Pages deploy
+# uploads coverage_report wholesale and merges the ThreadX and SMP artifacts
+# into one tree, and every configuration directory has the same name in both
+# suites. Left at the top level, default_build_coverage/ from one suite would
+# overwrite the other's on the published site. Nested here, the top level still
+# holds exactly the one suite directory the deploy expects.
+mkdir -p coverage_report/per_configuration/$1
+gcovr --gcov-executable "$GCOV" -r "$repo_root" -f "$filter" "$objdir" \
+ --json coverage_report/per_configuration/$1.json \
+ --xml-pretty --output coverage_report/per_configuration/$1.xml
+gcovr --gcov-executable "$GCOV" -r "$repo_root" -f "$filter" "$objdir" --html --html-details --output coverage_report/per_configuration/$1/index.html
+
+# An empty report is not an error as far as gcovr is concerned: it warns and
+# exits 0. Worse, it advertises line-rate="1.0" alongside lines-valid="0", so
+# every downstream consumer reads "no data at all" as "100% covered". A coverage
+# threshold cannot catch that, because an empty report passes any threshold. So
+# the assertion belongs here, next to the paths that would cause it.
+if ! grep -q "<class " coverage_report/per_configuration/$1.xml; then
+ echo "coverage.sh: the report for '$1' contains no files." >&2
+ echo "Expected gcda files under $objdir." >&2
+ exit 1
+fi
diff --git a/test/tx/cmake/module_manager/CMakeLists.txt b/test/tx/cmake/module_manager/CMakeLists.txt
new file mode 100644
index 00000000..17511fd5
--- /dev/null
+++ b/test/tx/cmake/module_manager/CMakeLists.txt
@@ -0,0 +1,254 @@
+cmake_minimum_required(VERSION 3.13 FATAL_ERROR)
+cmake_policy(SET CMP0057 NEW)
+
+project(module_manager_test LANGUAGES C)
+
+set(REPO_ROOT ${CMAKE_CURRENT_LIST_DIR}/../../../..)
+set(SOURCE_DIR ${REPO_ROOT}/test/tx/module_manager)
+
+# The Module Manager is not part of the ThreadX library this test tree builds, and
+# its control blocks come from a module port rather than a base port. These tests
+# therefore compile the manager sources they exercise directly against one module
+# port's headers, and stand stubs behind the kernel services those sources call, so
+# that they run on the host like every other test here. It is the same technique the
+# thread_transition directory uses, for the same reason: the code under test cannot
+# be reached through the library the suite links.
+#
+# Every module port is an embedded architecture, so the interrupt primitives those
+# sources use are inline assembly the host assembler cannot take. The port shim
+# header is force-included ahead of every translation unit here and replaces the
+# three of them with host equivalents that count.
+
+set(module_manager_dir ${REPO_ROOT}/common_modules/module_manager)
+set(cortex_a7_module_dir ${REPO_ROOT}/ports_module/cortex_a7/gnu)
+set(cortex_m4_module_dir ${REPO_ROOT}/ports_module/cortex_m4/gnu)
+
+add_executable(
+ threadx_module_manager_thread_kernel_stack_test
+ ${SOURCE_DIR}/threadx_module_manager_thread_kernel_stack_test.c
+ ${module_manager_dir}/src/txm_module_manager_util.c
+ ${module_manager_dir}/src/txm_module_manager_object_allocate.c
+ ${module_manager_dir}/src/txm_module_manager_object_deallocate.c)
+
+target_include_directories(
+ threadx_module_manager_thread_kernel_stack_test
+ PRIVATE ${SOURCE_DIR}
+ ${REPO_ROOT}/common/inc
+ ${REPO_ROOT}/common_modules/inc
+ ${module_manager_dir}/inc
+ ${cortex_a7_module_dir}/inc)
+
+# The dispatch layer is one header of static functions, one per kernel service. A
+# test reaches the dispatcher it exercises by including that header, which defines
+# every other dispatcher too, and an unoptimised build emits them all -- so their
+# references to services this harness stands behind nothing for would reach the
+# linker.
+#
+# The header guards each dispatcher with its own TXM_<SERVICE>_CALL_NOT_USED macro,
+# which is the supported way to trim the dispatch table. Defining every guard except
+# the ones a test needs leaves that test's translation unit holding only the
+# dispatcher under test. The list is read out of the header rather than written down
+# here, so a service added later is excluded without anyone having to remember.
+file(STRINGS ${module_manager_dir}/inc/txm_module_manager_dispatch.h
+ dispatch_guard_lines REGEX "^#ifndef TXM_[A-Z0-9_]+_CALL_NOT_USED")
+
+set(dispatch_guards "")
+foreach(guard_line ${dispatch_guard_lines})
+ string(REGEX MATCH "TXM_[A-Z0-9_]+_CALL_NOT_USED" guard "${guard_line}")
+ list(APPEND dispatch_guards ${guard})
+endforeach()
+list(REMOVE_DUPLICATES dispatch_guards)
+
+# The dispatchers this test calls, which therefore must stay in.
+set(kernel_stack_dispatchers TXM_THREAD_DELETE_CALL_NOT_USED)
+
+set(kernel_stack_guards ${dispatch_guards})
+list(REMOVE_ITEM kernel_stack_guards ${kernel_stack_dispatchers})
+
+target_compile_definitions(threadx_module_manager_thread_kernel_stack_test
+ PRIVATE ${kernel_stack_guards})
+
+# The port shim is force-included ahead of every translation unit, the test included,
+# because the manager sources and the dispatch header both reach for the port's
+# interrupt primitives.
+target_compile_options(
+ threadx_module_manager_thread_kernel_stack_test
+ PRIVATE -Wall
+ -Wextra
+ -include ${SOURCE_DIR}/threadx_module_manager_host_test_port.h)
+
+add_test(${CMAKE_BUILD_TYPE}::threadx_module_manager_thread_kernel_stack_test
+ threadx_module_manager_thread_kernel_stack_test)
+
+# This test drives the manager services directly rather than through a dispatcher,
+# so it needs none of the dispatch table and no guard list.
+add_executable(
+ threadx_module_manager_object_authentication_test
+ ${SOURCE_DIR}/threadx_module_manager_object_authentication_test.c
+ ${module_manager_dir}/src/txm_module_manager_util.c
+ ${module_manager_dir}/src/txm_module_manager_object_allocate.c
+ ${module_manager_dir}/src/txm_module_manager_object_deallocate.c
+ ${module_manager_dir}/src/txm_module_manager_thread_reset.c)
+
+target_include_directories(
+ threadx_module_manager_object_authentication_test
+ PRIVATE ${SOURCE_DIR}
+ ${REPO_ROOT}/common/inc
+ ${REPO_ROOT}/common_modules/inc
+ ${module_manager_dir}/inc
+ ${cortex_a7_module_dir}/inc)
+
+target_compile_options(
+ threadx_module_manager_object_authentication_test
+ PRIVATE -include ${SOURCE_DIR}/threadx_module_manager_host_test_port.h)
+
+add_test(${CMAKE_BUILD_TYPE}::threadx_module_manager_object_authentication_test
+ threadx_module_manager_object_authentication_test)
+
+# This test drives the manager services directly rather than through a dispatcher,
+# so it needs none of the dispatch table and no guard list.
+add_executable(
+ threadx_module_manager_delete_ownership_test
+ ${SOURCE_DIR}/threadx_module_manager_delete_ownership_test.c
+ ${module_manager_dir}/src/txm_module_manager_util.c
+ ${module_manager_dir}/src/txm_module_manager_object_allocate.c
+ ${module_manager_dir}/src/txm_module_manager_object_deallocate.c)
+
+target_include_directories(
+ threadx_module_manager_delete_ownership_test
+ PRIVATE ${SOURCE_DIR}
+ ${REPO_ROOT}/common/inc
+ ${REPO_ROOT}/common_modules/inc
+ ${module_manager_dir}/inc
+ ${cortex_a7_module_dir}/inc)
+
+target_compile_options(
+ threadx_module_manager_delete_ownership_test
+ PRIVATE -include ${SOURCE_DIR}/threadx_module_manager_host_test_port.h)
+
+add_test(${CMAKE_BUILD_TYPE}::threadx_module_manager_delete_ownership_test
+ threadx_module_manager_delete_ownership_test)
+
+# This test drives the allocation and deallocation services directly rather than
+# through a dispatcher, so it needs none of the dispatch table and no guard list.
+add_executable(
+ threadx_module_manager_live_object_deallocation_test
+ ${SOURCE_DIR}/threadx_module_manager_live_object_deallocation_test.c
+ ${module_manager_dir}/src/txm_module_manager_util.c
+ ${module_manager_dir}/src/txm_module_manager_object_allocate.c
+ ${module_manager_dir}/src/txm_module_manager_object_deallocate.c)
+
+target_include_directories(
+ threadx_module_manager_live_object_deallocation_test
+ PRIVATE ${SOURCE_DIR}
+ ${REPO_ROOT}/common/inc
+ ${REPO_ROOT}/common_modules/inc
+ ${module_manager_dir}/inc
+ ${cortex_a7_module_dir}/inc)
+
+target_compile_options(
+ threadx_module_manager_live_object_deallocation_test
+ PRIVATE -Wall
+ -Wextra
+ -include ${SOURCE_DIR}/threadx_module_manager_host_test_port.h)
+
+add_test(${CMAKE_BUILD_TYPE}::threadx_module_manager_live_object_deallocation_test
+ threadx_module_manager_live_object_deallocation_test)
+add_executable(
+ threadx_module_manager_object_allocate_overflow_test
+ ${SOURCE_DIR}/threadx_module_manager_object_allocate_overflow_test.c
+ ${module_manager_dir}/src/txm_module_manager_util.c
+ ${module_manager_dir}/src/txm_module_manager_object_allocate.c
+ ${module_manager_dir}/src/txm_module_manager_object_deallocate.c)
+
+target_include_directories(
+ threadx_module_manager_object_allocate_overflow_test
+ PRIVATE ${REPO_ROOT}/common/inc
+ ${REPO_ROOT}/common_modules/inc
+ ${module_manager_dir}/inc
+ ${cortex_a7_module_dir}/inc)
+
+target_compile_options(
+ threadx_module_manager_object_allocate_overflow_test
+ PRIVATE -include ${SOURCE_DIR}/threadx_module_manager_host_test_port.h)
+
+add_test(${CMAKE_BUILD_TYPE}::threadx_module_manager_object_allocate_overflow_test
+ threadx_module_manager_object_allocate_overflow_test)
+
+add_executable(
+ threadx_module_manager_block_pool_parameters_test
+ ${SOURCE_DIR}/threadx_module_manager_block_pool_parameters_test.c
+ ${module_manager_dir}/src/txm_module_manager_util.c)
+
+target_include_directories(
+ threadx_module_manager_block_pool_parameters_test
+ PRIVATE ${SOURCE_DIR}
+ ${REPO_ROOT}/common/inc
+ ${REPO_ROOT}/common_modules/inc
+ ${module_manager_dir}/inc
+ ${cortex_m4_module_dir}/inc)
+
+target_compile_options(
+ threadx_module_manager_block_pool_parameters_test
+ PRIVATE -include ${SOURCE_DIR}/threadx_module_manager_host_test_port.h)
+
+add_test(${CMAKE_BUILD_TYPE}::threadx_module_manager_block_pool_parameters_test
+ threadx_module_manager_block_pool_parameters_test)
+
+add_executable(
+ threadx_module_manager_object_name_range_test
+ ${SOURCE_DIR}/threadx_module_manager_object_name_range_test.c
+ ${module_manager_dir}/src/txm_module_manager_object_pointer_get.c
+ ${module_manager_dir}/src/txm_module_manager_object_pointer_get_extended.c
+ ${module_manager_dir}/src/txm_module_manager_util.c)
+
+target_include_directories(
+ threadx_module_manager_object_name_range_test
+ PRIVATE ${SOURCE_DIR}
+ ${REPO_ROOT}/common/inc
+ ${REPO_ROOT}/common_modules/inc
+ ${module_manager_dir}/inc
+ ${cortex_m4_module_dir}/inc)
+
+target_compile_options(
+ threadx_module_manager_object_name_range_test
+ PRIVATE -include ${SOURCE_DIR}/threadx_module_manager_host_test_port.h)
+
+add_test(${CMAKE_BUILD_TYPE}::threadx_module_manager_object_name_range_test
+ threadx_module_manager_object_name_range_test)
+
+add_executable(
+ threadx_module_manager_queue_message_range_test
+ ${SOURCE_DIR}/threadx_module_manager_queue_message_range_test.c
+ ${module_manager_dir}/src/txm_module_manager_util.c)
+
+target_include_directories(
+ threadx_module_manager_queue_message_range_test
+ PRIVATE ${SOURCE_DIR}
+ ${REPO_ROOT}/common/inc
+ ${REPO_ROOT}/common_modules/inc
+ ${module_manager_dir}/inc
+ ${cortex_m4_module_dir}/inc)
+
+target_compile_options(
+ threadx_module_manager_queue_message_range_test
+ PRIVATE -include ${SOURCE_DIR}/threadx_module_manager_host_test_port.h)
+
+add_test(${CMAKE_BUILD_TYPE}::threadx_module_manager_queue_message_range_test
+ threadx_module_manager_queue_message_range_test)
+
+add_executable(
+ threadx_module_manager_cortex_a7_range_check_test
+ ${SOURCE_DIR}/threadx_module_manager_cortex_a7_range_check_test.c
+ ${cortex_a7_module_dir}/module_manager/src/txm_module_manager_inside_data_check.c)
+
+target_include_directories(
+ threadx_module_manager_cortex_a7_range_check_test
+ PRIVATE ${REPO_ROOT}/common/inc
+ ${REPO_ROOT}/ports/cortex_a7/gnu/inc
+ ${REPO_ROOT}/common_modules/inc
+ ${REPO_ROOT}/common_modules/module_manager/inc
+ ${cortex_a7_module_dir}/inc)
+
+add_test(${CMAKE_BUILD_TYPE}::threadx_module_manager_cortex_a7_range_check_test
+ threadx_module_manager_cortex_a7_range_check_test)
diff --git a/test/tx/cmake/regression/CMakeLists.txt b/test/tx/cmake/regression/CMakeLists.txt
index 34fba1b2..6b94a857 100644
--- a/test/tx/cmake/regression/CMakeLists.txt
+++ b/test/tx/cmake/regression/CMakeLists.txt
@@ -80,6 +80,7 @@ set(regression_test_cases
${SOURCE_DIR}/threadx_thread_create_preemption_threshold_test.c
${SOURCE_DIR}/threadx_thread_delayed_suspension_test.c
${SOURCE_DIR}/threadx_thread_information_test.c
+ ${SOURCE_DIR}/threadx_thread_misaligned_stack_test.c
${SOURCE_DIR}/threadx_thread_multi_level_preemption_threshold_test.c
${SOURCE_DIR}/threadx_thread_multiple_non_current_test.c
${SOURCE_DIR}/threadx_thread_multiple_sleep_test.c
@@ -96,7 +97,9 @@ set(regression_test_cases
${SOURCE_DIR}/threadx_thread_sleep_for_100ticks_test.c
${SOURCE_DIR}/threadx_thread_sleep_terminate_test.c
${SOURCE_DIR}/threadx_thread_stack_checking_test.c
+ ${SOURCE_DIR}/threadx_thread_stack_fill_value_test.c
${SOURCE_DIR}/threadx_thread_terminate_delete_test.c
+ ${SOURCE_DIR}/threadx_thread_exit_callback_transition_test.c
${SOURCE_DIR}/threadx_thread_time_slice_change_test.c
${SOURCE_DIR}/threadx_thread_wait_abort_and_isr_test.c
${SOURCE_DIR}/threadx_thread_wait_abort_test.c
@@ -109,6 +112,7 @@ set(regression_test_cases
${SOURCE_DIR}/threadx_timer_multiple_test.c
${SOURCE_DIR}/threadx_timer_simple_test.c
${SOURCE_DIR}/threadx_trace_basic_test.c
+ ${SOURCE_DIR}/threadx_trace_entry_update_test.c
${SOURCE_DIR}/threadx_initialize_kernel_setup_test.c)
add_custom_command(
diff --git a/test/tx/cmake/riscv/CMakeLists.txt b/test/tx/cmake/riscv/CMakeLists.txt
index a4f7e7e8..41b44949 100644
--- a/test/tx/cmake/riscv/CMakeLists.txt
+++ b/test/tx/cmake/riscv/CMakeLists.txt
@@ -5,7 +5,7 @@ cmake_policy(SET CMP0057 NEW)
project(threadx_riscv_test LANGUAGES C ASM)
# Build configurations (same defines as Linux tests, minus coverage instrumentation)
-set(BUILD_CONFIGURATIONS default_build disable_notify_callbacks_build
+set(BUILD_CONFIGURATIONS default_build optimized_build disable_notify_callbacks_build
stack_checking_build stack_checking_rand_fill_build trace_build)
set(CMAKE_CONFIGURATION_TYPES
${BUILD_CONFIGURATIONS}
@@ -26,14 +26,21 @@ message(STATUS "THREADX_ARCH: ${THREADX_ARCH}")
# Per-configuration compile definitions
set(default_build -DTX_QUEUE_MESSAGE_MAX_SIZE=32)
+set(optimized_build -DTX_QUEUE_MESSAGE_MAX_SIZE=32)
set(disable_notify_callbacks_build -DTX_QUEUE_MESSAGE_MAX_SIZE=32 -DTX_DISABLE_NOTIFY_CALLBACKS)
set(stack_checking_build -DTX_QUEUE_MESSAGE_MAX_SIZE=32 -DTX_ENABLE_STACK_CHECKING)
set(stack_checking_rand_fill_build -DTX_QUEUE_MESSAGE_MAX_SIZE=32 -DTX_ENABLE_STACK_CHECKING -DTX_ENABLE_RANDOM_NUMBER_STACK_FILLING)
set(trace_build -DTX_QUEUE_MESSAGE_MAX_SIZE=32 -DTX_ENABLE_EVENT_TRACE)
+if(CMAKE_BUILD_TYPE STREQUAL "optimized_build")
+ set(REGRESSION_OPTIMIZATION -O2)
+else()
+ set(REGRESSION_OPTIMIZATION -O0)
+endif()
+
add_compile_options(
-std=c99
- -O0
+ ${REGRESSION_OPTIMIZATION}
-g3
-fdiagnostics-color
-Werror
diff --git a/test/tx/cmake/riscv/bsp/entry.S b/test/tx/cmake/riscv/bsp/entry.S
index 2f16c052..5abe9852 100644
--- a/test/tx/cmake/riscv/bsp/entry.S
+++ b/test/tx/cmake/riscv/bsp/entry.S
@@ -18,6 +18,7 @@
.extern _sysstack_end
.extern _bss_start
.extern _bss_end
+.extern __global_pointer$
_start:
csrr t0, mhartid
@@ -56,6 +57,11 @@ _start:
li x30, 0
li x31, 0
+ .option push
+ .option norelax
+ la gp, __global_pointer$
+ .option pop
+
/* Set up stack pointer from linker symbol. */
la sp, _sysstack_end
diff --git a/test/tx/cmake/riscv/bsp/hwtimer.c b/test/tx/cmake/riscv/bsp/hwtimer.c
index 99821f1e..8b81268a 100644
--- a/test/tx/cmake/riscv/bsp/hwtimer.c
+++ b/test/tx/cmake/riscv/bsp/hwtimer.c
@@ -9,25 +9,99 @@
**************************************************************************/
#include "csr.h"
+#include "tx_api.h"
#include "hwtimer.h"
-#include <tx_port.h>
#define CLINT (0x02000000L)
#define CLINT_TIME (CLINT + 0xBFF8)
#define CLINT_TIMECMP(id) (CLINT + 0x4000 + 8 * (id))
+#if __riscv_xlen == 32
+
+/* RV32 has no 64-bit load/store, so the 64-bit CLINT registers are
+ accessed as two volatile 32-bit MMIO words. */
+#define MTIME_LO (*(volatile uint32_t *)(CLINT_TIME))
+#define MTIME_HI (*(volatile uint32_t *)(CLINT_TIME + 4))
+#define MTIMECMP_LO(id) (*(volatile uint32_t *)(CLINT_TIMECMP(id)))
+#define MTIMECMP_HI(id) (*(volatile uint32_t *)(CLINT_TIMECMP(id) + 4))
+
+/* RV32 mtime read: re-read the high word until it is stable, so a
+ low-word rollover between the two loads cannot tear the value. */
+static uint64_t clint_time_read(void)
+{
+ uint32_t hi;
+ uint32_t lo;
+
+ do
+ {
+ hi = MTIME_HI;
+ lo = MTIME_LO;
+ } while (hi != MTIME_HI);
+
+ return ((uint64_t)hi << 32) | lo;
+}
+
+/* Only this hart writes its own mtimecmp, so a plain two-word read
+ cannot tear. */
+static uint64_t clint_timecmp_read(uintptr_t hart)
+{
+ uint32_t lo = MTIMECMP_LO(hart);
+ uint32_t hi = MTIMECMP_HI(hart);
+
+ return ((uint64_t)hi << 32) | lo;
+}
+
+/* RV32 mtimecmp write, safe sequence: park the low word at all-ones
+ first, so no intermediate 64-bit value compares below mtime and
+ raises a spurious timer interrupt. */
+static void clint_timecmp_write(uintptr_t hart, uint64_t value)
+{
+ MTIMECMP_LO(hart) = 0xFFFFFFFF;
+ MTIMECMP_HI(hart) = (uint32_t)(value >> 32);
+ MTIMECMP_LO(hart) = (uint32_t)value;
+}
+
+#else /* __riscv_xlen == 64 */
+
+/* RV64: naturally aligned 64-bit MMIO accesses are single loads/stores. */
+static uint64_t clint_time_read(void)
+{
+ return *(volatile uint64_t *)CLINT_TIME;
+}
+
+static uint64_t clint_timecmp_read(uintptr_t hart)
+{
+ return *(volatile uint64_t *)CLINT_TIMECMP(hart);
+}
+
+static void clint_timecmp_write(uintptr_t hart, uint64_t value)
+{
+ *(volatile uint64_t *)CLINT_TIMECMP(hart) = value;
+}
+
+#endif /* __riscv_xlen */
+
int hwtimer_init(void)
{
uintptr_t hart = riscv_get_core();
- uint64_t time = *((volatile uint64_t *)CLINT_TIME);
- *((volatile uint64_t *)CLINT_TIMECMP(hart)) = time + TICKNUM_PER_TIMER;
+
+ clint_timecmp_write(hart, clint_time_read() + TICKNUM_PER_TIMER);
return 0;
}
int hwtimer_handler(void)
{
uintptr_t hart = riscv_get_core();
- uint64_t time = *((volatile uint64_t *)CLINT_TIME);
- *((volatile uint64_t *)CLINT_TIMECMP(hart)) = time + TICKNUM_PER_TIMER;
+
+ /* Absolute re-arm: advance from the previous compare value, so trap
+ latency does not accumulate as tick drift; catch up if the next
+ compare already passed. */
+ uint64_t next = clint_timecmp_read(hart) + TICKNUM_PER_TIMER;
+ uint64_t now = clint_time_read();
+
+ if (next <= now)
+ next = now + TICKNUM_PER_TIMER;
+
+ clint_timecmp_write(hart, next);
return 0;
}
diff --git a/test/tx/cmake/riscv/bsp/hwtimer.h b/test/tx/cmake/riscv/bsp/hwtimer.h
index e6b9efa4..bde0bba5 100644
--- a/test/tx/cmake/riscv/bsp/hwtimer.h
+++ b/test/tx/cmake/riscv/bsp/hwtimer.h
@@ -13,8 +13,8 @@
#include <stdint.h>
-#define TICKNUM_PER_SECOND 10000000
-#define TICKNUM_PER_TIMER (TICKNUM_PER_SECOND / 10)
+#define TICKNUM_PER_SECOND 10000000UL
+#define TICKNUM_PER_TIMER (TICKNUM_PER_SECOND / TX_TIMER_TICKS_PER_SECOND)
int hwtimer_init(void);
int hwtimer_handler(void);
diff --git a/test/tx/cmake/riscv/bsp/link.lds b/test/tx/cmake/riscv/bsp/link.lds
index 42f08fb6..717530b3 100644
--- a/test/tx/cmake/riscv/bsp/link.lds
+++ b/test/tx/cmake/riscv/bsp/link.lds
@@ -12,6 +12,12 @@ MEMORY
RAM (rwx) : ORIGIN = 0x80000000, LENGTH = 128M
}
+PHDRS
+{
+ text PT_LOAD FLAGS(5);
+ data PT_LOAD FLAGS(6);
+}
+
SECTIONS
{
. = 0x80000000;
@@ -21,23 +27,24 @@ SECTIONS
*(.text .text.*)
. = ALIGN(0x1000);
PROVIDE(etext = .);
- } > RAM
+ } > RAM :text
.rodata : {
. = ALIGN(16);
*(.srodata .srodata.*)
. = ALIGN(16);
*(.rodata .rodata.*)
- } > RAM
+ } > RAM :text
.data : {
- . = ALIGN(16);
+ . = ALIGN(0x1000);
+ PROVIDE(__global_pointer$ = . + 0x800);
*(.sdata .sdata.*)
. = ALIGN(16);
*(.data .data.*)
- } > RAM
+ } > RAM :data
- .bss : {
+ .bss (NOLOAD) : {
. = ALIGN(16);
_bss_start = .;
*(.sbss .sbss.*)
@@ -45,14 +52,23 @@ SECTIONS
*(.bss .bss.*)
*(COMMON)
_bss_end = .;
- } > RAM
+ } > RAM :data
- .stack : {
+ .stack (NOLOAD) : {
. = ALIGN(4096);
_sysstack_start = .;
. += 0x4000;
_sysstack_end = .;
- } > RAM
+ } > RAM :data
+
+ /* Fixed 64 KiB newlib sbrk heap window. _end stays after it, so the
+ ThreadX first-unused-memory pointer starts past the heap. */
+ .heap (NOLOAD) : {
+ . = ALIGN(16);
+ _heap_start = .;
+ . += 0x10000;
+ _heap_end = .;
+ } > RAM :data
PROVIDE(_end = .);
diff --git a/test/tx/cmake/riscv/bsp/plic.c b/test/tx/cmake/riscv/bsp/plic.c
index a2b44fec..16dad815 100644
--- a/test/tx/cmake/riscv/bsp/plic.c
+++ b/test/tx/cmake/riscv/bsp/plic.c
@@ -52,32 +52,47 @@ int plic_unregister_callback(int irqno)
int plic_init(void)
{
+ uintptr_t hart = riscv_get_core();
+
for (int i = 0; i < MAX_CALLBACK_NUM; i++)
{
callbacks[i] = NULL;
}
+
+ /* Do not depend on the reset state or on a prior boot stage: accept
+ every priority (threshold 0) and start with all sources for this
+ hart disabled. */
+ *(volatile uint32_t *)PLIC_MPRIORITY(hart) = 0;
+ for (int w = 0; w < MAX_CALLBACK_NUM / 32; w++)
+ *(volatile uint32_t *)(PLIC_MENABLE(hart) + w * 4) = 0;
+
return 0;
}
int plic_claim(void)
{
uintptr_t hart = riscv_get_core();
- return (int)(*(uint32_t *)PLIC_MCLAIM(hart));
+ return (int)(*(volatile uint32_t *)PLIC_MCLAIM(hart));
}
void plic_complete(int irqno)
{
uintptr_t hart = riscv_get_core();
- *(uint32_t *)(PLIC_MCOMPLETE(hart)) = (uint32_t)irqno;
+ *(volatile uint32_t *)(PLIC_MCOMPLETE(hart)) = (uint32_t)irqno;
}
int plic_irq_intr(void)
{
int ret = -1;
int irqno = plic_claim();
- if (irqno <= 0 || irqno >= MAX_CALLBACK_NUM) {
- plic_complete(irqno);
- return 0; // spurious or out-of-range, not an error
+
+ if (irqno == 0)
+ return 0;
+
+ if (irqno < 0 || irqno >= MAX_CALLBACK_NUM) {
+ if (irqno > 0)
+ plic_complete(irqno);
+ return -1;
}
if (callbacks[irqno] != NULL)
ret = (callbacks[irqno])(irqno);
diff --git a/test/tx/cmake/riscv/bsp/syscalls.c b/test/tx/cmake/riscv/bsp/syscalls.c
index 802fe8af..40ca86f3 100644
--- a/test/tx/cmake/riscv/bsp/syscalls.c
+++ b/test/tx/cmake/riscv/bsp/syscalls.c
@@ -15,6 +15,7 @@
#include <sys/stat.h>
#include <errno.h>
#include <stdint.h>
+#include <time.h>
#define UART0_THR (*(volatile unsigned char *)0x10000000L)
#define UART0_LSR (*(volatile unsigned char *)0x10000005L)
@@ -25,6 +26,39 @@
#define VIRT_TEST_PASS 0x5555u
#define VIRT_TEST_FAIL 0x3333u
+#define QEMU_TIMEBASE_HZ 10000000ULL
+
+static uint64_t read_time_counter(void)
+{
+#if __riscv_xlen == 64
+ uint64_t value;
+ __asm__ volatile("rdtime %0" : "=r" (value));
+ return value;
+#else
+ uint32_t high;
+ uint32_t low;
+ uint32_t high_check;
+
+ do
+ {
+ __asm__ volatile("rdtimeh %0" : "=r" (high));
+ __asm__ volatile("rdtime %0" : "=r" (low));
+ __asm__ volatile("rdtimeh %0" : "=r" (high_check));
+ } while (high != high_check);
+
+ return ((uint64_t)high << 32) | low;
+#endif
+}
+
+time_t time(time_t *timer)
+{
+ time_t seconds = (time_t)(read_time_counter() / QEMU_TIMEBASE_HZ);
+
+ if (timer != NULL)
+ *timer = seconds;
+ return seconds;
+}
+
/* Called by testcontrol.c when EXTERNAL_EXIT is defined. */
__attribute__((noreturn)) void external_exit(unsigned int code)
{
@@ -50,13 +84,24 @@ int _write(int fd, const char *buf, int count)
return count;
}
-extern char _end[];
+/* Fixed 64 KiB heap window from the linker script. It keeps the newlib
+ heap out of the ThreadX first-unused-memory area, which starts at
+ _end (placed after _heap_end). */
+extern char _heap_start[];
+extern char _heap_end[];
static char *heap_ptr = 0;
void *_sbrk(int incr)
{
if (heap_ptr == 0)
- heap_ptr = _end;
+ heap_ptr = _heap_start;
+
+ if ((incr > 0 && _heap_end - heap_ptr < incr) ||
+ (incr < 0 && heap_ptr - _heap_start < -incr))
+ {
+ errno = ENOMEM;
+ return (void *)-1;
+ }
char *prev = heap_ptr;
heap_ptr += incr;
diff --git a/test/tx/cmake/riscv/bsp/trap.c b/test/tx/cmake/riscv/bsp/trap.c
index 0ac7990a..f54ea2ff 100644
--- a/test/tx/cmake/riscv/bsp/trap.c
+++ b/test/tx/cmake/riscv/bsp/trap.c
@@ -36,9 +36,6 @@ extern void _exit(int code) __attribute__((noreturn));
void trap_handler(uintptr_t mcause, uintptr_t mepc, uintptr_t mtval)
{
- (void)mepc;
- (void)mtval;
-
if (OS_IS_INTERUPT(mcause))
{
if (OS_IS_TICK_INT(mcause))
diff --git a/test/tx/cmake/riscv/bsp/tx_initialize_low_level.S b/test/tx/cmake/riscv/bsp/tx_initialize_low_level.S
index 8d474d66..d72c9b6a 100644
--- a/test/tx/cmake/riscv/bsp/tx_initialize_low_level.S
+++ b/test/tx/cmake/riscv/bsp/tx_initialize_low_level.S
@@ -1,5 +1,6 @@
/***************************************************************************
* Copyright (c) 2026 10xEngineers
+ * 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
@@ -8,10 +9,13 @@
* SPDX-License-Identifier: MIT
**************************************************************************/
+// Portions of this file were generated with AI assistance.
+
/* Trap entry and low-level init for RISC-V QEMU virt regression tests.
Supports both RV32 and RV64 via __riscv_xlen conditionals. */
#include "csr.h"
+#include "tx_port.h"
#if __riscv_xlen == 64
#define STORE sd
@@ -33,13 +37,28 @@
.extern trap_handler
.extern _tx_thread_context_restore
-trap_entry:
+/* Take the interrupt frame size from the port that this BSP is linked
+ against, so the two can never disagree. A port that publishes no
+ contract keeps the historical layout. */
+#ifndef TX_RISCV_TRAP_FRAME_SIZE
#if defined(__riscv_float_abi_single) || defined(__riscv_float_abi_double)
- addi sp, sp, -(65 * REGBYTES)
+#define TX_RISCV_TRAP_FRAME_SIZE (65 * REGBYTES)
#else
- addi sp, sp, -(32 * REGBYTES)
+#define TX_RISCV_TRAP_FRAME_SIZE (32 * REGBYTES)
+#endif
+#endif
+
+/* The RISC-V psABI requires sp to be 16-byte aligned at a call. */
+#ifndef TX_RISCV_TRAP_CALL_FRAME_SIZE
+#define TX_RISCV_TRAP_CALL_FRAME_SIZE 16
#endif
+ .equ TX_TRAP_FRAME_SIZE, TX_RISCV_TRAP_FRAME_SIZE
+ .equ TX_TRAP_CALL_FRAME_SIZE, TX_RISCV_TRAP_CALL_FRAME_SIZE
+
+trap_entry:
+ addi sp, sp, -TX_TRAP_FRAME_SIZE
+
STORE x1, (28 * REGBYTES)(sp)
call _tx_thread_context_save
@@ -47,11 +66,11 @@ trap_entry:
csrr a0, mcause
csrr a1, mepc
csrr a2, mtval
- addi sp, sp, -REGBYTES
+ addi sp, sp, -TX_TRAP_CALL_FRAME_SIZE
STORE ra, 0(sp)
call trap_handler
LOAD ra, 0(sp)
- addi sp, sp, REGBYTES
+ addi sp, sp, TX_TRAP_CALL_FRAME_SIZE
call _tx_thread_context_restore
@@ -80,7 +99,7 @@ _tx_initialize_low_level:
csrrc zero, mstatus, t0
li t0, (MSTATUS_MPP_M | MSTATUS_MPIE)
csrrs zero, mstatus, t0
- li t0, (MIE_MTIE | MIE_MSIE | MIE_MEIE)
+ li t0, (MIE_MTIE | MIE_MEIE) // No MSIE: software interrupts are never dispatched here
csrrs zero, mie, t0
#ifdef __riscv_flen
@@ -89,11 +108,11 @@ _tx_initialize_low_level:
fscsr x0
#endif
- addi sp, sp, -REGBYTES
+ addi sp, sp, -TX_TRAP_CALL_FRAME_SIZE
STORE ra, 0(sp)
call board_init
LOAD ra, 0(sp)
- addi sp, sp, REGBYTES
+ addi sp, sp, TX_TRAP_CALL_FRAME_SIZE
la t0, trap_entry
csrw mtvec, t0
diff --git a/test/tx/cmake/riscv/regression/CMakeLists.txt b/test/tx/cmake/riscv/regression/CMakeLists.txt
index 80b6e3bd..b15247ff 100644
--- a/test/tx/cmake/riscv/regression/CMakeLists.txt
+++ b/test/tx/cmake/riscv/regression/CMakeLists.txt
@@ -110,11 +110,17 @@ set(regression_test_cases
${SOURCE_DIR}/threadx_timer_multiple_accuracy_test.c
${SOURCE_DIR}/threadx_timer_multiple_test.c
${SOURCE_DIR}/threadx_timer_simple_test.c
- # threadx_trace_basic_test.c excluded: references tx_saved_posture
- # (the Linux port's interrupt save variable) which is named
- # interrupt_save on RISC-V.
+ ${SOURCE_DIR}/threadx_trace_basic_test.c
)
+# Architecture specific tests living next to this file. The RV32 stack
+# builder still leaves the mstatus slot of the frame unwritten, so the new
+# thread FPU state test only applies to RV64 for now.
+if(THREADX_ARCH STREQUAL "risc-v64")
+ list(APPEND regression_test_cases
+ ${CMAKE_CURRENT_LIST_DIR}/threadx_riscv_new_thread_fpu_state_test.c)
+endif()
+
# Tests that provide their own main() and must NOT link with testcontrol.
set(standalone_test_cases
${SOURCE_DIR}/threadx_initialize_kernel_setup_test.c
diff --git a/test/tx/cmake/riscv/regression/threadx_riscv_new_thread_fpu_state_test.c b/test/tx/cmake/riscv/regression/threadx_riscv_new_thread_fpu_state_test.c
new file mode 100644
index 00000000..b388ad62
--- /dev/null
+++ b/test/tx/cmake/riscv/regression/threadx_riscv_new_thread_fpu_state_test.c
@@ -0,0 +1,278 @@
+/***************************************************************************/
+/* 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 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 */
+/***************************************************************************/
+
+/* This test is designed to verify that a thread created after another
+ thread has used the floating point registers starts with a clean
+ floating point state.
+
+ The interrupt stack frame reserves slot 29 for mstatus, and the context
+ restore path uses the FS field of that slot to decide whether the saved
+ floating point registers have to be recovered. When the stack builder
+ leaves the slot untouched, the decision is made on whatever the thread
+ stack happened to contain, so the new thread can silently inherit the
+ floating point registers of the thread that ran before it.
+
+ Thread 0 fills every floating point register with a non-zero value and
+ then creates thread 1. The test checks that the stack builder wrote the
+ mstatus slot at all, that the slot describes a live floating point state,
+ and that thread 1 observes all floating point registers as zero once it
+ preempts thread 0.
+
+ An unwritten slot still holds the stack fill pattern, so it is detected
+ by comparing the slot against a stack word the builder never touches. */
+
+#include <stdio.h>
+#include "tx_api.h"
+
+
+/* The mstatus slot of the interrupt stack frame. */
+
+#define FRAME_MSTATUS_INDEX 29
+
+
+#if defined(__riscv_float_abi_double)
+
+#define FP_SUPPORTED 1
+#define FP_LOAD "fld"
+#define FP_STORE "fsd"
+#define FP_WORD_SIZE "8"
+typedef double FP_WORD;
+
+#elif defined(__riscv_float_abi_single)
+
+#define FP_SUPPORTED 1
+#define FP_LOAD "flw"
+#define FP_STORE "fsw"
+#define FP_WORD_SIZE "4"
+typedef float FP_WORD;
+
+#else
+
+#define FP_SUPPORTED 0
+
+#endif
+
+
+#if FP_SUPPORTED
+
+/* Walk f0-f31 in register number order so that the index is both the
+ register number and the slot in the state buffers. */
+
+#define FP_REG_LIST(op) \
+ op(ft0, 0) op(ft1, 1) op(ft2, 2) op(ft3, 3) \
+ op(ft4, 4) op(ft5, 5) op(ft6, 6) op(ft7, 7) \
+ op(fs0, 8) op(fs1, 9) op(fa0, 10) op(fa1, 11) \
+ op(fa2, 12) op(fa3, 13) op(fa4, 14) op(fa5, 15) \
+ op(fa6, 16) op(fa7, 17) op(fs2, 18) op(fs3, 19) \
+ op(fs4, 20) op(fs5, 21) op(fs6, 22) op(fs7, 23) \
+ op(fs8, 24) op(fs9, 25) op(fs10, 26) op(fs11, 27) \
+ op(ft8, 28) op(ft9, 29) op(ft10, 30) op(ft11, 31)
+
+#define FP_LOAD_ONE(reg, index) FP_LOAD " " #reg ", " #index "*" FP_WORD_SIZE "(%0)\n"
+#define FP_STORE_ONE(reg, index) FP_STORE " " #reg ", " #index "*" FP_WORD_SIZE "(%0)\n"
+#define FP_CLOBBER_ONE(reg, index) #reg,
+
+#define FP_REGISTERS 32
+
+static FP_WORD dirty_pattern[FP_REGISTERS];
+static FP_WORD thread_1_fp_state[FP_REGISTERS];
+static unsigned long thread_1_fp_dirty = 0;
+
+#endif
+
+
+static TX_THREAD thread_0;
+static TX_THREAD thread_1;
+
+/* Give thread 1 a private stack so it can be cleared before the thread is
+ created. */
+
+static unsigned long thread_1_stack[TEST_STACK_SIZE_PRINTF/sizeof(unsigned long)];
+
+static unsigned long thread_1_executed = 0;
+
+
+/* Define thread prototypes. */
+
+static void thread_0_entry(ULONG thread_input);
+static void thread_1_entry(ULONG thread_input);
+
+
+/* Prototype for test control return. */
+void test_control_return(UINT status);
+
+
+/* Define what the initial system looks like. */
+
+#ifdef CTEST
+void test_application_define(void *first_unused_memory)
+#else
+void threadx_riscv_new_thread_fpu_state_application_define(void *first_unused_memory)
+#endif
+{
+
+UINT status;
+CHAR *pointer;
+
+
+ /* Put first available memory address into a character pointer. */
+ pointer = (CHAR *) first_unused_memory;
+
+ status = tx_thread_create(&thread_0, "thread 0", thread_0_entry, 0,
+ pointer, TEST_STACK_SIZE_PRINTF,
+ 16, 16, TX_NO_TIME_SLICE, TX_AUTO_START);
+
+ /* Check for status. */
+ if (status != TX_SUCCESS)
+ {
+
+ printf("Running RISC-V New Thread FPU State Test............................ ERROR #1\n");
+ test_control_return(1);
+ }
+}
+
+
+
+/* Define the test threads. */
+
+static void thread_0_entry(ULONG thread_input)
+{
+
+UINT status;
+unsigned long *frame;
+unsigned long stack_fill;
+unsigned long frame_fs;
+#if FP_SUPPORTED
+UINT i;
+#endif
+
+
+ /* Inform user. */
+ printf("Running RISC-V New Thread FPU State Test............................ ");
+
+#if FP_SUPPORTED
+
+ /* Build a pattern that is non-zero in every register. */
+ for (i = 0; i < FP_REGISTERS; i++)
+ {
+
+ dirty_pattern[i] = (FP_WORD) (i + 1);
+ }
+
+ /* Make the floating point state of this thread dirty. */
+ __asm__ volatile (FP_REG_LIST(FP_LOAD_ONE)
+ :
+ : "r" (dirty_pattern)
+ : FP_REG_LIST(FP_CLOBBER_ONE) "memory");
+#endif
+
+ /* Create thread 1 without starting it, so the built frame can be
+ inspected before the thread is scheduled. */
+ status = tx_thread_create(&thread_1, "thread 1", thread_1_entry, 0,
+ thread_1_stack, sizeof(thread_1_stack),
+ 15, 15, TX_NO_TIME_SLICE, TX_DONT_START);
+
+ /* Check for status. */
+ if (status != TX_SUCCESS)
+ {
+
+ printf("ERROR #2\n");
+ test_control_return(1);
+ }
+
+ /* The bottom of the stack is never part of the frame, so it still holds
+ the fill pattern that thread creation left behind. */
+ frame = (unsigned long *) thread_1.tx_thread_stack_ptr;
+ stack_fill = thread_1_stack[0];
+
+ /* The stack builder must have written the mstatus slot. */
+ if (frame[FRAME_MSTATUS_INDEX] == stack_fill)
+ {
+
+ printf("ERROR #3\n");
+ test_control_return(1);
+ }
+
+ /* The built frame must describe a live floating point state. */
+ frame_fs = (frame[FRAME_MSTATUS_INDEX] >> 13) & 0x3UL;
+
+ if (frame_fs == 0)
+ {
+
+ printf("ERROR #4\n");
+ test_control_return(1);
+ }
+
+ /* Let thread 1 preempt and run to completion. */
+ status = tx_thread_resume(&thread_1);
+
+ /* Check for status. */
+ if ((status != TX_SUCCESS) || (thread_1_executed != 1))
+ {
+
+ printf("ERROR #5\n");
+ test_control_return(1);
+ }
+
+#if FP_SUPPORTED
+
+ /* Thread 1 must not have inherited the floating point registers of
+ thread 0. */
+ if (thread_1_fp_dirty)
+ {
+
+ printf("ERROR #6\n");
+ test_control_return(1);
+ }
+#endif
+
+ /* Successful test. */
+ printf("SUCCESS!\n");
+ test_control_return(0);
+}
+
+
+static void thread_1_entry(ULONG thread_input)
+{
+
+#if FP_SUPPORTED
+
+const unsigned char *state_bytes;
+UINT i;
+
+
+ /* Capture the floating point registers before anything else can use
+ them. */
+ __asm__ volatile (FP_REG_LIST(FP_STORE_ONE)
+ :
+ : "r" (thread_1_fp_state)
+ : "memory");
+
+ state_bytes = (const unsigned char *) thread_1_fp_state;
+
+ for (i = 0; i < (UINT) sizeof(thread_1_fp_state); i++)
+ {
+
+ if (state_bytes[i] != 0)
+ {
+
+ thread_1_fp_dirty = 1;
+ break;
+ }
+ }
+#endif
+
+ thread_1_executed = 1;
+}
diff --git a/test/tx/cmake/riscv/run.sh b/test/tx/cmake/riscv/run.sh
index 3b0905e7..0e4fb7a1 100755
--- a/test/tx/cmake/riscv/run.sh
+++ b/test/tx/cmake/riscv/run.sh
@@ -61,7 +61,11 @@ function generate() {
function build() {
local arch=$1
local build=$2
- cmake --build "build/${arch}_${build}"
+ # -k 0 keeps Ninja going after a target fails, so one broken target no
+ # longer decides whether the targets after it exist. ctest reports a
+ # missing binary as a failing test, which turned a single link error into
+ # a failure count that varied with build scheduling order.
+ cmake --build "build/${arch}_${build}" -- -k 0
}
function run_test() {
@@ -69,13 +73,20 @@ function run_test() {
local build=$2
local parallel_jobs=$3
- pushd "build/${arch}_${build}"
+ # Guard the pushd: with the caller capturing this function's status, set -e
+ # no longer aborts here, so a missing build directory would otherwise let
+ # ctest run in the source tree and report "no tests" as success.
+ pushd "build/${arch}_${build}" || return 1
[ -z "${CTEST_PARALLEL_LEVEL}" ] && parallel="-j${parallel_jobs}"
if [ -z "${CTEST_REPEAT_FAIL}" ]; then
repeat_fail=2
else
repeat_fail=${CTEST_REPEAT_FAIL}
fi
+ # ctest's status is captured and returned past the popd. popd succeeds, so
+ # without this the function would return 0 and a failing configuration
+ # would be reported as a pass.
+ local status=0
ctest $parallel --timeout 1000 \
-O "${arch}_${build}.txt" \
-T test --no-compress-output \
@@ -83,8 +94,9 @@ function run_test() {
--test-output-size-failed 4194304 \
--output-on-failure \
--repeat until-pass:${repeat_fail} \
- --output-junit "${arch}_${build}.xml"
+ --output-junit "${arch}_${build}.xml" || status=$?
popd
+ return $status
}
# Determine repo root and script directory
@@ -129,11 +141,16 @@ if [ "$command" == "build" ]; then
echo ""
done
+ # A failing configuration must not stop the ones after it: under set -e
+ # the loop would abort and leave them unbuilt, which then reads as a wall
+ # of missing-binary test failures. The status is accumulated and returned.
+ build_status=0
for item in $builds; do
echo "Building ${arch} ${item}"
- build "$arch" "$item"
+ build "$arch" "$item" || build_status=$?
echo ""
done
+ [ $build_status -eq 0 ] || exit $build_status
elif [ "$command" == "test" ]; then
cores=$(nproc)
if [ -z "${CTEST_PARALLEL_LEVEL}" ]; then
@@ -152,10 +169,16 @@ elif [ "$command" == "test" ]; then
done
exit $exit_code
else
+ # ctest's status is accumulated rather than allowed to abort the loop.
+ # set -e would otherwise stop at the first failing configuration and
+ # leave every configuration after it untested. This mirrors what the
+ # parallel branch above already does with wait.
+ exit_code=0
for item in $builds; do
echo "Testing ${arch} ${item}"
- run_test "$arch" "$item" "$parallel_jobs"
+ run_test "$arch" "$item" "$parallel_jobs" || exit_code=$?
done
+ exit $exit_code
fi
else
help
diff --git a/test/tx/cmake/thread_stack_alignment/CMakeLists.txt b/test/tx/cmake/thread_stack_alignment/CMakeLists.txt
new file mode 100644
index 00000000..4040a093
--- /dev/null
+++ b/test/tx/cmake/thread_stack_alignment/CMakeLists.txt
@@ -0,0 +1,89 @@
+##############################################################################
+# 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 Codex (gpt-6.1-sol).
+# 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
+##############################################################################
+
+cmake_minimum_required(VERSION 3.13 FATAL_ERROR)
+project(thread_stack_alignment_test LANGUAGES C)
+
+set(CMAKE_C_STANDARD 99)
+set(CMAKE_C_STANDARD_REQUIRED ON)
+set(CMAKE_C_EXTENSIONS OFF)
+
+if(NOT CMAKE_SYSTEM_NAME STREQUAL "Linux" OR
+ NOT CMAKE_SYSTEM_PROCESSOR MATCHES "^(x86_64|amd64|AMD64)$" OR
+ NOT CMAKE_C_COMPILER_ID MATCHES "^(GNU|Clang)$")
+ message(FATAL_ERROR "Stack alignment regression requires native x86_64 Linux/GNU port widths")
+endif()
+
+# The host suite normally builds with -m32 and configuration-specific TX_ macros.
+# Keep that coverage intact, but compile this directory independently at native64
+# width: the defect requires ALIGN_TYPE to be wider than ULONG. Clear only this
+# directory's inherited options/definitions, then select the tested features.
+set_property(DIRECTORY PROPERTY COMPILE_OPTIONS "")
+set_property(DIRECTORY PROPERTY LINK_OPTIONS "")
+set_property(DIRECTORY PROPERTY COMPILE_DEFINITIONS "")
+add_compile_options(-m64 -Wall -Wextra -ffunction-sections -fdata-sections)
+add_link_options(-m64 -Wl,--gc-sections)
+
+enable_testing()
+get_filename_component(REPO_ROOT "${CMAKE_CURRENT_LIST_DIR}/../../../.." ABSOLUTE)
+set(TEST_SOURCE_DIR ${REPO_ROOT}/test/tx/thread_stack_alignment)
+
+# Enforce warning-free compilation for the new tests and changed service.
+# Unchanged MISRA/kernel sources keep their existing native64 diagnostics.
+set_source_files_properties(
+ ${TEST_SOURCE_DIR}/threadx_thread_stack_alignment_test.c
+ ${TEST_SOURCE_DIR}/threadx_thread_stack_alignment_schedule_test.c
+ ${REPO_ROOT}/common/src/tx_thread_create.c
+ PROPERTIES COMPILE_OPTIONS -Werror)
+
+foreach(mode misra control)
+ set(test_target tx_stack_alignment_${mode})
+ add_executable(${test_target}
+ ${TEST_SOURCE_DIR}/threadx_thread_stack_alignment_test.c
+ ${REPO_ROOT}/common/src/tx_thread_create.c
+ ${REPO_ROOT}/common/src/tx_misra.c)
+ target_include_directories(${test_target} PRIVATE
+ ${REPO_ROOT}/common/inc ${REPO_ROOT}/ports/linux/gnu/inc)
+ target_compile_definitions(${test_target} PRIVATE TX_ENABLE_STACK_CHECKING)
+ if(mode STREQUAL "misra")
+ target_compile_definitions(${test_target} PRIVATE TX_MISRA_ENABLE)
+ endif()
+ add_test(NAME thread_stack_alignment_native64_${mode} COMMAND ${test_target})
+ set_tests_properties(thread_stack_alignment_native64_${mode}
+ PROPERTIES TIMEOUT 20 LABELS "native64;stack_alignment")
+endforeach()
+
+# Use the existing source lists to build a separate real Linux kernel. This
+# target also works when the parent already owns the normal native32 threadx.
+set(PROJECT_NAME tx_stack_alignment_linux)
+add_library(${PROJECT_NAME} STATIC)
+add_subdirectory(${REPO_ROOT}/common common_native64)
+add_subdirectory(${REPO_ROOT}/ports/linux/gnu linux_native64)
+target_compile_definitions(${PROJECT_NAME} PUBLIC
+ TX_MISRA_ENABLE TX_ENABLE_STACK_CHECKING TX_TIMER_PROCESS_IN_ISR)
+target_link_libraries(${PROJECT_NAME} PUBLIC pthread)
+
+add_executable(tx_stack_alignment_schedule
+ ${TEST_SOURCE_DIR}/threadx_thread_stack_alignment_schedule_test.c)
+target_link_libraries(tx_stack_alignment_schedule PRIVATE tx_stack_alignment_linux)
+target_link_options(tx_stack_alignment_schedule PRIVATE -Wl,--wrap=_tx_thread_stack_build)
+
+# Each process owns one kernel instance, with a fresh worker and observer.
+foreach(offset RANGE 0 3)
+ add_test(NAME thread_stack_alignment_native64_schedule_${offset}
+ COMMAND tx_stack_alignment_schedule ${offset})
+ set_tests_properties(thread_stack_alignment_native64_schedule_${offset}
+ PROPERTIES TIMEOUT 20 RUN_SERIAL TRUE LABELS "native64;stack_alignment")
+endforeach()
diff --git a/test/tx/cmake/thread_transition/CMakeLists.txt b/test/tx/cmake/thread_transition/CMakeLists.txt
new file mode 100644
index 00000000..342ca839
--- /dev/null
+++ b/test/tx/cmake/thread_transition/CMakeLists.txt
@@ -0,0 +1,131 @@
+# Portions of this file were generated with AI assistance.
+
+cmake_minimum_required(VERSION 3.13 FATAL_ERROR)
+cmake_policy(SET CMP0057 NEW)
+
+project(thread_transition_test LANGUAGES C)
+
+set(REPO_ROOT ${CMAKE_CURRENT_LIST_DIR}/../../../..)
+set(SOURCE_DIR ${REPO_ROOT}/test/tx/thread_transition)
+
+# TX_NOT_INTERRUPTABLE and TX_DISABLE_ERROR_CHECKING are not among the five build
+# configurations this tree compiles, and the tree builds the whole ThreadX library
+# once per configuration, so neither can be reached from inside the regression
+# suite. Both matter to the completion and termination transitions:
+#
+# - TX_NOT_INTERRUPTABLE selects a different transition, in which the exit
+# notification callback runs with interrupts disabled and the interruptable
+# suspension path -- which is what sets and clears the transition marker in the
+# configurations the tree does build -- is not used at all. The marker is set
+# and cleared there by stores that exist only in that configuration, and a
+# marker left set would make a normally completed thread permanently
+# undeletable.
+#
+# - TX_DISABLE_ERROR_CHECKING removes the _txe_ wrappers, so the public names bind
+# straight to the core services. The refusal lives in the core services for that
+# reason, and the way to show it is to call the public names in a build with no
+# wrappers.
+#
+# So the four sources that change are compiled directly into a test executable, once
+# per combination, with recorders standing behind the scheduler services they call.
+# That is the same technique the module manager tests in this tree use, for the same
+# reason: the code under test cannot be reached through the library the suite links.
+#
+# These executables are deliberately left out of the coverage instrumentation. The
+# same source compiled under different feature macros has a different line set, and
+# merging those into the union the coverage report takes would confuse the figure
+# rather than add to it. The coverage figure belongs to the five configurations the
+# tree builds, and every line this change adds outside a TX_NOT_INTERRUPTABLE guard
+# is in them.
+
+set(transition_kernel_sources
+ ${REPO_ROOT}/common/src/tx_thread_shell_entry.c
+ ${REPO_ROOT}/common/src/tx_thread_terminate.c
+ ${REPO_ROOT}/common/src/tx_thread_delete.c
+ ${REPO_ROOT}/common/src/tx_thread_reset.c
+ ${REPO_ROOT}/common/src/txe_thread_delete.c
+ ${REPO_ROOT}/common/src/txe_thread_reset.c
+ ${REPO_ROOT}/common/src/txe_thread_terminate.c
+ ${REPO_ROOT}/common/src/tx_thread_initialize.c)
+
+set(transition_sources
+ ${SOURCE_DIR}/threadx_thread_transition_configuration_test.c
+ ${transition_kernel_sources})
+
+# The shim goes on the kernel sources only, not on the test. It defines
+# TX_SOURCE_CODE, which is what tells tx_api.h to leave the public service names
+# unmapped so that internal sources can use the core names; the test needs the
+# opposite, because calling the public names and letting tx_api.h decide what they
+# bind to is exactly what the TX_DISABLE_ERROR_CHECKING configuration is here to
+# demonstrate.
+if(MSVC)
+ set(transition_kernel_compile_options
+ "/FI${SOURCE_DIR}/threadx_thread_transition_host_test_port.h")
+ set(transition_undefine_options /UTX_ENABLE_EVENT_TRACE
+ /UTX_ENABLE_EVENT_LOG)
+else()
+ set(transition_kernel_compile_options
+ "-include;${SOURCE_DIR}/threadx_thread_transition_host_test_port.h")
+ set(transition_undefine_options -UTX_ENABLE_EVENT_TRACE
+ -UTX_ENABLE_EVENT_LOG)
+endif()
+
+set_source_files_properties(
+ ${transition_kernel_sources}
+ DIRECTORY ${CMAKE_CURRENT_LIST_DIR}
+ PROPERTIES COMPILE_OPTIONS "${transition_kernel_compile_options}")
+
+# Each entry is a test-name suffix, a colon, and the feature macros that define the
+# configuration, separated by "|". A semicolon cannot be used as that separator: it
+# is CMake's own list separator, so the foreach below would iterate the macros
+# instead of the configurations.
+set(transition_configurations
+ "ni:TX_NOT_INTERRUPTABLE"
+ "ni_no_error_checking:TX_NOT_INTERRUPTABLE|TX_DISABLE_ERROR_CHECKING"
+ "ni_no_notify:TX_NOT_INTERRUPTABLE|TX_DISABLE_NOTIFY_CALLBACKS"
+ "no_error_checking:TX_DISABLE_ERROR_CHECKING")
+
+foreach(configuration ${transition_configurations})
+
+ string(REPLACE ":" ";" configuration_parts ${configuration})
+ list(GET configuration_parts 0 configuration_name)
+ list(GET configuration_parts 1 configuration_macro_text)
+ string(REPLACE "|" ";" configuration_macros ${configuration_macro_text})
+
+ set(test_name threadx_thread_transition_${configuration_name}_test)
+ # Keep the internal target name short. CMake includes it in every object path,
+ # and the descriptive test name can otherwise reach the legacy MSVC MAX_PATH
+ # boundary in a normally nested Windows worktree.
+ set(test_target tx_tt_${configuration_name})
+
+ add_executable(${test_target} ${transition_sources})
+
+ target_include_directories(
+ ${test_target}
+ PRIVATE ${SOURCE_DIR}
+ ${REPO_ROOT}/common/inc
+ ${REPO_ROOT}/ports/${THREADX_ARCH}/${THREADX_TOOLCHAIN}/inc)
+
+ target_compile_definitions(${test_target} PRIVATE ${configuration_macros})
+
+ # This directory is configured once per build configuration of the tree, so
+ # these executables inherit whichever feature macros that configuration sets --
+ # which is useful, since it means the two configurations above are also seen in
+ # combination with stack checking and with the other four. The one that cannot
+ # be inherited is event tracing: the trace macros in these sources reference the
+ # trace component's buffer and registry, and this harness links four kernel
+ # sources rather than the library, so the references would not resolve. The
+ # trace insertions in these same sources are covered by the tree's own
+ # trace_build configuration, where the whole library is linked, so nothing is
+ # lost by turning tracing off here. Event logging is turned off for the same
+ # reason; no configuration of the tree enables it.
+ #
+ # The undefine options have to reach the test source as well as the kernel sources,
+ # because TX_ENABLE_EVENT_TRACE is visible to tx_api.h and the two must agree on
+ # what the headers declare. Target compile options land after the directory's
+ # definition flags on the command line, which is what makes them effective.
+ target_compile_options(${test_target} PRIVATE ${transition_undefine_options})
+
+ add_test(${CMAKE_BUILD_TYPE}::${test_name} ${test_target})
+
+endforeach()
diff --git a/test/tx/module_manager/threadx_module_manager_block_pool_parameters_test.c b/test/tx/module_manager/threadx_module_manager_block_pool_parameters_test.c
new file mode 100644
index 00000000..4fbd3e50
--- /dev/null
+++ b/test/tx/module_manager/threadx_module_manager_block_pool_parameters_test.c
@@ -0,0 +1,1452 @@
+/***************************************************************************
+ * 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
+ **************************************************************************/
+
+
+/**************************************************************************/
+/**************************************************************************/
+/** */
+/** ThreadX Test */
+/** */
+/** Module Manager extra-parameter array range validation */
+/** */
+/**************************************************************************/
+/**************************************************************************/
+
+/* A kernel service with more parameters than the module-to-manager call carries
+ receives the surplus in an array the module library builds and the module passes by
+ pointer. The Module Manager's dispatch layer proves that array lies inside the
+ requesting module's data before it reads any of it, by handing the port's data check
+ the array's address and its length as sizeof(ALIGN_TYPE[n]).
+
+ Block-pool create declared n as 2 and then read indices 0 through 3. The two words it
+ did not prove were read twice each: once by the dispatch layer's own buffer check,
+ which takes index 1 as a pool start and index 2 as the length to range-check it over,
+ and once when the argument list was built. So the out-of-bounds read was performed by
+ the validation code, before the kernel service was reached at all, and the size the
+ caller's pool start was then validated against was itself a word the manager had not
+ proved the module owned. Its two siblings have always been right: queue create
+ validates four words for the same four-word shape, and byte-pool create validates
+ three for three.
+
+ This test drives all three create dispatchers and measures the extent each one
+ validates, rather than checking cases either side of it. For each region a module's
+ parameter array may legitimately live in, it anchors the array at the end of the
+ region and walks it backwards a byte at a time; the smallest amount of room the
+ dispatcher accepts is the extent it validated. It must be four words for block-pool
+ create and queue create and three for byte-pool create, which is one word per index
+ the dispatcher goes on to use -- and the highest index each one uses is measured too,
+ through what the stubbed service records, so the invariant the three are checked
+ against is measured on both sides rather than written down.
+
+ Two things make a dispatcher testable on the host at all, and both come from the
+ first test in this directory.
+
+ The dispatch table is a header of static functions, one per kernel service, each
+ wrapped in #ifndef TXM_<SERVICE>_CALL_NOT_USED so that an integrator can refuse a
+ service to modules. There are 96 of those guards. Defining the 93 this test does not
+ exercise compiles the header down to the three create services under test, which is
+ what keeps the link to a handful of stubs instead of the hundred-odd kernel entry
+ points the whole table reaches. It also exercises that conditional compilation.
+
+ The extent has to reach a check that honours it. The manager's portable checks do,
+ and so does the inline data check of the Cortex-M module ports, which is the shape
+ the memory-protected module ports share: an address range is inside the module's
+ data, or inside a shared memory region registered to it, or it is not accepted. This
+ test therefore compiles against a Cortex-M module port's headers. The Cortex-A7
+ port's data check translates one address and ignores the length, so on that port no
+ extent -- right or wrong -- reaches anything; that is corrected separately, and the
+ defect this test covers is in the portable dispatch layer.
+
+ Both checks on this path are data-only. The array is checked with
+ ENSURE_INSIDE_MODULE_DATA and the pool start with PARAM_CHECK_BUFFER_WRITE, which is
+ that same check plus a null clause. Neither falls back to the portable code-region
+ check, so a parameter array or a pool start in the module's read-only image is
+ refused however much room is left, and the cases below assert that in both
+ directions: it is the reason this finding has nothing that survives a data check
+ which does not honour its length.
+
+ What a module can learn from the answers is measured here too, because it is what
+ separates a bounded over-read from a disclosure. Acceptance is decided by comparing
+ a length the module supplies against the room between a pool start the module also
+ supplies and the end of a region the module owns, and the dispatch layer's refusal
+ is TXM_MODULE_INVALID_MEMORY, which none of these services can return. So a module
+ that fixes the length and moves the pool start is running one comparison per request
+ against a threshold it chooses, and can bisect for the length. The cases below run
+ that bisection and record how far it gets, on a word the module owns -- which is no
+ disclosure at all. It becomes one only when composed with the extent measurement
+ above, which is what says that before the fix the word being bisected for came from
+ outside the module's region. Index 3 is weaker still: it reaches the service, which
+ compares it against sizeof(TX_BLOCK_POOL), so it yields one equality bit.
+
+ Nothing on this path disables interrupts, and the port shim counts every disable, so
+ a run that leaves the counter at zero is also the evidence that nothing here raises
+ _tx_thread_preempt_disable: in ThreadX that flag is only ever changed between a
+ TX_DISABLE and its TX_RESTORE. A fault taken during this over-read therefore costs
+ the requesting thread and not the system's ability to preempt. */
+
+#include <stdio.h>
+#include <string.h>
+
+#include "threadx_module_manager_host_test_port.h"
+
+#include "tx_thread.h"
+#include "tx_timer.h"
+#include "tx_queue.h"
+#include "tx_event_flags.h"
+#include "tx_semaphore.h"
+#include "tx_mutex.h"
+#include "tx_block_pool.h"
+#include "tx_byte_pool.h"
+#include "txm_module.h"
+#include "txm_module_manager_util.h"
+
+
+/* Refuse every service in the dispatch table except the three under test. The list is
+ the header's own guard names; a service added to the table appears here as a link
+ error naming the kernel entry point it reaches, which is the right way to find out. */
+
+#define TXM_BLOCK_ALLOCATE_CALL_NOT_USED
+#define TXM_BLOCK_POOL_DELETE_CALL_NOT_USED
+#define TXM_BLOCK_POOL_INFO_GET_CALL_NOT_USED
+#define TXM_BLOCK_POOL_PERFORMANCE_INFO_GET_CALL_NOT_USED
+#define TXM_BLOCK_POOL_PERFORMANCE_SYSTEM_INFO_GET_CALL_NOT_USED
+#define TXM_BLOCK_POOL_PRIORITIZE_CALL_NOT_USED
+#define TXM_BLOCK_RELEASE_CALL_NOT_USED
+#define TXM_BYTE_ALLOCATE_CALL_NOT_USED
+#define TXM_BYTE_POOL_DELETE_CALL_NOT_USED
+#define TXM_BYTE_POOL_INFO_GET_CALL_NOT_USED
+#define TXM_BYTE_POOL_PERFORMANCE_INFO_GET_CALL_NOT_USED
+#define TXM_BYTE_POOL_PERFORMANCE_SYSTEM_INFO_GET_CALL_NOT_USED
+#define TXM_BYTE_POOL_PRIORITIZE_CALL_NOT_USED
+#define TXM_BYTE_RELEASE_CALL_NOT_USED
+#define TXM_EVENT_FLAGS_CREATE_CALL_NOT_USED
+#define TXM_EVENT_FLAGS_DELETE_CALL_NOT_USED
+#define TXM_EVENT_FLAGS_GET_CALL_NOT_USED
+#define TXM_EVENT_FLAGS_INFO_GET_CALL_NOT_USED
+#define TXM_EVENT_FLAGS_PERFORMANCE_INFO_GET_CALL_NOT_USED
+#define TXM_EVENT_FLAGS_PERFORMANCE_SYSTEM_INFO_GET_CALL_NOT_USED
+#define TXM_EVENT_FLAGS_SET_CALL_NOT_USED
+#define TXM_EVENT_FLAGS_SET_NOTIFY_CALL_NOT_USED
+#define TXM_MODULE_OBJECT_ALLOCATE_CALL_NOT_USED
+#define TXM_MODULE_OBJECT_DEALLOCATE_CALL_NOT_USED
+#define TXM_MODULE_OBJECT_POINTER_GET_CALL_NOT_USED
+#define TXM_MODULE_OBJECT_POINTER_GET_EXTENDED_CALL_NOT_USED
+#define TXM_MUTEX_CREATE_CALL_NOT_USED
+#define TXM_MUTEX_DELETE_CALL_NOT_USED
+#define TXM_MUTEX_GET_CALL_NOT_USED
+#define TXM_MUTEX_INFO_GET_CALL_NOT_USED
+#define TXM_MUTEX_PERFORMANCE_INFO_GET_CALL_NOT_USED
+#define TXM_MUTEX_PERFORMANCE_SYSTEM_INFO_GET_CALL_NOT_USED
+#define TXM_MUTEX_PRIORITIZE_CALL_NOT_USED
+#define TXM_MUTEX_PUT_CALL_NOT_USED
+#define TXM_QUEUE_DELETE_CALL_NOT_USED
+#define TXM_QUEUE_FLUSH_CALL_NOT_USED
+#define TXM_QUEUE_FRONT_SEND_CALL_NOT_USED
+#define TXM_QUEUE_INFO_GET_CALL_NOT_USED
+#define TXM_QUEUE_PERFORMANCE_INFO_GET_CALL_NOT_USED
+#define TXM_QUEUE_PERFORMANCE_SYSTEM_INFO_GET_CALL_NOT_USED
+#define TXM_QUEUE_PRIORITIZE_CALL_NOT_USED
+#define TXM_QUEUE_RECEIVE_CALL_NOT_USED
+#define TXM_QUEUE_SEND_CALL_NOT_USED
+#define TXM_QUEUE_SEND_NOTIFY_CALL_NOT_USED
+#define TXM_SEMAPHORE_CEILING_PUT_CALL_NOT_USED
+#define TXM_SEMAPHORE_CREATE_CALL_NOT_USED
+#define TXM_SEMAPHORE_DELETE_CALL_NOT_USED
+#define TXM_SEMAPHORE_GET_CALL_NOT_USED
+#define TXM_SEMAPHORE_INFO_GET_CALL_NOT_USED
+#define TXM_SEMAPHORE_PERFORMANCE_INFO_GET_CALL_NOT_USED
+#define TXM_SEMAPHORE_PERFORMANCE_SYSTEM_INFO_GET_CALL_NOT_USED
+#define TXM_SEMAPHORE_PRIORITIZE_CALL_NOT_USED
+#define TXM_SEMAPHORE_PUT_CALL_NOT_USED
+#define TXM_SEMAPHORE_PUT_NOTIFY_CALL_NOT_USED
+#define TXM_THREAD_CREATE_CALL_NOT_USED
+#define TXM_THREAD_DELETE_CALL_NOT_USED
+#define TXM_THREAD_ENTRY_EXIT_NOTIFY_CALL_NOT_USED
+#define TXM_THREAD_IDENTIFY_CALL_NOT_USED
+#define TXM_THREAD_INFO_GET_CALL_NOT_USED
+#define TXM_THREAD_INTERRUPT_CONTROL_CALL_NOT_USED
+#define TXM_THREAD_PERFORMANCE_INFO_GET_CALL_NOT_USED
+#define TXM_THREAD_PERFORMANCE_SYSTEM_INFO_GET_CALL_NOT_USED
+#define TXM_THREAD_PREEMPTION_CHANGE_CALL_NOT_USED
+#define TXM_THREAD_PRIORITY_CHANGE_CALL_NOT_USED
+#define TXM_THREAD_RELINQUISH_CALL_NOT_USED
+#define TXM_THREAD_RESET_CALL_NOT_USED
+#define TXM_THREAD_RESUME_CALL_NOT_USED
+#define TXM_THREAD_SLEEP_CALL_NOT_USED
+#define TXM_THREAD_STACK_ERROR_NOTIFY_CALL_NOT_USED
+#define TXM_THREAD_SUSPEND_CALL_NOT_USED
+#define TXM_THREAD_SYSTEM_SUSPEND_CALL_NOT_USED
+#define TXM_THREAD_TERMINATE_CALL_NOT_USED
+#define TXM_THREAD_TIME_SLICE_CHANGE_CALL_NOT_USED
+#define TXM_THREAD_WAIT_ABORT_CALL_NOT_USED
+#define TXM_TIMER_ACTIVATE_CALL_NOT_USED
+#define TXM_TIMER_CHANGE_CALL_NOT_USED
+#define TXM_TIMER_CREATE_CALL_NOT_USED
+#define TXM_TIMER_DEACTIVATE_CALL_NOT_USED
+#define TXM_TIMER_DELETE_CALL_NOT_USED
+#define TXM_TIMER_INFO_GET_CALL_NOT_USED
+#define TXM_TIMER_PERFORMANCE_INFO_GET_CALL_NOT_USED
+#define TXM_TIMER_PERFORMANCE_SYSTEM_INFO_GET_CALL_NOT_USED
+#define TXM_TIME_GET_CALL_NOT_USED
+#define TXM_TIME_SET_CALL_NOT_USED
+#define TXM_TRACE_BUFFER_FULL_NOTIFY_CALL_NOT_USED
+#define TXM_TRACE_DISABLE_CALL_NOT_USED
+#define TXM_TRACE_ENABLE_CALL_NOT_USED
+#define TXM_TRACE_EVENT_FILTER_CALL_NOT_USED
+#define TXM_TRACE_EVENT_UNFILTER_CALL_NOT_USED
+#define TXM_TRACE_INTERRUPT_CONTROL_CALL_NOT_USED
+#define TXM_TRACE_ISR_ENTER_INSERT_CALL_NOT_USED
+#define TXM_TRACE_ISR_EXIT_INSERT_CALL_NOT_USED
+#define TXM_TRACE_USER_EVENT_INSERT_CALL_NOT_USED
+
+#include "txm_module_manager_dispatch.h"
+
+/* The three under test have to have survived that, and the rest have to have gone. */
+
+#if defined(TXM_BLOCK_POOL_CREATE_CALL_NOT_USED) || defined(TXM_BYTE_POOL_CREATE_CALL_NOT_USED) || defined(TXM_QUEUE_CREATE_CALL_NOT_USED)
+#error "the create services under test must be compiled in"
+#endif
+
+#ifndef TXM_BLOCK_POOL_DELETE_CALL_NOT_USED
+#error "the services this test does not exercise must be compiled out"
+#endif
+
+
+/* Define the stand-in interrupt lock the port shim counts through. */
+
+unsigned int test_interrupt_disable_depth;
+unsigned int test_interrupt_disable_max_depth;
+unsigned int test_interrupt_restore_underflows;
+
+
+/* Stand in for the module port's load-time alignment adjustment, which the utility
+ source declares and no case here reaches. */
+
+VOID _txm_module_manager_alignment_adjust(TXM_MODULE_PREAMBLE *module_preamble, ULONG *code_size,
+ ULONG *code_alignment, ULONG *data_size, ULONG *data_alignment)
+{
+ (VOID) module_preamble;
+ (VOID) code_size;
+ (VOID) code_alignment;
+ (VOID) data_size;
+ (VOID) data_alignment;
+}
+
+
+static UINT test_failures;
+static ULONG test_checks;
+
+
+/* Record the outcome of one expectation. */
+static VOID test_expect(const char *description, ULONG actual, ULONG expected)
+{
+ test_checks++;
+
+ if (actual != expected)
+ {
+ printf("FAIL: %s (expected %lu, got %lu)\n", description, (unsigned long) expected, (unsigned long) actual);
+ test_failures++;
+ }
+}
+
+
+/* Model the module's memory as one arena, so that the three regions a parameter array
+ or a pool start may be aimed at have known addresses and known gaps between them. The
+ gaps are memory the module may not reach: they are what an array anchored to the end
+ of a region overruns into, and they are inside the arena so that the words a case
+ plants past the end of a region are words this test owns. */
+
+#define TEST_REGION_BYTES ((ULONG) 256)
+#define TEST_GAP_BYTES ((ULONG) 64)
+
+static union
+{
+ ALIGN_TYPE test_arena_alignment;
+ UCHAR test_arena_bytes[(((ULONG) 3) * TEST_REGION_BYTES) + (((ULONG) 4) * TEST_GAP_BYTES)];
+} test_arena;
+
+#define TEST_DATA_START (&test_arena.test_arena_bytes[TEST_GAP_BYTES])
+#define TEST_CODE_START (&test_arena.test_arena_bytes[(((ULONG) 2) * TEST_GAP_BYTES) + TEST_REGION_BYTES])
+#define TEST_SHARED_START (&test_arena.test_arena_bytes[(((ULONG) 3) * TEST_GAP_BYTES) + (((ULONG) 2) * TEST_REGION_BYTES)])
+
+
+/* Model the manager's object pool, because these three services create objects and the
+ creation check requires the control block to be one of the module's own allocations
+ out of that pool: inside it, carrying an allocation header that names this module and
+ records exactly the size the service expects. */
+
+TX_BYTE_POOL _txm_module_manager_object_pool;
+UINT _txm_module_manager_object_pool_created;
+
+
+/* The kernel's created lists. A created object lives on the list for its type, and
+ the manager reads those lists, so a test that stands in for the kernel has to
+ provide them. All stay empty here, because this test creates no kernel object; it
+ exercises the parameter checking a create dispatcher does beforehand. */
+
+TX_BLOCK_POOL *_tx_block_pool_created_ptr;
+ULONG _tx_block_pool_created_count;
+TX_BYTE_POOL *_tx_byte_pool_created_ptr;
+ULONG _tx_byte_pool_created_count;
+TX_EVENT_FLAGS_GROUP *_tx_event_flags_created_ptr;
+ULONG _tx_event_flags_created_count;
+TX_MUTEX *_tx_mutex_created_ptr;
+ULONG _tx_mutex_created_count;
+TX_QUEUE *_tx_queue_created_ptr;
+ULONG _tx_queue_created_count;
+TX_SEMAPHORE *_tx_semaphore_created_ptr;
+ULONG _tx_semaphore_created_count;
+TX_THREAD *_tx_thread_created_ptr;
+ULONG _tx_thread_created_count;
+TX_TIMER *_tx_timer_created_ptr;
+ULONG _tx_timer_created_count;
+
+#define TEST_OBJECT_SLOT_BYTES ((ULONG) 256)
+#define TEST_OBJECT_SLOTS ((ULONG) 3)
+
+static union
+{
+ ALIGN_TYPE test_object_pool_alignment;
+ UCHAR test_object_pool_bytes[TEST_OBJECT_SLOTS * TEST_OBJECT_SLOT_BYTES];
+} test_object_pool;
+
+
+/* Define the requesting modules. One has memory protection, and is the one every range
+ case below drives; the other does not, and is how the cases show that the dispatch
+ layer's checks are not reached for it at all. */
+
+static TXM_MODULE_INSTANCE test_protected_module;
+static TXM_MODULE_INSTANCE test_unprotected_module;
+
+
+/* Place a module's regions on the arena. */
+static VOID test_module_setup(TXM_MODULE_INSTANCE *module_instance, ULONG property_flags)
+{
+ memset((VOID *) module_instance, 0, sizeof(TXM_MODULE_INSTANCE));
+
+ module_instance -> txm_module_instance_property_flags = property_flags;
+
+ module_instance -> txm_module_instance_data_start = (VOID *) TEST_DATA_START;
+ module_instance -> txm_module_instance_data_end = (VOID *) (TEST_DATA_START + (TEST_REGION_BYTES - ((ULONG) 1)));
+
+ module_instance -> txm_module_instance_code_start = (VOID *) TEST_CODE_START;
+ module_instance -> txm_module_instance_code_end = (VOID *) (TEST_CODE_START + (TEST_REGION_BYTES - ((ULONG) 1)));
+
+ module_instance -> txm_module_instance_shared_memory_address = (ULONG) TEST_SHARED_START;
+ module_instance -> txm_module_instance_shared_memory_length = TEST_REGION_BYTES;
+}
+
+
+/* Hand one slot of the object pool to a module as an allocation of a given size, and
+ return the control block address inside it. The allocation header sits in front of
+ the object, which is what the creation check reads. */
+static VOID *test_object_allocate(ULONG slot, TXM_MODULE_INSTANCE *module_instance, ULONG object_size)
+{
+
+TXM_MODULE_ALLOCATED_OBJECT *allocated_object_ptr;
+
+
+ allocated_object_ptr = (TXM_MODULE_ALLOCATED_OBJECT *) &test_object_pool.test_object_pool_bytes[slot * TEST_OBJECT_SLOT_BYTES];
+
+ memset((VOID *) allocated_object_ptr, 0, (size_t) TEST_OBJECT_SLOT_BYTES);
+
+ allocated_object_ptr -> txm_module_allocated_object_module_instance = module_instance;
+ allocated_object_ptr -> txm_module_object_size = object_size;
+
+ return((VOID *) (allocated_object_ptr + 1));
+}
+
+
+/* Record what the privileged create services were handed.
+
+ Whether the service was reached at all is half of every expectation below: a refusal
+ that still calls the service has refused nothing. What each index of the parameter
+ array carried when it arrived is the other half, and it is what measures the highest
+ index a dispatcher actually uses. */
+
+static ULONG test_service_calls;
+static ALIGN_TYPE test_service_object_ptr;
+static ALIGN_TYPE test_service_name_ptr;
+static ALIGN_TYPE test_service_extra[4];
+static ULONG test_service_extra_count;
+
+
+/* Stand in for the three privileged create services.
+
+ Each keeps the two checks the real one makes before it touches the caller's pool
+ memory, in the real one's order: a control-block size that is not the size of the
+ control block is refused with that object's own error, and a null pool start is
+ refused with TX_PTR_ERROR. Both matter here. The first is the whole of what a module
+ can learn from the last word of the array, since it reaches the service and nothing
+ else does. The second is why the dispatch layer's buffer check accepting a null pool
+ start outright -- whatever length it was given, proved or not -- costs nothing: the
+ service refuses the pointer before dereferencing it.
+
+ What the real services do that these do not is search the created list for a
+ duplicate. That is reached only after both checks above and touches nothing the
+ caller supplied, so no case here depends on it. */
+
+static UINT test_service_record(ALIGN_TYPE object_ptr, ALIGN_TYPE name_ptr, ULONG extra_count,
+ ALIGN_TYPE extra_0, ALIGN_TYPE extra_1, ALIGN_TYPE extra_2, ALIGN_TYPE extra_3)
+{
+ test_service_calls++;
+ test_service_object_ptr = object_ptr;
+ test_service_name_ptr = name_ptr;
+ test_service_extra_count = extra_count;
+ test_service_extra[0] = extra_0;
+ test_service_extra[1] = extra_1;
+ test_service_extra[2] = extra_2;
+ test_service_extra[3] = extra_3;
+
+ return(TX_SUCCESS);
+}
+
+
+UINT _txe_block_pool_create(TX_BLOCK_POOL *pool_ptr, TX_NAME_CONST CHAR *name_ptr, ULONG block_size,
+ VOID *pool_start, ULONG pool_size, UINT pool_control_block_size)
+{
+
+UINT status;
+
+
+ (VOID) test_service_record((ALIGN_TYPE) pool_ptr, (ALIGN_TYPE) name_ptr, ((ULONG) 4),
+ (ALIGN_TYPE) block_size, (ALIGN_TYPE) pool_start,
+ (ALIGN_TYPE) pool_size, (ALIGN_TYPE) pool_control_block_size);
+
+ if (pool_ptr == TX_NULL)
+ {
+ status = TX_POOL_ERROR;
+ }
+ else if (pool_control_block_size != ((UINT) sizeof(TX_BLOCK_POOL)))
+ {
+ status = TX_POOL_ERROR;
+ }
+ else if (pool_start == TX_NULL)
+ {
+ status = TX_PTR_ERROR;
+ }
+ else
+ {
+ status = TX_SUCCESS;
+ }
+
+ return(status);
+}
+
+
+UINT _txe_byte_pool_create(TX_BYTE_POOL *pool_ptr, TX_NAME_CONST CHAR *name_ptr, VOID *pool_start,
+ ULONG pool_size, UINT pool_control_block_size)
+{
+
+UINT status;
+
+
+ (VOID) test_service_record((ALIGN_TYPE) pool_ptr, (ALIGN_TYPE) name_ptr, ((ULONG) 3),
+ (ALIGN_TYPE) pool_start, (ALIGN_TYPE) pool_size,
+ (ALIGN_TYPE) pool_control_block_size, ((ALIGN_TYPE) 0));
+
+ if (pool_ptr == TX_NULL)
+ {
+ status = TX_POOL_ERROR;
+ }
+ else if (pool_control_block_size != ((UINT) sizeof(TX_BYTE_POOL)))
+ {
+ status = TX_POOL_ERROR;
+ }
+ else if (pool_start == TX_NULL)
+ {
+ status = TX_PTR_ERROR;
+ }
+ else
+ {
+ status = TX_SUCCESS;
+ }
+
+ return(status);
+}
+
+
+UINT _txe_queue_create(TX_QUEUE *queue_ptr, TX_NAME_CONST CHAR *name_ptr, UINT message_size,
+ VOID *queue_start, ULONG queue_size, UINT queue_control_block_size)
+{
+
+UINT status;
+
+
+ (VOID) test_service_record((ALIGN_TYPE) queue_ptr, (ALIGN_TYPE) name_ptr, ((ULONG) 4),
+ (ALIGN_TYPE) message_size, (ALIGN_TYPE) queue_start,
+ (ALIGN_TYPE) queue_size, (ALIGN_TYPE) queue_control_block_size);
+
+ if (queue_ptr == TX_NULL)
+ {
+ status = TX_QUEUE_ERROR;
+ }
+ else if (queue_control_block_size != ((UINT) sizeof(TX_QUEUE)))
+ {
+ status = TX_QUEUE_ERROR;
+ }
+ else if (queue_start == TX_NULL)
+ {
+ status = TX_PTR_ERROR;
+ }
+ else
+ {
+ status = TX_SUCCESS;
+ }
+
+ return(status);
+}
+
+
+/* Name the three services under test, and record for each one the shape the finding is
+ about: how many words its parameter array must hold, which index carries the pool or
+ queue start, which index carries the length that start is range-checked over, and
+ which index carries the control-block size. Every one of those numbers is checked
+ against the dispatcher's behaviour below rather than trusted. */
+
+#define TEST_SERVICE_BLOCK_POOL_CREATE ((UINT) 0)
+#define TEST_SERVICE_BYTE_POOL_CREATE ((UINT) 1)
+#define TEST_SERVICE_QUEUE_CREATE ((UINT) 2)
+#define TEST_SERVICE_COUNT ((UINT) 3)
+
+typedef struct TEST_SERVICE_SHAPE_STRUCT
+{
+ const char *test_service_name;
+ ULONG test_service_words;
+ ULONG test_service_start_index;
+ ULONG test_service_length_index;
+ ULONG test_service_control_block_index;
+ ULONG test_service_control_block_size;
+ ULONG test_service_control_block_error;
+} TEST_SERVICE_SHAPE;
+
+static const TEST_SERVICE_SHAPE test_service_shapes[TEST_SERVICE_COUNT] =
+{
+ { "block-pool create", ((ULONG) 4), ((ULONG) 1), ((ULONG) 2), ((ULONG) 3), (ULONG) sizeof(TX_BLOCK_POOL), (ULONG) TX_POOL_ERROR },
+ { "byte-pool create", ((ULONG) 3), ((ULONG) 0), ((ULONG) 1), ((ULONG) 2), (ULONG) sizeof(TX_BYTE_POOL), (ULONG) TX_POOL_ERROR },
+ { "queue create", ((ULONG) 4), ((ULONG) 1), ((ULONG) 2), ((ULONG) 3), (ULONG) sizeof(TX_QUEUE), (ULONG) TX_QUEUE_ERROR }
+};
+
+
+/* The control block each service's request names, and the name pointer it carries. The
+ control blocks are the module's own allocations out of the object pool, which is what
+ the creation check requires; the name lives in the module's data. */
+
+static VOID *test_service_control_block[TEST_SERVICE_COUNT];
+
+#define TEST_NAME_PTR ((ALIGN_TYPE) TEST_DATA_START)
+
+
+/* Build the parameter array a request will carry, at a chosen address, and hand back
+ that address as the array pointer the request carries.
+
+ Every word is written, including any that lies past the end of the region the array
+ is anchored to: those are the words the dispatcher must not read, and writing them
+ here is what makes a read of them observable rather than a read of whatever the arena
+ happened to hold. The words are assembled aligned and copied into place, because the
+ addresses these cases use advance a byte at a time and neither check on this path
+ requires the array to be aligned. */
+
+#define TEST_PARAMETER_FILL ((ALIGN_TYPE) 0xA5A5A5A5UL)
+#define TEST_PARAMETER_WORDS ((ULONG) 4)
+
+static ALIGN_TYPE *test_build_parameters(UINT service, UCHAR *address,
+ ALIGN_TYPE start_value, ALIGN_TYPE length_value,
+ ALIGN_TYPE control_block_value)
+{
+
+const TEST_SERVICE_SHAPE *shape;
+ALIGN_TYPE words[TEST_PARAMETER_WORDS];
+ULONG index;
+
+
+ shape = &test_service_shapes[service];
+
+ /* Fill every word with a value that is wrong for every index, so that a word the
+ lines below leave unset would be visible in what the service records. */
+ for (index = ((ULONG) 0); index < TEST_PARAMETER_WORDS; index++)
+ {
+ words[index] = TEST_PARAMETER_FILL;
+ }
+
+ words[shape -> test_service_start_index] = start_value;
+ words[shape -> test_service_length_index] = length_value;
+ words[shape -> test_service_control_block_index] = control_block_value;
+
+ memcpy((VOID *) address, (VOID *) words, sizeof(words));
+
+ return((ALIGN_TYPE *) address);
+}
+
+
+/* Read back one word of a parameter array that may be unaligned. */
+static ALIGN_TYPE test_parameter_word(const ALIGN_TYPE *extra_parameters, ULONG index)
+{
+
+ALIGN_TYPE word;
+
+
+ memcpy((VOID *) &word, (VOID *) (((const UCHAR *) extra_parameters) + (index * sizeof(ALIGN_TYPE))), sizeof(word));
+
+ return(word);
+}
+
+
+/* Make one request of one dispatcher, resetting the record of the service behind it
+ first so that a caller can tell a refusal from an acceptance. */
+static ALIGN_TYPE test_dispatch(UINT service, TXM_MODULE_INSTANCE *module_instance,
+ ALIGN_TYPE object_ptr, ALIGN_TYPE name_ptr, ALIGN_TYPE *extra_parameters)
+{
+
+ALIGN_TYPE return_value;
+ULONG index;
+
+
+ test_service_calls = ((ULONG) 0);
+ test_service_object_ptr = ((ALIGN_TYPE) 0);
+ test_service_name_ptr = ((ALIGN_TYPE) 0);
+ test_service_extra_count = ((ULONG) 0);
+
+ for (index = ((ULONG) 0); index < TEST_PARAMETER_WORDS; index++)
+ {
+ test_service_extra[index] = ((ALIGN_TYPE) 0);
+ }
+
+ if (service == TEST_SERVICE_BLOCK_POOL_CREATE)
+ {
+ return_value = _txm_module_manager_tx_block_pool_create_dispatch(module_instance, object_ptr, name_ptr, extra_parameters);
+ }
+ else if (service == TEST_SERVICE_BYTE_POOL_CREATE)
+ {
+ return_value = _txm_module_manager_tx_byte_pool_create_dispatch(module_instance, object_ptr, name_ptr, extra_parameters);
+ }
+ else
+ {
+ return_value = _txm_module_manager_tx_queue_create_dispatch(module_instance, object_ptr, name_ptr, extra_parameters);
+ }
+
+ return(return_value);
+}
+
+
+/* Report whether one request was accepted, checking as it goes that acceptance and
+ refusal are each consistent.
+
+ A refused request returns TXM_MODULE_INVALID_MEMORY and does not reach the service.
+ An accepted one reaches it exactly once, carrying the control block and name the
+ caller named and, at every index the service has a parameter for, the word that is
+ in the array at that index. That last part is the measurement of how far the
+ dispatcher reads: the service's own parameter list bounds the highest index it can
+ be handed, and each of those indices arriving with the array's own word is what says
+ the dispatcher read it. */
+static UINT test_accepted(UINT service, TXM_MODULE_INSTANCE *module_instance,
+ ALIGN_TYPE object_ptr, ALIGN_TYPE name_ptr, ALIGN_TYPE *extra_parameters)
+{
+
+const TEST_SERVICE_SHAPE *shape;
+ALIGN_TYPE return_value;
+UINT accepted;
+ULONG index;
+char description[160];
+
+
+ shape = &test_service_shapes[service];
+
+ return_value = test_dispatch(service, module_instance, object_ptr, name_ptr, extra_parameters);
+
+ if (test_service_calls == ((ULONG) 0))
+ {
+ accepted = TX_FALSE;
+
+ test_expect("a refused request returns TXM_MODULE_INVALID_MEMORY",
+ (ULONG) return_value, (ULONG) TXM_MODULE_INVALID_MEMORY);
+ }
+ else
+ {
+ accepted = TX_TRUE;
+
+ test_expect("an accepted request reaches the service once", test_service_calls, ((ULONG) 1));
+ test_expect("...with the control block the caller named", (ULONG) test_service_object_ptr, (ULONG) object_ptr);
+ test_expect("...and the name the caller supplied", (ULONG) test_service_name_ptr, (ULONG) name_ptr);
+ test_expect("...and one parameter per word the service declares",
+ test_service_extra_count, shape -> test_service_words);
+
+ for (index = ((ULONG) 0); index < shape -> test_service_words; index++)
+ {
+ snprintf(description, sizeof(description), "...and the array's word %lu at parameter %lu of %s",
+ (unsigned long) index, (unsigned long) index, shape -> test_service_name);
+ test_expect(description,
+ (ULONG) test_service_extra[index],
+ (ULONG) test_parameter_word(extra_parameters, index));
+ }
+ }
+
+ return(accepted);
+}
+
+
+/* Measure the extent a dispatcher validates.
+
+ The array is anchored so that its first word sits at the end of one of the module's
+ regions and walked backwards into it a byte at a time, so that the room left between
+ the array and the end of the region grows by one on each step. The smallest amount of
+ room the dispatcher accepts is the extent it validated. Everything else about the
+ request is valid, so acceptance turns on the extent alone.
+
+ TEST_NOT_ACCEPTED means nothing within the walk was accepted, which is the right
+ answer for a region a parameter array may not be in at all. */
+
+#define TEST_NOT_ACCEPTED ((ULONG) 0xFFFFFFFF)
+
+/* A length that no region can satisfy, and one that any of them can, chosen so that
+ either is recognisable in what a service records if a dispatcher reads a word the
+ cases below plant past the end of a region. */
+#define TEST_LENGTH_UNSATISFIABLE ((ALIGN_TYPE) 0xFFFFFFFFUL)
+#define TEST_LENGTH_SATISFIABLE ((ALIGN_TYPE) 0xB4)
+
+/* Walking two words past the widest extent any of the three validates is enough to
+ establish that none of them validates more. */
+#define TEST_WALK_WORDS ((ULONG) 6)
+
+static ULONG test_measure_extent(UINT service, TXM_MODULE_INSTANCE *module_instance, UCHAR *region_start)
+{
+
+const TEST_SERVICE_SHAPE *shape;
+ALIGN_TYPE *extra_parameters;
+UCHAR *region_end_plus_one;
+ULONG room;
+ULONG extent;
+
+
+ shape = &test_service_shapes[service];
+
+ region_end_plus_one = region_start + TEST_REGION_BYTES;
+
+ extent = TEST_NOT_ACCEPTED;
+
+ for (room = ((ULONG) 0); room <= (TEST_WALK_WORDS * ((ULONG) sizeof(ALIGN_TYPE))); room++)
+ {
+ extra_parameters = test_build_parameters(service, region_end_plus_one - room,
+ (ALIGN_TYPE) TEST_DATA_START, ((ALIGN_TYPE) sizeof(ALIGN_TYPE)),
+ (ALIGN_TYPE) shape -> test_service_control_block_size);
+
+ if (test_accepted(service, module_instance, (ALIGN_TYPE) test_service_control_block[service],
+ TEST_NAME_PTR, extra_parameters) == TX_TRUE)
+ {
+ extent = room;
+ break;
+ }
+ }
+
+ return(extent);
+}
+
+
+/* Recover a length word by bisection, the way a module could.
+
+ The dispatch layer's buffer check answers one comparison per request: with the length
+ word fixed, a request is accepted exactly when that length is at least one and fits
+ between the pool start the module chose and the end of a region the module owns. The
+ module therefore chooses the right-hand side of the comparison by moving the pool
+ start, and its refusal -- TXM_MODULE_INVALID_MEMORY, which none of these services can
+ return -- is distinguishable from every answer the service gives. Bisecting on the
+ room recovers the length in a number of requests logarithmic in the size of the
+ module's own region.
+
+ Run here on a word the module owns, which discloses nothing. It becomes disclosure
+ only composed with the extent measurement, which is what says that before the fix the
+ word bisected for lay outside the module's region. */
+
+#define TEST_NOT_RECOVERED ((ULONG) 0xFFFFFFFF)
+
+/* Somewhere inside the module's data with room for the whole array and clear of the
+ pool starts the bisection sweeps over. */
+#define TEST_BISECT_ARRAY (TEST_DATA_START + ((ULONG) 128))
+
+static UINT test_bisect_probe(UINT service, ULONG room, ALIGN_TYPE hidden_length)
+{
+
+const TEST_SERVICE_SHAPE *shape;
+ALIGN_TYPE *extra_parameters;
+
+
+ shape = &test_service_shapes[service];
+
+ extra_parameters = test_build_parameters(service, TEST_BISECT_ARRAY,
+ (ALIGN_TYPE) ((TEST_DATA_START + TEST_REGION_BYTES) - room),
+ hidden_length,
+ (ALIGN_TYPE) shape -> test_service_control_block_size);
+
+ return(test_accepted(service, &test_protected_module, (ALIGN_TYPE) test_service_control_block[service],
+ TEST_NAME_PTR, extra_parameters));
+}
+
+static ULONG test_bisect_length(UINT service, ALIGN_TYPE hidden_length, ULONG *probes)
+{
+
+ULONG low;
+ULONG high;
+ULONG middle;
+ULONG recovered;
+
+
+ *probes = ((ULONG) 0);
+
+ /* All the room there is. A length the module cannot fit even here is one the
+ bisection cannot resolve: all it learns is that the word exceeds the region. */
+ high = TEST_REGION_BYTES;
+ (*probes)++;
+
+ if (test_bisect_probe(service, high, hidden_length) == TX_FALSE)
+ {
+ recovered = TEST_NOT_RECOVERED;
+ }
+ else
+ {
+ /* The smallest accepted room lies in (low, high]. No room at all is refused for
+ every length, so low starts there and needs no probe. */
+ low = ((ULONG) 0);
+
+ while ((high - low) > ((ULONG) 1))
+ {
+ middle = low + ((high - low) / ((ULONG) 2));
+ (*probes)++;
+
+ if (test_bisect_probe(service, middle, hidden_length) == TX_TRUE)
+ {
+ high = middle;
+ }
+ else
+ {
+ low = middle;
+ }
+ }
+
+ recovered = high;
+ }
+
+ return(recovered);
+}
+
+
+/* Define an array and a control block outside every region the module owns, for the
+ cases about a module that has no memory protection and is therefore checked
+ nowhere. */
+
+static ALIGN_TYPE test_outside_parameters[TEST_PARAMETER_WORDS];
+static TX_BLOCK_POOL test_outside_block_pool;
+static TX_BYTE_POOL test_outside_byte_pool;
+static TX_QUEUE test_outside_queue;
+
+static VOID *test_outside_control_block[TEST_SERVICE_COUNT];
+
+
+/* The lengths the bisection is run for. The first is the smallest a pool start can be
+ checked over, the last two are the resolution limit: a length no room in the module's
+ own region can satisfy tells the module only that, and a length of zero is refused
+ outright by the overflow guard in the data check. */
+
+static const ALIGN_TYPE test_bisect_lengths[] =
+{
+ ((ALIGN_TYPE) 1),
+ ((ALIGN_TYPE) 7),
+ ((ALIGN_TYPE) 64),
+ ((ALIGN_TYPE) 255),
+ (ALIGN_TYPE) TEST_REGION_BYTES,
+ (ALIGN_TYPE) (TEST_REGION_BYTES + ((ULONG) 1)),
+ ((ALIGN_TYPE) 0xDEADBEEFUL),
+ ((ALIGN_TYPE) 0)
+};
+
+#define TEST_BISECT_LENGTH_COUNT (sizeof(test_bisect_lengths) / sizeof(test_bisect_lengths[0]))
+
+/* One initial probe plus one per halving of a 256-byte region. */
+#define TEST_BISECT_PROBE_LIMIT ((ULONG) 9)
+
+
+int main(void)
+{
+
+const TEST_SERVICE_SHAPE *shape;
+UINT service;
+UINT length_index;
+ULONG words_in_region;
+ULONG room;
+ULONG probes;
+ULONG recovered;
+ALIGN_TYPE hidden_length;
+ALIGN_TYPE return_value;
+ALIGN_TYPE *extra_parameters;
+UCHAR saved_object_pool[sizeof(test_object_pool)];
+char description[200];
+
+
+ memset((VOID *) &test_arena, 0, sizeof(test_arena));
+ memset((VOID *) &test_object_pool, 0, sizeof(test_object_pool));
+
+ test_module_setup(&test_protected_module, (ULONG) TXM_MODULE_MEMORY_PROTECTION);
+ test_module_setup(&test_unprotected_module, ((ULONG) 0));
+
+ /* Stand up the manager's object pool over the arena the control blocks come out
+ of, and give each service's request a control block the module allocated from
+ it, sized as that service's creation check requires. */
+ memset((VOID *) &_txm_module_manager_object_pool, 0, sizeof(TX_BYTE_POOL));
+ _txm_module_manager_object_pool.tx_byte_pool_start = test_object_pool.test_object_pool_bytes;
+ _txm_module_manager_object_pool.tx_byte_pool_size = (ULONG) sizeof(test_object_pool);
+ _txm_module_manager_object_pool_created = TX_TRUE;
+
+ test_service_control_block[TEST_SERVICE_BLOCK_POOL_CREATE] =
+ test_object_allocate(((ULONG) 0), &test_protected_module, (ULONG) sizeof(TX_BLOCK_POOL));
+ test_service_control_block[TEST_SERVICE_BYTE_POOL_CREATE] =
+ test_object_allocate(((ULONG) 1), &test_protected_module, (ULONG) sizeof(TX_BYTE_POOL));
+ test_service_control_block[TEST_SERVICE_QUEUE_CREATE] =
+ test_object_allocate(((ULONG) 2), &test_protected_module, (ULONG) sizeof(TX_QUEUE));
+
+ test_outside_control_block[TEST_SERVICE_BLOCK_POOL_CREATE] = (VOID *) &test_outside_block_pool;
+ test_outside_control_block[TEST_SERVICE_BYTE_POOL_CREATE] = (VOID *) &test_outside_byte_pool;
+ test_outside_control_block[TEST_SERVICE_QUEUE_CREATE] = (VOID *) &test_outside_queue;
+
+ printf("Module Manager extra-parameter array range validation test\n");
+
+ /**********************************************************************/
+ /* The extent each dispatcher validates. */
+ /**********************************************************************/
+
+ /* One word per index the service has a parameter for: four for block-pool create
+ and queue create, three for byte-pool create. In the module's writable data and
+ in a shared region registered to it, which are the two regions the data check
+ accepts. */
+
+ for (service = ((UINT) 0); service < TEST_SERVICE_COUNT; service++)
+ {
+ shape = &test_service_shapes[service];
+
+ snprintf(description, sizeof(description), "%s validates %lu words of a parameter array in the module's data",
+ shape -> test_service_name, (unsigned long) shape -> test_service_words);
+ test_expect(description,
+ test_measure_extent(service, &test_protected_module, TEST_DATA_START),
+ shape -> test_service_words * ((ULONG) sizeof(ALIGN_TYPE)));
+
+ snprintf(description, sizeof(description), "%s validates %lu words of a parameter array in a shared region",
+ shape -> test_service_name, (unsigned long) shape -> test_service_words);
+ test_expect(description,
+ test_measure_extent(service, &test_protected_module, TEST_SHARED_START),
+ shape -> test_service_words * ((ULONG) sizeof(ALIGN_TYPE)));
+
+ /* Both checks on this path are the data check, with no fallback to the portable
+ code-region check, so a parameter array in the module's read-only image is
+ refused however much room is left. */
+ snprintf(description, sizeof(description),
+ "%s refuses a parameter array in the module's read-only image at any extent",
+ shape -> test_service_name);
+ test_expect(description,
+ test_measure_extent(service, &test_protected_module, TEST_CODE_START),
+ TEST_NOT_ACCEPTED);
+ }
+
+ /**********************************************************************/
+ /* A word the dispatcher did not prove cannot decide the answer. */
+ /**********************************************************************/
+
+ /* This is the defect stated at the anchor point it turned on. With fewer words of
+ room than the service has parameters, the request must be refused whatever the
+ words past the end of the region hold -- and the two plantings below differ only
+ in those words: one describes a pool start and length inside the module, the other
+ a length no region can satisfy. Block-pool create accepted the first of them with
+ two words of room and went on to read four. */
+
+ for (service = ((UINT) 0); service < TEST_SERVICE_COUNT; service++)
+ {
+ shape = &test_service_shapes[service];
+
+ for (words_in_region = ((ULONG) 1); words_in_region < shape -> test_service_words; words_in_region++)
+ {
+ room = words_in_region * ((ULONG) sizeof(ALIGN_TYPE));
+
+ extra_parameters = test_build_parameters(service, (TEST_DATA_START + TEST_REGION_BYTES) - room,
+ (ALIGN_TYPE) TEST_DATA_START, TEST_LENGTH_SATISFIABLE,
+ (ALIGN_TYPE) shape -> test_service_control_block_size);
+
+ snprintf(description, sizeof(description),
+ "%s refuses %lu words of room with a satisfiable length past the end of the region",
+ shape -> test_service_name, (unsigned long) words_in_region);
+ test_expect(description,
+ (ULONG) test_accepted(service, &test_protected_module,
+ (ALIGN_TYPE) test_service_control_block[service],
+ TEST_NAME_PTR, extra_parameters),
+ (ULONG) TX_FALSE);
+
+ if (test_service_calls != ((ULONG) 0))
+ {
+ printf(" ...and reached the service carrying word %lu from outside the region: 0x%lX\n",
+ (unsigned long) shape -> test_service_length_index,
+ (unsigned long) test_service_extra[shape -> test_service_length_index]);
+ }
+
+ extra_parameters = test_build_parameters(service, (TEST_DATA_START + TEST_REGION_BYTES) - room,
+ (ALIGN_TYPE) TEST_DATA_START, TEST_LENGTH_UNSATISFIABLE,
+ (ALIGN_TYPE) shape -> test_service_control_block_size);
+
+ snprintf(description, sizeof(description),
+ "%s refuses %lu words of room with an unsatisfiable length past the end of the region",
+ shape -> test_service_name, (unsigned long) words_in_region);
+ test_expect(description,
+ (ULONG) test_accepted(service, &test_protected_module,
+ (ALIGN_TYPE) test_service_control_block[service],
+ TEST_NAME_PTR, extra_parameters),
+ (ULONG) TX_FALSE);
+ }
+
+ /* And the boundary the other way: an array whose last word ends exactly at the
+ end of the region is accepted, and every one of its words reaches the
+ service, which is what test_accepted checks of an acceptance. */
+
+ room = shape -> test_service_words * ((ULONG) sizeof(ALIGN_TYPE));
+
+ extra_parameters = test_build_parameters(service, (TEST_DATA_START + TEST_REGION_BYTES) - room,
+ (ALIGN_TYPE) TEST_DATA_START, ((ALIGN_TYPE) sizeof(ALIGN_TYPE)),
+ (ALIGN_TYPE) shape -> test_service_control_block_size);
+
+ snprintf(description, sizeof(description),
+ "%s accepts a parameter array that reaches exactly the end of the region",
+ shape -> test_service_name);
+ test_expect(description,
+ (ULONG) test_accepted(service, &test_protected_module,
+ (ALIGN_TYPE) test_service_control_block[service],
+ TEST_NAME_PTR, extra_parameters),
+ (ULONG) TX_TRUE);
+ }
+
+ /**********************************************************************/
+ /* What a refusal tells the module apart from. */
+ /**********************************************************************/
+
+ /* The bisection below depends on the dispatch layer's refusal being distinguishable
+ from every status the services can return. It is, and none of these services has
+ TXM_MODULE_INVALID_MEMORY among its return values, so a module can always tell
+ "the manager refused this" from "the kernel refused this". */
+
+ test_expect("TXM_MODULE_INVALID_MEMORY is not TX_SUCCESS",
+ (ULONG) (TXM_MODULE_INVALID_MEMORY != TX_SUCCESS), (ULONG) TX_TRUE);
+ test_expect("TXM_MODULE_INVALID_MEMORY is not TX_POOL_ERROR",
+ (ULONG) (TXM_MODULE_INVALID_MEMORY != TX_POOL_ERROR), (ULONG) TX_TRUE);
+ test_expect("TXM_MODULE_INVALID_MEMORY is not TX_QUEUE_ERROR",
+ (ULONG) (TXM_MODULE_INVALID_MEMORY != TX_QUEUE_ERROR), (ULONG) TX_TRUE);
+ test_expect("TXM_MODULE_INVALID_MEMORY is not TX_PTR_ERROR",
+ (ULONG) (TXM_MODULE_INVALID_MEMORY != TX_PTR_ERROR), (ULONG) TX_TRUE);
+ test_expect("TXM_MODULE_INVALID_MEMORY is not TX_SIZE_ERROR",
+ (ULONG) (TXM_MODULE_INVALID_MEMORY != TX_SIZE_ERROR), (ULONG) TX_TRUE);
+
+ /**********************************************************************/
+ /* How much of a length word the answers give back. */
+ /**********************************************************************/
+
+ /* Measured rather than argued, because it is what separates a bounded over-read
+ from a disclosure and the arithmetic is easy to overstate. The bisection recovers
+ a length exactly when it lies within the module's own region, in one request per
+ halving of that region -- nine here for 256 bytes, and the logarithm of the region
+ size on a real module. Above that it recovers nothing but the fact that the word
+ exceeds the region, because the room the module can offer is bounded by a region
+ it already owns: the pool start has to be inside one to be accepted at all. */
+
+ for (service = ((UINT) 0); service < TEST_SERVICE_COUNT; service++)
+ {
+ shape = &test_service_shapes[service];
+
+ for (length_index = ((UINT) 0); length_index < ((UINT) TEST_BISECT_LENGTH_COUNT); length_index++)
+ {
+ hidden_length = test_bisect_lengths[length_index];
+
+ recovered = test_bisect_length(service, hidden_length, &probes);
+
+ if ((hidden_length >= ((ALIGN_TYPE) 1)) && (hidden_length <= (ALIGN_TYPE) TEST_REGION_BYTES))
+ {
+ snprintf(description, sizeof(description), "%s: bisecting recovers a length of %lu exactly",
+ shape -> test_service_name, (unsigned long) hidden_length);
+ test_expect(description, recovered, (ULONG) hidden_length);
+ }
+ else
+ {
+ snprintf(description, sizeof(description),
+ "%s: a length of 0x%lX is outside what the module's own region can resolve",
+ shape -> test_service_name, (unsigned long) hidden_length);
+ test_expect(description, recovered, TEST_NOT_RECOVERED);
+ }
+
+ snprintf(description, sizeof(description), "%s: bisecting a length costs no more than %lu requests",
+ shape -> test_service_name, (unsigned long) TEST_BISECT_PROBE_LIMIT);
+ test_expect(description, (ULONG) (probes <= TEST_BISECT_PROBE_LIMIT), (ULONG) TX_TRUE);
+ }
+ }
+
+ /**********************************************************************/
+ /* The last word of the array, which reaches the service. */
+ /**********************************************************************/
+
+ /* The control-block size is the one word no check in the dispatch layer looks at.
+ It reaches the service, which compares it against the size of the control block
+ and refuses anything else with that object's own error. So it yields one equality
+ bit per request and nothing more, which is the weaker of the two things a module
+ learns from this array. */
+
+ for (service = ((UINT) 0); service < TEST_SERVICE_COUNT; service++)
+ {
+ shape = &test_service_shapes[service];
+
+ extra_parameters = test_build_parameters(service, TEST_BISECT_ARRAY,
+ (ALIGN_TYPE) TEST_DATA_START, ((ALIGN_TYPE) sizeof(ALIGN_TYPE)),
+ (ALIGN_TYPE) shape -> test_service_control_block_size);
+
+ return_value = test_dispatch(service, &test_protected_module,
+ (ALIGN_TYPE) test_service_control_block[service],
+ TEST_NAME_PTR, extra_parameters);
+
+ snprintf(description, sizeof(description), "%s accepts the size of its own control block",
+ shape -> test_service_name);
+ test_expect(description, (ULONG) return_value, (ULONG) TX_SUCCESS);
+
+ extra_parameters = test_build_parameters(service, TEST_BISECT_ARRAY,
+ (ALIGN_TYPE) TEST_DATA_START, ((ALIGN_TYPE) sizeof(ALIGN_TYPE)),
+ (ALIGN_TYPE) (shape -> test_service_control_block_size + ((ULONG) 1)));
+
+ return_value = test_dispatch(service, &test_protected_module,
+ (ALIGN_TYPE) test_service_control_block[service],
+ TEST_NAME_PTR, extra_parameters);
+
+ snprintf(description, sizeof(description), "%s refuses any other control-block size, at the service",
+ shape -> test_service_name);
+ test_expect(description, (ULONG) return_value, shape -> test_service_control_block_error);
+
+ snprintf(description, sizeof(description), "...having reached the service to do it (%s)",
+ shape -> test_service_name);
+ test_expect(description, test_service_calls, ((ULONG) 1));
+ }
+
+ /**********************************************************************/
+ /* The two null escape hatches. */
+ /**********************************************************************/
+
+ /* The buffer check accepts a null pool start outright, whatever length it is given
+ and whether or not that length was proved, because a null pointer is how a module
+ asks for an optional output it does not want. For these three services nothing is
+ optional, and the service refuses the null pointer with TX_PTR_ERROR before
+ dereferencing it -- so correcting the extent is not what refuses one, and the
+ length word is read on this path too, which is why proving it matters here as
+ much as anywhere. */
+
+ for (service = ((UINT) 0); service < TEST_SERVICE_COUNT; service++)
+ {
+ shape = &test_service_shapes[service];
+
+ extra_parameters = test_build_parameters(service, TEST_BISECT_ARRAY,
+ ((ALIGN_TYPE) 0), TEST_LENGTH_UNSATISFIABLE,
+ (ALIGN_TYPE) shape -> test_service_control_block_size);
+
+ return_value = test_dispatch(service, &test_protected_module,
+ (ALIGN_TYPE) test_service_control_block[service],
+ TEST_NAME_PTR, extra_parameters);
+
+ snprintf(description, sizeof(description), "%s passes a null pool start to the service at any length",
+ shape -> test_service_name);
+ test_expect(description, test_service_calls, ((ULONG) 1));
+
+ snprintf(description, sizeof(description), "...and the service refuses it with TX_PTR_ERROR (%s)",
+ shape -> test_service_name);
+ test_expect(description, (ULONG) return_value, (ULONG) TX_PTR_ERROR);
+ }
+
+ /* The array check has no null clause at all, and must not grow one: the array is not
+ an optional output, it is the request. A null array is refused by the dispatcher
+ and never reaches the service, which is the only reason nothing dereferences it. */
+
+ for (service = ((UINT) 0); service < TEST_SERVICE_COUNT; service++)
+ {
+ shape = &test_service_shapes[service];
+
+ return_value = test_dispatch(service, &test_protected_module,
+ (ALIGN_TYPE) test_service_control_block[service],
+ TEST_NAME_PTR, (ALIGN_TYPE *) TX_NULL);
+
+ snprintf(description, sizeof(description), "%s refuses a null parameter array", shape -> test_service_name);
+ test_expect(description, (ULONG) return_value, (ULONG) TXM_MODULE_INVALID_MEMORY);
+
+ snprintf(description, sizeof(description), "...without reaching the service (%s)", shape -> test_service_name);
+ test_expect(description, test_service_calls, ((ULONG) 0));
+ }
+
+ /**********************************************************************/
+ /* An array whose extent runs off the end of the address space. */
+ /**********************************************************************/
+
+ /* The data check tests for that before comparing anything, so a module cannot reach
+ past the top of memory by wrapping the sum. Nothing is written at that address:
+ the request is refused before the array is read, which is the point. */
+
+ for (service = ((UINT) 0); service < TEST_SERVICE_COUNT; service++)
+ {
+ shape = &test_service_shapes[service];
+
+ return_value = test_dispatch(service, &test_protected_module,
+ (ALIGN_TYPE) test_service_control_block[service],
+ TEST_NAME_PTR,
+ (ALIGN_TYPE *) (~((ALIGN_TYPE) 0) - ((ALIGN_TYPE) 3)));
+
+ snprintf(description, sizeof(description), "%s refuses an array whose extent wraps the address space",
+ shape -> test_service_name);
+ test_expect(description, (ULONG) return_value, (ULONG) TXM_MODULE_INVALID_MEMORY);
+
+ snprintf(description, sizeof(description), "...without reaching the service (%s)", shape -> test_service_name);
+ test_expect(description, test_service_calls, ((ULONG) 0));
+ }
+
+ /**********************************************************************/
+ /* A refusal changes nothing. */
+ /**********************************************************************/
+
+ memcpy((VOID *) saved_object_pool, (VOID *) &test_object_pool, sizeof(test_object_pool));
+
+ for (service = ((UINT) 0); service < TEST_SERVICE_COUNT; service++)
+ {
+ shape = &test_service_shapes[service];
+
+ room = ((ULONG) 1) * ((ULONG) sizeof(ALIGN_TYPE));
+
+ extra_parameters = test_build_parameters(service, (TEST_DATA_START + TEST_REGION_BYTES) - room,
+ (ALIGN_TYPE) TEST_DATA_START, ((ALIGN_TYPE) sizeof(ALIGN_TYPE)),
+ (ALIGN_TYPE) shape -> test_service_control_block_size);
+
+ return_value = test_dispatch(service, &test_protected_module,
+ (ALIGN_TYPE) test_service_control_block[service],
+ TEST_NAME_PTR, extra_parameters);
+
+ snprintf(description, sizeof(description), "%s refuses with TXM_MODULE_INVALID_MEMORY", shape -> test_service_name);
+ test_expect(description, (ULONG) return_value, (ULONG) TXM_MODULE_INVALID_MEMORY);
+
+ snprintf(description, sizeof(description), "...%s does not reach the service", shape -> test_service_name);
+ test_expect(description, test_service_calls, ((ULONG) 0));
+
+ snprintf(description, sizeof(description),
+ "...and %s leaves every control block and allocation header untouched", shape -> test_service_name);
+ test_expect(description,
+ (ULONG) (memcmp((VOID *) saved_object_pool, (VOID *) &test_object_pool, sizeof(test_object_pool)) == 0),
+ (ULONG) TX_TRUE);
+ }
+
+ /**********************************************************************/
+ /* Where the pool start itself may be. */
+ /**********************************************************************/
+
+ /* The pool start is checked with the same data-only check the array is, plus the
+ null clause, so it may be in the module's writable data or in a shared region
+ registered to it and nowhere else. A pool in the module's read-only image is
+ refused however small, which is the second place this path has no code-region
+ fallback to lean on. */
+
+ for (service = ((UINT) 0); service < TEST_SERVICE_COUNT; service++)
+ {
+ shape = &test_service_shapes[service];
+
+ extra_parameters = test_build_parameters(service, TEST_BISECT_ARRAY,
+ (ALIGN_TYPE) TEST_SHARED_START, ((ALIGN_TYPE) sizeof(ALIGN_TYPE)),
+ (ALIGN_TYPE) shape -> test_service_control_block_size);
+
+ snprintf(description, sizeof(description), "%s accepts a pool start in a shared region", shape -> test_service_name);
+ test_expect(description,
+ (ULONG) test_accepted(service, &test_protected_module,
+ (ALIGN_TYPE) test_service_control_block[service],
+ TEST_NAME_PTR, extra_parameters),
+ (ULONG) TX_TRUE);
+
+ extra_parameters = test_build_parameters(service, TEST_BISECT_ARRAY,
+ (ALIGN_TYPE) TEST_CODE_START, ((ALIGN_TYPE) sizeof(ALIGN_TYPE)),
+ (ALIGN_TYPE) shape -> test_service_control_block_size);
+
+ snprintf(description, sizeof(description), "%s refuses a pool start in the module's read-only image",
+ shape -> test_service_name);
+ test_expect(description,
+ (ULONG) test_accepted(service, &test_protected_module,
+ (ALIGN_TYPE) test_service_control_block[service],
+ TEST_NAME_PTR, extra_parameters),
+ (ULONG) TX_FALSE);
+ }
+
+ /**********************************************************************/
+ /* The control block and the name still have to be what they were. */
+ /**********************************************************************/
+
+ /* The array extent is the third of four checks, so correcting it must not have
+ displaced the two in front of it. A creation request still needs a control block
+ the module allocated from the manager's object pool at exactly the size this
+ service expects, and a name inside the module. */
+
+ for (service = ((UINT) 0); service < TEST_SERVICE_COUNT; service++)
+ {
+ shape = &test_service_shapes[service];
+
+ extra_parameters = test_build_parameters(service, TEST_BISECT_ARRAY,
+ (ALIGN_TYPE) TEST_DATA_START, ((ALIGN_TYPE) sizeof(ALIGN_TYPE)),
+ (ALIGN_TYPE) shape -> test_service_control_block_size);
+
+ snprintf(description, sizeof(description), "%s refuses a null control block", shape -> test_service_name);
+ test_expect(description,
+ (ULONG) test_accepted(service, &test_protected_module, ((ALIGN_TYPE) 0), TEST_NAME_PTR, extra_parameters),
+ (ULONG) TX_FALSE);
+
+ snprintf(description, sizeof(description), "%s refuses a control block inside the module's data",
+ shape -> test_service_name);
+ test_expect(description,
+ (ULONG) test_accepted(service, &test_protected_module, (ALIGN_TYPE) TEST_DATA_START,
+ TEST_NAME_PTR, extra_parameters),
+ (ULONG) TX_FALSE);
+
+ /* An allocation of the module's own, at the wrong size for this service. */
+ (VOID) test_object_allocate((ULONG) service, &test_protected_module,
+ shape -> test_service_control_block_size + ((ULONG) 1));
+
+ snprintf(description, sizeof(description), "%s refuses an allocation of the wrong size",
+ shape -> test_service_name);
+ test_expect(description,
+ (ULONG) test_accepted(service, &test_protected_module,
+ (ALIGN_TYPE) test_service_control_block[service],
+ TEST_NAME_PTR, extra_parameters),
+ (ULONG) TX_FALSE);
+
+ /* An allocation of the right size belonging to another module. */
+ (VOID) test_object_allocate((ULONG) service, &test_unprotected_module,
+ shape -> test_service_control_block_size);
+
+ snprintf(description, sizeof(description), "%s refuses another module's allocation",
+ shape -> test_service_name);
+ test_expect(description,
+ (ULONG) test_accepted(service, &test_protected_module,
+ (ALIGN_TYPE) test_service_control_block[service],
+ TEST_NAME_PTR, extra_parameters),
+ (ULONG) TX_FALSE);
+
+ /* Put it back the way the rest of the run needs it. */
+ (VOID) test_object_allocate((ULONG) service, &test_protected_module,
+ shape -> test_service_control_block_size);
+
+ /* A module naming a constant names it out of its own read-only image, which the
+ string check accepts because it is the one check here that is not data-only. */
+ snprintf(description, sizeof(description), "%s accepts a name in the module's read-only image",
+ shape -> test_service_name);
+ test_expect(description,
+ (ULONG) test_accepted(service, &test_protected_module,
+ (ALIGN_TYPE) test_service_control_block[service],
+ (ALIGN_TYPE) TEST_CODE_START, extra_parameters),
+ (ULONG) TX_TRUE);
+
+ snprintf(description, sizeof(description), "%s refuses a name outside every region the module owns",
+ shape -> test_service_name);
+ test_expect(description,
+ (ULONG) test_accepted(service, &test_protected_module,
+ (ALIGN_TYPE) test_service_control_block[service],
+ (ALIGN_TYPE) &test_outside_parameters[0], extra_parameters),
+ (ULONG) TX_FALSE);
+
+ snprintf(description, sizeof(description), "%s accepts a null name, which the kernel allows",
+ shape -> test_service_name);
+ test_expect(description,
+ (ULONG) test_accepted(service, &test_protected_module,
+ (ALIGN_TYPE) test_service_control_block[service],
+ ((ALIGN_TYPE) 0), extra_parameters),
+ (ULONG) TX_TRUE);
+ }
+
+ /**********************************************************************/
+ /* A module without memory protection is unaffected. */
+ /**********************************************************************/
+
+ /* Every check the fix touches sits inside the dispatchers' memory-protection block.
+ A module that was loaded without protection reaches the kernel service with no
+ check of any kind, before the fix and after it, and these cases are here so that
+ a later change to the extent cannot quietly start refusing its requests. */
+
+ for (service = ((UINT) 0); service < TEST_SERVICE_COUNT; service++)
+ {
+ shape = &test_service_shapes[service];
+
+ extra_parameters = test_build_parameters(service, (UCHAR *) test_outside_parameters,
+ (ALIGN_TYPE) test_outside_parameters, ((ALIGN_TYPE) sizeof(ALIGN_TYPE)),
+ (ALIGN_TYPE) shape -> test_service_control_block_size);
+
+ snprintf(description, sizeof(description),
+ "an unprotected module's %s is accepted with an array, a pool and a control block outside every region",
+ shape -> test_service_name);
+ test_expect(description,
+ (ULONG) test_accepted(service, &test_unprotected_module,
+ (ALIGN_TYPE) test_outside_control_block[service],
+ (ALIGN_TYPE) test_outside_parameters, extra_parameters),
+ (ULONG) TX_TRUE);
+
+ /* And with one word of room, which is the shape a protected module is refused
+ for above. */
+ room = ((ULONG) 1) * ((ULONG) sizeof(ALIGN_TYPE));
+
+ extra_parameters = test_build_parameters(service, (TEST_DATA_START + TEST_REGION_BYTES) - room,
+ (ALIGN_TYPE) TEST_DATA_START, ((ALIGN_TYPE) sizeof(ALIGN_TYPE)),
+ (ALIGN_TYPE) shape -> test_service_control_block_size);
+
+ snprintf(description, sizeof(description),
+ "an unprotected module's %s is accepted with one word of room", shape -> test_service_name);
+ test_expect(description,
+ (ULONG) test_accepted(service, &test_unprotected_module,
+ (ALIGN_TYPE) test_outside_control_block[service],
+ TEST_NAME_PTR, extra_parameters),
+ (ULONG) TX_TRUE);
+
+ /* Nothing screens an unprotected module's control block before the service, so
+ the service is where a null one is refused. For a protected module the
+ creation check refuses it first, which is why this case is here and not
+ above. */
+ return_value = test_dispatch(service, &test_unprotected_module, ((ALIGN_TYPE) 0),
+ TEST_NAME_PTR, extra_parameters);
+
+ snprintf(description, sizeof(description),
+ "an unprotected module's %s reaches the service with a null control block",
+ shape -> test_service_name);
+ test_expect(description, test_service_calls, ((ULONG) 1));
+
+ snprintf(description, sizeof(description), "...and the service refuses it (%s)", shape -> test_service_name);
+ test_expect(description, (ULONG) return_value, shape -> test_service_control_block_error);
+ }
+
+ /**********************************************************************/
+ /* What the whole run has to have held. */
+ /**********************************************************************/
+
+ /* Nothing on these paths disables interrupts, and the shim counts every disable, so
+ the lock has to have been left as it was found and never taken at all. The second
+ of those is also the evidence that nothing here raises _tx_thread_preempt_disable:
+ in ThreadX that flag is only ever changed between a TX_DISABLE and its TX_RESTORE.
+ A fault taken on this over-read therefore costs the requesting thread, not the
+ system's ability to preempt. */
+
+ test_expect("the interrupt lock is balanced", (ULONG) test_interrupt_disable_depth, ((ULONG) 0));
+ test_expect("the interrupt lock was never taken", (ULONG) test_interrupt_disable_max_depth, ((ULONG) 0));
+ test_expect("the interrupt lock never underflowed", (ULONG) test_interrupt_restore_underflows, ((ULONG) 0));
+
+ test_expect("every case ran", (ULONG) (test_checks > ((ULONG) 700)), (ULONG) TX_TRUE);
+
+ if (test_failures == 0U)
+ {
+ printf("SUCCESS! %lu expectations\n", (unsigned long) test_checks);
+ return(0);
+ }
+
+ printf("ERROR: %u expectation(s) failed of %lu\n", test_failures, (unsigned long) test_checks);
+ return(1);
+}
diff --git a/test/tx/module_manager/threadx_module_manager_cortex_a7_mmu_test.c b/test/tx/module_manager/threadx_module_manager_cortex_a7_mmu_test.c
new file mode 100644
index 00000000..8cc08108
--- /dev/null
+++ b/test/tx/module_manager/threadx_module_manager_cortex_a7_mmu_test.c
@@ -0,0 +1,205 @@
+/***************************************************************************
+ * 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
+ **************************************************************************/
+
+
+/**************************************************************************/
+/**************************************************************************/
+/** */
+/** ThreadX Test */
+/** */
+/** Module Manager Cortex-A7 range validation, against a live MMU */
+/** */
+/**************************************************************************/
+/**************************************************************************/
+
+/* The host test for this check stands a simulated page map behind the port's
+ one architecture primitive, which lets it run everywhere but leaves the CP15
+ address translation itself unexercised: an encoding that named the wrong
+ operation, or a PAR fault bit read the wrong way round, would pass it.
+
+ This test closes that gap. It builds a short-descriptor translation table,
+ turns the MMU on, and drives the real check through the real translation
+ operations on a real Cortex-A7 translation regime. It is a bare-metal image,
+ built and run by scripts/check_module_mmu_a7.sh under an emulator.
+
+ The sections below are mapped for their access permissions only; nothing is
+ dereferenced through them, and none of the addresses name a real target. */
+
+#include <stdio.h>
+
+#define TX_SOURCE_CODE
+
+#include "tx_api.h"
+#include "txm_module.h"
+
+
+/* Short-descriptor section entry fields. AP[2:0] selects the access
+ permissions: 011 is unprivileged read/write, 010 is unprivileged read only,
+ 001 leaves the section privileged-only. */
+
+#define TEST_SECTION_TYPE 0x00000002
+#define TEST_AP_LOW(value) (((ULONG) (value)) << 10)
+#define TEST_AP_USER_RW TEST_AP_LOW(3)
+#define TEST_AP_USER_RO TEST_AP_LOW(2)
+#define TEST_AP_PRIVILEGED_ONLY TEST_AP_LOW(1)
+
+#define TEST_LEVEL1_ENTRIES 4096
+#define TEST_SECTION_SIZE 0x00100000
+
+/* One section of each kind the check has to tell apart, plus an unmapped
+ section immediately after the read-only one so a range can be made to leave
+ its mapping partway through. */
+
+#define TEST_SHARED_RW 0x40000000
+#define TEST_SHARED_RO 0x40100000
+#define TEST_GUARD 0x40200000
+#define TEST_MODULE_DATA 0x41000000
+#define TEST_MODULE_DATA_SIZE 0x00001000
+#define TEST_KERNEL_ONLY 0x30000000
+
+static ULONG test_level1_table[TEST_LEVEL1_ENTRIES] __attribute__((aligned(16384)));
+static UINT test_failures = 0;
+
+
+/* Reproduce the behaviour this check replaced: one translation of the first
+ address, for reading, with the size discarded. Kept so the boundary cases
+ can show both answers against the same live MMU. */
+static UINT test_first_address_only_check(ALIGN_TYPE obj_ptr)
+{
+ return(_txm_module_manager_address_probe((ULONG) obj_ptr, TXM_MODULE_MANAGER_ACCESS_READ));
+}
+
+
+/* Build a flat translation table, then enable the MMU. */
+static VOID test_mmu_enable(VOID)
+{
+
+ULONG index;
+ULONG sctlr;
+ULONG zero = 0;
+ULONG dacr = 1; /* Domain 0 as a client, so AP is checked. */
+
+
+ /* Everything is privileged-only until a section below says otherwise. */
+ for (index = 0; index < TEST_LEVEL1_ENTRIES; index++)
+ {
+ test_level1_table[index] = (index * TEST_SECTION_SIZE) | TEST_SECTION_TYPE | TEST_AP_PRIVILEGED_ONLY;
+ }
+
+ test_level1_table[TEST_SHARED_RW / TEST_SECTION_SIZE] =
+ TEST_SHARED_RW | TEST_SECTION_TYPE | TEST_AP_USER_RW;
+ test_level1_table[TEST_SHARED_RO / TEST_SECTION_SIZE] =
+ TEST_SHARED_RO | TEST_SECTION_TYPE | TEST_AP_USER_RO;
+ test_level1_table[TEST_MODULE_DATA / TEST_SECTION_SIZE] =
+ TEST_MODULE_DATA | TEST_SECTION_TYPE | TEST_AP_USER_RW;
+
+ /* Leave the guard section faulting. */
+ test_level1_table[TEST_GUARD / TEST_SECTION_SIZE] = 0;
+
+ __asm volatile ("MCR p15, 0, %0, c2, c0, 2" : : "r"(zero)); /* TTBCR */
+ __asm volatile ("MCR p15, 0, %0, c2, c0, 0" : : "r"((ULONG) test_level1_table)); /* TTBR0 */
+ __asm volatile ("MCR p15, 0, %0, c3, c0, 0" : : "r"(dacr)); /* DACR */
+ __asm volatile ("MCR p15, 0, %0, c13, c0, 1" : : "r"(zero)); /* CONTEXTIDR */
+ __asm volatile ("MCR p15, 0, %0, c8, c7, 0" : : "r"(zero)); /* TLBIALL */
+ __asm volatile ("DSB");
+ __asm volatile ("ISB");
+
+ __asm volatile ("MRC p15, 0, %0, c1, c0, 0" : "=r"(sctlr));
+ sctlr = sctlr | ((ULONG) 1); /* SCTLR.M */
+ __asm volatile ("MCR p15, 0, %0, c1, c0, 0" : : "r"(sctlr));
+ __asm volatile ("ISB");
+}
+
+
+static VOID test_expect(const char *description, UINT actual, UINT expected)
+{
+ if (actual != expected)
+ {
+ printf(" FAIL: %s (expected %u, got %u)\n", description, expected, actual);
+ test_failures++;
+ }
+}
+
+
+int main(VOID)
+{
+
+TXM_MODULE_INSTANCE module;
+TXM_MODULE_INSTANCE *module_instance;
+ALIGN_TYPE straddle;
+
+
+ printf("Cortex-A7 module range validation against a live MMU................... ");
+
+ test_mmu_enable();
+
+ module_instance = &module;
+ module_instance -> txm_module_instance_id = TXM_MODULE_ID;
+ module_instance -> txm_module_instance_data_start = (VOID *) TEST_MODULE_DATA;
+ module_instance -> txm_module_instance_data_end = (VOID *) (TEST_MODULE_DATA + TEST_MODULE_DATA_SIZE - 1);
+ module_instance -> txm_module_instance_code_start = (VOID *) 0;
+ module_instance -> txm_module_instance_code_end = (VOID *) 0;
+ module_instance -> txm_module_instance_asid = 0;
+
+ /* The module's own data is answered from the manager's records. */
+ test_expect("recorded module data is writable",
+ _txm_module_manager_inside_data_check(module_instance, (ALIGN_TYPE) TEST_MODULE_DATA, 4,
+ TXM_MODULE_MANAGER_ACCESS_WRITE), TX_TRUE);
+ test_expect("a range crossing the recorded data end is rejected",
+ _txm_module_manager_inside_data_check(module_instance,
+ (ALIGN_TYPE) (TEST_MODULE_DATA + TEST_MODULE_DATA_SIZE - 2), 4,
+ TXM_MODULE_MANAGER_ACCESS_WRITE), TX_FALSE);
+
+ /* Shared memory is answered by the MMU, one page at a time. */
+ test_expect("shared read/write memory is writable",
+ _txm_module_manager_inside_data_check(module_instance, (ALIGN_TYPE) TEST_SHARED_RW, 4,
+ TXM_MODULE_MANAGER_ACCESS_WRITE), TX_TRUE);
+ test_expect("shared read-only memory is readable",
+ _txm_module_manager_inside_data_check(module_instance, (ALIGN_TYPE) TEST_SHARED_RO, 4,
+ TXM_MODULE_MANAGER_ACCESS_READ), TX_TRUE);
+
+ /* The write intent is the reason the port asks for two translations rather
+ than one; a read-only mapping must not serve as a kernel destination. */
+ test_expect("shared read-only memory is not writable",
+ _txm_module_manager_inside_data_check(module_instance, (ALIGN_TYPE) TEST_SHARED_RO, 4,
+ TXM_MODULE_MANAGER_ACCESS_WRITE), TX_FALSE);
+
+ /* A range that leaves its mapping partway through. This is the case the
+ replaced check accepted, and the reason this test exists. */
+ straddle = (ALIGN_TYPE) (TEST_GUARD - 2);
+ test_expect("a range crossing into the guard section is rejected",
+ _txm_module_manager_inside_data_check(module_instance, straddle, 4,
+ TXM_MODULE_MANAGER_ACCESS_READ), TX_FALSE);
+ test_expect("...and translating only its first address would have accepted it",
+ test_first_address_only_check(straddle), TX_TRUE);
+
+ /* Privileged memory is reachable by neither intent, and is outside the module. */
+ test_expect("privileged-only memory is not readable",
+ _txm_module_manager_inside_data_check(module_instance, (ALIGN_TYPE) TEST_KERNEL_ONLY, 4,
+ TXM_MODULE_MANAGER_ACCESS_READ), TX_FALSE);
+ test_expect("privileged-only memory is outside the module",
+ _txm_module_manager_outside_data_check(module_instance, (ALIGN_TYPE) TEST_KERNEL_ONLY, 64), TX_TRUE);
+ test_expect("shared memory is not outside the module",
+ _txm_module_manager_outside_data_check(module_instance, (ALIGN_TYPE) TEST_SHARED_RW, 64), TX_FALSE);
+
+ if (test_failures == 0)
+ {
+ printf("SUCCESS!\n");
+ return(0);
+ }
+
+ printf("ERROR: %u expectation(s) failed\n", test_failures);
+ return(1);
+}
diff --git a/test/tx/module_manager/threadx_module_manager_cortex_a7_range_check_test.c b/test/tx/module_manager/threadx_module_manager_cortex_a7_range_check_test.c
new file mode 100644
index 00000000..a0cca868
--- /dev/null
+++ b/test/tx/module_manager/threadx_module_manager_cortex_a7_range_check_test.c
@@ -0,0 +1,443 @@
+/***************************************************************************
+ * 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
+ **************************************************************************/
+
+
+/**************************************************************************/
+/**************************************************************************/
+/** */
+/** ThreadX Test */
+/** */
+/** Module Manager Cortex-A7 data range validation */
+/** */
+/**************************************************************************/
+/**************************************************************************/
+
+/* This test exercises the Cortex-A7 module data range check through the same
+ macros the module dispatchers use. The check normally asks the MMU about one
+ page at a time, so the test stands a simulated page map behind that one
+ architecture primitive and drives every outcome from the host build.
+
+ No address in this test is dereferenced and none of them name a real target.
+ They are labels in a table that the stubbed primitive answers questions about,
+ which is what lets the test describe a range that leaves a module's mapping
+ without ever building something that could read past one. */
+
+#include <stdio.h>
+
+#define TX_SOURCE_CODE
+
+#include "tx_api.h"
+#include "txm_module.h"
+#include "txm_module_manager_util.h"
+
+
+/* Define the simulated address space.
+
+ The module's recorded data region deliberately ends partway through its last
+ page, because that is what the module manager's page-granular mapping does in
+ practice: the mapping reaches to the end of the page while the recorded region
+ does not. The bytes in between are the rounding slack, and the check must not
+ hand them out. */
+
+#define TEST_PAGE_SIZE 4096
+
+#define TEST_DATA_START 0x20001000UL
+#define TEST_DATA_END 0x200027FFUL /* Ends mid-page: slack follows. */
+#define TEST_DATA_WINDOW_END 0x20002FFFUL /* Last byte the MMU maps for it. */
+
+#define TEST_CODE_START 0x10001000UL
+#define TEST_CODE_END 0x10001FFFUL
+
+#define TEST_SHARED_RW_START 0x20010000UL /* Two contiguous read/write pages. */
+#define TEST_SHARED_RW_END 0x20011FFFUL
+#define TEST_GUARD_PAGE 0x20012000UL /* Unmapped, directly after them. */
+
+#define TEST_SPLIT_FIRST_PAGE 0x20030000UL /* Mapped. */
+#define TEST_SPLIT_MIDDLE_PAGE 0x20031000UL /* Unmapped. */
+#define TEST_SPLIT_LAST_PAGE 0x20032000UL /* Mapped. */
+
+#define TEST_SHARED_RO_START 0x20020000UL /* Readable, not writable. */
+#define TEST_SHARED_RO_END 0x20020FFFUL
+
+#define TEST_KERNEL_OBJECT 0x30000000UL /* Unmapped for the module. */
+
+#define TEST_MODULE_ASID 5UL
+#define TEST_OTHER_MODULE_ASID 6UL
+
+
+/* Define one entry of the simulated page map. */
+
+typedef struct TEST_PAGE_STRUCT
+{
+ ULONG test_page_base;
+ UINT test_page_readable;
+ UINT test_page_writable;
+} TEST_PAGE;
+
+
+static TEST_PAGE test_page_map[] =
+{
+ /* The module's own data pages, mapped read/write to the end of the page. */
+ {TEST_DATA_START, TX_TRUE, TX_TRUE},
+ {TEST_DATA_START + TEST_PAGE_SIZE, TX_TRUE, TX_TRUE},
+
+ /* The module's code, readable but never writable from unprivileged mode. */
+ {TEST_CODE_START, TX_TRUE, TX_FALSE},
+
+ /* Read/write shared memory, followed by an unmapped guard page. */
+ {TEST_SHARED_RW_START, TX_TRUE, TX_TRUE},
+ {TEST_SHARED_RW_START + TEST_PAGE_SIZE, TX_TRUE, TX_TRUE},
+
+ /* Read-only shared memory. */
+ {TEST_SHARED_RO_START, TX_TRUE, TX_FALSE},
+
+ /* Two mapped pages with an unmapped page between them. */
+ {TEST_SPLIT_FIRST_PAGE, TX_TRUE, TX_TRUE},
+ {TEST_SPLIT_LAST_PAGE, TX_TRUE, TX_TRUE}
+};
+
+#define TEST_PAGE_MAP_ENTRIES (sizeof(test_page_map) / sizeof(test_page_map[0]))
+
+
+/* Define the state the stubbed architecture primitives answer from. */
+
+static ULONG test_current_asid = TEST_MODULE_ASID;
+static ULONG test_probe_count = 0UL;
+static UINT test_failures = 0U;
+
+
+/* Stand in for the CP15 address translation the real port performs. */
+UINT _txm_module_manager_address_probe(ULONG address, UINT write_request)
+{
+
+ULONG page_base;
+ULONG index;
+UINT granted;
+
+
+ test_probe_count++;
+
+ page_base = address & (~((ULONG) (TEST_PAGE_SIZE - 1)));
+ granted = TX_FALSE;
+
+ for (index = 0UL; index < (ULONG) TEST_PAGE_MAP_ENTRIES; index++)
+ {
+ if (test_page_map[index].test_page_base == page_base)
+ {
+ if (write_request == TXM_MODULE_MANAGER_ACCESS_WRITE)
+ {
+ granted = test_page_map[index].test_page_writable;
+ }
+ else
+ {
+ granted = test_page_map[index].test_page_readable;
+ }
+ }
+ }
+
+ return(granted);
+}
+
+
+/* Stand in for the CONTEXTIDR read the real port performs. */
+ULONG _txm_module_manager_current_asid_get(VOID)
+{
+ return(test_current_asid);
+}
+
+
+/* Model the implementation this test guards against. It translated the first
+ address of the range and nothing else, and it only ever asked about reading.
+ Keeping it here lets each boundary case show both answers side by side. */
+static UINT test_first_address_only_check(ALIGN_TYPE obj_ptr)
+{
+ return(_txm_module_manager_address_probe((ULONG) obj_ptr, TXM_MODULE_MANAGER_ACCESS_READ));
+}
+
+
+/* Record the outcome of one expectation. */
+static VOID test_expect(const char *description, UINT actual, UINT expected)
+{
+ if (actual != expected)
+ {
+ printf("FAIL: %s (expected %u, got %u)\n", description, expected, actual);
+ test_failures++;
+ }
+}
+
+
+/* Build a module instance describing the simulated module. */
+static VOID test_module_build(TXM_MODULE_INSTANCE *module_instance)
+{
+ module_instance -> txm_module_instance_id = TXM_MODULE_ID;
+ module_instance -> txm_module_instance_data_start = (VOID *) TEST_DATA_START;
+ module_instance -> txm_module_instance_data_end = (VOID *) TEST_DATA_END;
+ module_instance -> txm_module_instance_code_start = (VOID *) TEST_CODE_START;
+ module_instance -> txm_module_instance_code_end = (VOID *) TEST_CODE_END;
+ module_instance -> txm_module_instance_asid = TEST_MODULE_ASID;
+}
+
+
+int main(void)
+{
+
+TXM_MODULE_INSTANCE module;
+TXM_MODULE_INSTANCE *module_instance;
+ALIGN_TYPE address;
+ULONG probes_before;
+ULONG huge_size;
+
+
+ printf("Cortex-A7 module data range validation test............................ ");
+
+ module_instance = &module;
+ test_module_build(module_instance);
+
+ /**********************************************************************/
+ /* Normal valid behaviour. */
+ /**********************************************************************/
+
+ test_expect("one byte inside module data is writable",
+ TXM_MODULE_MANAGER_PARAM_CHECK_BUFFER_WRITE(module_instance, (ALIGN_TYPE) TEST_DATA_START, 1UL), TX_TRUE);
+
+ test_expect("one word inside module data is writable",
+ TXM_MODULE_MANAGER_PARAM_CHECK_BUFFER_WRITE(module_instance, (ALIGN_TYPE) TEST_DATA_START, (ULONG) sizeof(ULONG)), TX_TRUE);
+
+ test_expect("one word inside module data is readable",
+ TXM_MODULE_MANAGER_PARAM_CHECK_BUFFER_READ(module_instance, (ALIGN_TYPE) TEST_DATA_START, (ULONG) sizeof(ULONG)), TX_TRUE);
+
+ test_expect("a range spanning both module data pages is writable",
+ TXM_MODULE_MANAGER_PARAM_CHECK_BUFFER_WRITE(module_instance, (ALIGN_TYPE) TEST_DATA_START, 0x1800UL), TX_TRUE);
+
+ /* The module's own data is answered from the manager's records, so it costs
+ no translations at all. This is what keeps the common case deterministic. */
+ probes_before = test_probe_count;
+ (VOID) TXM_MODULE_MANAGER_PARAM_CHECK_BUFFER_WRITE(module_instance, (ALIGN_TYPE) TEST_DATA_START, 0x1800UL);
+ test_expect("module data is answered without translating",
+ (UINT) (test_probe_count == probes_before), TX_TRUE);
+
+ /**********************************************************************/
+ /* Null and invalid parameters. */
+ /**********************************************************************/
+
+ /* A null buffer is how the dispatchers say "not supplied", and the macros
+ accept it. Only the range check below it must reject a null module. */
+ test_expect("a null buffer pointer is accepted by the buffer macros",
+ TXM_MODULE_MANAGER_PARAM_CHECK_BUFFER_WRITE(module_instance, (ALIGN_TYPE) 0, (ULONG) sizeof(ULONG)), TX_TRUE);
+
+ test_expect("a null module instance is rejected",
+ _txm_module_manager_inside_data_check(TX_NULL, (ALIGN_TYPE) TEST_DATA_START, (ULONG) sizeof(ULONG), TXM_MODULE_MANAGER_ACCESS_WRITE), TX_FALSE);
+
+ test_expect("a null module instance is not outside anything either",
+ _txm_module_manager_outside_data_check(TX_NULL, (ALIGN_TYPE) TEST_KERNEL_OBJECT, (ULONG) sizeof(ULONG)), TX_FALSE);
+
+ /* A module torn down to an empty data region owns nothing, and the address
+ range must then be settled by the MMU rather than by stale records. */
+ module.txm_module_instance_data_start = (VOID *) TEST_DATA_END;
+ module.txm_module_instance_data_end = (VOID *) TEST_DATA_START;
+ test_expect("an inverted data region grants nothing from records",
+ _txm_module_manager_inside_data_check(module_instance, (ALIGN_TYPE) TEST_KERNEL_OBJECT, (ULONG) sizeof(ULONG), TXM_MODULE_MANAGER_ACCESS_WRITE), TX_FALSE);
+ test_module_build(module_instance);
+
+ /**********************************************************************/
+ /* Exact boundaries, and the one byte past each of them. */
+ /**********************************************************************/
+
+ test_expect("a range starting exactly at data start is accepted",
+ TXM_MODULE_MANAGER_PARAM_CHECK_BUFFER_WRITE(module_instance, (ALIGN_TYPE) TEST_DATA_START, 4UL), TX_TRUE);
+
+ test_expect("a range starting one byte before data start is rejected",
+ TXM_MODULE_MANAGER_PARAM_CHECK_BUFFER_WRITE(module_instance, (ALIGN_TYPE) (TEST_DATA_START - 1UL), 4UL), TX_FALSE);
+
+ test_expect("a range ending exactly at data end is accepted",
+ TXM_MODULE_MANAGER_PARAM_CHECK_BUFFER_WRITE(module_instance, (ALIGN_TYPE) (TEST_DATA_END - 3UL), 4UL), TX_TRUE);
+
+ test_expect("the last byte of data on its own is accepted",
+ TXM_MODULE_MANAGER_PARAM_CHECK_BUFFER_WRITE(module_instance, (ALIGN_TYPE) TEST_DATA_END, 1UL), TX_TRUE);
+
+ /* One byte further is the rounding slack: mapped, but not the module's. */
+ address = (ALIGN_TYPE) (TEST_DATA_END - 3UL);
+ test_expect("a range ending one byte past data end is rejected",
+ TXM_MODULE_MANAGER_PARAM_CHECK_BUFFER_WRITE(module_instance, address, 5UL), TX_FALSE);
+ test_expect("...and the first-address-only check would have accepted it",
+ test_first_address_only_check(address), TX_TRUE);
+
+ test_expect("the one-past-the-end byte is rejected on its own",
+ TXM_MODULE_MANAGER_PARAM_CHECK_BUFFER_WRITE(module_instance, (ALIGN_TYPE) (TEST_DATA_END + 1UL), 1UL), TX_FALSE);
+
+ test_expect("the whole rounding slack is rejected",
+ TXM_MODULE_MANAGER_PARAM_CHECK_BUFFER_WRITE(module_instance, (ALIGN_TYPE) (TEST_DATA_END + 1UL), (ULONG) (TEST_DATA_WINDOW_END - TEST_DATA_END)), TX_FALSE);
+
+ /* A word that straddles the recorded end crosses it by one word. */
+ address = (ALIGN_TYPE) (TEST_DATA_END - 1UL);
+ test_expect("a word straddling data end is rejected",
+ TXM_MODULE_MANAGER_PARAM_CHECK_BUFFER_WRITE(module_instance, address, (ULONG) sizeof(ULONG)), TX_FALSE);
+ test_expect("...and the first-address-only check would have accepted it",
+ test_first_address_only_check(address), TX_TRUE);
+
+ /**********************************************************************/
+ /* Integer overflow and underflow. */
+ /**********************************************************************/
+
+ test_expect("a zero-length range is rejected",
+ TXM_MODULE_MANAGER_PARAM_CHECK_BUFFER_WRITE(module_instance, (ALIGN_TYPE) TEST_DATA_START, 0UL), TX_FALSE);
+
+ /* A range whose end wraps must be refused before anything is computed from
+ the wrapped address. */
+ probes_before = test_probe_count;
+ test_expect("a range whose end wraps past the top of memory is rejected",
+ TXM_MODULE_MANAGER_PARAM_CHECK_BUFFER_WRITE(module_instance, (ALIGN_TYPE) TEST_DATA_START, 0xFFFFFFFFUL), TX_FALSE);
+ test_expect("...without translating the wrapped address",
+ (UINT) (test_probe_count == probes_before), TX_TRUE);
+
+ test_expect("a range starting at the very top of memory with size two is rejected",
+ TXM_MODULE_MANAGER_PARAM_CHECK_BUFFER_WRITE(module_instance, (ALIGN_TYPE) 0xFFFFFFFFUL, 2UL), TX_FALSE);
+
+ test_expect("a one-byte range at the very top of memory does not wrap, and is simply unmapped",
+ TXM_MODULE_MANAGER_PARAM_CHECK_BUFFER_WRITE(module_instance, (ALIGN_TYPE) 0xFFFFFFFFUL, 1UL), TX_FALSE);
+
+ test_expect("a zero-length range is not outside the module either",
+ _txm_module_manager_outside_data_check(module_instance, (ALIGN_TYPE) TEST_KERNEL_OBJECT, 0UL), TX_FALSE);
+
+ /**********************************************************************/
+ /* Shared memory, guard pages, and holes. */
+ /**********************************************************************/
+
+ test_expect("a range inside read/write shared memory is writable",
+ TXM_MODULE_MANAGER_PARAM_CHECK_BUFFER_WRITE(module_instance, (ALIGN_TYPE) TEST_SHARED_RW_START, (ULONG) sizeof(ULONG)), TX_TRUE);
+
+ test_expect("a range spanning two shared pages is writable",
+ TXM_MODULE_MANAGER_PARAM_CHECK_BUFFER_WRITE(module_instance, (ALIGN_TYPE) TEST_SHARED_RW_START, 0x1800UL), TX_TRUE);
+
+ test_expect("a range ending exactly at the end of shared memory is accepted",
+ TXM_MODULE_MANAGER_PARAM_CHECK_BUFFER_WRITE(module_instance, (ALIGN_TYPE) (TEST_SHARED_RW_END - 3UL), 4UL), TX_TRUE);
+
+ /* One byte more crosses into the guard page. */
+ address = (ALIGN_TYPE) (TEST_SHARED_RW_END - 3UL);
+ test_expect("a range crossing one byte into the guard page is rejected",
+ TXM_MODULE_MANAGER_PARAM_CHECK_BUFFER_WRITE(module_instance, address, 5UL), TX_FALSE);
+ test_expect("...and the first-address-only check would have accepted it",
+ test_first_address_only_check(address), TX_TRUE);
+
+ test_expect("a three-page range with an unmapped middle page is rejected",
+ TXM_MODULE_MANAGER_PARAM_CHECK_BUFFER_WRITE(module_instance, (ALIGN_TYPE) TEST_SPLIT_FIRST_PAGE, (ULONG) ((TEST_SPLIT_LAST_PAGE + 4UL) - TEST_SPLIT_FIRST_PAGE)), TX_FALSE);
+
+ test_expect("the first page of that range on its own is still accepted",
+ TXM_MODULE_MANAGER_PARAM_CHECK_BUFFER_WRITE(module_instance, (ALIGN_TYPE) TEST_SPLIT_FIRST_PAGE, 4UL), TX_TRUE);
+
+ /**********************************************************************/
+ /* Read intent versus write intent. */
+ /**********************************************************************/
+
+ test_expect("read-only shared memory can be read from",
+ TXM_MODULE_MANAGER_PARAM_CHECK_BUFFER_READ(module_instance, (ALIGN_TYPE) TEST_SHARED_RO_START, (ULONG) sizeof(ULONG)), TX_TRUE);
+
+ test_expect("read-only shared memory cannot be written to",
+ TXM_MODULE_MANAGER_PARAM_CHECK_BUFFER_WRITE(module_instance, (ALIGN_TYPE) TEST_SHARED_RO_START, (ULONG) sizeof(ULONG)), TX_FALSE);
+
+ test_expect("a read-only shared range crossing its own end is rejected for reading too",
+ TXM_MODULE_MANAGER_PARAM_CHECK_BUFFER_READ(module_instance, (ALIGN_TYPE) (TEST_SHARED_RO_END - 3UL), 5UL), TX_FALSE);
+
+ test_expect("module code can be read from",
+ TXM_MODULE_MANAGER_PARAM_CHECK_BUFFER_READ(module_instance, (ALIGN_TYPE) TEST_CODE_START, (ULONG) sizeof(ULONG)), TX_TRUE);
+
+ test_expect("module code cannot be used as a kernel write destination",
+ TXM_MODULE_MANAGER_PARAM_CHECK_BUFFER_WRITE(module_instance, (ALIGN_TYPE) TEST_CODE_START, (ULONG) sizeof(ULONG)), TX_FALSE);
+
+ test_expect("a string may be dereferenced out of module code",
+ TXM_MODULE_MANAGER_PARAM_CHECK_DEREFERENCE_STRING(module_instance, (ALIGN_TYPE) TEST_CODE_START), TX_TRUE);
+
+ test_expect("a range running off the end of module code is rejected",
+ TXM_MODULE_MANAGER_PARAM_CHECK_BUFFER_READ(module_instance, (ALIGN_TYPE) (TEST_CODE_END - 3UL), 5UL), TX_FALSE);
+
+ /**********************************************************************/
+ /* The outside direction. */
+ /**********************************************************************/
+
+ test_expect("a kernel object clear of the module is outside it",
+ TXM_MODULE_MANAGER_ENSURE_OUTSIDE_MODULE(module_instance, (ALIGN_TYPE) TEST_KERNEL_OBJECT, 64UL), TX_TRUE);
+
+ test_expect("the module's own data is not outside it",
+ TXM_MODULE_MANAGER_ENSURE_OUTSIDE_MODULE(module_instance, (ALIGN_TYPE) TEST_DATA_START, 64UL), TX_FALSE);
+
+ test_expect("shared memory is not outside the module",
+ TXM_MODULE_MANAGER_ENSURE_OUTSIDE_MODULE(module_instance, (ALIGN_TYPE) TEST_SHARED_RW_START, 64UL), TX_FALSE);
+
+ test_expect("module code is not outside the module",
+ TXM_MODULE_MANAGER_ENSURE_OUTSIDE_MODULE(module_instance, (ALIGN_TYPE) TEST_CODE_START, 64UL), TX_FALSE);
+
+ /* A range that reaches into the module only partway is inside neither answer.
+ Reporting it as outside would let it pass as a kernel object, which is the
+ mirror image of the defect this test guards against. */
+ test_expect("a range straddling the end of module data is not outside it",
+ TXM_MODULE_MANAGER_ENSURE_OUTSIDE_MODULE(module_instance, (ALIGN_TYPE) (TEST_DATA_END - 3UL), 16UL), TX_FALSE);
+
+ test_expect("a range straddling the guard page after shared memory is not outside the module",
+ TXM_MODULE_MANAGER_ENSURE_OUTSIDE_MODULE(module_instance, (ALIGN_TYPE) (TEST_SHARED_RW_END - 3UL), 16UL), TX_FALSE);
+
+ /**********************************************************************/
+ /* Cross-module and application-owned memory. */
+ /**********************************************************************/
+
+ /* Translation answers for whichever context is loaded. If another module's
+ context is loaded, this module's records and the MMU disagree, so neither
+ check may answer at all. */
+ test_current_asid = TEST_OTHER_MODULE_ASID;
+
+ test_expect("nothing is inside the module while another context is loaded",
+ TXM_MODULE_MANAGER_PARAM_CHECK_BUFFER_WRITE(module_instance, (ALIGN_TYPE) TEST_DATA_START, (ULONG) sizeof(ULONG)), TX_FALSE);
+
+ test_expect("nothing is provably outside it either",
+ TXM_MODULE_MANAGER_ENSURE_OUTSIDE_MODULE(module_instance, (ALIGN_TYPE) TEST_KERNEL_OBJECT, 64UL), TX_FALSE);
+
+ test_current_asid = TEST_MODULE_ASID;
+
+ test_expect("the module is served again once its own context is loaded",
+ TXM_MODULE_MANAGER_PARAM_CHECK_BUFFER_WRITE(module_instance, (ALIGN_TYPE) TEST_DATA_START, (ULONG) sizeof(ULONG)), TX_TRUE);
+
+ /**********************************************************************/
+ /* The bound on how much work one request may ask for. */
+ /**********************************************************************/
+
+ huge_size = ((ULONG) TXM_MODULE_MANAGER_DATA_CHECK_MAX_PAGES) * ((ULONG) TEST_PAGE_SIZE);
+
+ probes_before = test_probe_count;
+ test_expect("a range one page longer than the maximum is rejected",
+ TXM_MODULE_MANAGER_PARAM_CHECK_BUFFER_WRITE(module_instance, (ALIGN_TYPE) TEST_SHARED_RW_START, huge_size + 1UL), TX_FALSE);
+ test_expect("...without translating any of it",
+ (UINT) (test_probe_count == probes_before), TX_TRUE);
+
+ test_expect("an over-long range is not outside the module either",
+ _txm_module_manager_outside_data_check(module_instance, (ALIGN_TYPE) TEST_KERNEL_OBJECT, huge_size + 1UL), TX_FALSE);
+
+ /* A range at exactly the maximum is still walked, and the walk stops at the
+ first page that fails rather than translating all of it. */
+ probes_before = test_probe_count;
+ test_expect("a range of exactly the maximum length is walked and rejected here",
+ TXM_MODULE_MANAGER_PARAM_CHECK_BUFFER_WRITE(module_instance, (ALIGN_TYPE) TEST_SHARED_RW_START, huge_size), TX_FALSE);
+ test_expect("...stopping at the first page that fails",
+ (UINT) ((test_probe_count - probes_before) == 3UL), TX_TRUE);
+
+ /**********************************************************************/
+
+ if (test_failures == 0U)
+ {
+ printf("SUCCESS!\n");
+ return(0);
+ }
+
+ printf("ERROR: %u expectation(s) failed\n", test_failures);
+ return(1);
+}
diff --git a/test/tx/module_manager/threadx_module_manager_delete_ownership_test.c b/test/tx/module_manager/threadx_module_manager_delete_ownership_test.c
new file mode 100644
index 00000000..95a85ddc
--- /dev/null
+++ b/test/tx/module_manager/threadx_module_manager_delete_ownership_test.c
@@ -0,0 +1,1031 @@
+/***************************************************************************
+ * 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
+ **************************************************************************/
+
+
+/**************************************************************************/
+/**************************************************************************/
+/** */
+/** ThreadX Test */
+/** */
+/** Module Manager kernel object delete ownership */
+/** */
+/**************************************************************************/
+/**************************************************************************/
+
+/* A module reaches a kernel object's delete service through the Module Manager's
+ dispatch layer, which ran one check on the pointer: that the object lies outside
+ the module's own code and data. That is the policy for using an object, and using
+ an object a module does not own is supported -- the manager hands application-owned
+ objects to modules by name through its object lookup service, and sharing is what
+ that service is for. Destroying one is not sharing it.
+
+ Two things follow from the missing question, and this test covers both.
+
+ The manager's private header sits immediately in front of each allocation it makes,
+ and records which module the allocation belongs to. The deallocation the delete path
+ performs afterwards reached that header by subtracting from whatever address the
+ caller supplied, and read it before deciding whether it was a header at all. For an
+ object the application allocated statically, or for any address outside the object
+ pool, the words in front of it are unrelated memory read in privileged mode -- and
+ acted on: an address whose preceding words happen to name the calling module was
+ unlinked from that module's allocation list and handed to the byte pool.
+
+ The manager now finds the allocation by searching the module's own allocation list
+ instead. The caller's address is compared, never dereferenced, so an address that
+ names none of this module's allocations is refused without a privileged read of
+ anything in front of it -- and the search establishes what the header read could
+ not, that the address is the exact start of an allocation rather than somewhere
+ inside one.
+
+ The same search is the ownership question the delete dispatchers now ask before the
+ delete rather than after it. It is a separate question from whether an object is
+ there and of the expected type, and it is composed with that one rather than
+ replacing it.
+
+ This test drives the search and the deallocation on the host. The object pool behind
+ it is modelled -- a bump allocator that hands back the block it most recently
+ released, so that an address a module has given up can be seen to come back to
+ another module -- while the record the fix actually consults is built the way the
+ system builds it, by running the real _txm_module_manager_object_allocate.
+
+ The object standing for one the application owns is deliberately preceded by a
+ header naming the requesting module. Out-of-bounds reads do not fault on a host, so
+ that is how this test makes one observable: on the unfixed manager those words are
+ read and believed, and the application's object memory is released; here they must
+ be neither read nor believed. */
+
+#include <stdio.h>
+#include <string.h>
+
+#include "threadx_module_manager_host_test_port.h"
+
+#include "tx_thread.h"
+#include "tx_timer.h"
+#include "tx_queue.h"
+#include "tx_event_flags.h"
+#include "tx_semaphore.h"
+#include "tx_mutex.h"
+#include "tx_block_pool.h"
+#include "tx_byte_pool.h"
+#include "txm_module.h"
+#include "txm_module_manager_util.h"
+
+
+/* Define the stand-in interrupt lock the port shim counts through. */
+
+unsigned int test_interrupt_disable_depth;
+unsigned int test_interrupt_disable_max_depth;
+unsigned int test_interrupt_restore_underflows;
+
+
+/* Define the modelled object pool. */
+
+#define TEST_POOL_BYTES ((ULONG) 8192)
+#define TEST_MODULE_MEMORY_BYTES 512U
+
+static union
+{
+ ALIGN_TYPE test_pool_alignment;
+ UCHAR test_pool_bytes[TEST_POOL_BYTES];
+} test_pool_arena;
+
+static ULONG test_pool_offset;
+
+
+/* Define the memory that stands for the calling module's own code and data. */
+
+static union
+{
+ ALIGN_TYPE test_data_alignment;
+ UCHAR test_data_bytes[TEST_MODULE_MEMORY_BYTES];
+} test_module_data;
+
+static union
+{
+ ALIGN_TYPE test_code_alignment;
+ UCHAR test_code_bytes[TEST_MODULE_MEMORY_BYTES];
+} test_module_code;
+
+
+/* Define an object that stands for one the application owns and shared with a
+ module by name. It lives outside the object pool, so the manager holds no
+ allocation record of it, and it carries in front of it exactly what the manager
+ would have put there had it allocated it -- a header naming the module that does
+ the asking below. Nothing outside this test can arrange those words; they are
+ here so that reading them can be seen to have happened. */
+
+static struct
+{
+ TXM_MODULE_ALLOCATED_OBJECT test_host_header;
+ TX_QUEUE test_host_object;
+} test_host_area;
+
+
+/* Define the thread that stands for the module thread making the request. The
+ manager takes the calling module from it. */
+
+static TX_THREAD test_current_thread;
+
+
+/* Define the block the modelled pool most recently released, so that a later
+ allocation of the same size lands on the address a released one had, and so that
+ a case can see whether anything was released at all. */
+
+static UCHAR *test_released_start;
+static ULONG test_released_bytes;
+static ULONG test_releases;
+
+
+/* Define the manager state the code under test reaches for. */
+
+TX_BYTE_POOL _txm_module_manager_object_pool;
+UINT _txm_module_manager_object_pool_created;
+TX_MUTEX _txm_module_manager_mutex;
+TX_THREAD *_tx_thread_current_ptr;
+
+
+/* The kernel's created lists. A created object lives on the list for its type, and
+ the manager reads those lists, so a test that stands in for the kernel has to
+ provide them. All stay empty here, because this test creates no kernel object;
+ it exercises ownership of the manager's own allocations. */
+
+TX_BLOCK_POOL *_tx_block_pool_created_ptr;
+ULONG _tx_block_pool_created_count;
+TX_BYTE_POOL *_tx_byte_pool_created_ptr;
+ULONG _tx_byte_pool_created_count;
+TX_EVENT_FLAGS_GROUP *_tx_event_flags_created_ptr;
+ULONG _tx_event_flags_created_count;
+TX_MUTEX *_tx_mutex_created_ptr;
+ULONG _tx_mutex_created_count;
+TX_QUEUE *_tx_queue_created_ptr;
+ULONG _tx_queue_created_count;
+TX_SEMAPHORE *_tx_semaphore_created_ptr;
+ULONG _tx_semaphore_created_count;
+TX_THREAD *_tx_thread_created_ptr;
+ULONG _tx_thread_created_count;
+TX_TIMER *_tx_timer_created_ptr;
+ULONG _tx_timer_created_count;
+
+
+static UINT test_failures;
+static ULONG test_checks;
+
+
+/* Record the outcome of one expectation. */
+static VOID test_expect(const char *description, ULONG actual, ULONG expected)
+{
+ test_checks++;
+
+ if (actual != expected)
+ {
+ printf("FAIL: %s (expected %lu, got %lu)\n", description, (unsigned long) expected, (unsigned long) actual);
+ test_failures++;
+ }
+}
+
+
+/* Report every module object type the manager knows and the size of its control
+ block, so that the cases below can be written once and run against all of them.
+ The size is what a delete dispatcher passes, and the eight sizes are not all
+ distinct: on a 32-bit target a byte pool, a mutex and a timer are the same size,
+ which is why the check below is an ownership check and not a type check. */
+
+typedef struct TEST_OBJECT_KIND_STRUCT
+{
+ const char *test_kind_name;
+ ULONG test_kind_size;
+} TEST_OBJECT_KIND;
+
+static const TEST_OBJECT_KIND test_object_kinds[] =
+{
+ { "block pool", (ULONG) sizeof(TX_BLOCK_POOL) },
+ { "byte pool", (ULONG) sizeof(TX_BYTE_POOL) },
+ { "event flags", (ULONG) sizeof(TX_EVENT_FLAGS_GROUP) },
+ { "mutex", (ULONG) sizeof(TX_MUTEX) },
+ { "queue", (ULONG) sizeof(TX_QUEUE) },
+ { "semaphore", (ULONG) sizeof(TX_SEMAPHORE) },
+ { "thread", (ULONG) sizeof(TX_THREAD) },
+ { "timer", (ULONG) sizeof(TX_TIMER) }
+};
+
+#define TEST_OBJECT_KIND_COUNT (sizeof(test_object_kinds) / sizeof(test_object_kinds[0]))
+
+
+/* Round a request up the way _tx_byte_allocate does. */
+static ULONG test_round_up(ULONG memory_size)
+{
+ return((((memory_size + ((ULONG) sizeof(ALIGN_TYPE))) - ((ULONG) 1)) / ((ULONG) sizeof(ALIGN_TYPE))) * ((ULONG) sizeof(ALIGN_TYPE)));
+}
+
+
+/* Stand in for the byte pool the manager allocates object memory from.
+
+ The two size bounds are the real ones. The placement is a bump allocator with one
+ refinement: a request that exactly fits the block most recently released is given
+ that block back. A first-fit byte pool reuses freed memory, and the cases about an
+ address that has changed hands need the address to actually come back. */
+UINT _txe_byte_allocate(TX_BYTE_POOL *pool_ptr, VOID **memory_ptr, ULONG memory_size, ULONG wait_option)
+{
+
+ULONG rounded_size;
+UCHAR *block_start;
+
+
+ (VOID) wait_option;
+
+ if ((pool_ptr != &_txm_module_manager_object_pool) || (memory_ptr == TX_NULL))
+ {
+ return(TX_POOL_ERROR);
+ }
+
+ if (memory_size == ((ULONG) 0))
+ {
+ return(TX_SIZE_ERROR);
+ }
+
+ if (memory_size > pool_ptr -> tx_byte_pool_size)
+ {
+ return(TX_SIZE_ERROR);
+ }
+
+ rounded_size = test_round_up(memory_size);
+
+ if ((test_released_start != TX_NULL) && (test_released_bytes == rounded_size))
+ {
+ block_start = test_released_start;
+ test_released_start = TX_NULL;
+ test_released_bytes = ((ULONG) 0);
+ }
+ else
+ {
+ if ((test_pool_offset + rounded_size) > TEST_POOL_BYTES)
+ {
+ return(TX_NO_MEMORY);
+ }
+
+ block_start = &test_pool_arena.test_pool_bytes[test_pool_offset];
+ test_pool_offset = test_pool_offset + rounded_size;
+ }
+
+ pool_ptr -> tx_byte_pool_available = pool_ptr -> tx_byte_pool_available - rounded_size;
+
+ *memory_ptr = (VOID *) block_start;
+
+ return(TX_SUCCESS);
+}
+
+
+/* Stand in for the release side, remembering the block so it can come back, and
+ counting releases so that a case can see whether one happened.
+
+ The bounds test is the one thing the model must keep: releasing an address the
+ pool never owned is the corruption the fix exists to prevent, so it is reported
+ as a failure rather than silently absorbed. */
+UINT _txe_byte_release(VOID *memory_ptr)
+{
+
+UCHAR *block_start;
+
+
+ block_start = (UCHAR *) memory_ptr;
+
+ if ((block_start < test_pool_arena.test_pool_bytes) ||
+ (block_start >= &test_pool_arena.test_pool_bytes[TEST_POOL_BYTES]))
+ {
+ printf("FAIL: an address outside the object pool was released to it\n");
+ test_failures++;
+
+ return(TX_PTR_ERROR);
+ }
+
+ test_releases++;
+
+ /* The model does not track block lengths, and the only released block a case
+ needs to come back is the one it just released, whose length it knows. The
+ length is recorded by the caller through test_release_size. */
+ test_released_start = block_start;
+
+ return(TX_SUCCESS);
+}
+
+
+/* Tell the model how long the block just released was. */
+static VOID test_release_size(ULONG object_size)
+{
+ test_released_bytes = test_round_up(object_size + ((ULONG) sizeof(TXM_MODULE_ALLOCATED_OBJECT)));
+}
+
+
+/* Stand in for the protection mutex. */
+UINT _txe_mutex_get(TX_MUTEX *mutex_ptr, ULONG wait_option)
+{
+ (VOID) mutex_ptr;
+ (VOID) wait_option;
+
+ return(TX_SUCCESS);
+}
+
+
+UINT _txe_mutex_put(TX_MUTEX *mutex_ptr)
+{
+ (VOID) mutex_ptr;
+
+ return(TX_SUCCESS);
+}
+
+
+/* Stand in for the module port's data range check. */
+UINT _txm_module_manager_inside_data_check(TXM_MODULE_INSTANCE *module_instance, ALIGN_TYPE obj_ptr,
+ ULONG obj_size, UINT write_request)
+{
+
+ULONG data_start;
+ULONG data_end;
+
+
+ (VOID) module_instance;
+ (VOID) write_request;
+
+ data_start = (ULONG) (ALIGN_TYPE) test_module_data.test_data_bytes;
+ data_end = data_start + ((ULONG) TEST_MODULE_MEMORY_BYTES);
+
+ /* The whole range has to lie inside the module's data, not just its first
+ address, and the subtraction is done on the end rather than the start so that
+ a size which would carry the range past the top of the address space cannot
+ wrap into an accepting answer. */
+ if (((ULONG) obj_ptr >= data_start) && (obj_size <= (data_end - (ULONG) obj_ptr)))
+ {
+ return(TX_TRUE);
+ }
+
+ return(TX_FALSE);
+}
+
+
+/* Outside is not the negation of inside: a range that only partly reaches into the
+ module's data is neither. It has to end before that data begins or begin after it
+ ends, and a size that would carry the end past the top of the address space is
+ refused rather than allowed to compare as though it had not. */
+UINT _txm_module_manager_outside_data_check(TXM_MODULE_INSTANCE *module_instance, ALIGN_TYPE obj_ptr,
+ ULONG obj_size)
+{
+
+ULONG data_start;
+ULONG data_end;
+
+
+ (VOID) module_instance;
+
+ data_start = (ULONG) (ALIGN_TYPE) test_module_data.test_data_bytes;
+ data_end = data_start + ((ULONG) TEST_MODULE_MEMORY_BYTES);
+
+ if (obj_size > (~((ULONG) 0) - (ULONG) obj_ptr))
+ {
+ return(TX_FALSE);
+ }
+
+ if ((((ULONG) obj_ptr + obj_size) <= data_start) || ((ULONG) obj_ptr >= data_end))
+ {
+ return(TX_TRUE);
+ }
+
+ return(TX_FALSE);
+}
+
+
+/* Stand in for the port primitive a manager source under test refers to but that
+ no case here exercises. */
+VOID _txm_module_manager_alignment_adjust(TXM_MODULE_PREAMBLE *module_preamble, ULONG *code_size,
+ ULONG *code_alignment, ULONG *data_size, ULONG *data_alignment)
+{
+ (VOID) module_preamble;
+ (VOID) code_size;
+ (VOID) code_alignment;
+ (VOID) data_size;
+ (VOID) data_alignment;
+}
+
+
+/* Bring the pool, the modules and the application's object back to a known state.
+
+ Everything is reset together deliberately. The allocation lists point into the
+ pool, so a case that left one behind would have the next case reading the
+ wreckage of an earlier one rather than what it was written to check. */
+static VOID test_reset(TXM_MODULE_INSTANCE *module_a, TXM_MODULE_INSTANCE *module_b)
+{
+
+UINT index;
+
+
+ for (index = 0U; index < (UINT) TEST_POOL_BYTES; index++)
+ {
+ test_pool_arena.test_pool_bytes[index] = (UCHAR) 0;
+ }
+
+ for (index = 0U; index < TEST_MODULE_MEMORY_BYTES; index++)
+ {
+ test_module_data.test_data_bytes[index] = (UCHAR) 0;
+ test_module_code.test_code_bytes[index] = (UCHAR) 0;
+ }
+
+ test_pool_offset = ((ULONG) 0);
+ test_released_start = TX_NULL;
+ test_released_bytes = ((ULONG) 0);
+ test_releases = ((ULONG) 0);
+
+ _txm_module_manager_object_pool.tx_byte_pool_id = TX_BYTE_POOL_ID;
+ _txm_module_manager_object_pool.tx_byte_pool_start = test_pool_arena.test_pool_bytes;
+ _txm_module_manager_object_pool.tx_byte_pool_size = TEST_POOL_BYTES;
+ _txm_module_manager_object_pool.tx_byte_pool_available = TEST_POOL_BYTES;
+ _txm_module_manager_object_pool_created = TX_TRUE;
+
+ module_a -> txm_module_instance_object_list_count = ((ULONG) 0);
+ module_a -> txm_module_instance_object_list_head = TX_NULL;
+ module_b -> txm_module_instance_object_list_count = ((ULONG) 0);
+ module_b -> txm_module_instance_object_list_head = TX_NULL;
+
+ /* The application's object, and the header in front of it naming module A. The
+ links are made to point at the header itself, which is what an allocation
+ list of one looks like, so that the unfixed manager would find nothing
+ unusual about it either. */
+ (VOID) memset((VOID *) &test_host_area, 0, sizeof(test_host_area));
+ test_host_area.test_host_header.txm_module_allocated_object_module_instance = module_a;
+ test_host_area.test_host_header.txm_module_allocated_object_next = &test_host_area.test_host_header;
+ test_host_area.test_host_header.txm_module_allocated_object_previous = &test_host_area.test_host_header;
+ test_host_area.test_host_header.txm_module_object_size = (ULONG) sizeof(TX_QUEUE);
+ test_host_area.test_host_object.tx_queue_id = TX_QUEUE_ID;
+
+ /* The requesting thread belongs to module A throughout, except where a case
+ says otherwise. */
+ test_current_thread.tx_thread_module_instance_ptr = module_a;
+ _tx_thread_current_ptr = &test_current_thread;
+}
+
+
+/* Zero the interrupt lock accounting before a call into the manager. */
+static VOID test_interrupts_clear(VOID)
+{
+ test_interrupt_disable_depth = 0U;
+ test_interrupt_disable_max_depth = 0U;
+ test_interrupt_restore_underflows = 0U;
+}
+
+
+/* Whatever the manager decided, it must have left the interrupt lock as it found
+ it. */
+static VOID test_interrupts_check(const char *what)
+{
+ if ((test_interrupt_disable_depth != 0U) || (test_interrupt_restore_underflows != 0U))
+ {
+ printf("FAIL: %s left the interrupt lock unbalanced (depth %u, underflows %u)\n",
+ what, test_interrupt_disable_depth, test_interrupt_restore_underflows);
+ test_failures++;
+ }
+}
+
+
+/* Allocate object memory for a module through the real manager path. */
+static VOID *test_allocate(TXM_MODULE_INSTANCE *module_instance, ULONG object_size)
+{
+
+VOID *object_ptr;
+UINT status;
+
+
+ object_ptr = TX_NULL;
+ status = _txm_module_manager_object_allocate(&object_ptr, object_size, module_instance);
+
+ if (status != TX_SUCCESS)
+ {
+ printf("FAIL: the modelled pool refused an allocation of %lu bytes (status %u)\n",
+ (unsigned long) object_size, status);
+ test_failures++;
+
+ return(TX_NULL);
+ }
+
+ return(object_ptr);
+}
+
+
+/* Ask the question a delete dispatcher asks: may this module destroy the object at
+ this address? The macro is what the dispatchers use, so the macro is what is
+ called here. */
+static ULONG test_owner_check(TXM_MODULE_INSTANCE *module_instance, VOID *object_ptr, ULONG object_size)
+{
+
+ULONG status;
+
+
+ test_interrupts_clear();
+
+ status = (ULONG) TXM_MODULE_MANAGER_PARAM_CHECK_OBJECT_FOR_DELETION(module_instance,
+ (ALIGN_TYPE) object_ptr, object_size);
+
+ test_interrupts_check("the ownership check");
+
+ return(status);
+}
+
+
+/* Ask the manager to give an address back, with the same accounting. */
+static ULONG test_deallocate(VOID *object_ptr)
+{
+
+ULONG status;
+
+
+ test_interrupts_clear();
+
+ status = (ULONG) _txm_module_manager_object_deallocate(object_ptr);
+
+ test_interrupts_check("the deallocation");
+
+ return(status);
+}
+
+
+int main(void)
+{
+
+TXM_MODULE_INSTANCE module_a;
+TXM_MODULE_INSTANCE module_b;
+const TEST_OBJECT_KIND *kind;
+VOID *object;
+VOID *second_object;
+VOID *third_object;
+VOID *reused_object;
+TXM_MODULE_ALLOCATED_OBJECT *header_ptr;
+ALIGN_TYPE address;
+ULONG index;
+ULONG offset;
+ULONG reported_size;
+char description[160];
+
+
+ /* Report expectations before anything can crash, so that a case that brings the
+ process down is still preceded by everything that passed. */
+ setvbuf(stdout, TX_NULL, _IONBF, 0);
+
+ test_failures = 0U;
+ test_checks = ((ULONG) 0);
+
+ /* Set up two memory-protected modules. Module A does the asking throughout;
+ module B is there to own allocations A did not make. */
+ module_a.txm_module_instance_property_flags = TXM_MODULE_MEMORY_PROTECTION;
+ module_a.txm_module_instance_code_start = (VOID *) test_module_code.test_code_bytes;
+ module_a.txm_module_instance_code_end = (VOID *) &test_module_code.test_code_bytes[TEST_MODULE_MEMORY_BYTES - 1U];
+ module_a.txm_module_instance_data_start = (VOID *) test_module_data.test_data_bytes;
+ module_a.txm_module_instance_data_end = (VOID *) &test_module_data.test_data_bytes[TEST_MODULE_MEMORY_BYTES - 1U];
+
+ module_b = module_a;
+
+ /**********************************************************************/
+ /* Every object type, owned and not owned. */
+ /**********************************************************************/
+
+ for (index = ((ULONG) 0); index < (ULONG) TEST_OBJECT_KIND_COUNT; index++)
+ {
+ kind = &test_object_kinds[index];
+
+ test_reset(&module_a, &module_b);
+
+ object = test_allocate(&module_a, kind -> test_kind_size);
+
+ (void) snprintf(description, sizeof(description),
+ "the module that allocated a %s may delete it", kind -> test_kind_name);
+ test_expect(description, test_owner_check(&module_a, object, kind -> test_kind_size), (ULONG) TX_TRUE);
+
+ /* The search has to have run with interrupts disabled, which is what keeps
+ it from observing the allocation list being changed under it. */
+ test_expect("...found with interrupts disabled, one deep",
+ (ULONG) test_interrupt_disable_max_depth, ((ULONG) 1));
+
+ (void) snprintf(description, sizeof(description),
+ "another module may not delete that %s", kind -> test_kind_name);
+ test_expect(description, test_owner_check(&module_b, object, kind -> test_kind_size), (ULONG) TX_FALSE);
+
+ (void) snprintf(description, sizeof(description),
+ "...and the refusal also ran with interrupts disabled");
+ test_expect(description, (ULONG) test_interrupt_disable_max_depth, ((ULONG) 1));
+
+ (void) snprintf(description, sizeof(description),
+ "a %s allocation is not accepted as a larger object", kind -> test_kind_name);
+ test_expect(description,
+ test_owner_check(&module_a, object, kind -> test_kind_size + ((ULONG) sizeof(ALIGN_TYPE))),
+ (ULONG) TX_FALSE);
+
+ (void) snprintf(description, sizeof(description),
+ "a %s allocation is not accepted as a smaller object", kind -> test_kind_name);
+ test_expect(description,
+ test_owner_check(&module_a, object, kind -> test_kind_size - ((ULONG) sizeof(ALIGN_TYPE))),
+ (ULONG) TX_FALSE);
+
+ /* The search returns the allocation's own header, which is the one the
+ deallocation goes on to unlink. */
+ test_interrupts_clear();
+ reported_size = ((ULONG) 0);
+ header_ptr = _txm_module_manager_allocated_object_find(&module_a, (ALIGN_TYPE) object, &reported_size);
+ test_interrupts_check("the search");
+
+ (void) snprintf(description, sizeof(description),
+ "the search finds the header in front of the %s", kind -> test_kind_name);
+ test_expect(description,
+ (ULONG) (header_ptr == (((TXM_MODULE_ALLOCATED_OBJECT *) object) - 1)), (ULONG) TX_TRUE);
+
+ (void) snprintf(description, sizeof(description),
+ "...and reports the size the %s allocation was made for", kind -> test_kind_name);
+ test_expect(description, reported_size, kind -> test_kind_size);
+
+ /* A caller that does not want the size gets the same answer. */
+ test_interrupts_clear();
+ header_ptr = _txm_module_manager_allocated_object_find(&module_a, (ALIGN_TYPE) object, TX_NULL);
+ test_interrupts_check("the search");
+
+ test_expect("a search that does not ask for the size finds the same allocation",
+ (ULONG) (header_ptr == (((TXM_MODULE_ALLOCATED_OBJECT *) object) - 1)), (ULONG) TX_TRUE);
+
+ /* Another module asking for the same address is told there is nothing, and
+ is told it without a size being written anywhere. */
+ test_interrupts_clear();
+ reported_size = (ULONG) 0xA5A5A5A5UL;
+ header_ptr = _txm_module_manager_allocated_object_find(&module_b, (ALIGN_TYPE) object, &reported_size);
+ test_interrupts_check("the search");
+
+ test_expect("a search of another module's allocations finds nothing",
+ (ULONG) (header_ptr == TX_NULL), (ULONG) TX_TRUE);
+ test_expect("...and reports no size",
+ reported_size, (ULONG) 0xA5A5A5A5UL);
+
+ /* The deallocation the delete dispatcher performs after a successful delete
+ still goes through for the module that owns the memory. */
+ test_release_size(kind -> test_kind_size);
+
+ (void) snprintf(description, sizeof(description),
+ "the owning module gets its %s memory back", kind -> test_kind_name);
+ test_expect(description, test_deallocate(object), (ULONG) TX_SUCCESS);
+ test_expect("...released exactly once", test_releases, ((ULONG) 1));
+ test_expect("...leaving nothing on its allocation list",
+ module_a.txm_module_instance_object_list_count, ((ULONG) 0));
+ }
+
+ /**********************************************************************/
+ /* The reported call: an object the application owns. */
+ /**********************************************************************/
+
+ test_reset(&module_a, &module_b);
+
+ object = test_allocate(&module_a, (ULONG) sizeof(TX_QUEUE));
+
+ /* What the fixture is: the words in front of the application's object name the
+ module that is about to ask, so a manager that reads them finds its own
+ header there. That is the read the fix removes. */
+ test_expect("the words in front of the application's object name the asking module",
+ (ULONG) ((((TXM_MODULE_ALLOCATED_OBJECT *) &test_host_area.test_host_object) - 1)
+ -> txm_module_allocated_object_module_instance == &module_a),
+ (ULONG) TX_TRUE);
+
+ test_expect("the application's object is outside the object pool",
+ (ULONG) (((UCHAR *) &test_host_area.test_host_object) < test_pool_arena.test_pool_bytes ||
+ ((UCHAR *) &test_host_area.test_host_object) >= &test_pool_arena.test_pool_bytes[TEST_POOL_BYTES]),
+ (ULONG) TX_TRUE);
+
+ test_expect("a module may not delete an object the application owns",
+ test_owner_check(&module_a, &test_host_area.test_host_object, (ULONG) sizeof(TX_QUEUE)),
+ (ULONG) TX_FALSE);
+
+ test_expect("and the manager will not give its memory back either",
+ test_deallocate(&test_host_area.test_host_object), (ULONG) TX_PTR_ERROR);
+
+ test_expect("...having released nothing", test_releases, ((ULONG) 0));
+
+ test_expect("...left the module's own allocation on its list",
+ module_a.txm_module_instance_object_list_count, ((ULONG) 1));
+
+ test_expect("...with the list head where it was",
+ (ULONG) (module_a.txm_module_instance_object_list_head == (((TXM_MODULE_ALLOCATED_OBJECT *) object) - 1)),
+ (ULONG) TX_TRUE);
+
+ test_expect("...and the application's object untouched",
+ (ULONG) test_host_area.test_host_object.tx_queue_id, (ULONG) TX_QUEUE_ID);
+
+ /* The module's own object is unaffected by the refusal, and still deletable. */
+ test_release_size((ULONG) sizeof(TX_QUEUE));
+ test_expect("the module can still delete its own object afterwards",
+ test_deallocate(object), (ULONG) TX_SUCCESS);
+
+ /**********************************************************************/
+ /* An object another module owns. */
+ /**********************************************************************/
+
+ test_reset(&module_a, &module_b);
+
+ object = test_allocate(&module_a, (ULONG) sizeof(TX_QUEUE));
+ second_object = test_allocate(&module_b, (ULONG) sizeof(TX_QUEUE));
+
+ test_expect("module A may delete its own object",
+ test_owner_check(&module_a, object, (ULONG) sizeof(TX_QUEUE)), (ULONG) TX_TRUE);
+ test_expect("module A may not delete module B's object",
+ test_owner_check(&module_a, second_object, (ULONG) sizeof(TX_QUEUE)), (ULONG) TX_FALSE);
+ test_expect("module B may delete its own object",
+ test_owner_check(&module_b, second_object, (ULONG) sizeof(TX_QUEUE)), (ULONG) TX_TRUE);
+ test_expect("module B may not delete module A's object",
+ test_owner_check(&module_b, object, (ULONG) sizeof(TX_QUEUE)), (ULONG) TX_FALSE);
+
+ /* And the deallocation refuses it too, so a module that reached it another way
+ is stopped there as well. */
+ test_expect("module A cannot give back module B's memory",
+ test_deallocate(second_object), (ULONG) TX_PTR_ERROR);
+ test_expect("...having released nothing", test_releases, ((ULONG) 0));
+ test_expect("...leaving module B's allocation on module B's list",
+ module_b.txm_module_instance_object_list_count, ((ULONG) 1));
+
+ /**********************************************************************/
+ /* Addresses that are not the start of an allocation. */
+ /**********************************************************************/
+
+ test_reset(&module_a, &module_b);
+
+ object = test_allocate(&module_a, (ULONG) sizeof(TX_QUEUE));
+ second_object = test_allocate(&module_a, (ULONG) sizeof(TX_QUEUE));
+
+ test_expect("the header in front of an allocation is not the allocation",
+ test_owner_check(&module_a, (VOID *) ((((TXM_MODULE_ALLOCATED_OBJECT *) object) - 1)),
+ (ULONG) sizeof(TX_QUEUE)),
+ (ULONG) TX_FALSE);
+
+ /* Every aligned address inside the object, which is where a shifted pointer
+ lands. The bytes in front of one of these are the object's own, so a manager
+ that read a header there would be reading what the module put there. */
+ for (offset = (ULONG) sizeof(ALIGN_TYPE);
+ offset < (ULONG) sizeof(TX_QUEUE);
+ offset = offset + (ULONG) sizeof(ALIGN_TYPE))
+ {
+ address = ((ALIGN_TYPE) object) + (ALIGN_TYPE) offset;
+
+ (void) snprintf(description, sizeof(description),
+ "an address %lu bytes into an allocation is not that allocation", (unsigned long) offset);
+ test_expect(description, test_owner_check(&module_a, (VOID *) address, (ULONG) sizeof(TX_QUEUE)),
+ (ULONG) TX_FALSE);
+
+ (void) snprintf(description, sizeof(description),
+ "...and the manager will not give back %lu bytes in", (unsigned long) offset);
+ test_expect(description, test_deallocate((VOID *) address), (ULONG) TX_PTR_ERROR);
+ }
+
+ test_expect("the address one past an allocation is not that allocation",
+ test_owner_check(&module_a, (VOID *) (((ALIGN_TYPE) object) + (ALIGN_TYPE) sizeof(TX_QUEUE)),
+ (ULONG) sizeof(TX_QUEUE)),
+ (ULONG) TX_FALSE);
+
+ test_expect("nothing was released by any of that", test_releases, ((ULONG) 0));
+ test_expect("...and both allocations are still on the list",
+ module_a.txm_module_instance_object_list_count, ((ULONG) 2));
+
+ /**********************************************************************/
+ /* Addresses with nothing behind them at all. */
+ /**********************************************************************/
+
+ test_reset(&module_a, &module_b);
+
+ object = test_allocate(&module_a, (ULONG) sizeof(TX_QUEUE));
+
+ test_expect("a null pointer names no allocation",
+ test_owner_check(&module_a, TX_NULL, (ULONG) sizeof(TX_QUEUE)), (ULONG) TX_FALSE);
+ test_expect("...and is refused by the deallocation",
+ test_deallocate(TX_NULL), (ULONG) TX_PTR_ERROR);
+
+ test_expect("an address with no room for a header in front of it names no allocation",
+ test_owner_check(&module_a, (VOID *) ((ALIGN_TYPE) sizeof(TXM_MODULE_ALLOCATED_OBJECT)),
+ (ULONG) sizeof(TX_QUEUE)),
+ (ULONG) TX_FALSE);
+
+ test_expect("the start of the object pool names no allocation",
+ test_owner_check(&module_a, (VOID *) test_pool_arena.test_pool_bytes, (ULONG) sizeof(TX_QUEUE)),
+ (ULONG) TX_FALSE);
+
+ test_expect("the last byte of the object pool names no allocation",
+ test_owner_check(&module_a, (VOID *) &test_pool_arena.test_pool_bytes[TEST_POOL_BYTES - 1U],
+ (ULONG) sizeof(TX_QUEUE)),
+ (ULONG) TX_FALSE);
+
+ test_expect("the address one past the object pool names no allocation",
+ test_owner_check(&module_a, (VOID *) &test_pool_arena.test_pool_bytes[TEST_POOL_BYTES],
+ (ULONG) sizeof(TX_QUEUE)),
+ (ULONG) TX_FALSE);
+
+ test_expect("an address inside the calling module's own data names no allocation",
+ test_owner_check(&module_a, (VOID *) test_module_data.test_data_bytes, (ULONG) sizeof(TX_QUEUE)),
+ (ULONG) TX_FALSE);
+
+ test_expect("nothing was released by any of that", test_releases, ((ULONG) 0));
+
+ /**********************************************************************/
+ /* A request that comes from no module. */
+ /**********************************************************************/
+
+ test_reset(&module_a, &module_b);
+
+ object = test_allocate(&module_a, (ULONG) sizeof(TX_QUEUE));
+
+ test_expect("no module owns anything",
+ test_owner_check(TX_NULL, object, (ULONG) sizeof(TX_QUEUE)), (ULONG) TX_FALSE);
+
+ test_interrupts_clear();
+ header_ptr = _txm_module_manager_allocated_object_find(TX_NULL, (ALIGN_TYPE) object, TX_NULL);
+ test_interrupts_check("the search");
+
+ test_expect("...and there is no list to search",
+ (ULONG) (header_ptr == TX_NULL), (ULONG) TX_TRUE);
+ test_expect("...reached without disabling interrupts at all",
+ (ULONG) test_interrupt_disable_max_depth, ((ULONG) 0));
+
+ /* The deallocation service is documented as callable by the application, and an
+ application thread carries no module instance. */
+ test_current_thread.tx_thread_module_instance_ptr = TX_NULL;
+
+ test_expect("a caller that is not a module cannot give a module's memory back",
+ test_deallocate(object), (ULONG) TX_PTR_ERROR);
+ test_expect("...having released nothing", test_releases, ((ULONG) 0));
+
+ test_current_thread.tx_thread_module_instance_ptr = &module_a;
+
+ /**********************************************************************/
+ /* Memory that has changed hands. */
+ /**********************************************************************/
+
+ test_reset(&module_a, &module_b);
+
+ object = test_allocate(&module_a, (ULONG) sizeof(TX_QUEUE));
+
+ test_release_size((ULONG) sizeof(TX_QUEUE));
+ test_expect("module A gives its memory back", test_deallocate(object), (ULONG) TX_SUCCESS);
+
+ test_expect("...and may not delete the object there any more",
+ test_owner_check(&module_a, object, (ULONG) sizeof(TX_QUEUE)), (ULONG) TX_FALSE);
+
+ test_expect("...nor give the same address back a second time",
+ test_deallocate(object), (ULONG) TX_PTR_ERROR);
+
+ /* The pool hands the same address to module B, and the stale pointer module A
+ still holds now names memory that belongs to someone else. */
+ reused_object = test_allocate(&module_b, (ULONG) sizeof(TX_QUEUE));
+
+ test_expect("the pool handed the same address to module B",
+ (ULONG) (reused_object == object), (ULONG) TX_TRUE);
+ test_expect("module A's stale pointer does not let it delete module B's object",
+ test_owner_check(&module_a, reused_object, (ULONG) sizeof(TX_QUEUE)), (ULONG) TX_FALSE);
+ test_expect("module B may delete what it was given",
+ test_owner_check(&module_b, reused_object, (ULONG) sizeof(TX_QUEUE)), (ULONG) TX_TRUE);
+ test_expect("module A's stale pointer does not get the memory back either",
+ test_deallocate(reused_object), (ULONG) TX_PTR_ERROR);
+ test_expect("...leaving module B's allocation on module B's list",
+ module_b.txm_module_instance_object_list_count, ((ULONG) 1));
+
+ /**********************************************************************/
+ /* What bounds the search. */
+ /**********************************************************************/
+
+ test_reset(&module_a, &module_b);
+
+ object = test_allocate(&module_a, (ULONG) sizeof(TX_QUEUE));
+ second_object = test_allocate(&module_a, (ULONG) sizeof(TX_QUEUE));
+ third_object = test_allocate(&module_a, (ULONG) sizeof(TX_QUEUE));
+
+ test_expect("three allocations are on the list", module_a.txm_module_instance_object_list_count, ((ULONG) 3));
+ test_expect("each of the three is found",
+ test_owner_check(&module_a, object, (ULONG) sizeof(TX_QUEUE)) +
+ test_owner_check(&module_a, second_object, (ULONG) sizeof(TX_QUEUE)) +
+ test_owner_check(&module_a, third_object, (ULONG) sizeof(TX_QUEUE)),
+ ((ULONG) 3) * (ULONG) TX_TRUE);
+
+ /* A count smaller than the list stops the search early. Only the allocations
+ the count covers are found, which is what keeps the cost of a check bounded
+ by what the manager itself recorded. */
+ module_a.txm_module_instance_object_list_count = ((ULONG) 1);
+
+ test_expect("a search bounded by a count of one reaches the head",
+ test_owner_check(&module_a, module_a.txm_module_instance_object_list_head + 1,
+ (ULONG) sizeof(TX_QUEUE)),
+ (ULONG) TX_TRUE);
+ test_expect("...and goes no further",
+ test_owner_check(&module_a, object, (ULONG) sizeof(TX_QUEUE)) +
+ test_owner_check(&module_a, second_object, (ULONG) sizeof(TX_QUEUE)) +
+ test_owner_check(&module_a, third_object, (ULONG) sizeof(TX_QUEUE)),
+ (ULONG) TX_TRUE);
+
+ module_a.txm_module_instance_object_list_count = ((ULONG) 3);
+
+ /* A count larger than the list, with the chain broken, stops the search at the
+ break rather than running on into whatever follows. */
+ (((TXM_MODULE_ALLOCATED_OBJECT *) second_object) - 1) -> txm_module_allocated_object_next = TX_NULL;
+ module_a.txm_module_instance_object_list_count = ((ULONG) 64);
+
+ test_expect("a search stops at a broken link rather than running on",
+ test_owner_check(&module_a, third_object, (ULONG) sizeof(TX_QUEUE)), (ULONG) TX_FALSE);
+ test_expect("...having still found what came before the break",
+ test_owner_check(&module_a, second_object, (ULONG) sizeof(TX_QUEUE)), (ULONG) TX_TRUE);
+
+ /* An empty list, which is the state a module starts in and the state the
+ manager is left in by module stop. */
+ module_a.txm_module_instance_object_list_count = ((ULONG) 0);
+ module_a.txm_module_instance_object_list_head = TX_NULL;
+
+ test_expect("a module that has allocated nothing owns nothing",
+ test_owner_check(&module_a, object, (ULONG) sizeof(TX_QUEUE)), (ULONG) TX_FALSE);
+ test_expect("...and gets nothing back",
+ test_deallocate(object), (ULONG) TX_PTR_ERROR);
+
+ /**********************************************************************/
+ /* Giving memory back, in each of the shapes the list can be in. */
+ /**********************************************************************/
+
+ test_reset(&module_a, &module_b);
+
+ object = test_allocate(&module_a, (ULONG) sizeof(TX_QUEUE));
+ second_object = test_allocate(&module_a, (ULONG) sizeof(TX_QUEUE));
+ third_object = test_allocate(&module_a, (ULONG) sizeof(TX_QUEUE));
+
+ test_expect("the first allocation is the list head",
+ (ULONG) (module_a.txm_module_instance_object_list_head == (((TXM_MODULE_ALLOCATED_OBJECT *) object) - 1)),
+ (ULONG) TX_TRUE);
+
+ /* Releasing one that is not the head links its neighbours together and leaves
+ the head alone. */
+ test_release_size((ULONG) sizeof(TX_QUEUE));
+ test_expect("releasing an allocation in the middle succeeds",
+ test_deallocate(second_object), (ULONG) TX_SUCCESS);
+ test_expect("...leaving two on the list", module_a.txm_module_instance_object_list_count, ((ULONG) 2));
+ test_expect("...with the head where it was",
+ (ULONG) (module_a.txm_module_instance_object_list_head == (((TXM_MODULE_ALLOCATED_OBJECT *) object) - 1)),
+ (ULONG) TX_TRUE);
+ test_expect("...and the released one no longer owned",
+ test_owner_check(&module_a, second_object, (ULONG) sizeof(TX_QUEUE)), (ULONG) TX_FALSE);
+
+ /* Releasing the head moves it on. */
+ test_release_size((ULONG) sizeof(TX_QUEUE));
+ test_expect("releasing the head allocation succeeds",
+ test_deallocate(object), (ULONG) TX_SUCCESS);
+ test_expect("...leaving one on the list", module_a.txm_module_instance_object_list_count, ((ULONG) 1));
+ test_expect("...and the head moved on to it",
+ (ULONG) (module_a.txm_module_instance_object_list_head == (((TXM_MODULE_ALLOCATED_OBJECT *) third_object) - 1)),
+ (ULONG) TX_TRUE);
+
+ /* Releasing the last empties the list. */
+ test_release_size((ULONG) sizeof(TX_QUEUE));
+ test_expect("releasing the last allocation succeeds",
+ test_deallocate(third_object), (ULONG) TX_SUCCESS);
+ test_expect("...emptying the list", module_a.txm_module_instance_object_list_count, ((ULONG) 0));
+ test_expect("...and clearing its head",
+ (ULONG) (module_a.txm_module_instance_object_list_head == TX_NULL), (ULONG) TX_TRUE);
+ test_expect("three allocations were released and no more", test_releases, ((ULONG) 3));
+
+ /**********************************************************************/
+ /* An object pool that was never created. */
+ /**********************************************************************/
+
+ test_reset(&module_a, &module_b);
+
+ object = test_allocate(&module_a, (ULONG) sizeof(TX_QUEUE));
+
+ _txm_module_manager_object_pool_created = TX_FALSE;
+
+ test_expect("with no object pool the deallocation reports the feature is not available",
+ test_deallocate(object), (ULONG) TX_NOT_AVAILABLE);
+ test_expect("...having released nothing", test_releases, ((ULONG) 0));
+
+ /* The ownership question does not depend on the pool flag: it is answered from
+ the module's allocation list, which is where the manager recorded what it
+ handed out. */
+ test_expect("...and the ownership question is still answered from the allocation list",
+ test_owner_check(&module_a, object, (ULONG) sizeof(TX_QUEUE)), (ULONG) TX_TRUE);
+
+ _txm_module_manager_object_pool_created = TX_TRUE;
+
+ /**********************************************************************/
+ /* What the whole run has to have held. */
+ /**********************************************************************/
+
+ test_expect("every case ran", (ULONG) (test_checks > ((ULONG) 100)), (ULONG) TX_TRUE);
+
+ if (test_failures == 0U)
+ {
+ printf("SUCCESS! %lu expectations\n", (unsigned long) test_checks);
+ return(0);
+ }
+
+ printf("ERROR: %u expectation(s) failed of %lu\n", test_failures, (unsigned long) test_checks);
+ return(1);
+}
diff --git a/test/tx/module_manager/threadx_module_manager_host_test_port.h b/test/tx/module_manager/threadx_module_manager_host_test_port.h
new file mode 100644
index 00000000..36607d20
--- /dev/null
+++ b/test/tx/module_manager/threadx_module_manager_host_test_port.h
@@ -0,0 +1,80 @@
+/***************************************************************************
+ * 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
+ **************************************************************************/
+
+
+/**************************************************************************/
+/**************************************************************************/
+/** */
+/** ThreadX Test */
+/** */
+/** Module Manager host test port shim */
+/** */
+/**************************************************************************/
+/**************************************************************************/
+
+/* The Module Manager is not in the ThreadX library this test tree builds, and its
+ control blocks come from a module port rather than a base port, so the manager
+ sources under test are compiled directly against one module port's headers. Every
+ module port is an embedded architecture, and their interrupt primitives are
+ inline assembly for that architecture, which the host assembler cannot take.
+
+ This header is force-included ahead of each manager source under test. It brings
+ the port's headers in first, so every declaration the source needs is the port's
+ own, and then replaces the three interrupt primitives with host equivalents that
+ count instead of executing. Counting is not only what makes them portable: a test
+ can assert that a source under test left the interrupt lock balanced, and that a
+ section which has to run with interrupts disabled did so. */
+
+#ifndef THREADX_MODULE_MANAGER_HOST_TEST_PORT_H
+#define THREADX_MODULE_MANAGER_HOST_TEST_PORT_H
+
+#define TX_SOURCE_CODE
+
+#include "tx_api.h"
+
+#undef TX_INTERRUPT_SAVE_AREA
+#undef TX_DISABLE
+#undef TX_RESTORE
+
+/* Nesting depth of the stand-in interrupt lock, and the deepest it has ever been.
+ Defined by the test, which is the only translation unit that reads them. */
+extern unsigned int test_interrupt_disable_depth;
+extern unsigned int test_interrupt_disable_max_depth;
+extern unsigned int test_interrupt_restore_underflows;
+
+#define TX_INTERRUPT_SAVE_AREA
+
+#define TX_DISABLE \
+ { \
+ test_interrupt_disable_depth++; \
+ if (test_interrupt_disable_depth > test_interrupt_disable_max_depth) \
+ { \
+ test_interrupt_disable_max_depth = test_interrupt_disable_depth; \
+ } \
+ }
+
+#define TX_RESTORE \
+ { \
+ if (test_interrupt_disable_depth == 0U) \
+ { \
+ test_interrupt_restore_underflows++; \
+ } \
+ else \
+ { \
+ test_interrupt_disable_depth--; \
+ } \
+ }
+
+#endif
diff --git a/test/tx/module_manager/threadx_module_manager_live_object_deallocation_test.c b/test/tx/module_manager/threadx_module_manager_live_object_deallocation_test.c
new file mode 100644
index 00000000..a351ce3b
--- /dev/null
+++ b/test/tx/module_manager/threadx_module_manager_live_object_deallocation_test.c
@@ -0,0 +1,1262 @@
+/***************************************************************************
+ * 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
+ **************************************************************************/
+
+
+/**************************************************************************/
+/**************************************************************************/
+/** */
+/** ThreadX Test */
+/** */
+/** Module Manager live kernel object deallocation */
+/** */
+/**************************************************************************/
+/**************************************************************************/
+
+/* A module allocates the memory for its kernel objects from the Module Manager's
+ object pool, and can ask for that memory back by address. The manager gave that
+ memory back whatever was in it, including a control block the kernel was still
+ using: the object stays on the created list for its type, a thread stays on the
+ ready or suspension list it was on and stays schedulable, and an active timer
+ stays on the timer list. None of those consult a control block ID, so nothing
+ about the freed memory stops the kernel reading a name pointer out of it,
+ writing through the created links in it, restoring a context from the stack
+ pointer in it, or calling the timeout function pointer in it -- while the byte
+ pool is free to have handed those same bytes to the next allocation.
+
+ The manager now refuses to release the memory of an object that is still
+ created, and answers the question from the kernel's created lists rather than
+ from the control block, because a control block is not evidence: an allocation
+ that was never made into an object can carry the value of an ID, and objects of
+ distinct types share a size.
+
+ This test drives that refusal on the host. The object pool behind it is
+ modelled -- a bump allocator that hands back the block it most recently
+ released, so that the address of a freed object can be seen to come back -- but
+ the two records the manager consults are built the way the system builds them:
+ the module allocation list by running the real
+ _txm_module_manager_object_allocate, and the kernel created lists the way the
+ create and delete services maintain them.
+
+ What the test asserts is that memory is given back when, and only when, no live
+ kernel object is in it: refused for every one of the eight object types, and
+ still released for storage that was never created, for storage carrying nothing
+ but a plausible ID, and for an object that has been deleted -- which is the
+ release the delete dispatchers perform, and the one this must not break. */
+
+#include <stdio.h>
+
+#include "threadx_module_manager_host_test_port.h"
+
+#include "tx_thread.h"
+#include "tx_timer.h"
+#include "tx_queue.h"
+#include "tx_event_flags.h"
+#include "tx_semaphore.h"
+#include "tx_mutex.h"
+#include "tx_block_pool.h"
+#include "tx_byte_pool.h"
+#include "txm_module.h"
+#include "txm_module_manager_util.h"
+
+
+/* Define the stand-in interrupt lock the port shim counts through. */
+
+unsigned int test_interrupt_disable_depth;
+unsigned int test_interrupt_disable_max_depth;
+unsigned int test_interrupt_restore_underflows;
+
+
+/* Define the modelled object pool. */
+
+#define TEST_POOL_BYTES ((ULONG) 8192)
+#define TEST_MODULE_MEMORY_BYTES 512U
+
+static union
+{
+ ALIGN_TYPE test_pool_alignment;
+ UCHAR test_pool_bytes[TEST_POOL_BYTES];
+} test_pool_arena;
+
+static ULONG test_pool_offset;
+
+
+/* Define the memory that stands for the calling module's own code and data. */
+
+static union
+{
+ ALIGN_TYPE test_data_alignment;
+ UCHAR test_data_bytes[TEST_MODULE_MEMORY_BYTES];
+} test_module_data;
+
+static union
+{
+ ALIGN_TYPE test_code_alignment;
+ UCHAR test_code_bytes[TEST_MODULE_MEMORY_BYTES];
+} test_module_code;
+
+
+/* Define an object that stands for one the application owns and shared with a
+ module. It lives outside the pool, so the manager has no allocation record of
+ it and the kernel created list is the only record of it there is. */
+
+static TX_QUEUE test_host_queue;
+
+
+/* Define the thread that stands for the module thread making the request. The
+ manager takes the calling module from it. */
+
+static TX_THREAD test_current_thread;
+
+
+/* Define the block the modelled pool most recently released, so that a later
+ allocation of the same size lands on the address a freed object had, and so
+ that a case can see whether anything was released at all. */
+
+static UCHAR *test_released_start;
+static ULONG test_released_bytes;
+static ULONG test_releases;
+
+
+/* Define the manager state the code under test reaches for. */
+
+TX_BYTE_POOL _txm_module_manager_object_pool;
+UINT _txm_module_manager_object_pool_created;
+TX_MUTEX _txm_module_manager_mutex;
+TX_THREAD *_tx_thread_current_ptr;
+
+
+/* Define the kernel created lists the liveness check walks. */
+
+TX_BLOCK_POOL *_tx_block_pool_created_ptr;
+ULONG _tx_block_pool_created_count;
+TX_BYTE_POOL *_tx_byte_pool_created_ptr;
+ULONG _tx_byte_pool_created_count;
+TX_EVENT_FLAGS_GROUP *_tx_event_flags_created_ptr;
+ULONG _tx_event_flags_created_count;
+TX_MUTEX *_tx_mutex_created_ptr;
+ULONG _tx_mutex_created_count;
+TX_QUEUE *_tx_queue_created_ptr;
+ULONG _tx_queue_created_count;
+TX_SEMAPHORE *_tx_semaphore_created_ptr;
+ULONG _tx_semaphore_created_count;
+TX_THREAD *_tx_thread_created_ptr;
+ULONG _tx_thread_created_count;
+TX_TIMER *_tx_timer_created_ptr;
+ULONG _tx_timer_created_count;
+
+
+static UINT test_failures;
+static ULONG test_checks;
+
+
+/* Record the outcome of one expectation. */
+static VOID test_expect(const char *description, ULONG actual, ULONG expected)
+{
+ test_checks++;
+
+ if (actual != expected)
+ {
+ printf("FAIL: %s (expected %lu, got %lu)\n", description, (unsigned long) expected, (unsigned long) actual);
+ test_failures++;
+ }
+}
+
+
+/* Report every module object type the manager knows, and the name, control block
+ size and control block ID of each, so that the cases below can be written once
+ and run against all of them. */
+
+typedef struct TEST_OBJECT_KIND_STRUCT
+{
+ UINT test_kind_type;
+ const char *test_kind_name;
+ ULONG test_kind_size;
+ ULONG test_kind_id;
+} TEST_OBJECT_KIND;
+
+static const TEST_OBJECT_KIND test_object_kinds[] =
+{
+ { TXM_BLOCK_POOL_OBJECT, "block pool", (ULONG) sizeof(TX_BLOCK_POOL), TX_BLOCK_POOL_ID },
+ { TXM_BYTE_POOL_OBJECT, "byte pool", (ULONG) sizeof(TX_BYTE_POOL), TX_BYTE_POOL_ID },
+ { TXM_EVENT_FLAGS_OBJECT, "event flags", (ULONG) sizeof(TX_EVENT_FLAGS_GROUP), TX_EVENT_FLAGS_ID },
+ { TXM_MUTEX_OBJECT, "mutex", (ULONG) sizeof(TX_MUTEX), TX_MUTEX_ID },
+ { TXM_QUEUE_OBJECT, "queue", (ULONG) sizeof(TX_QUEUE), TX_QUEUE_ID },
+ { TXM_SEMAPHORE_OBJECT, "semaphore", (ULONG) sizeof(TX_SEMAPHORE), TX_SEMAPHORE_ID },
+ { TXM_THREAD_OBJECT, "thread", (ULONG) sizeof(TX_THREAD), TX_THREAD_ID },
+ { TXM_TIMER_OBJECT, "timer", (ULONG) sizeof(TX_TIMER), TX_TIMER_ID }
+};
+
+#define TEST_OBJECT_KIND_COUNT (sizeof(test_object_kinds) / sizeof(test_object_kinds[0]))
+
+
+/* Round a request up the way _tx_byte_allocate does. */
+static ULONG test_round_up(ULONG memory_size)
+{
+ return((((memory_size + ((ULONG) sizeof(ALIGN_TYPE))) - ((ULONG) 1)) / ((ULONG) sizeof(ALIGN_TYPE))) * ((ULONG) sizeof(ALIGN_TYPE)));
+}
+
+
+/* Stand in for the byte pool the manager allocates object memory from.
+
+ The two size bounds are the real ones. The placement is a bump allocator with
+ one refinement: a request that exactly fits the block most recently released is
+ given that block back. A first-fit byte pool reuses freed memory, and the cases
+ about an address that has been released need the address to actually come back. */
+UINT _txe_byte_allocate(TX_BYTE_POOL *pool_ptr, VOID **memory_ptr, ULONG memory_size, ULONG wait_option)
+{
+
+ULONG rounded_size;
+UCHAR *block_start;
+
+
+ (VOID) wait_option;
+
+ if ((pool_ptr != &_txm_module_manager_object_pool) || (memory_ptr == TX_NULL))
+ {
+ return(TX_POOL_ERROR);
+ }
+
+ if (memory_size == ((ULONG) 0))
+ {
+ return(TX_SIZE_ERROR);
+ }
+
+ if (memory_size > pool_ptr -> tx_byte_pool_size)
+ {
+ return(TX_SIZE_ERROR);
+ }
+
+ rounded_size = test_round_up(memory_size);
+
+ if ((test_released_start != TX_NULL) && (test_released_bytes == rounded_size))
+ {
+ block_start = test_released_start;
+ test_released_start = TX_NULL;
+ test_released_bytes = ((ULONG) 0);
+ }
+ else
+ {
+ if ((test_pool_offset + rounded_size) > TEST_POOL_BYTES)
+ {
+ return(TX_NO_MEMORY);
+ }
+
+ block_start = &test_pool_arena.test_pool_bytes[test_pool_offset];
+ test_pool_offset = test_pool_offset + rounded_size;
+ }
+
+ pool_ptr -> tx_byte_pool_available = pool_ptr -> tx_byte_pool_available - rounded_size;
+
+ *memory_ptr = (VOID *) block_start;
+
+ return(TX_SUCCESS);
+}
+
+
+/* Stand in for the release side, remembering the block so it can come back, and
+ counting releases so that a case can see whether one happened. */
+UINT _txe_byte_release(VOID *memory_ptr)
+{
+
+UCHAR *block_start;
+
+
+ block_start = (UCHAR *) memory_ptr;
+
+ if ((block_start < test_pool_arena.test_pool_bytes) ||
+ (block_start >= &test_pool_arena.test_pool_bytes[TEST_POOL_BYTES]))
+ {
+ return(TX_PTR_ERROR);
+ }
+
+ test_releases++;
+
+ /* The model does not track block lengths, and the only released block a case
+ needs to come back is the one it just released, whose length it knows. The
+ length is recorded by the caller through test_release_size. */
+ test_released_start = block_start;
+
+ return(TX_SUCCESS);
+}
+
+
+/* Tell the model how long the block just released was. */
+static VOID test_release_size(ULONG object_size)
+{
+ test_released_bytes = test_round_up(object_size + ((ULONG) sizeof(TXM_MODULE_ALLOCATED_OBJECT)));
+}
+
+
+/* Stand in for the protection mutex. */
+UINT _txe_mutex_get(TX_MUTEX *mutex_ptr, ULONG wait_option)
+{
+ (VOID) mutex_ptr;
+ (VOID) wait_option;
+
+ return(TX_SUCCESS);
+}
+
+
+UINT _txe_mutex_put(TX_MUTEX *mutex_ptr)
+{
+ (VOID) mutex_ptr;
+
+ return(TX_SUCCESS);
+}
+
+
+/* Stand in for the module port's data range check. */
+UINT _txm_module_manager_inside_data_check(TXM_MODULE_INSTANCE *module_instance, ALIGN_TYPE obj_ptr,
+ ULONG obj_size, UINT write_request)
+{
+
+ULONG data_start;
+ULONG data_end;
+
+
+ (VOID) module_instance;
+ (VOID) write_request;
+
+ data_start = (ULONG) (ALIGN_TYPE) test_module_data.test_data_bytes;
+ data_end = data_start + ((ULONG) TEST_MODULE_MEMORY_BYTES);
+
+ /* The whole range has to lie inside the module's data, not just its first
+ address, and the subtraction is done on the end rather than the start so that
+ a size which would carry the range past the top of the address space cannot
+ wrap into an accepting answer. */
+ if (((ULONG) obj_ptr >= data_start) && (obj_size <= (data_end - (ULONG) obj_ptr)))
+ {
+ return(TX_TRUE);
+ }
+
+ return(TX_FALSE);
+}
+
+
+/* Outside is not the negation of inside: a range that only partly reaches into the
+ module's data is neither. It has to end before that data begins or begin after it
+ ends, and a size that would carry the end past the top of the address space is
+ refused rather than allowed to compare as though it had not. */
+UINT _txm_module_manager_outside_data_check(TXM_MODULE_INSTANCE *module_instance, ALIGN_TYPE obj_ptr,
+ ULONG obj_size)
+{
+
+ULONG data_start;
+ULONG data_end;
+
+
+ (VOID) module_instance;
+
+ data_start = (ULONG) (ALIGN_TYPE) test_module_data.test_data_bytes;
+ data_end = data_start + ((ULONG) TEST_MODULE_MEMORY_BYTES);
+
+ if (obj_size > (~((ULONG) 0) - (ULONG) obj_ptr))
+ {
+ return(TX_FALSE);
+ }
+
+ if ((((ULONG) obj_ptr + obj_size) <= data_start) || ((ULONG) obj_ptr >= data_end))
+ {
+ return(TX_TRUE);
+ }
+
+ return(TX_FALSE);
+}
+
+
+/* Stand in for the port primitive a manager source under test refers to but that
+ no case here exercises. */
+VOID _txm_module_manager_alignment_adjust(TXM_MODULE_PREAMBLE *module_preamble, ULONG *code_size,
+ ULONG *code_alignment, ULONG *data_size, ULONG *data_alignment)
+{
+ (VOID) module_preamble;
+ (VOID) code_size;
+ (VOID) code_alignment;
+ (VOID) data_size;
+ (VOID) data_alignment;
+}
+
+
+/* Do to an object what the kernel's create service does: write the control block
+ ID, and link the object onto the created list for its type.
+
+ The chain each list is built as ends in a self-link on its oldest member rather
+ than closing back on the head, so that a walk bounded by the created count
+ visits every member and a walk that ignored the count would not run off the
+ end. What is being tested is the bound, not the model. */
+static VOID test_object_create(VOID *object_ptr, UINT object_type)
+{
+ switch (object_type)
+ {
+
+ case TXM_BLOCK_POOL_OBJECT:
+ {
+ TX_BLOCK_POOL *block_pool_ptr = (TX_BLOCK_POOL *) object_ptr;
+
+ block_pool_ptr -> tx_block_pool_id = TX_BLOCK_POOL_ID;
+ block_pool_ptr -> tx_block_pool_created_next = (_tx_block_pool_created_ptr == TX_NULL) ? block_pool_ptr : _tx_block_pool_created_ptr;
+ _tx_block_pool_created_ptr = block_pool_ptr;
+ _tx_block_pool_created_count++;
+ break;
+ }
+
+ case TXM_BYTE_POOL_OBJECT:
+ {
+ TX_BYTE_POOL *byte_pool_ptr = (TX_BYTE_POOL *) object_ptr;
+
+ byte_pool_ptr -> tx_byte_pool_id = TX_BYTE_POOL_ID;
+ byte_pool_ptr -> tx_byte_pool_created_next = (_tx_byte_pool_created_ptr == TX_NULL) ? byte_pool_ptr : _tx_byte_pool_created_ptr;
+ _tx_byte_pool_created_ptr = byte_pool_ptr;
+ _tx_byte_pool_created_count++;
+ break;
+ }
+
+ case TXM_EVENT_FLAGS_OBJECT:
+ {
+ TX_EVENT_FLAGS_GROUP *event_flags_ptr = (TX_EVENT_FLAGS_GROUP *) object_ptr;
+
+ event_flags_ptr -> tx_event_flags_group_id = TX_EVENT_FLAGS_ID;
+ event_flags_ptr -> tx_event_flags_group_created_next = (_tx_event_flags_created_ptr == TX_NULL) ? event_flags_ptr : _tx_event_flags_created_ptr;
+ _tx_event_flags_created_ptr = event_flags_ptr;
+ _tx_event_flags_created_count++;
+ break;
+ }
+
+ case TXM_MUTEX_OBJECT:
+ {
+ TX_MUTEX *mutex_ptr = (TX_MUTEX *) object_ptr;
+
+ mutex_ptr -> tx_mutex_id = TX_MUTEX_ID;
+ mutex_ptr -> tx_mutex_created_next = (_tx_mutex_created_ptr == TX_NULL) ? mutex_ptr : _tx_mutex_created_ptr;
+ _tx_mutex_created_ptr = mutex_ptr;
+ _tx_mutex_created_count++;
+ break;
+ }
+
+ case TXM_QUEUE_OBJECT:
+ {
+ TX_QUEUE *queue_ptr = (TX_QUEUE *) object_ptr;
+
+ queue_ptr -> tx_queue_id = TX_QUEUE_ID;
+ queue_ptr -> tx_queue_created_next = (_tx_queue_created_ptr == TX_NULL) ? queue_ptr : _tx_queue_created_ptr;
+ _tx_queue_created_ptr = queue_ptr;
+ _tx_queue_created_count++;
+ break;
+ }
+
+ case TXM_SEMAPHORE_OBJECT:
+ {
+ TX_SEMAPHORE *semaphore_ptr = (TX_SEMAPHORE *) object_ptr;
+
+ semaphore_ptr -> tx_semaphore_id = TX_SEMAPHORE_ID;
+ semaphore_ptr -> tx_semaphore_created_next = (_tx_semaphore_created_ptr == TX_NULL) ? semaphore_ptr : _tx_semaphore_created_ptr;
+ _tx_semaphore_created_ptr = semaphore_ptr;
+ _tx_semaphore_created_count++;
+ break;
+ }
+
+ case TXM_THREAD_OBJECT:
+ {
+ TX_THREAD *thread_ptr = (TX_THREAD *) object_ptr;
+
+ thread_ptr -> tx_thread_id = TX_THREAD_ID;
+ thread_ptr -> tx_thread_created_next = (_tx_thread_created_ptr == TX_NULL) ? thread_ptr : _tx_thread_created_ptr;
+ _tx_thread_created_ptr = thread_ptr;
+ _tx_thread_created_count++;
+ break;
+ }
+
+ case TXM_TIMER_OBJECT:
+ default:
+ {
+ TX_TIMER *timer_ptr = (TX_TIMER *) object_ptr;
+
+ timer_ptr -> tx_timer_id = TX_TIMER_ID;
+ timer_ptr -> tx_timer_created_next = (_tx_timer_created_ptr == TX_NULL) ? timer_ptr : _tx_timer_created_ptr;
+ _tx_timer_created_ptr = timer_ptr;
+ _tx_timer_created_count++;
+ break;
+ }
+ }
+}
+
+
+/* Do to an object what the kernel's delete service does: clear the control block
+ ID, and take the object off the created list for its type.
+
+ Every case that deletes has one object of that type on the list, which is the
+ shape that matters here, so the model empties the list rather than unlinking a
+ member of a longer one. */
+static VOID test_object_delete(VOID *object_ptr, UINT object_type)
+{
+ switch (object_type)
+ {
+
+ case TXM_BLOCK_POOL_OBJECT:
+
+ ((TX_BLOCK_POOL *) object_ptr) -> tx_block_pool_id = TX_CLEAR_ID;
+ _tx_block_pool_created_ptr = TX_NULL;
+ _tx_block_pool_created_count = ((ULONG) 0);
+ break;
+
+ case TXM_BYTE_POOL_OBJECT:
+
+ ((TX_BYTE_POOL *) object_ptr) -> tx_byte_pool_id = TX_CLEAR_ID;
+ _tx_byte_pool_created_ptr = TX_NULL;
+ _tx_byte_pool_created_count = ((ULONG) 0);
+ break;
+
+ case TXM_EVENT_FLAGS_OBJECT:
+
+ ((TX_EVENT_FLAGS_GROUP *) object_ptr) -> tx_event_flags_group_id = TX_CLEAR_ID;
+ _tx_event_flags_created_ptr = TX_NULL;
+ _tx_event_flags_created_count = ((ULONG) 0);
+ break;
+
+ case TXM_MUTEX_OBJECT:
+
+ ((TX_MUTEX *) object_ptr) -> tx_mutex_id = TX_CLEAR_ID;
+ _tx_mutex_created_ptr = TX_NULL;
+ _tx_mutex_created_count = ((ULONG) 0);
+ break;
+
+ case TXM_QUEUE_OBJECT:
+
+ ((TX_QUEUE *) object_ptr) -> tx_queue_id = TX_CLEAR_ID;
+ _tx_queue_created_ptr = TX_NULL;
+ _tx_queue_created_count = ((ULONG) 0);
+ break;
+
+ case TXM_SEMAPHORE_OBJECT:
+
+ ((TX_SEMAPHORE *) object_ptr) -> tx_semaphore_id = TX_CLEAR_ID;
+ _tx_semaphore_created_ptr = TX_NULL;
+ _tx_semaphore_created_count = ((ULONG) 0);
+ break;
+
+ case TXM_THREAD_OBJECT:
+
+ ((TX_THREAD *) object_ptr) -> tx_thread_id = TX_CLEAR_ID;
+ _tx_thread_created_ptr = TX_NULL;
+ _tx_thread_created_count = ((ULONG) 0);
+ break;
+
+ case TXM_TIMER_OBJECT:
+ default:
+
+ ((TX_TIMER *) object_ptr) -> tx_timer_id = TX_CLEAR_ID;
+ _tx_timer_created_ptr = TX_NULL;
+ _tx_timer_created_count = ((ULONG) 0);
+ break;
+ }
+}
+
+
+/* Break the created list for a type after its first member, and tell the kernel it
+ is longer than it is. A search bounded only by the count would run off the end of
+ the chain; this is what makes the second bound observable for every type. */
+static VOID test_object_orphan(VOID *object_ptr, UINT object_type)
+{
+ switch (object_type)
+ {
+
+ case TXM_BLOCK_POOL_OBJECT:
+
+ ((TX_BLOCK_POOL *) object_ptr) -> tx_block_pool_created_next = TX_NULL;
+ _tx_block_pool_created_count = ((ULONG) 64);
+ break;
+
+ case TXM_BYTE_POOL_OBJECT:
+
+ ((TX_BYTE_POOL *) object_ptr) -> tx_byte_pool_created_next = TX_NULL;
+ _tx_byte_pool_created_count = ((ULONG) 64);
+ break;
+
+ case TXM_EVENT_FLAGS_OBJECT:
+
+ ((TX_EVENT_FLAGS_GROUP *) object_ptr) -> tx_event_flags_group_created_next = TX_NULL;
+ _tx_event_flags_created_count = ((ULONG) 64);
+ break;
+
+ case TXM_MUTEX_OBJECT:
+
+ ((TX_MUTEX *) object_ptr) -> tx_mutex_created_next = TX_NULL;
+ _tx_mutex_created_count = ((ULONG) 64);
+ break;
+
+ case TXM_QUEUE_OBJECT:
+
+ ((TX_QUEUE *) object_ptr) -> tx_queue_created_next = TX_NULL;
+ _tx_queue_created_count = ((ULONG) 64);
+ break;
+
+ case TXM_SEMAPHORE_OBJECT:
+
+ ((TX_SEMAPHORE *) object_ptr) -> tx_semaphore_created_next = TX_NULL;
+ _tx_semaphore_created_count = ((ULONG) 64);
+ break;
+
+ case TXM_THREAD_OBJECT:
+
+ ((TX_THREAD *) object_ptr) -> tx_thread_created_next = TX_NULL;
+ _tx_thread_created_count = ((ULONG) 64);
+ break;
+
+ case TXM_TIMER_OBJECT:
+ default:
+
+ ((TX_TIMER *) object_ptr) -> tx_timer_created_next = TX_NULL;
+ _tx_timer_created_count = ((ULONG) 64);
+ break;
+ }
+}
+
+
+/* Read the control block ID word of an object. */
+static ULONG test_object_id(VOID *object_ptr)
+{
+ return(*((ULONG *) object_ptr));
+}
+
+
+/* Plant a value at an offset into an allocation, and report what was there. */
+static ULONG test_plant(VOID *object_ptr, ULONG offset, ULONG value)
+{
+
+ULONG *word_ptr;
+ULONG previous;
+
+
+ word_ptr = (ULONG *) (VOID *) (((UCHAR *) object_ptr) + offset);
+ previous = *word_ptr;
+ *word_ptr = value;
+
+ return(previous);
+}
+
+
+/* Bring the pool, the created lists and the modules back to a known state.
+
+ Everything is reset together deliberately. The allocation lists point into the
+ pool and the created lists point at objects in it, so a case that left either
+ behind would have the next case reading the wreckage of an earlier one rather
+ than what it was written to check. */
+static VOID test_reset(TXM_MODULE_INSTANCE *module_a, TXM_MODULE_INSTANCE *module_b)
+{
+
+UINT index;
+
+
+ for (index = 0U; index < (UINT) TEST_POOL_BYTES; index++)
+ {
+ test_pool_arena.test_pool_bytes[index] = (UCHAR) 0;
+ }
+
+ for (index = 0U; index < TEST_MODULE_MEMORY_BYTES; index++)
+ {
+ test_module_data.test_data_bytes[index] = (UCHAR) 0;
+ test_module_code.test_code_bytes[index] = (UCHAR) 0;
+ }
+
+ test_pool_offset = ((ULONG) 0);
+ test_released_start = TX_NULL;
+ test_released_bytes = ((ULONG) 0);
+ test_releases = ((ULONG) 0);
+
+ _txm_module_manager_object_pool.tx_byte_pool_id = TX_BYTE_POOL_ID;
+ _txm_module_manager_object_pool.tx_byte_pool_start = test_pool_arena.test_pool_bytes;
+ _txm_module_manager_object_pool.tx_byte_pool_size = TEST_POOL_BYTES;
+ _txm_module_manager_object_pool.tx_byte_pool_available = TEST_POOL_BYTES;
+ _txm_module_manager_object_pool_created = TX_TRUE;
+
+ _tx_block_pool_created_ptr = TX_NULL;
+ _tx_block_pool_created_count = ((ULONG) 0);
+ _tx_byte_pool_created_ptr = TX_NULL;
+ _tx_byte_pool_created_count = ((ULONG) 0);
+ _tx_event_flags_created_ptr = TX_NULL;
+ _tx_event_flags_created_count = ((ULONG) 0);
+ _tx_mutex_created_ptr = TX_NULL;
+ _tx_mutex_created_count = ((ULONG) 0);
+ _tx_queue_created_ptr = TX_NULL;
+ _tx_queue_created_count = ((ULONG) 0);
+ _tx_semaphore_created_ptr = TX_NULL;
+ _tx_semaphore_created_count = ((ULONG) 0);
+ _tx_thread_created_ptr = TX_NULL;
+ _tx_thread_created_count = ((ULONG) 0);
+ _tx_timer_created_ptr = TX_NULL;
+ _tx_timer_created_count = ((ULONG) 0);
+
+ module_a -> txm_module_instance_object_list_count = ((ULONG) 0);
+ module_a -> txm_module_instance_object_list_head = TX_NULL;
+ module_b -> txm_module_instance_object_list_count = ((ULONG) 0);
+ module_b -> txm_module_instance_object_list_head = TX_NULL;
+
+ /* The requesting thread belongs to module A throughout. It is not in the pool
+ and not on a created list, so it is nothing the cases below can find. */
+ test_current_thread.tx_thread_module_instance_ptr = module_a;
+ _tx_thread_current_ptr = &test_current_thread;
+}
+
+
+/* Allocate object memory for a module through the real manager path. */
+static VOID *test_allocate(TXM_MODULE_INSTANCE *module_instance, ULONG object_size)
+{
+
+VOID *object_ptr;
+UINT status;
+
+
+ object_ptr = TX_NULL;
+ status = _txm_module_manager_object_allocate(&object_ptr, object_size, module_instance);
+
+ if (status != TX_SUCCESS)
+ {
+ printf("FAIL: the modelled pool refused an allocation of %lu bytes (status %u)\n",
+ (unsigned long) object_size, status);
+ test_failures++;
+
+ return(TX_NULL);
+ }
+
+ return(object_ptr);
+}
+
+
+/* Allocate and create one object of a type, the way a module does. */
+static VOID *test_allocate_and_create(TXM_MODULE_INSTANCE *module_instance, const TEST_OBJECT_KIND *kind)
+{
+
+VOID *object_ptr;
+
+
+ object_ptr = test_allocate(module_instance, kind -> test_kind_size);
+
+ if (object_ptr != TX_NULL)
+ {
+ test_object_create(object_ptr, kind -> test_kind_type);
+ }
+
+ return(object_ptr);
+}
+
+
+/* Ask the manager to give an address back, and report what it said. Whatever it
+ decided, it must have left the interrupt lock as it found it. */
+static ULONG test_deallocate(VOID *object_ptr)
+{
+
+ULONG status;
+
+
+ test_interrupt_disable_depth = 0U;
+ test_interrupt_disable_max_depth = 0U;
+ test_interrupt_restore_underflows = 0U;
+
+ status = (ULONG) _txm_module_manager_object_deallocate(object_ptr);
+
+ if ((test_interrupt_disable_depth != 0U) || (test_interrupt_restore_underflows != 0U))
+ {
+ printf("FAIL: deallocation left the interrupt lock unbalanced (depth %u, underflows %u)\n",
+ test_interrupt_disable_depth, test_interrupt_restore_underflows);
+ test_failures++;
+ }
+
+ return(status);
+}
+
+
+/* Ask the manager whether a live kernel object is at an address, with the same
+ accounting. */
+static ULONG test_live_check(VOID *object_ptr)
+{
+
+ULONG status;
+
+
+ test_interrupt_disable_depth = 0U;
+ test_interrupt_disable_max_depth = 0U;
+ test_interrupt_restore_underflows = 0U;
+
+ status = (ULONG) _txm_module_manager_live_object_check((ALIGN_TYPE) object_ptr);
+
+ if ((test_interrupt_disable_depth != 0U) || (test_interrupt_restore_underflows != 0U))
+ {
+ printf("FAIL: the liveness check left the interrupt lock unbalanced (depth %u, underflows %u)\n",
+ test_interrupt_disable_depth, test_interrupt_restore_underflows);
+ test_failures++;
+ }
+
+ return(status);
+}
+
+
+int main(void)
+{
+
+TXM_MODULE_INSTANCE module_a;
+TXM_MODULE_INSTANCE module_b;
+const TEST_OBJECT_KIND *kind;
+const TEST_OBJECT_KIND *other_kind;
+VOID *object;
+VOID *other_object;
+VOID *raw_object;
+VOID *reused_object;
+ULONG index;
+ULONG other_index;
+ULONG saved;
+ULONG status;
+char description[160];
+
+
+ /* Report expectations before anything can crash, so that a case that brings
+ the process down is still preceded by everything that passed. */
+ setvbuf(stdout, TX_NULL, _IONBF, 0);
+
+ test_failures = 0U;
+ test_checks = ((ULONG) 0);
+
+ /* Set up two memory-protected modules. Module A does the asking throughout;
+ module B is there to own allocations A did not make. */
+ module_a.txm_module_instance_property_flags = TXM_MODULE_MEMORY_PROTECTION;
+ module_a.txm_module_instance_code_start = (VOID *) test_module_code.test_code_bytes;
+ module_a.txm_module_instance_code_end = (VOID *) &test_module_code.test_code_bytes[TEST_MODULE_MEMORY_BYTES - 1U];
+ module_a.txm_module_instance_data_start = (VOID *) test_module_data.test_data_bytes;
+ module_a.txm_module_instance_data_end = (VOID *) &test_module_data.test_data_bytes[TEST_MODULE_MEMORY_BYTES - 1U];
+
+ module_b = module_a;
+
+ /**********************************************************************/
+ /* The reported call: giving back the memory of a live object. */
+ /**********************************************************************/
+
+ for (index = ((ULONG) 0); index < (ULONG) TEST_OBJECT_KIND_COUNT; index++)
+ {
+ kind = &test_object_kinds[index];
+
+ test_reset(&module_a, &module_b);
+
+ object = test_allocate_and_create(&module_a, kind);
+
+ (void) snprintf(description, sizeof(description),
+ "a live %s is on the created list before anything is asked", kind -> test_kind_name);
+ test_expect(description, test_live_check(object), (ULONG) TX_TRUE);
+
+ /* The scan has to have run with interrupts disabled, which is the
+ protection the created lists are maintained under. */
+ test_expect("...found with interrupts disabled, one deep",
+ (ULONG) test_interrupt_disable_max_depth, ((ULONG) 1));
+
+ status = test_deallocate(object);
+
+ (void) snprintf(description, sizeof(description),
+ "the memory of a live %s is not given back", kind -> test_kind_name);
+ test_expect(description, status, (ULONG) TX_DELETE_ERROR);
+
+ test_expect("...nothing was released to the pool", test_releases, ((ULONG) 0));
+
+ test_expect("...the allocation is still on the module's list",
+ module_a.txm_module_instance_object_list_count, ((ULONG) 1));
+
+ test_expect("...at the head of it",
+ (ULONG) (module_a.txm_module_instance_object_list_head == (((TXM_MODULE_ALLOCATED_OBJECT *) object) - 1)),
+ (ULONG) TX_TRUE);
+
+ (void) snprintf(description, sizeof(description),
+ "...the %s still carries its control block ID", kind -> test_kind_name);
+ test_expect(description, test_object_id(object), kind -> test_kind_id);
+
+ (void) snprintf(description, sizeof(description),
+ "...and is still a live %s", kind -> test_kind_name);
+ test_expect(description, test_live_check(object), (ULONG) TX_TRUE);
+
+ /* Deleting it is what makes the memory releasable, and the release the
+ delete dispatchers perform after a successful delete then goes through. */
+ test_object_delete(object, kind -> test_kind_type);
+
+ (void) snprintf(description, sizeof(description),
+ "a deleted %s is no longer live", kind -> test_kind_name);
+ test_expect(description, test_live_check(object), (ULONG) TX_FALSE);
+
+ test_release_size(kind -> test_kind_size);
+ status = test_deallocate(object);
+
+ test_expect("and its memory is given back", status, (ULONG) TX_SUCCESS);
+
+ test_expect("...leaving the module's allocation list empty",
+ module_a.txm_module_instance_object_list_count, ((ULONG) 0));
+
+ /* The address is free to come back now, which is what the kernel would
+ have been sharing with the next owner had the live object been freed. */
+ reused_object = test_allocate(&module_a, kind -> test_kind_size);
+
+ test_expect("...and the address free to be handed out again",
+ (ULONG) (reused_object == object), (ULONG) TX_TRUE);
+ }
+
+ /**********************************************************************/
+ /* Storage that is not an object is still released. */
+ /**********************************************************************/
+
+ test_reset(&module_a, &module_b);
+
+ /* An allocation that was never made into an object. This is what a module has
+ after a create that failed or was abandoned, and releasing it is the reason
+ the request exists at all. */
+ raw_object = test_allocate(&module_a, (ULONG) sizeof(TX_QUEUE));
+
+ test_expect("an uncreated allocation holds no live object",
+ test_live_check(raw_object), (ULONG) TX_FALSE);
+
+ test_release_size((ULONG) sizeof(TX_QUEUE));
+ test_expect("and its memory is given back",
+ test_deallocate(raw_object), (ULONG) TX_SUCCESS);
+ test_expect("...leaving the module's allocation list empty",
+ module_a.txm_module_instance_object_list_count, ((ULONG) 0));
+
+ /* An allocation carrying nothing but the value of a control block ID. A
+ control block ID is not evidence that an object is there, and refusing on
+ one would strand memory a module is entitled to have back. */
+ for (index = ((ULONG) 0); index < (ULONG) TEST_OBJECT_KIND_COUNT; index++)
+ {
+ kind = &test_object_kinds[index];
+
+ test_reset(&module_a, &module_b);
+
+ raw_object = test_allocate(&module_a, kind -> test_kind_size);
+ (VOID) test_plant(raw_object, ((ULONG) 0), kind -> test_kind_id);
+
+ (void) snprintf(description, sizeof(description),
+ "an uncreated allocation carrying a %s ID holds no live object", kind -> test_kind_name);
+ test_expect(description, test_live_check(raw_object), (ULONG) TX_FALSE);
+
+ test_release_size(kind -> test_kind_size);
+
+ test_expect("...and its memory is given back",
+ test_deallocate(raw_object), (ULONG) TX_SUCCESS);
+ }
+
+ /**********************************************************************/
+ /* The allocation size is not what decides it. */
+ /**********************************************************************/
+
+ test_reset(&module_a, &module_b);
+
+ /* A module running without memory protection creates objects through no
+ size-checked path, so an object can be at the start of an allocation far
+ larger than its control block. Deciding which created lists to search from
+ the size of the allocation would miss it. */
+ object = test_allocate(&module_a, (ULONG) sizeof(TX_THREAD));
+ test_object_create(object, TXM_QUEUE_OBJECT);
+
+ test_expect("a queue at the start of a thread-sized allocation is live",
+ test_live_check(object), (ULONG) TX_TRUE);
+ test_expect("and the memory is not given back",
+ test_deallocate(object), (ULONG) TX_DELETE_ERROR);
+ test_expect("...with nothing released to the pool", test_releases, ((ULONG) 0));
+
+ /**********************************************************************/
+ /* Only the exact address of an object counts. */
+ /**********************************************************************/
+
+ test_reset(&module_a, &module_b);
+
+ kind = &test_object_kinds[6];
+ object = test_allocate_and_create(&module_a, kind);
+
+ /* Every aligned interior offset of a live object, with that object's own ID
+ planted at the offset. None of them is the address of an object. */
+ for (index = (ULONG) sizeof(ULONG); index < kind -> test_kind_size; index = index + ((ULONG) sizeof(ULONG)))
+ {
+ saved = test_plant(object, index, kind -> test_kind_id);
+
+ (void) snprintf(description, sizeof(description),
+ "an address %lu bytes into a live thread holds no live object", (unsigned long) index);
+ test_expect(description, test_live_check((VOID *) (((UCHAR *) object) + index)), (ULONG) TX_FALSE);
+
+ (VOID) test_plant(object, index, saved);
+ }
+
+ test_expect("the address one past a live thread holds no live object",
+ test_live_check((VOID *) (((UCHAR *) object) + kind -> test_kind_size)), (ULONG) TX_FALSE);
+
+ /**********************************************************************/
+ /* Every type is searched, and each type only finds its own. */
+ /**********************************************************************/
+
+ for (index = ((ULONG) 0); index < (ULONG) TEST_OBJECT_KIND_COUNT; index++)
+ {
+ kind = &test_object_kinds[index];
+
+ test_reset(&module_a, &module_b);
+
+ /* One object of this type, and one allocation of every other type's size
+ that was never created. Only the created one is found, whatever ID the
+ others are carrying. */
+ object = test_allocate_and_create(&module_a, kind);
+
+ for (other_index = ((ULONG) 0); other_index < (ULONG) TEST_OBJECT_KIND_COUNT; other_index++)
+ {
+ if (other_index == index)
+ {
+ continue;
+ }
+
+ other_kind = &test_object_kinds[other_index];
+ other_object = test_allocate(&module_a, other_kind -> test_kind_size);
+ (VOID) test_plant(other_object, ((ULONG) 0), other_kind -> test_kind_id);
+
+ (void) snprintf(description, sizeof(description),
+ "with a live %s created, an uncreated %s is not found",
+ kind -> test_kind_name, other_kind -> test_kind_name);
+ test_expect(description, test_live_check(other_object), (ULONG) TX_FALSE);
+ }
+
+ (void) snprintf(description, sizeof(description),
+ "and the live %s is", kind -> test_kind_name);
+ test_expect(description, test_live_check(object), (ULONG) TX_TRUE);
+ }
+
+ /**********************************************************************/
+ /* Liveness is the kernel's record, not the pool's. */
+ /**********************************************************************/
+
+ test_reset(&module_a, &module_b);
+
+ /* An object the application created lives outside the pool, and the manager
+ has no allocation record of it. The kernel created list is the only record
+ there is, and it is the record this consults. */
+ test_object_create(&test_host_queue, TXM_QUEUE_OBJECT);
+
+ test_expect("an application-owned queue outside the pool is live",
+ test_live_check(&test_host_queue), (ULONG) TX_TRUE);
+
+ test_object_delete(&test_host_queue, TXM_QUEUE_OBJECT);
+
+ test_expect("and is not once it has been deleted",
+ test_live_check(&test_host_queue), (ULONG) TX_FALSE);
+
+ /**********************************************************************/
+ /* The bounds on the search. */
+ /**********************************************************************/
+
+ test_reset(&module_a, &module_b);
+
+ /* Three queues, and a created count that says one. A list whose links have
+ been damaged must not make the scan run past the count the kernel keeps
+ beside it. */
+ object = test_allocate_and_create(&module_a, &test_object_kinds[4]);
+ other_object = test_allocate_and_create(&module_a, &test_object_kinds[4]);
+ raw_object = test_allocate_and_create(&module_a, &test_object_kinds[4]);
+
+ test_expect("all three queues are on the list", _tx_queue_created_count, ((ULONG) 3));
+ test_expect("the newest is found", test_live_check(raw_object), (ULONG) TX_TRUE);
+ test_expect("the oldest is found too", test_live_check(object), (ULONG) TX_TRUE);
+
+ _tx_queue_created_count = ((ULONG) 1);
+
+ test_expect("with the count saying one, the head is still found",
+ test_live_check(raw_object), (ULONG) TX_TRUE);
+ test_expect("...but the second member is not walked to",
+ test_live_check(other_object), (ULONG) TX_FALSE);
+ test_expect("...nor the third",
+ test_live_check(object), (ULONG) TX_FALSE);
+
+ /* A count larger than the list. The walk has to stop at the end of the chain
+ as well as at the count. */
+ _tx_queue_created_count = ((ULONG) 64);
+ ((TX_QUEUE *) raw_object) -> tx_queue_created_next = TX_NULL;
+
+ test_expect("a count larger than the list does not run the scan off the end",
+ test_live_check(&test_host_queue), (ULONG) TX_FALSE);
+
+ /* An empty list is the ordinary state of most types on most systems. */
+ _tx_queue_created_ptr = TX_NULL;
+ _tx_queue_created_count = ((ULONG) 0);
+
+ test_expect("nothing is live once every list is empty",
+ test_live_check(raw_object), (ULONG) TX_FALSE);
+
+ /* The same bound, for every type: a chain that ends before the count does. */
+ for (index = ((ULONG) 0); index < (ULONG) TEST_OBJECT_KIND_COUNT; index++)
+ {
+ kind = &test_object_kinds[index];
+
+ test_reset(&module_a, &module_b);
+
+ object = test_allocate_and_create(&module_a, kind);
+ test_object_orphan(object, kind -> test_kind_type);
+
+ (void) snprintf(description, sizeof(description),
+ "a %s list that ends before its count does not run the scan off the end",
+ kind -> test_kind_name);
+ test_expect(description, test_live_check(&test_host_queue), (ULONG) TX_FALSE);
+
+ (void) snprintf(description, sizeof(description),
+ "...and the %s at its head is still found", kind -> test_kind_name);
+ test_expect(description, test_live_check(object), (ULONG) TX_TRUE);
+ }
+
+ /**********************************************************************/
+ /* Allocations belonging to another module. */
+ /**********************************************************************/
+
+ test_reset(&module_a, &module_b);
+
+ /* Module B's live object. Module A is refused, and the refusal is the
+ ownership one: whether A may operate on B's objects at all is a separate
+ question, and this request never reaches the liveness one. */
+ object = test_allocate_and_create(&module_b, &test_object_kinds[5]);
+
+ test_expect("module A cannot give back module B's live allocation",
+ test_deallocate(object), (ULONG) TX_PTR_ERROR);
+ test_expect("...and B's allocation list is untouched",
+ module_b.txm_module_instance_object_list_count, ((ULONG) 1));
+ test_expect("...with nothing released to the pool", test_releases, ((ULONG) 0));
+
+ /* And B's uncreated storage, which A is refused for the same reason. */
+ raw_object = test_allocate(&module_b, (ULONG) sizeof(TX_QUEUE));
+
+ test_expect("module A cannot give back module B's uncreated allocation",
+ test_deallocate(raw_object), (ULONG) TX_PTR_ERROR);
+ test_expect("...and B's allocation list still holds both",
+ module_b.txm_module_instance_object_list_count, ((ULONG) 2));
+
+ /* B can give its own uncreated storage back, so the ownership test has not
+ been turned into a refusal of everything. */
+ test_current_thread.tx_thread_module_instance_ptr = &module_b;
+
+ test_release_size((ULONG) sizeof(TX_QUEUE));
+ test_expect("module B can give back its own uncreated allocation",
+ test_deallocate(raw_object), (ULONG) TX_SUCCESS);
+ test_expect("...but not its own live object",
+ test_deallocate(object), (ULONG) TX_DELETE_ERROR);
+
+ test_current_thread.tx_thread_module_instance_ptr = &module_a;
+
+ /**********************************************************************/
+ /* An address that is not the start of one of this module's allocations.*/
+ /**********************************************************************/
+
+ test_reset(&module_a, &module_b);
+
+ object = test_allocate(&module_a, (ULONG) sizeof(TX_QUEUE));
+
+ /* An address inside an allocation names none of this module's allocations.
+ The search establishes that without reading the words in front of the
+ address, so an interior address is refused rather than taken for a header. */
+ test_expect("an address inside an allocation is refused as naming no allocation",
+ test_deallocate((VOID *) (((UCHAR *) object) + sizeof(ULONG))), (ULONG) TX_PTR_ERROR);
+
+ test_expect("...leaving the module's allocation list as it was",
+ module_a.txm_module_instance_object_list_count, ((ULONG) 1));
+
+ /**********************************************************************/
+ /* Types the manager does not know. */
+ /**********************************************************************/
+
+ test_reset(&module_a, &module_b);
+
+ object = test_allocate_and_create(&module_a, &test_object_kinds[4]);
+
+ /* The per-type check is asked directly here, because the search over the
+ types the manager knows can never present it with one it does not. A
+ kernel stack allocation is the case that exists: the manager allocates it
+ from the same pool and gives it a type of its own, and there is no created
+ list for it. */
+ test_expect("a kernel stack is not a type with a created list",
+ (ULONG) _txm_module_manager_created_object_type_check((ALIGN_TYPE) object,
+ (UINT) TXM_THREAD_KERNEL_STACK_OBJECT),
+ (ULONG) TX_FALSE);
+
+ test_expect("nor is a type of zero",
+ (ULONG) _txm_module_manager_created_object_type_check((ALIGN_TYPE) object, 0U),
+ (ULONG) TX_FALSE);
+
+ test_expect("and the live queue is still found by the search",
+ test_live_check(object), (ULONG) TX_TRUE);
+
+ /**********************************************************************/
+ /* Releasing one allocation out of several. */
+ /**********************************************************************/
+
+ test_reset(&module_a, &module_b);
+
+ /* Allocations are appended behind the head, so the first one made is the
+ head. Releasing it has to move the head on rather than leave it pointing
+ at memory that has gone back to the pool. */
+ object = test_allocate(&module_a, (ULONG) sizeof(TX_QUEUE));
+ other_object = test_allocate(&module_a, (ULONG) sizeof(TX_QUEUE));
+
+ test_expect("the first allocation is the list head",
+ (ULONG) (module_a.txm_module_instance_object_list_head == (((TXM_MODULE_ALLOCATED_OBJECT *) object) - 1)),
+ (ULONG) TX_TRUE);
+
+ test_release_size((ULONG) sizeof(TX_QUEUE));
+ test_expect("releasing the head allocation succeeds",
+ test_deallocate(object), (ULONG) TX_SUCCESS);
+ test_expect("...leaving one allocation on the list",
+ module_a.txm_module_instance_object_list_count, ((ULONG) 1));
+ test_expect("...and the head moved on to it",
+ (ULONG) (module_a.txm_module_instance_object_list_head == (((TXM_MODULE_ALLOCATED_OBJECT *) other_object) - 1)),
+ (ULONG) TX_TRUE);
+
+ test_release_size((ULONG) sizeof(TX_QUEUE));
+ test_expect("releasing the last allocation succeeds",
+ test_deallocate(other_object), (ULONG) TX_SUCCESS);
+ test_expect("...emptying the list",
+ module_a.txm_module_instance_object_list_count, ((ULONG) 0));
+ test_expect("...and clearing its head",
+ (ULONG) (module_a.txm_module_instance_object_list_head == TX_NULL), (ULONG) TX_TRUE);
+
+ /* Asking for the same address again. The header in front of it still names this
+ module, because nothing has overwritten it, so the empty allocation list is
+ what has to refuse the second release rather than the owner test. */
+ test_expect("releasing the same address a second time is refused",
+ test_deallocate(other_object), (ULONG) TX_PTR_ERROR);
+
+ /* An address with no room for a header in front of it anywhere in the address
+ space. The header pointer is null, and the address itself is never read. */
+ test_expect("an address whose header would be at zero is refused",
+ test_deallocate((VOID *) ((ALIGN_TYPE) sizeof(TXM_MODULE_ALLOCATED_OBJECT))),
+ (ULONG) TX_PTR_ERROR);
+
+ /**********************************************************************/
+ /* An object pool that was never created. */
+ /**********************************************************************/
+
+ test_reset(&module_a, &module_b);
+
+ _txm_module_manager_object_pool_created = TX_FALSE;
+
+ test_expect("and the deallocation reports the feature is not available",
+ test_deallocate((VOID *) test_pool_arena.test_pool_bytes), (ULONG) TX_NOT_AVAILABLE);
+
+ _txm_module_manager_object_pool_created = TX_TRUE;
+
+ /**********************************************************************/
+ /* What the whole run has to have held. */
+ /**********************************************************************/
+
+ test_expect("every case ran", (ULONG) (test_checks > ((ULONG) 100)), (ULONG) TX_TRUE);
+
+ if (test_failures == 0U)
+ {
+ printf("SUCCESS! %lu expectations\n", (unsigned long) test_checks);
+ return(0);
+ }
+
+ printf("ERROR: %u expectation(s) failed of %lu\n", test_failures, (unsigned long) test_checks);
+ return(1);
+}
diff --git a/test/tx/module_manager/threadx_module_manager_object_allocate_overflow_test.c b/test/tx/module_manager/threadx_module_manager_object_allocate_overflow_test.c
new file mode 100644
index 00000000..38314ba4
--- /dev/null
+++ b/test/tx/module_manager/threadx_module_manager_object_allocate_overflow_test.c
@@ -0,0 +1,830 @@
+/***************************************************************************
+ * 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
+ **************************************************************************/
+
+
+/**************************************************************************/
+/**************************************************************************/
+/** */
+/** ThreadX Test */
+/** */
+/** Module Manager object allocation size arithmetic */
+/** */
+/**************************************************************************/
+/**************************************************************************/
+
+/* A module asks the Module Manager for object memory and chooses the size. The
+ manager adds its own private header to that size, and the header it writes is
+ larger than the smallest allocation the byte pool will hand out, so the
+ addition has to be checked before the pool is asked for anything.
+
+ This test drives _txm_module_manager_object_allocate on the host with the
+ Cortex-A7 module port's control blocks. The byte pool behind it is modelled
+ rather than run: the model applies exactly the two bounds _txe_byte_allocate
+ applies, refusing a zero size and a size larger than the pool, and it hands
+ out a block of precisely the requested length with a guard band immediately
+ after it. An allocation that the manager then overruns is therefore caught as
+ a real out-of-bounds write rather than inferred from arithmetic, which is what
+ makes the refusals below testable: revert the check and the manager writes its
+ sixteen-byte header into an eight-byte block and stamps on the guard. */
+
+#include <stdio.h>
+
+#define TX_SOURCE_CODE
+
+#include "tx_api.h"
+#include "tx_byte_pool.h"
+#include "tx_thread.h"
+#include "txm_module.h"
+#include "txm_module_manager_util.h"
+
+
+/* Define the modelled pool.
+
+ The arena is physically larger than the pool the manager is told about, so the
+ guard band that follows every block has somewhere to live without the pool's
+ own accounting having to know about it. */
+
+#define TEST_ARENA_BYTES 8192U
+#define TEST_POOL_BYTES 4096UL
+#define TEST_GUARD_BYTES 32U
+#define TEST_GUARD_PATTERN 0xE7U
+#define TEST_MAX_BLOCKS 32U
+
+#define TEST_HEADER_BYTES ((ULONG) sizeof(TXM_MODULE_ALLOCATED_OBJECT))
+#define TEST_ULONG_MAX ((ULONG) ~((ULONG) 0))
+
+/* The object size that makes the unchecked addition wrap to a chosen value. */
+#define TEST_SIZE_WRAPPING_TO(want) ((ULONG) (((ULONG) (want)) - TEST_HEADER_BYTES))
+
+
+/* Define one block the modelled pool has handed out. */
+
+typedef struct TEST_BLOCK_STRUCT
+{
+ UCHAR *test_block_start;
+ ULONG test_block_bytes;
+ UINT test_block_live;
+} TEST_BLOCK;
+
+
+/* Define the state the modelled pool and the stubbed services answer from. */
+
+static union
+{
+ ALIGN_TYPE test_arena_alignment;
+ UCHAR test_arena_bytes[TEST_ARENA_BYTES];
+} test_arena;
+
+static ULONG test_arena_offset;
+static TEST_BLOCK test_blocks[TEST_MAX_BLOCKS];
+static UINT test_block_count;
+
+static ULONG test_allocate_calls;
+static ULONG test_allocate_size;
+static ULONG test_allocate_wait;
+static ULONG test_release_calls;
+static ULONG test_mutex_get_calls;
+static ULONG test_mutex_put_calls;
+
+/* Counted across the whole run rather than per case, because what they answer --
+ was the pool ever asked for less than the header the manager then writes, and
+ was it ever asked on behalf of something other than itself -- is about the run
+ as a whole. */
+static UINT test_undersized_requests;
+static UINT test_misdirected_requests;
+
+static UINT test_failures;
+
+
+/* Define the manager state the code under test reaches for. */
+
+TX_BYTE_POOL _txm_module_manager_object_pool;
+UINT _txm_module_manager_object_pool_created;
+TX_MUTEX _txm_module_manager_mutex;
+TX_THREAD *_tx_thread_current_ptr;
+
+
+/* Define the stand-in interrupt lock the port shim counts through. */
+
+unsigned int test_interrupt_disable_depth;
+unsigned int test_interrupt_disable_max_depth;
+unsigned int test_interrupt_restore_underflows;
+
+
+/* The kernel's created lists. A created object lives on the list for its type, and
+ the manager reads those lists, so a test that stands in for the kernel has to
+ provide them. All stay empty here, because this test creates no kernel object;
+ it exercises the arithmetic the allocation does before any object exists. */
+
+TX_BLOCK_POOL *_tx_block_pool_created_ptr;
+ULONG _tx_block_pool_created_count;
+TX_BYTE_POOL *_tx_byte_pool_created_ptr;
+ULONG _tx_byte_pool_created_count;
+TX_EVENT_FLAGS_GROUP *_tx_event_flags_created_ptr;
+ULONG _tx_event_flags_created_count;
+TX_MUTEX *_tx_mutex_created_ptr;
+ULONG _tx_mutex_created_count;
+TX_QUEUE *_tx_queue_created_ptr;
+ULONG _tx_queue_created_count;
+TX_SEMAPHORE *_tx_semaphore_created_ptr;
+ULONG _tx_semaphore_created_count;
+TX_THREAD *_tx_thread_created_ptr;
+ULONG _tx_thread_created_count;
+TX_TIMER *_tx_timer_created_ptr;
+ULONG _tx_timer_created_count;
+
+
+/* This test models no module memory of its own, so no address is ever inside the
+ calling module's data. */
+UINT _txm_module_manager_inside_data_check(TXM_MODULE_INSTANCE *module_instance, ALIGN_TYPE obj_ptr,
+ ULONG obj_size, UINT write_request)
+{
+ (VOID) module_instance;
+ (VOID) obj_ptr;
+ (VOID) obj_size;
+ (VOID) write_request;
+
+ return(TX_FALSE);
+}
+
+
+/* No module data is modelled here, so every range lies outside it. */
+UINT _txm_module_manager_outside_data_check(TXM_MODULE_INSTANCE *module_instance, ALIGN_TYPE obj_ptr,
+ ULONG obj_size)
+{
+ (VOID) module_instance;
+ (VOID) obj_ptr;
+ (VOID) obj_size;
+
+ return(TX_TRUE);
+}
+
+
+/* Not reached from the allocation path under test. */
+VOID _txm_module_manager_alignment_adjust(TXM_MODULE_PREAMBLE *module_preamble, ULONG *code_size,
+ ULONG *code_alignment, ULONG *data_size, ULONG *data_alignment)
+{
+ (VOID) module_preamble;
+ (VOID) code_size;
+ (VOID) code_alignment;
+ (VOID) data_size;
+ (VOID) data_alignment;
+}
+
+
+/* Record the outcome of one expectation. */
+static VOID test_expect(const char *description, ULONG actual, ULONG expected)
+{
+ if (actual != expected)
+ {
+ printf("FAIL: %s (expected %lu, got %lu)\n", description, (unsigned long) expected, (unsigned long) actual);
+ test_failures++;
+ }
+}
+
+
+/* Round a request up the way _tx_byte_allocate does. */
+static ULONG test_round_up(ULONG memory_size)
+{
+ return((((memory_size + ((ULONG) sizeof(ALIGN_TYPE))) - ((ULONG) 1)) / ((ULONG) sizeof(ALIGN_TYPE))) * ((ULONG) sizeof(ALIGN_TYPE)));
+}
+
+
+/* Lay a guard band down after a block. */
+static VOID test_guard_write(UCHAR *guard_ptr)
+{
+
+UINT index;
+
+
+ for (index = 0U; index < TEST_GUARD_BYTES; index++)
+ {
+ guard_ptr[index] = (UCHAR) TEST_GUARD_PATTERN;
+ }
+}
+
+
+/* Report how many guard bytes anything has written through, across every block
+ the modelled pool has ever handed out, live or released. */
+static ULONG test_guard_damage(VOID)
+{
+
+UINT block;
+UINT index;
+UCHAR *guard_ptr;
+ULONG damaged;
+
+
+ damaged = ((ULONG) 0);
+
+ for (block = 0U; block < test_block_count; block++)
+ {
+ guard_ptr = test_blocks[block].test_block_start + test_blocks[block].test_block_bytes;
+
+ for (index = 0U; index < TEST_GUARD_BYTES; index++)
+ {
+ if (guard_ptr[index] != (UCHAR) TEST_GUARD_PATTERN)
+ {
+ damaged++;
+ }
+ }
+ }
+
+ return(damaged);
+}
+
+
+/* Stand in for the byte pool the manager allocates objects from.
+
+ The two size bounds and the alignment round-up are the real ones. What is not
+ real is the placement: every block ends where a guard band begins, so nothing
+ the manager writes past the end of its allocation can land in memory that a
+ later assertion would have accepted. */
+UINT _txe_byte_allocate(TX_BYTE_POOL *pool_ptr, VOID **memory_ptr, ULONG memory_size, ULONG wait_option)
+{
+
+ULONG rounded_size;
+UCHAR *block_start;
+
+
+ test_allocate_calls++;
+ test_allocate_size = memory_size;
+ test_allocate_wait = wait_option;
+
+ /* The manager must never ask for less than the header it is about to write.
+ This is the invariant the checked addition exists to hold, so the model
+ records a breach here as well as letting the guard band catch the write. */
+ if (memory_size < TEST_HEADER_BYTES)
+ {
+ test_undersized_requests++;
+ }
+
+ /* _txe_byte_allocate answers a null pool, a pool that is not one, and a null
+ destination with TX_POOL_ERROR and TX_PTR_ERROR. The manager always supplies
+ its own pool and a real destination, so rather than model arms this test
+ could never reach, note a breach and let the assertion at the end of the run
+ report it. */
+ if ((pool_ptr != &_txm_module_manager_object_pool) ||
+ (pool_ptr -> tx_byte_pool_id != TX_BYTE_POOL_ID) ||
+ (memory_ptr == TX_NULL))
+ {
+ test_misdirected_requests++;
+
+ return(TX_POOL_ERROR);
+ }
+
+ if (memory_size == ((ULONG) 0))
+ {
+ return(TX_SIZE_ERROR);
+ }
+
+ if (memory_size > pool_ptr -> tx_byte_pool_size)
+ {
+ return(TX_SIZE_ERROR);
+ }
+
+ rounded_size = test_round_up(memory_size);
+
+ if ((rounded_size > pool_ptr -> tx_byte_pool_available) ||
+ (test_block_count == TEST_MAX_BLOCKS) ||
+ ((test_arena_offset + rounded_size + ((ULONG) TEST_GUARD_BYTES)) > ((ULONG) TEST_ARENA_BYTES)))
+ {
+ return(TX_NO_MEMORY);
+ }
+
+ block_start = &test_arena.test_arena_bytes[test_arena_offset];
+
+ test_blocks[test_block_count].test_block_start = block_start;
+ test_blocks[test_block_count].test_block_bytes = rounded_size;
+ test_blocks[test_block_count].test_block_live = TX_TRUE;
+ test_block_count++;
+
+ test_guard_write(block_start + rounded_size);
+ test_arena_offset = test_arena_offset + rounded_size + ((ULONG) TEST_GUARD_BYTES);
+
+ pool_ptr -> tx_byte_pool_available = pool_ptr -> tx_byte_pool_available - rounded_size;
+ pool_ptr -> tx_byte_pool_fragments++;
+ pool_ptr -> tx_byte_pool_search = block_start;
+
+ *memory_ptr = (VOID *) block_start;
+
+ return(TX_SUCCESS);
+}
+
+
+/* Stand in for the release side, so a normal allocation can be given back. */
+UINT _txe_byte_release(VOID *memory_ptr)
+{
+
+UINT block;
+
+
+ test_release_calls++;
+
+ for (block = 0U; block < test_block_count; block++)
+ {
+ if ((test_blocks[block].test_block_start == (UCHAR *) memory_ptr) &&
+ (test_blocks[block].test_block_live == TX_TRUE))
+ {
+ test_blocks[block].test_block_live = TX_FALSE;
+ _txm_module_manager_object_pool.tx_byte_pool_available =
+ _txm_module_manager_object_pool.tx_byte_pool_available + test_blocks[block].test_block_bytes;
+
+ return(TX_SUCCESS);
+ }
+ }
+
+ /* A release of something this pool never handed out, or handed out twice. */
+ return(TX_PTR_ERROR);
+}
+
+
+/* Stand in for the protection mutex, to show when it is and is not taken. */
+UINT _txe_mutex_get(TX_MUTEX *mutex_ptr, ULONG wait_option)
+{
+ (VOID) mutex_ptr;
+ (VOID) wait_option;
+ test_mutex_get_calls++;
+
+ return(TX_SUCCESS);
+}
+
+
+UINT _txe_mutex_put(TX_MUTEX *mutex_ptr)
+{
+ (VOID) mutex_ptr;
+ test_mutex_put_calls++;
+
+ return(TX_SUCCESS);
+}
+
+
+/* Bring the modelled pool up, empty. */
+static VOID test_pool_reset(VOID)
+{
+
+UINT index;
+
+
+ for (index = 0U; index < TEST_ARENA_BYTES; index++)
+ {
+ test_arena.test_arena_bytes[index] = (UCHAR) 0;
+ }
+
+ test_arena_offset = ((ULONG) 0);
+ test_block_count = 0U;
+ test_allocate_calls = ((ULONG) 0);
+ test_allocate_size = ((ULONG) 0);
+ test_allocate_wait = ((ULONG) 0);
+ test_release_calls = ((ULONG) 0);
+ test_mutex_get_calls = ((ULONG) 0);
+ test_mutex_put_calls = ((ULONG) 0);
+
+ _txm_module_manager_object_pool.tx_byte_pool_id = TX_BYTE_POOL_ID;
+ _txm_module_manager_object_pool.tx_byte_pool_start = test_arena.test_arena_bytes;
+ _txm_module_manager_object_pool.tx_byte_pool_size = TEST_POOL_BYTES;
+ _txm_module_manager_object_pool.tx_byte_pool_available = TEST_POOL_BYTES;
+ _txm_module_manager_object_pool.tx_byte_pool_fragments = 0U;
+ _txm_module_manager_object_pool.tx_byte_pool_search = test_arena.test_arena_bytes;
+ _txm_module_manager_object_pool_created = TX_TRUE;
+}
+
+
+/* Bring the pool and the module that allocates from it back to a known state.
+
+ Both are reset together deliberately. The module's allocation list points into
+ the pool, so a case that left objects behind would have the next case linking
+ new objects onto addresses this function has just zeroed, and every later
+ assertion would then be reading the wreckage of an earlier one rather than
+ what it was written to check. */
+static VOID test_reset(TXM_MODULE_INSTANCE *module_instance)
+{
+ test_pool_reset();
+
+ module_instance -> txm_module_instance_object_list_count = ((ULONG) 0);
+ module_instance -> txm_module_instance_object_list_head = TX_NULL;
+}
+
+
+/* Describe everything a refused request must have left alone. */
+typedef struct TEST_SNAPSHOT_STRUCT
+{
+ ULONG test_snapshot_available;
+ UINT test_snapshot_fragments;
+ UCHAR *test_snapshot_search;
+ ULONG test_snapshot_list_count;
+ TXM_MODULE_ALLOCATED_OBJECT *test_snapshot_list_head;
+ ULONG test_snapshot_allocate_calls;
+ ULONG test_snapshot_mutex_get_calls;
+} TEST_SNAPSHOT;
+
+
+static VOID test_snapshot_take(TEST_SNAPSHOT *snapshot, TXM_MODULE_INSTANCE *module_instance)
+{
+ snapshot -> test_snapshot_available = _txm_module_manager_object_pool.tx_byte_pool_available;
+ snapshot -> test_snapshot_fragments = _txm_module_manager_object_pool.tx_byte_pool_fragments;
+ snapshot -> test_snapshot_search = _txm_module_manager_object_pool.tx_byte_pool_search;
+ snapshot -> test_snapshot_list_count = module_instance -> txm_module_instance_object_list_count;
+ snapshot -> test_snapshot_list_head = module_instance -> txm_module_instance_object_list_head;
+ snapshot -> test_snapshot_allocate_calls = test_allocate_calls;
+ snapshot -> test_snapshot_mutex_get_calls = test_mutex_get_calls;
+}
+
+
+/* Assert that a refused request changed nothing an accepted one would have. */
+static VOID test_refusal_check(const char *description, TEST_SNAPSHOT *snapshot,
+ TXM_MODULE_INSTANCE *module_instance, VOID *object, UINT status)
+{
+ test_expect(description, (ULONG) status, (ULONG) TXM_MODULE_MATH_OVERFLOW);
+ test_expect("...returning a null object pointer", (ULONG) (object == TX_NULL), (ULONG) TX_TRUE);
+ test_expect("...without asking the pool for anything",
+ test_allocate_calls, snapshot -> test_snapshot_allocate_calls);
+ test_expect("...without taking the protection mutex",
+ test_mutex_get_calls, snapshot -> test_snapshot_mutex_get_calls);
+ test_expect("...leaving the bytes available unchanged",
+ _txm_module_manager_object_pool.tx_byte_pool_available, snapshot -> test_snapshot_available);
+ test_expect("...leaving the fragment count unchanged",
+ (ULONG) _txm_module_manager_object_pool.tx_byte_pool_fragments, (ULONG) snapshot -> test_snapshot_fragments);
+ test_expect("...leaving the search pointer unchanged",
+ (ULONG) (_txm_module_manager_object_pool.tx_byte_pool_search == snapshot -> test_snapshot_search), (ULONG) TX_TRUE);
+ test_expect("...leaving the module's object count unchanged",
+ module_instance -> txm_module_instance_object_list_count, snapshot -> test_snapshot_list_count);
+ test_expect("...leaving the module's object list head unchanged",
+ (ULONG) (module_instance -> txm_module_instance_object_list_head == snapshot -> test_snapshot_list_head), (ULONG) TX_TRUE);
+ test_expect("...and touching no guard band", test_guard_damage(), ((ULONG) 0));
+}
+
+
+/* Model the arithmetic this test guards against, so that each refused size can
+ show what the previous implementation would have asked the pool for. */
+static ULONG test_unchecked_allocation_size(ULONG object_size)
+{
+ return((ULONG) (object_size + sizeof(TXM_MODULE_ALLOCATED_OBJECT)));
+}
+
+
+/* Confirm that one accepted allocation is shaped the way the rest of the manager
+ requires: the pool was asked for the object plus the header, the module was
+ handed the address just past the header, and the header records the size that
+ _txm_module_manager_object_size_check will later be asked to match. */
+static VOID test_accepted_check(const char *description, TXM_MODULE_INSTANCE *module_instance,
+ VOID *object, UINT status, ULONG object_size)
+{
+
+TXM_MODULE_ALLOCATED_OBJECT *header;
+
+
+ test_expect(description, (ULONG) status, (ULONG) TX_SUCCESS);
+ test_expect("...returning a non-null object pointer", (ULONG) (object != TX_NULL), (ULONG) TX_TRUE);
+
+ if (object == TX_NULL)
+ {
+ return;
+ }
+
+ test_expect("...having asked the pool for the object and the header",
+ test_allocate_size, object_size + TEST_HEADER_BYTES);
+ test_expect("...without waiting", test_allocate_wait, (ULONG) TX_NO_WAIT);
+
+ header = ((TXM_MODULE_ALLOCATED_OBJECT *) object) - 1;
+
+ test_expect("...placing the object immediately after the header",
+ (ULONG) (((UCHAR *) header) == test_blocks[test_block_count - 1U].test_block_start), (ULONG) TX_TRUE);
+ /* The size recorded here is what _txm_module_manager_object_size_check later
+ compares an object-creation request against, so a size the header did not
+ record faithfully would fail every creation dispatcher. */
+ test_expect("...recording the requested size in the header",
+ header -> txm_module_object_size, object_size);
+ test_expect("...recording the owning module in the header",
+ (ULONG) (header -> txm_module_allocated_object_module_instance == module_instance), (ULONG) TX_TRUE);
+ test_expect("...and touching no guard band", test_guard_damage(), ((ULONG) 0));
+}
+
+
+/* Walk the module's allocation list and report whether it is a consistent ring
+ of the expected length, owned throughout by the expected module. */
+static UINT test_list_consistent(TXM_MODULE_INSTANCE *module_instance, ULONG expected_count)
+{
+
+TXM_MODULE_ALLOCATED_OBJECT *object;
+ULONG walked;
+
+
+ if (module_instance -> txm_module_instance_object_list_count != expected_count)
+ {
+ return(TX_FALSE);
+ }
+
+ if (expected_count == ((ULONG) 0))
+ {
+ return((UINT) (module_instance -> txm_module_instance_object_list_head == TX_NULL));
+ }
+
+ object = module_instance -> txm_module_instance_object_list_head;
+
+ if (object == TX_NULL)
+ {
+ return(TX_FALSE);
+ }
+
+ for (walked = ((ULONG) 0); walked < expected_count; walked++)
+ {
+ if (object -> txm_module_allocated_object_module_instance != module_instance)
+ {
+ return(TX_FALSE);
+ }
+
+ if (object -> txm_module_allocated_object_next -> txm_module_allocated_object_previous != object)
+ {
+ return(TX_FALSE);
+ }
+
+ object = object -> txm_module_allocated_object_next;
+ }
+
+ /* The ring must close on the head after exactly the expected number of steps. */
+ return((UINT) (object == module_instance -> txm_module_instance_object_list_head));
+}
+
+
+/* The control block sizes a module can ask the manager to allocate for it. */
+typedef struct TEST_OBJECT_SIZE_STRUCT
+{
+ const char *test_object_name;
+ ULONG test_object_size;
+} TEST_OBJECT_SIZE;
+
+
+int main(void)
+{
+
+TXM_MODULE_INSTANCE module;
+TXM_MODULE_INSTANCE *module_instance;
+TX_THREAD module_thread;
+TEST_SNAPSHOT snapshot;
+VOID *object;
+VOID *objects[8];
+UINT status;
+UINT index;
+ULONG object_size;
+ULONG wrapped_size;
+
+TEST_OBJECT_SIZE object_sizes[] =
+{
+ {"a thread", (ULONG) sizeof(TX_THREAD)},
+ {"a queue", (ULONG) sizeof(TX_QUEUE)},
+ {"a semaphore", (ULONG) sizeof(TX_SEMAPHORE)},
+ {"a mutex", (ULONG) sizeof(TX_MUTEX)},
+ {"an event flags group",(ULONG) sizeof(TX_EVENT_FLAGS_GROUP)},
+ {"a block pool", (ULONG) sizeof(TX_BLOCK_POOL)},
+ {"a byte pool", (ULONG) sizeof(TX_BYTE_POOL)},
+ {"a timer", (ULONG) sizeof(TX_TIMER)}
+};
+
+#define TEST_OBJECT_SIZE_ENTRIES (sizeof(object_sizes) / sizeof(object_sizes[0]))
+
+
+ /* Unbuffered, so that the expectations reported below survive a crash. An
+ undersized allocation is a memory-safety failure, and a build in which one
+ gets through has to say which expectation noticed before it dies. */
+ setvbuf(stdout, TX_NULL, _IONBF, 0U);
+
+ printf("Module Manager object allocation size arithmetic test.................. ");
+
+ module_instance = &module;
+ module.txm_module_instance_id = TXM_MODULE_ID;
+ module.txm_module_instance_object_list_count = ((ULONG) 0);
+ module.txm_module_instance_object_list_head = TX_NULL;
+
+ module_thread.tx_thread_module_instance_ptr = (VOID *) module_instance;
+ _tx_thread_current_ptr = &module_thread;
+
+ test_reset(module_instance);
+
+ /**********************************************************************/
+ /* The header the manager writes is larger than the smallest block */
+ /* the pool will hand out, which is what makes a wrapped size unsafe. */
+ /**********************************************************************/
+
+ test_expect("the object header spans more than one pool alignment unit",
+ (ULONG) (TEST_HEADER_BYTES > (ULONG) sizeof(ALIGN_TYPE)), (ULONG) TX_TRUE);
+
+ /**********************************************************************/
+ /* Sizes for which the header-inclusive addition wraps. */
+ /**********************************************************************/
+
+ /* The smallest size that overflows at all, and so the tight side of the new
+ comparison. It wraps to exactly zero, a request the pool would have refused
+ on its own, so this boundary fails safe even unfixed; it is here for the
+ boundary rather than for the consequence. */
+ test_reset(module_instance);
+
+ object_size = (TEST_ULONG_MAX - TEST_HEADER_BYTES) + ((ULONG) 1);
+ test_expect("the smallest overflowing size is one past the largest that fits",
+ object_size, TEST_SIZE_WRAPPING_TO(0UL));
+ test_expect("...and wraps to zero", test_unchecked_allocation_size(object_size), ((ULONG) 0));
+ test_snapshot_take(&snapshot, module_instance);
+ object = (VOID *) &module;
+ status = _txm_module_manager_object_allocate(&object, object_size, module_instance);
+ test_refusal_check("the smallest overflowing size is refused",
+ &snapshot, module_instance, object, status);
+
+ /* Wrapping to eight. This is the exploitable one: the pool accepts an eight
+ byte request and the manager then writes sixteen bytes into it. */
+ test_reset(module_instance);
+
+ object_size = TEST_SIZE_WRAPPING_TO(8UL);
+ wrapped_size = test_unchecked_allocation_size(object_size);
+ test_expect("the size that wraps to eight is the one expected", wrapped_size, ((ULONG) 8));
+ test_expect("...a request the pool would have accepted",
+ (ULONG) ((wrapped_size != ((ULONG) 0)) && (wrapped_size <= TEST_POOL_BYTES)), (ULONG) TX_TRUE);
+ test_expect("...for fewer bytes than the header alone occupies",
+ (ULONG) (wrapped_size < TEST_HEADER_BYTES), (ULONG) TX_TRUE);
+ test_snapshot_take(&snapshot, module_instance);
+ object = (VOID *) &module;
+ status = _txm_module_manager_object_allocate(&object, object_size, module_instance);
+ test_refusal_check("a size whose header-inclusive total wraps to a block smaller than the header is refused",
+ &snapshot, module_instance, object, status);
+
+ /* Wrapping to one, the smallest request the pool would have served at all. */
+ test_reset(module_instance);
+
+ object_size = TEST_SIZE_WRAPPING_TO(1UL);
+ test_expect("the size that wraps to one is the one expected",
+ test_unchecked_allocation_size(object_size), ((ULONG) 1));
+ test_snapshot_take(&snapshot, module_instance);
+ object = (VOID *) &module;
+ status = _txm_module_manager_object_allocate(&object, object_size, module_instance);
+ test_refusal_check("a size whose header-inclusive total wraps to a single byte is refused",
+ &snapshot, module_instance, object, status);
+
+ /* The largest size expressible at all, which wraps to one byte less than the
+ header. */
+ test_reset(module_instance);
+
+ test_snapshot_take(&snapshot, module_instance);
+ object = (VOID *) &module;
+ status = _txm_module_manager_object_allocate(&object, TEST_ULONG_MAX, module_instance);
+ test_refusal_check("the largest expressible size is refused", &snapshot, module_instance, object, status);
+
+ /**********************************************************************/
+ /* The largest size that does not overflow. */
+ /**********************************************************************/
+
+ /* This one must pass the check and reach the pool carrying its true total,
+ which the pool then refuses on its own size bound. It covers the accepting
+ side of the new comparison at the tightest point there is. */
+ test_reset(module_instance);
+
+ object_size = TEST_ULONG_MAX - TEST_HEADER_BYTES;
+ test_snapshot_take(&snapshot, module_instance);
+ object = (VOID *) &module;
+ status = _txm_module_manager_object_allocate(&object, object_size, module_instance);
+
+ test_expect("the largest non-overflowing size is refused by the pool, not by the check",
+ (ULONG) status, (ULONG) TX_SIZE_ERROR);
+ test_expect("...having reached the pool", test_allocate_calls, snapshot.test_snapshot_allocate_calls + ((ULONG) 1));
+ test_expect("...asking for the full untruncated total", test_allocate_size, TEST_ULONG_MAX);
+ test_expect("...returning a null object pointer", (ULONG) (object == TX_NULL), (ULONG) TX_TRUE);
+ test_expect("...leaving the module's object count unchanged",
+ module_instance -> txm_module_instance_object_list_count, snapshot.test_snapshot_list_count);
+ test_expect("...leaving the bytes available unchanged",
+ _txm_module_manager_object_pool.tx_byte_pool_available, snapshot.test_snapshot_available);
+ test_expect("...and releasing the protection mutex it took",
+ test_mutex_get_calls, test_mutex_put_calls);
+
+ /**********************************************************************/
+ /* Ordinary allocations, one for every control block a module can own. */
+ /**********************************************************************/
+
+ test_reset(module_instance);
+
+ for (index = 0U; index < (UINT) TEST_OBJECT_SIZE_ENTRIES; index++)
+ {
+ object = TX_NULL;
+ status = _txm_module_manager_object_allocate(&object, object_sizes[index].test_object_size, module_instance);
+ test_accepted_check(object_sizes[index].test_object_name, module_instance, object, status,
+ object_sizes[index].test_object_size);
+ objects[index] = object;
+ }
+
+ test_expect("the module's allocation list holds every object in a consistent ring",
+ (ULONG) test_list_consistent(module_instance, (ULONG) TEST_OBJECT_SIZE_ENTRIES), (ULONG) TX_TRUE);
+
+ /* Every one of them can be given back, and the list empties out. */
+ for (index = 0U; index < (UINT) TEST_OBJECT_SIZE_ENTRIES; index++)
+ {
+ status = _txm_module_manager_object_deallocate(objects[index]);
+ test_expect("an allocated object can be released", (ULONG) status, (ULONG) TX_SUCCESS);
+ }
+
+ test_expect("the allocation list is empty again",
+ (ULONG) test_list_consistent(module_instance, ((ULONG) 0)), (ULONG) TX_TRUE);
+ test_expect("every byte charged to the pool came back",
+ _txm_module_manager_object_pool.tx_byte_pool_available, TEST_POOL_BYTES);
+ test_expect("no guard band was touched by any of it", test_guard_damage(), ((ULONG) 0));
+ test_expect("the protection mutex was balanced throughout", test_mutex_get_calls, test_mutex_put_calls);
+
+ /**********************************************************************/
+ /* A zero-length object, the other end of the accepted range. */
+ /**********************************************************************/
+
+ test_reset(module_instance);
+ object = TX_NULL;
+ status = _txm_module_manager_object_allocate(&object, ((ULONG) 0), module_instance);
+ test_accepted_check("a zero-length object", module_instance, object, status, ((ULONG) 0));
+ test_expect("...for which the pool was asked for exactly the header",
+ test_allocate_size, TEST_HEADER_BYTES);
+ (VOID) _txm_module_manager_object_deallocate(object);
+
+ /**********************************************************************/
+ /* Sizes the pool cannot serve, which must still not corrupt anything. */
+ /**********************************************************************/
+
+ test_reset(module_instance);
+
+ object = TX_NULL;
+ status = _txm_module_manager_object_allocate(&object, TEST_POOL_BYTES, module_instance);
+ test_expect("a size the pool is too small for is refused by the pool",
+ (ULONG) status, (ULONG) TX_SIZE_ERROR);
+ test_expect("...returning a null object pointer", (ULONG) (object == TX_NULL), (ULONG) TX_TRUE);
+ test_expect("...leaving the allocation list empty",
+ (ULONG) test_list_consistent(module_instance, ((ULONG) 0)), (ULONG) TX_TRUE);
+
+ object = TX_NULL;
+ status = _txm_module_manager_object_allocate(&object, ((ULONG) 4000), module_instance);
+ test_expect("a size the pool can serve once succeeds", (ULONG) status, (ULONG) TX_SUCCESS);
+
+ object = TX_NULL;
+ status = _txm_module_manager_object_allocate(&object, ((ULONG) 4000), module_instance);
+ test_expect("the same size again finds no memory", (ULONG) status, (ULONG) TX_NO_MEMORY);
+ test_expect("...returning a null object pointer", (ULONG) (object == TX_NULL), (ULONG) TX_TRUE);
+ test_expect("...leaving the one live object in the list",
+ (ULONG) test_list_consistent(module_instance, ((ULONG) 1)), (ULONG) TX_TRUE);
+ test_expect("...and touching no guard band", test_guard_damage(), ((ULONG) 0));
+
+ /**********************************************************************/
+ /* Invalid and disabled configurations. */
+ /**********************************************************************/
+
+ test_reset(module_instance);
+
+ /* A null destination is rejected before the size is looked at, so it keeps
+ its own error even when the size would also have been refused. */
+ status = _txm_module_manager_object_allocate(TX_NULL, TEST_ULONG_MAX, module_instance);
+ test_expect("a null destination pointer is rejected on its own terms",
+ (ULONG) status, (ULONG) TXM_MODULE_INVALID_MEMORY);
+ test_expect("...without asking the pool for anything", test_allocate_calls, ((ULONG) 0));
+ test_expect("...without taking the protection mutex", test_mutex_get_calls, ((ULONG) 0));
+
+ /* With no object pool, an overflowing size is still refused as one: the
+ check runs before the manager looks at its own configuration. */
+ _txm_module_manager_object_pool_created = TX_FALSE;
+
+ test_snapshot_take(&snapshot, module_instance);
+ object = (VOID *) &module;
+ status = _txm_module_manager_object_allocate(&object, TEST_ULONG_MAX, module_instance);
+ test_refusal_check("an overflowing size is refused with no object pool created",
+ &snapshot, module_instance, object, status);
+
+ object = TX_NULL;
+ status = _txm_module_manager_object_allocate(&object, (ULONG) sizeof(TX_MUTEX), module_instance);
+ test_expect("an ordinary size reports the pool is not available",
+ (ULONG) status, (ULONG) TX_NOT_AVAILABLE);
+ test_expect("...returning a null object pointer", (ULONG) (object == TX_NULL), (ULONG) TX_TRUE);
+ test_expect("...and releasing the protection mutex it took",
+ test_mutex_get_calls, test_mutex_put_calls);
+
+ _txm_module_manager_object_pool_created = TX_TRUE;
+
+ /**********************************************************************/
+ /* The invariant the whole check exists to hold, over the whole run. */
+ /**********************************************************************/
+
+ test_expect("the pool was never asked for less than the header it must hold",
+ (ULONG) test_undersized_requests, ((ULONG) 0));
+ test_expect("every request named the manager's own pool and a real destination",
+ (ULONG) test_misdirected_requests, ((ULONG) 0));
+ test_expect("and nothing wrote through a guard band", test_guard_damage(), ((ULONG) 0));
+
+ if (test_failures == 0U)
+ {
+ printf("SUCCESS!\n");
+ return(0);
+ }
+
+ printf("ERROR: %u expectation(s) failed\n", test_failures);
+ return(1);
+}
diff --git a/test/tx/module_manager/threadx_module_manager_object_authentication_test.c b/test/tx/module_manager/threadx_module_manager_object_authentication_test.c
new file mode 100644
index 00000000..396f57f2
--- /dev/null
+++ b/test/tx/module_manager/threadx_module_manager_object_authentication_test.c
@@ -0,0 +1,1217 @@
+/***************************************************************************
+ * 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
+ **************************************************************************/
+
+
+/**************************************************************************/
+/**************************************************************************/
+/** */
+/** ThreadX Test */
+/** */
+/** Module Manager kernel object authentication */
+/** */
+/**************************************************************************/
+/**************************************************************************/
+
+/* A memory-protected module names the kernel objects it wants operated on by
+ address, and the Module Manager decides whether a privileged service may
+ dereference that address. Deciding it by asking only whether the address lies
+ outside the module is not enough, and the reason is the object pool: the pool
+ is outside every module, so an address shifted into the interior of one of the
+ module's own privileged allocations passes that test while denoting no object.
+ The bytes the shifted address then presents as a control block are bytes the
+ module put there through ordinary create and set services, so the control block
+ ID at the front of them can be made to read as any type the module likes.
+
+ This test drives the manager's authentication of such addresses on the host.
+ The object pool behind it is modelled rather than run -- a bump allocator that
+ reuses the block it most recently released, which is how the address of a freed
+ object comes back for the cases about stale addresses -- but the two records
+ authentication actually consults are built the way the system builds them: the
+ module allocation list is built by running the real
+ _txm_module_manager_object_allocate, and the kernel created lists are built the
+ way the create and delete services maintain them, by writing the control block
+ ID and linking the object in, and by clearing the ID and unlinking it.
+
+ What the test asserts is that an address is accepted when, and only when, it is
+ the exact address of a live object of the type the service expects. Every
+ aligned interior offset of a legitimate object is rejected as a foreign type,
+ including the offsets where this test has planted that type's own ID, which is
+ the case the reported attack is built on. */
+
+#include <stdio.h>
+
+#include "threadx_module_manager_host_test_port.h"
+
+#include "tx_thread.h"
+#include "tx_trace.h"
+#include "tx_timer.h"
+#include "tx_queue.h"
+#include "tx_event_flags.h"
+#include "tx_semaphore.h"
+#include "tx_mutex.h"
+#include "tx_block_pool.h"
+#include "tx_byte_pool.h"
+#include "txm_module.h"
+#include "txm_module_manager_util.h"
+
+
+/* Define the stand-in interrupt lock the port shim counts through. */
+
+unsigned int test_interrupt_disable_depth;
+unsigned int test_interrupt_disable_max_depth;
+unsigned int test_interrupt_restore_underflows;
+
+
+/* Define the modelled object pool. */
+
+#define TEST_POOL_BYTES ((ULONG) 8192)
+#define TEST_MODULE_MEMORY_BYTES 512U
+#define TEST_MAX_BLOCKS 32U
+
+static union
+{
+ ALIGN_TYPE test_pool_alignment;
+ UCHAR test_pool_bytes[TEST_POOL_BYTES];
+} test_pool_arena;
+
+static ULONG test_pool_offset;
+
+
+/* Define the memory that stands for the calling module's own code and data. A
+ kernel object here is one the module could write behind the kernel's back, so
+ authentication has to refuse it however genuine it looks. */
+
+static union
+{
+ ALIGN_TYPE test_data_alignment;
+ UCHAR test_data_bytes[TEST_MODULE_MEMORY_BYTES];
+} test_module_data;
+
+static union
+{
+ ALIGN_TYPE test_code_alignment;
+ UCHAR test_code_bytes[TEST_MODULE_MEMORY_BYTES];
+} test_module_code;
+
+
+/* Define the objects that stand for application-owned objects shared with a
+ module. These live outside the pool, so the manager has no allocation record of
+ them and the kernel created list is the only record of them there is. */
+
+static TX_QUEUE test_host_queue;
+static TX_THREAD test_host_thread;
+static TX_MUTEX test_host_stranger;
+
+
+/* Define the block the modelled pool most recently released, so that a later
+ allocation of the same size lands on the address a freed object had. */
+
+static UCHAR *test_released_start;
+static ULONG test_released_bytes;
+
+
+/* Define the manager state the code under test reaches for. */
+
+TX_BYTE_POOL _txm_module_manager_object_pool;
+UINT _txm_module_manager_object_pool_created;
+TX_MUTEX _txm_module_manager_mutex;
+TX_THREAD *_tx_thread_current_ptr;
+
+
+/* Define the kernel created lists authentication walks. */
+
+TX_BLOCK_POOL *_tx_block_pool_created_ptr;
+ULONG _tx_block_pool_created_count;
+TX_BYTE_POOL *_tx_byte_pool_created_ptr;
+ULONG _tx_byte_pool_created_count;
+TX_EVENT_FLAGS_GROUP *_tx_event_flags_created_ptr;
+ULONG _tx_event_flags_created_count;
+TX_MUTEX *_tx_mutex_created_ptr;
+ULONG _tx_mutex_created_count;
+TX_QUEUE *_tx_queue_created_ptr;
+ULONG _tx_queue_created_count;
+TX_SEMAPHORE *_tx_semaphore_created_ptr;
+ULONG _tx_semaphore_created_count;
+TX_THREAD *_tx_thread_created_ptr;
+ULONG _tx_thread_created_count;
+TX_TIMER *_tx_timer_created_ptr;
+ULONG _tx_timer_created_count;
+
+
+#ifdef TX_ENABLE_EVENT_TRACE
+
+/* Define the trace state the reset path's trace insert refers to when the tree is
+ configured with event tracing. The insert does nothing while the buffer pointer
+ is null, which is the state a system that has not enabled tracing is in, so the
+ cases below run identically in every configuration this tree builds. */
+
+TX_TRACE_HEADER *_tx_trace_header_ptr;
+TX_TRACE_BUFFER_ENTRY *_tx_trace_buffer_start_ptr;
+TX_TRACE_BUFFER_ENTRY *_tx_trace_buffer_end_ptr;
+TX_TRACE_BUFFER_ENTRY *_tx_trace_buffer_current_ptr;
+ULONG _tx_trace_event_enable_bits;
+ULONG _tx_trace_simulated_time;
+VOID (*_tx_trace_full_notify_function)(VOID *buffer);
+volatile ULONG _tx_thread_system_state;
+
+#endif
+
+
+static UINT test_failures;
+static ULONG test_checks;
+static ULONG test_stack_builds;
+
+
+/* Record the outcome of one expectation. */
+static VOID test_expect(const char *description, ULONG actual, ULONG expected)
+{
+ test_checks++;
+
+ if (actual != expected)
+ {
+ printf("FAIL: %s (expected %lu, got %lu)\n", description, (unsigned long) expected, (unsigned long) actual);
+ test_failures++;
+ }
+}
+
+
+/* Report every module object type this manager authenticates, and the name and
+ control block size of each, so that the cases below can be written once and run
+ against all of them. */
+
+typedef struct TEST_OBJECT_KIND_STRUCT
+{
+ UINT test_kind_type;
+ const char *test_kind_name;
+ ULONG test_kind_size;
+ ULONG test_kind_id;
+} TEST_OBJECT_KIND;
+
+static const TEST_OBJECT_KIND test_object_kinds[] =
+{
+ { TXM_BLOCK_POOL_OBJECT, "block pool", (ULONG) sizeof(TX_BLOCK_POOL), TX_BLOCK_POOL_ID },
+ { TXM_BYTE_POOL_OBJECT, "byte pool", (ULONG) sizeof(TX_BYTE_POOL), TX_BYTE_POOL_ID },
+ { TXM_EVENT_FLAGS_OBJECT, "event flags", (ULONG) sizeof(TX_EVENT_FLAGS_GROUP), TX_EVENT_FLAGS_ID },
+ { TXM_MUTEX_OBJECT, "mutex", (ULONG) sizeof(TX_MUTEX), TX_MUTEX_ID },
+ { TXM_QUEUE_OBJECT, "queue", (ULONG) sizeof(TX_QUEUE), TX_QUEUE_ID },
+ { TXM_SEMAPHORE_OBJECT, "semaphore", (ULONG) sizeof(TX_SEMAPHORE), TX_SEMAPHORE_ID },
+ { TXM_THREAD_OBJECT, "thread", (ULONG) sizeof(TX_THREAD), TX_THREAD_ID },
+ { TXM_TIMER_OBJECT, "timer", (ULONG) sizeof(TX_TIMER), TX_TIMER_ID }
+};
+
+#define TEST_OBJECT_KIND_COUNT (sizeof(test_object_kinds) / sizeof(test_object_kinds[0]))
+
+
+/* Round a request up the way _tx_byte_allocate does. */
+static ULONG test_round_up(ULONG memory_size)
+{
+ return((((memory_size + ((ULONG) sizeof(ALIGN_TYPE))) - ((ULONG) 1)) / ((ULONG) sizeof(ALIGN_TYPE))) * ((ULONG) sizeof(ALIGN_TYPE)));
+}
+
+
+/* Stand in for the byte pool the manager allocates object memory from.
+
+ The two size bounds are the real ones. The placement is a bump allocator with
+ one refinement: a request that exactly fits the block most recently released is
+ given that block back. A first-fit byte pool reuses freed memory, and the cases
+ about an address that has been freed and allocated again need the address to
+ actually come back. */
+UINT _txe_byte_allocate(TX_BYTE_POOL *pool_ptr, VOID **memory_ptr, ULONG memory_size, ULONG wait_option)
+{
+
+ULONG rounded_size;
+UCHAR *block_start;
+
+
+ (VOID) wait_option;
+
+ if ((pool_ptr != &_txm_module_manager_object_pool) || (memory_ptr == TX_NULL))
+ {
+ return(TX_POOL_ERROR);
+ }
+
+ if (memory_size == ((ULONG) 0))
+ {
+ return(TX_SIZE_ERROR);
+ }
+
+ if (memory_size > pool_ptr -> tx_byte_pool_size)
+ {
+ return(TX_SIZE_ERROR);
+ }
+
+ rounded_size = test_round_up(memory_size);
+
+ if ((test_released_start != TX_NULL) && (test_released_bytes == rounded_size))
+ {
+ block_start = test_released_start;
+ test_released_start = TX_NULL;
+ test_released_bytes = ((ULONG) 0);
+ }
+ else
+ {
+ if ((test_pool_offset + rounded_size) > TEST_POOL_BYTES)
+ {
+ return(TX_NO_MEMORY);
+ }
+
+ block_start = &test_pool_arena.test_pool_bytes[test_pool_offset];
+ test_pool_offset = test_pool_offset + rounded_size;
+ }
+
+ pool_ptr -> tx_byte_pool_available = pool_ptr -> tx_byte_pool_available - rounded_size;
+
+ *memory_ptr = (VOID *) block_start;
+
+ return(TX_SUCCESS);
+}
+
+
+/* Stand in for the release side, remembering the block so it can come back. */
+UINT _txe_byte_release(VOID *memory_ptr)
+{
+
+UCHAR *block_start;
+
+
+ block_start = (UCHAR *) memory_ptr;
+
+ if ((block_start < test_pool_arena.test_pool_bytes) ||
+ (block_start >= &test_pool_arena.test_pool_bytes[TEST_POOL_BYTES]))
+ {
+ return(TX_PTR_ERROR);
+ }
+
+ /* The model does not track block lengths, and the only released block a case
+ needs to come back is the one it just released, whose length it knows. The
+ length is recorded by the caller through test_release_size. */
+ test_released_start = block_start;
+
+ return(TX_SUCCESS);
+}
+
+
+/* Tell the model how long the block just released was. */
+static VOID test_release_size(ULONG object_size)
+{
+ test_released_bytes = test_round_up(object_size + ((ULONG) sizeof(TXM_MODULE_ALLOCATED_OBJECT)));
+}
+
+
+/* Stand in for the protection mutex. */
+UINT _txe_mutex_get(TX_MUTEX *mutex_ptr, ULONG wait_option)
+{
+ (VOID) mutex_ptr;
+ (VOID) wait_option;
+
+ return(TX_SUCCESS);
+}
+
+
+UINT _txe_mutex_put(TX_MUTEX *mutex_ptr)
+{
+ (VOID) mutex_ptr;
+
+ return(TX_SUCCESS);
+}
+
+
+/* Stand in for the module port's data range check, which the portable
+ outside-the-module test is built on. */
+UINT _txm_module_manager_inside_data_check(TXM_MODULE_INSTANCE *module_instance, ALIGN_TYPE obj_ptr,
+ ULONG obj_size, UINT write_request)
+{
+
+ULONG data_start;
+ULONG data_end;
+
+
+ (VOID) module_instance;
+ (VOID) write_request;
+
+ data_start = (ULONG) (ALIGN_TYPE) test_module_data.test_data_bytes;
+ data_end = data_start + ((ULONG) TEST_MODULE_MEMORY_BYTES);
+
+ /* The whole range has to lie inside the module's data, not just its first
+ address, and the subtraction is done on the end rather than the start so that
+ a size which would carry the range past the top of the address space cannot
+ wrap into an accepting answer. */
+ if (((ULONG) obj_ptr >= data_start) && (obj_size <= (data_end - (ULONG) obj_ptr)))
+ {
+ return(TX_TRUE);
+ }
+
+ return(TX_FALSE);
+}
+
+
+/* Outside is not the negation of inside: a range that only partly reaches into the
+ module's data is neither. It has to end before that data begins or begin after it
+ ends, and a size that would carry the end past the top of the address space is
+ refused rather than allowed to compare as though it had not. */
+UINT _txm_module_manager_outside_data_check(TXM_MODULE_INSTANCE *module_instance, ALIGN_TYPE obj_ptr,
+ ULONG obj_size)
+{
+
+ULONG data_start;
+ULONG data_end;
+
+
+ (VOID) module_instance;
+
+ data_start = (ULONG) (ALIGN_TYPE) test_module_data.test_data_bytes;
+ data_end = data_start + ((ULONG) TEST_MODULE_MEMORY_BYTES);
+
+ if (obj_size > (~((ULONG) 0) - (ULONG) obj_ptr))
+ {
+ return(TX_FALSE);
+ }
+
+ if ((((ULONG) obj_ptr + obj_size) <= data_start) || ((ULONG) obj_ptr >= data_end))
+ {
+ return(TX_TRUE);
+ }
+
+ return(TX_FALSE);
+}
+
+
+/* Stand in for the port primitives the manager sources under test refer to but
+ that no case here exercises. */
+VOID _txm_module_manager_alignment_adjust(TXM_MODULE_PREAMBLE *module_preamble, ULONG *code_size,
+ ULONG *code_alignment, ULONG *data_size, ULONG *data_alignment)
+{
+ (VOID) module_preamble;
+ (VOID) code_size;
+ (VOID) code_alignment;
+ (VOID) data_size;
+ (VOID) data_alignment;
+}
+
+
+VOID _txm_module_manager_thread_stack_build(TX_THREAD *thread_ptr, VOID (*shell_function)(TX_THREAD *, TXM_MODULE_INSTANCE *))
+{
+ (VOID) thread_ptr;
+ (VOID) shell_function;
+
+ test_stack_builds++;
+}
+
+
+/* Do to an object what the kernel's create service does to it, as far as
+ authentication can see: write the control block ID, and link the object onto
+ the created list for its type.
+
+ The chain each list is built as ends in a self-link on its oldest member rather
+ than closing back on the head, so that a walk bounded by the created count
+ visits every member and a walk that ignored the count would not run off the
+ end. What is being tested is the bound, not the model. */
+static VOID test_object_create(VOID *object_ptr, UINT object_type)
+{
+ switch (object_type)
+ {
+
+ case TXM_BLOCK_POOL_OBJECT:
+ {
+ TX_BLOCK_POOL *block_pool_ptr = (TX_BLOCK_POOL *) object_ptr;
+
+ block_pool_ptr -> tx_block_pool_id = TX_BLOCK_POOL_ID;
+ block_pool_ptr -> tx_block_pool_created_next = (_tx_block_pool_created_ptr == TX_NULL) ? block_pool_ptr : _tx_block_pool_created_ptr;
+ _tx_block_pool_created_ptr = block_pool_ptr;
+ _tx_block_pool_created_count++;
+ break;
+ }
+
+ case TXM_BYTE_POOL_OBJECT:
+ {
+ TX_BYTE_POOL *byte_pool_ptr = (TX_BYTE_POOL *) object_ptr;
+
+ byte_pool_ptr -> tx_byte_pool_id = TX_BYTE_POOL_ID;
+ byte_pool_ptr -> tx_byte_pool_created_next = (_tx_byte_pool_created_ptr == TX_NULL) ? byte_pool_ptr : _tx_byte_pool_created_ptr;
+ _tx_byte_pool_created_ptr = byte_pool_ptr;
+ _tx_byte_pool_created_count++;
+ break;
+ }
+
+ case TXM_EVENT_FLAGS_OBJECT:
+ {
+ TX_EVENT_FLAGS_GROUP *event_flags_ptr = (TX_EVENT_FLAGS_GROUP *) object_ptr;
+
+ event_flags_ptr -> tx_event_flags_group_id = TX_EVENT_FLAGS_ID;
+ event_flags_ptr -> tx_event_flags_group_created_next = (_tx_event_flags_created_ptr == TX_NULL) ? event_flags_ptr : _tx_event_flags_created_ptr;
+ _tx_event_flags_created_ptr = event_flags_ptr;
+ _tx_event_flags_created_count++;
+ break;
+ }
+
+ case TXM_MUTEX_OBJECT:
+ {
+ TX_MUTEX *mutex_ptr = (TX_MUTEX *) object_ptr;
+
+ mutex_ptr -> tx_mutex_id = TX_MUTEX_ID;
+ mutex_ptr -> tx_mutex_created_next = (_tx_mutex_created_ptr == TX_NULL) ? mutex_ptr : _tx_mutex_created_ptr;
+ _tx_mutex_created_ptr = mutex_ptr;
+ _tx_mutex_created_count++;
+ break;
+ }
+
+ case TXM_QUEUE_OBJECT:
+ {
+ TX_QUEUE *queue_ptr = (TX_QUEUE *) object_ptr;
+
+ queue_ptr -> tx_queue_id = TX_QUEUE_ID;
+ queue_ptr -> tx_queue_created_next = (_tx_queue_created_ptr == TX_NULL) ? queue_ptr : _tx_queue_created_ptr;
+ _tx_queue_created_ptr = queue_ptr;
+ _tx_queue_created_count++;
+ break;
+ }
+
+ case TXM_SEMAPHORE_OBJECT:
+ {
+ TX_SEMAPHORE *semaphore_ptr = (TX_SEMAPHORE *) object_ptr;
+
+ semaphore_ptr -> tx_semaphore_id = TX_SEMAPHORE_ID;
+ semaphore_ptr -> tx_semaphore_created_next = (_tx_semaphore_created_ptr == TX_NULL) ? semaphore_ptr : _tx_semaphore_created_ptr;
+ _tx_semaphore_created_ptr = semaphore_ptr;
+ _tx_semaphore_created_count++;
+ break;
+ }
+
+ case TXM_THREAD_OBJECT:
+ {
+ TX_THREAD *thread_ptr = (TX_THREAD *) object_ptr;
+
+ thread_ptr -> tx_thread_id = TX_THREAD_ID;
+ thread_ptr -> tx_thread_created_next = (_tx_thread_created_ptr == TX_NULL) ? thread_ptr : _tx_thread_created_ptr;
+ _tx_thread_created_ptr = thread_ptr;
+ _tx_thread_created_count++;
+ break;
+ }
+
+ case TXM_TIMER_OBJECT:
+ default:
+ {
+ TX_TIMER *timer_ptr = (TX_TIMER *) object_ptr;
+
+ timer_ptr -> tx_timer_id = TX_TIMER_ID;
+ timer_ptr -> tx_timer_created_next = (_tx_timer_created_ptr == TX_NULL) ? timer_ptr : _tx_timer_created_ptr;
+ _tx_timer_created_ptr = timer_ptr;
+ _tx_timer_created_count++;
+ break;
+ }
+ }
+}
+
+
+/* Do to an object what the kernel's delete service does: clear the control block
+ ID, and take the object off the created list for its type.
+
+ Every case that deletes has one object of that type on the list, which is the
+ shape that matters here, so the model empties the list rather than unlinking a
+ member of a longer one. */
+static VOID test_object_delete(VOID *object_ptr, UINT object_type)
+{
+ switch (object_type)
+ {
+
+ case TXM_BLOCK_POOL_OBJECT:
+
+ ((TX_BLOCK_POOL *) object_ptr) -> tx_block_pool_id = TX_CLEAR_ID;
+ _tx_block_pool_created_ptr = TX_NULL;
+ _tx_block_pool_created_count = ((ULONG) 0);
+ break;
+
+ case TXM_BYTE_POOL_OBJECT:
+
+ ((TX_BYTE_POOL *) object_ptr) -> tx_byte_pool_id = TX_CLEAR_ID;
+ _tx_byte_pool_created_ptr = TX_NULL;
+ _tx_byte_pool_created_count = ((ULONG) 0);
+ break;
+
+ case TXM_EVENT_FLAGS_OBJECT:
+
+ ((TX_EVENT_FLAGS_GROUP *) object_ptr) -> tx_event_flags_group_id = TX_CLEAR_ID;
+ _tx_event_flags_created_ptr = TX_NULL;
+ _tx_event_flags_created_count = ((ULONG) 0);
+ break;
+
+ case TXM_MUTEX_OBJECT:
+
+ ((TX_MUTEX *) object_ptr) -> tx_mutex_id = TX_CLEAR_ID;
+ _tx_mutex_created_ptr = TX_NULL;
+ _tx_mutex_created_count = ((ULONG) 0);
+ break;
+
+ case TXM_QUEUE_OBJECT:
+
+ ((TX_QUEUE *) object_ptr) -> tx_queue_id = TX_CLEAR_ID;
+ _tx_queue_created_ptr = TX_NULL;
+ _tx_queue_created_count = ((ULONG) 0);
+ break;
+
+ case TXM_SEMAPHORE_OBJECT:
+
+ ((TX_SEMAPHORE *) object_ptr) -> tx_semaphore_id = TX_CLEAR_ID;
+ _tx_semaphore_created_ptr = TX_NULL;
+ _tx_semaphore_created_count = ((ULONG) 0);
+ break;
+
+ case TXM_THREAD_OBJECT:
+
+ ((TX_THREAD *) object_ptr) -> tx_thread_id = TX_CLEAR_ID;
+ _tx_thread_created_ptr = TX_NULL;
+ _tx_thread_created_count = ((ULONG) 0);
+ break;
+
+ case TXM_TIMER_OBJECT:
+ default:
+
+ ((TX_TIMER *) object_ptr) -> tx_timer_id = TX_CLEAR_ID;
+ _tx_timer_created_ptr = TX_NULL;
+ _tx_timer_created_count = ((ULONG) 0);
+ break;
+ }
+}
+
+
+/* Bring the pool, the created lists and the modules back to a known state.
+
+ Everything is reset together deliberately. The allocation lists point into the
+ pool and the created lists point at objects in it, so a case that left either
+ behind would have the next case reading the wreckage of an earlier one rather
+ than what it was written to check. */
+static VOID test_reset(TXM_MODULE_INSTANCE *module_a, TXM_MODULE_INSTANCE *module_b)
+{
+
+UINT index;
+
+
+ for (index = 0U; index < (UINT) TEST_POOL_BYTES; index++)
+ {
+ test_pool_arena.test_pool_bytes[index] = (UCHAR) 0;
+ }
+
+ for (index = 0U; index < TEST_MODULE_MEMORY_BYTES; index++)
+ {
+ test_module_data.test_data_bytes[index] = (UCHAR) 0;
+ test_module_code.test_code_bytes[index] = (UCHAR) 0;
+ }
+
+ test_pool_offset = ((ULONG) 0);
+ test_released_start = TX_NULL;
+ test_released_bytes = ((ULONG) 0);
+
+ _txm_module_manager_object_pool.tx_byte_pool_id = TX_BYTE_POOL_ID;
+ _txm_module_manager_object_pool.tx_byte_pool_start = test_pool_arena.test_pool_bytes;
+ _txm_module_manager_object_pool.tx_byte_pool_size = TEST_POOL_BYTES;
+ _txm_module_manager_object_pool.tx_byte_pool_available = TEST_POOL_BYTES;
+ _txm_module_manager_object_pool_created = TX_TRUE;
+
+ _tx_block_pool_created_ptr = TX_NULL;
+ _tx_block_pool_created_count = ((ULONG) 0);
+ _tx_byte_pool_created_ptr = TX_NULL;
+ _tx_byte_pool_created_count = ((ULONG) 0);
+ _tx_event_flags_created_ptr = TX_NULL;
+ _tx_event_flags_created_count = ((ULONG) 0);
+ _tx_mutex_created_ptr = TX_NULL;
+ _tx_mutex_created_count = ((ULONG) 0);
+ _tx_queue_created_ptr = TX_NULL;
+ _tx_queue_created_count = ((ULONG) 0);
+ _tx_semaphore_created_ptr = TX_NULL;
+ _tx_semaphore_created_count = ((ULONG) 0);
+ _tx_thread_created_ptr = TX_NULL;
+ _tx_thread_created_count = ((ULONG) 0);
+ _tx_timer_created_ptr = TX_NULL;
+ _tx_timer_created_count = ((ULONG) 0);
+
+ module_a -> txm_module_instance_object_list_count = ((ULONG) 0);
+ module_a -> txm_module_instance_object_list_head = TX_NULL;
+ module_b -> txm_module_instance_object_list_count = ((ULONG) 0);
+ module_b -> txm_module_instance_object_list_head = TX_NULL;
+}
+
+
+/* Allocate object memory for a module through the real manager path. */
+static VOID *test_allocate(TXM_MODULE_INSTANCE *module_instance, ULONG object_size)
+{
+
+VOID *object_ptr;
+UINT status;
+
+
+ object_ptr = TX_NULL;
+ status = _txm_module_manager_object_allocate(&object_ptr, object_size, module_instance);
+
+ if (status != TX_SUCCESS)
+ {
+ printf("FAIL: the modelled pool refused an allocation of %lu bytes (status %u)\n",
+ (unsigned long) object_size, status);
+ test_failures++;
+
+ return(TX_NULL);
+ }
+
+ return(object_ptr);
+}
+
+
+/* Allocate and create one object of a type, the way a module does. */
+static VOID *test_allocate_and_create(TXM_MODULE_INSTANCE *module_instance, const TEST_OBJECT_KIND *kind)
+{
+
+VOID *object_ptr;
+
+
+ object_ptr = test_allocate(module_instance, kind -> test_kind_size);
+
+ if (object_ptr != TX_NULL)
+ {
+ test_object_create(object_ptr, kind -> test_kind_type);
+ }
+
+ return(object_ptr);
+}
+
+
+/* Ask the manager to authenticate an address, and report what it said. */
+static ULONG test_authenticate(TXM_MODULE_INSTANCE *module_instance, VOID *object_ptr, UINT object_type)
+{
+
+ULONG accepted;
+
+
+ test_interrupt_disable_depth = 0U;
+ test_interrupt_disable_max_depth = 0U;
+ test_interrupt_restore_underflows = 0U;
+
+ accepted = (ULONG) _txm_module_manager_param_check_typed_object_for_use(module_instance,
+ (ALIGN_TYPE) object_ptr,
+ object_type);
+
+ /* Whatever it decided, it must have left the interrupt lock as it found it. */
+ if ((test_interrupt_disable_depth != 0U) || (test_interrupt_restore_underflows != 0U))
+ {
+ printf("FAIL: authentication left the interrupt lock unbalanced (depth %u, underflows %u)\n",
+ test_interrupt_disable_depth, test_interrupt_restore_underflows);
+ test_failures++;
+ }
+
+ return(accepted);
+}
+
+
+/* Plant a value at an offset into an object, the way a module reaches interior
+ words of its own privileged allocations through ordinary create and set
+ services, and report what was there before. */
+static ULONG test_plant(VOID *object_ptr, ULONG offset, ULONG value)
+{
+
+ULONG *word_ptr;
+ULONG previous;
+
+
+ word_ptr = (ULONG *) (VOID *) (((UCHAR *) object_ptr) + offset);
+ previous = *word_ptr;
+ *word_ptr = value;
+
+ return(previous);
+}
+
+
+int main(void)
+{
+
+TXM_MODULE_INSTANCE module_a;
+TXM_MODULE_INSTANCE module_b;
+TXM_MODULE_INSTANCE *owner;
+const TEST_OBJECT_KIND *kind;
+const TEST_OBJECT_KIND *other_kind;
+VOID *object;
+VOID *other_object;
+VOID *raw_object;
+VOID *shifted;
+UCHAR *shifted_bytes;
+ULONG offset;
+ULONG interior_offsets;
+ULONG accepted_interiors;
+ULONG index;
+ULONG other_index;
+ULONG saved;
+UINT status;
+char description[128];
+
+
+ /* Report expectations before anything can crash, so that a case that brings
+ the process down is still preceded by everything that passed. */
+ setvbuf(stdout, TX_NULL, _IONBF, 0);
+
+ test_failures = 0U;
+ test_checks = ((ULONG) 0);
+
+ /* Set up two memory-protected modules. Module A does the asking throughout;
+ module B is there to own objects A did not allocate. */
+ module_a.txm_module_instance_property_flags = TXM_MODULE_MEMORY_PROTECTION;
+ module_a.txm_module_instance_code_start = (VOID *) test_module_code.test_code_bytes;
+ module_a.txm_module_instance_code_end = (VOID *) &test_module_code.test_code_bytes[TEST_MODULE_MEMORY_BYTES - 1U];
+ module_a.txm_module_instance_data_start = (VOID *) test_module_data.test_data_bytes;
+ module_a.txm_module_instance_data_end = (VOID *) &test_module_data.test_data_bytes[TEST_MODULE_MEMORY_BYTES - 1U];
+
+ module_b = module_a;
+
+ _tx_thread_current_ptr = TX_NULL;
+
+ /**********************************************************************/
+ /* The reported attack: an address shifted into a legitimate object. */
+ /**********************************************************************/
+
+ /* A module allocates and creates a timer of its own, entirely legitimately,
+ and then presents addresses inside it as though they were a thread. The
+ word at each of those addresses is set to TX_THREAD_ID first, which is what
+ the reported chain arranges through ordinary create and set services, so
+ that every address offered is one that an ID test on its own would accept. */
+ test_reset(&module_a, &module_b);
+
+ kind = &test_object_kinds[6];
+ object = test_allocate(&module_a, (ULONG) sizeof(TX_THREAD));
+ test_object_create(object, TXM_TIMER_OBJECT);
+
+ test_expect("a timer's own address is not accepted as a thread",
+ test_authenticate(&module_a, object, TXM_THREAD_OBJECT), (ULONG) TX_FALSE);
+
+ interior_offsets = ((ULONG) 0);
+ accepted_interiors = ((ULONG) 0);
+
+ for (offset = (ULONG) sizeof(ULONG); offset < (ULONG) sizeof(TX_THREAD); offset = offset + ((ULONG) sizeof(ULONG)))
+ {
+ shifted_bytes = ((UCHAR *) object) + offset;
+ shifted = (VOID *) shifted_bytes;
+
+ saved = test_plant(object, offset, TX_THREAD_ID);
+
+ interior_offsets++;
+
+ if (test_authenticate(&module_a, shifted, TXM_THREAD_OBJECT) == (ULONG) TX_TRUE)
+ {
+ accepted_interiors++;
+ }
+
+ (VOID) test_plant(object, offset, saved);
+ }
+
+ test_expect("every aligned interior offset of a legitimate object was offered",
+ (ULONG) (interior_offsets > ((ULONG) 0)), (ULONG) TX_TRUE);
+ test_expect("no aligned interior address carrying a planted thread ID is accepted as a thread",
+ accepted_interiors, ((ULONG) 0));
+
+ /* The unaligned offsets the report names, and the address one past the end. */
+ (VOID) test_plant(object, ((ULONG) 0), TX_THREAD_ID);
+
+ shifted = (VOID *) (((UCHAR *) object) + 1);
+ test_expect("an address one byte into a legitimate object is not accepted",
+ test_authenticate(&module_a, shifted, TXM_THREAD_OBJECT), (ULONG) TX_FALSE);
+
+ shifted = (VOID *) (((UCHAR *) object) + 4);
+ test_expect("an address four bytes into a legitimate object is not accepted",
+ test_authenticate(&module_a, shifted, TXM_THREAD_OBJECT), (ULONG) TX_FALSE);
+
+ shifted = (VOID *) (((UCHAR *) object) + (ULONG) sizeof(TX_THREAD));
+ test_expect("the address one past a legitimate object is not accepted",
+ test_authenticate(&module_a, shifted, TXM_THREAD_OBJECT), (ULONG) TX_FALSE);
+
+ /**********************************************************************/
+ /* Exact addresses of live objects, for every type the manager knows. */
+ /**********************************************************************/
+
+ for (index = ((ULONG) 0); index < (ULONG) TEST_OBJECT_KIND_COUNT; index++)
+ {
+ kind = &test_object_kinds[index];
+
+ test_reset(&module_a, &module_b);
+
+ object = test_allocate_and_create(&module_a, kind);
+
+ (void) snprintf(description, sizeof(description),
+ "the exact address of a live %s is accepted as one", kind -> test_kind_name);
+ test_expect(description, test_authenticate(&module_a, object, kind -> test_kind_type), (ULONG) TX_TRUE);
+
+ /* Every other type must refuse it, whatever its ID has been made to say. */
+ for (other_index = ((ULONG) 0); other_index < (ULONG) TEST_OBJECT_KIND_COUNT; other_index++)
+ {
+ if (other_index == index)
+ {
+ continue;
+ }
+
+ other_kind = &test_object_kinds[other_index];
+
+ saved = test_plant(object, ((ULONG) 0), other_kind -> test_kind_id);
+
+ (void) snprintf(description, sizeof(description),
+ "a %s carrying a %s ID is not accepted as a %s",
+ kind -> test_kind_name, other_kind -> test_kind_name, other_kind -> test_kind_name);
+ test_expect(description, test_authenticate(&module_a, object, other_kind -> test_kind_type), (ULONG) TX_FALSE);
+
+ (VOID) test_plant(object, ((ULONG) 0), saved);
+ }
+
+ /* A raw allocation of exactly the right size, never created. */
+ raw_object = test_allocate(&module_a, kind -> test_kind_size);
+
+ (void) snprintf(description, sizeof(description),
+ "an uncreated allocation the size of a %s is not accepted as one", kind -> test_kind_name);
+ test_expect(description, test_authenticate(&module_a, raw_object, kind -> test_kind_type), (ULONG) TX_FALSE);
+
+ /* Deleting the object takes it off the created list and clears its ID. */
+ test_object_delete(object, kind -> test_kind_type);
+
+ (void) snprintf(description, sizeof(description),
+ "a deleted %s is not accepted as one", kind -> test_kind_name);
+ test_expect(description, test_authenticate(&module_a, object, kind -> test_kind_type), (ULONG) TX_FALSE);
+ }
+
+ /**********************************************************************/
+ /* Addresses that have been freed, and addresses that have come back. */
+ /**********************************************************************/
+
+ test_reset(&module_a, &module_b);
+
+ kind = &test_object_kinds[4];
+ object = test_allocate_and_create(&module_a, kind);
+
+ test_expect("a live queue is accepted before anything is given back",
+ test_authenticate(&module_a, object, TXM_QUEUE_OBJECT), (ULONG) TX_TRUE);
+
+ /* Memory holding a live object is not given back at all. The object stays
+ created, the allocation stays owned, and the address stays authentic, so
+ the state this once had to defend against -- an object on the created list
+ whose memory the manager no longer owns -- cannot be reached. */
+ _tx_thread_current_ptr = (TX_THREAD *) test_allocate(&module_a, (ULONG) sizeof(TX_THREAD));
+ _tx_thread_current_ptr -> tx_thread_module_instance_ptr = &module_a;
+
+ status = _txm_module_manager_object_deallocate(object);
+ test_expect("memory holding a live object is not given back", (ULONG) status, (ULONG) TX_DELETE_ERROR);
+
+ test_expect("and the live object is still accepted afterwards",
+ test_authenticate(&module_a, object, TXM_QUEUE_OBJECT), (ULONG) TX_TRUE);
+
+ /* Deleted first, the same memory is given back, and the address comes round
+ again as a fresh, raw allocation. It is once again an exact allocation start
+ of the right size, and the ID of the object that used to be there can be
+ written back into it by the module that now owns it, so the created list is
+ the only thing that refuses it. */
+ test_object_delete(object, kind -> test_kind_type);
+ test_release_size(kind -> test_kind_size);
+ status = _txm_module_manager_object_deallocate(object);
+ test_expect("a deleted object's memory is given back", (ULONG) status, (ULONG) TX_SUCCESS);
+
+ raw_object = test_allocate(&module_a, kind -> test_kind_size);
+ test_expect("the freed address is handed out again",
+ (ULONG) (raw_object == object), (ULONG) TX_TRUE);
+ (VOID) test_plant(raw_object, ((ULONG) 0), kind -> test_kind_id);
+ test_expect("and is not accepted as the object that used to be there",
+ test_authenticate(&module_a, raw_object, TXM_QUEUE_OBJECT), (ULONG) TX_FALSE);
+
+ _tx_thread_current_ptr = TX_NULL;
+
+ /**********************************************************************/
+ /* The control block ID in memory the manager gives back. */
+ /**********************************************************************/
+
+ /* Authentication reads the kernel's created list, so it refuses a recycled
+ address whatever the words in it say. The size-based check is the one that
+ reads the control block ID, and it is kept for dispatchers outside this
+ repository, so clearing the ID on the way out is what stops memory a module
+ wrote an ID into presenting that ID to whoever holds the address next. */
+
+ test_reset(&module_a, &module_b);
+
+ kind = &test_object_kinds[4];
+ raw_object = test_allocate(&module_a, kind -> test_kind_size);
+
+ _tx_thread_current_ptr = (TX_THREAD *) test_allocate(&module_a, (ULONG) sizeof(TX_THREAD));
+ _tx_thread_current_ptr -> tx_thread_module_instance_ptr = &module_a;
+
+ (VOID) test_plant(raw_object, ((ULONG) 0), kind -> test_kind_id);
+
+ test_expect("memory carrying a planted ID reads as an object of that type",
+ (ULONG) _txm_module_manager_object_id_check((ALIGN_TYPE) raw_object, TXM_QUEUE_OBJECT),
+ (ULONG) TX_TRUE);
+
+ test_release_size(kind -> test_kind_size);
+ status = _txm_module_manager_object_deallocate(raw_object);
+ test_expect("the allocation is given back", (ULONG) status, (ULONG) TX_SUCCESS);
+
+ object = test_allocate(&module_a, kind -> test_kind_size);
+ test_expect("the address comes round again",
+ (ULONG) (object == raw_object), (ULONG) TX_TRUE);
+
+ test_expect("and no longer reads as an object of that type",
+ (ULONG) _txm_module_manager_object_id_check((ALIGN_TYPE) object, TXM_QUEUE_OBJECT),
+ (ULONG) TX_FALSE);
+
+ _tx_thread_current_ptr = TX_NULL;
+
+ /**********************************************************************/
+ /* Objects the application owns, and objects nobody owns. */
+ /**********************************************************************/
+
+ test_reset(&module_a, &module_b);
+
+ /* An application-owned object lives outside the pool, so the manager has no
+ allocation record of it. The kernel created list is the record that stands
+ in its place, and it is what makes sharing such an object with a module
+ possible at all. */
+ test_object_create(&test_host_queue, TXM_QUEUE_OBJECT);
+ test_object_create(&test_host_thread, TXM_THREAD_OBJECT);
+
+ test_expect("an application-owned queue on the created list is accepted",
+ test_authenticate(&module_a, &test_host_queue, TXM_QUEUE_OBJECT), (ULONG) TX_TRUE);
+ test_expect("an application-owned thread on the created list is accepted",
+ test_authenticate(&module_a, &test_host_thread, TXM_THREAD_OBJECT), (ULONG) TX_TRUE);
+ test_expect("but not as the wrong type",
+ test_authenticate(&module_a, &test_host_queue, TXM_THREAD_OBJECT), (ULONG) TX_FALSE);
+
+ /* Memory outside the pool that carries the right ID but is on no created
+ list. Without the created list this would be indistinguishable from a real
+ object, and every word of privileged memory that happened to hold an ID
+ would be usable as one. */
+ test_host_stranger.tx_mutex_id = TX_MUTEX_ID;
+ test_expect("memory carrying a mutex ID but on no created list is refused",
+ test_authenticate(&module_a, &test_host_stranger, TXM_MUTEX_OBJECT), (ULONG) TX_FALSE);
+
+ test_object_delete(&test_host_queue, TXM_QUEUE_OBJECT);
+ test_expect("a deleted application-owned queue is refused",
+ test_authenticate(&module_a, &test_host_queue, TXM_QUEUE_OBJECT), (ULONG) TX_FALSE);
+
+ test_object_delete(&test_host_thread, TXM_THREAD_OBJECT);
+
+ /**********************************************************************/
+ /* Objects another module allocated. */
+ /**********************************************************************/
+
+ test_reset(&module_a, &module_b);
+
+ kind = &test_object_kinds[5];
+ object = test_allocate_and_create(&module_b, kind);
+
+ /* A created object belonging to another module is authentic, and is accepted
+ as such: it is the exact address of a live semaphore. Whether module A is
+ *authorised* to operate on module B's semaphore is a separate question
+ about ownership, which this check does not answer and does not claim to. */
+ test_expect("another module's live semaphore is authentic",
+ test_authenticate(&module_a, object, TXM_SEMAPHORE_OBJECT), (ULONG) TX_TRUE);
+
+ /* An allocation another module has not created is not an object at all, and
+ neither module can use it as one. */
+ raw_object = test_allocate(&module_b, kind -> test_kind_size);
+ test_expect("another module's uncreated allocation is not an object",
+ test_authenticate(&module_a, raw_object, TXM_SEMAPHORE_OBJECT), (ULONG) TX_FALSE);
+ test_expect("nor is it one to the module that allocated it",
+ test_authenticate(&module_b, raw_object, TXM_SEMAPHORE_OBJECT), (ULONG) TX_FALSE);
+
+ /* An interior address of another module's object is refused the same way an
+ interior address of the caller's own object is. */
+ shifted = (VOID *) (((UCHAR *) object) + (ULONG) sizeof(ULONG));
+ (VOID) test_plant(object, (ULONG) sizeof(ULONG), TX_SEMAPHORE_ID);
+ test_expect("an interior address of another module's object is refused",
+ test_authenticate(&module_a, shifted, TXM_SEMAPHORE_OBJECT), (ULONG) TX_FALSE);
+
+ /**********************************************************************/
+ /* Addresses and types that are not objects at all. */
+ /**********************************************************************/
+
+ test_reset(&module_a, &module_b);
+
+ object = test_allocate_and_create(&module_a, &test_object_kinds[6]);
+
+ test_expect("a null address is refused",
+ test_authenticate(&module_a, TX_NULL, TXM_THREAD_OBJECT), (ULONG) TX_FALSE);
+ test_expect("an object type the manager does not know is refused",
+ test_authenticate(&module_a, object, 0U), (ULONG) TX_FALSE);
+ test_expect("and so is one past the types it does know",
+ test_authenticate(&module_a, object, TXM_TIMER_OBJECT + 1U), (ULONG) TX_FALSE);
+
+ /* A control block inside the calling module's own memory is one the module can
+ write behind the kernel's back, so it is refused however genuine it looks. */
+ owner = &module_a;
+ test_object_create(test_module_data.test_data_bytes, TXM_MUTEX_OBJECT);
+ test_expect("a mutex in the module's own data is refused",
+ test_authenticate(owner, test_module_data.test_data_bytes, TXM_MUTEX_OBJECT), (ULONG) TX_FALSE);
+
+ test_object_create(test_module_code.test_code_bytes, TXM_SEMAPHORE_OBJECT);
+ test_expect("a semaphore in the module's own code is refused",
+ test_authenticate(owner, test_module_code.test_code_bytes, TXM_SEMAPHORE_OBJECT), (ULONG) TX_FALSE);
+
+ /**********************************************************************/
+ /* The bound on every scan. */
+ /**********************************************************************/
+
+ test_reset(&module_a, &module_b);
+
+ object = test_allocate_and_create(&module_a, &test_object_kinds[6]);
+
+ /* A count that claims more objects than the list holds must not make either
+ scan walk past the end of it. The chain the model builds ends in a self
+ link, so a scan that ignored the count would spin rather than crash; the
+ count is what stops it. */
+ module_a.txm_module_instance_object_list_count = ((ULONG) 64);
+ _tx_thread_created_count = ((ULONG) 64);
+
+ test_expect("a live thread is still accepted when the counts overstate the lists",
+ test_authenticate(&module_a, object, TXM_THREAD_OBJECT), (ULONG) TX_TRUE);
+
+ other_object = (VOID *) &test_host_stranger;
+ test_expect("and an address on neither list is still refused",
+ test_authenticate(&module_a, other_object, TXM_THREAD_OBJECT), (ULONG) TX_FALSE);
+
+ /* An empty allocation list with a head that has not been cleared must not be
+ followed either. */
+ module_a.txm_module_instance_object_list_count = ((ULONG) 0);
+ _tx_thread_created_count = ((ULONG) 1);
+
+ test_expect("a live thread is accepted with an empty allocation list",
+ test_authenticate(&module_a, object, TXM_THREAD_OBJECT), (ULONG) TX_TRUE);
+
+ _tx_thread_created_count = ((ULONG) 0);
+ test_expect("and refused once the created list is empty too",
+ test_authenticate(&module_a, object, TXM_THREAD_OBJECT), (ULONG) TX_FALSE);
+
+ /**********************************************************************/
+ /* The size-based check kept for dispatchers outside this repository. */
+ /**********************************************************************/
+
+ test_reset(&module_a, &module_b);
+
+ kind = &test_object_kinds[3];
+ object = test_allocate_and_create(&module_a, kind);
+
+ test_expect("the size-based check accepts an exact allocation of that size",
+ (ULONG) _txm_module_manager_param_check_object_for_use(&module_a, (ALIGN_TYPE) object,
+ kind -> test_kind_size),
+ (ULONG) TX_TRUE);
+
+ test_expect("and refuses an allocation of a different size",
+ (ULONG) _txm_module_manager_param_check_object_for_use(&module_a, (ALIGN_TYPE) object,
+ kind -> test_kind_size + ((ULONG) 4)),
+ (ULONG) TX_FALSE);
+
+ /* The interior address the reported attack is built on is what this check has
+ to refuse, and it does, which is what makes hardening it worthwhile even
+ though it cannot be given a type. */
+ interior_offsets = ((ULONG) 0);
+ accepted_interiors = ((ULONG) 0);
+
+ for (offset = (ULONG) sizeof(ULONG); offset < kind -> test_kind_size; offset = offset + ((ULONG) sizeof(ULONG)))
+ {
+ shifted = (VOID *) (((UCHAR *) object) + offset);
+
+ saved = test_plant(object, offset, kind -> test_kind_id);
+
+ interior_offsets++;
+
+ if (_txm_module_manager_param_check_object_for_use(&module_a, (ALIGN_TYPE) shifted,
+ kind -> test_kind_size) == TX_TRUE)
+ {
+ accepted_interiors++;
+ }
+
+ (VOID) test_plant(object, offset, saved);
+ }
+
+ test_expect("the size-based check was offered every aligned interior offset",
+ (ULONG) (interior_offsets > ((ULONG) 0)), (ULONG) TX_TRUE);
+ test_expect("and accepted none of them",
+ accepted_interiors, ((ULONG) 0));
+
+ /**********************************************************************/
+ /* The disclosed sink: resetting a thread that is not a module thread. */
+ /**********************************************************************/
+
+ test_reset(&module_a, &module_b);
+
+ /* A thread the application created carries no module instance. Reset reads
+ the shell entry function out of that instance to rebuild the thread's
+ stack, so a null one would be followed in privileged mode. Such a thread is
+ authentic and can be found by name, and one that has run to completion
+ passes reset's own state test, so refusing it has to be reset's own job. */
+ _tx_thread_current_ptr = &test_host_thread;
+
+ object = test_allocate(&module_a, (ULONG) sizeof(TX_THREAD));
+ test_object_create(object, TXM_THREAD_OBJECT);
+
+ ((TX_THREAD *) object) -> tx_thread_state = TX_COMPLETED;
+ ((TX_THREAD *) object) -> tx_thread_module_instance_ptr = TX_NULL;
+
+ test_stack_builds = ((ULONG) 0);
+ status = _txm_module_manager_thread_reset((TX_THREAD *) object);
+
+ test_expect("resetting a completed thread with no module instance is refused",
+ (ULONG) status, (ULONG) TX_NOT_DONE);
+ test_expect("...without building a stack frame", test_stack_builds, ((ULONG) 0));
+ test_expect("...and without moving it out of the completed state",
+ (ULONG) ((TX_THREAD *) object) -> tx_thread_state, (ULONG) TX_COMPLETED);
+
+ /* A module thread in the same state is reset, so the guard has not broken the
+ operation it protects. */
+ ((TX_THREAD *) object) -> tx_thread_module_instance_ptr = &module_a;
+ module_a.txm_module_instance_shell_entry_function = TX_NULL;
+
+ test_stack_builds = ((ULONG) 0);
+ status = _txm_module_manager_thread_reset((TX_THREAD *) object);
+
+ test_expect("a completed module thread is reset", (ULONG) status, (ULONG) TX_SUCCESS);
+ test_expect("...building its stack frame once", test_stack_builds, ((ULONG) 1));
+ test_expect("...and leaving it suspended",
+ (ULONG) ((TX_THREAD *) object) -> tx_thread_state, (ULONG) TX_SUSPENDED);
+
+ _tx_thread_current_ptr = TX_NULL;
+
+ /**********************************************************************/
+ /* What the whole run has to have held. */
+ /**********************************************************************/
+
+ /**********************************************************************/
+ /* Why a size cannot stand in for a type. */
+ /**********************************************************************/
+
+ /* Authentication establishes an object's type from the kernel's created list
+ for that type, rather than from the size of its control block, because a size
+ does not identify one. Distinct control blocks share a size: on a 32-bit
+ target a byte pool, a mutex and a timer are all the same size as each other,
+ and a block pool is the same size as an event flags group.
+
+ This is asserted rather than left as a remark so that the reasoning is checked
+ against the structures rather than remembered. It holds while any two distinct
+ types collide, so adding a field to one of them does not make it fail; it
+ fails only if every type becomes distinguishable by size, which is when the
+ remark above would need revisiting. */
+ test_expect("distinct object types share a control block size",
+ (ULONG) ((sizeof(TX_BYTE_POOL) == sizeof(TX_MUTEX)) ||
+ (sizeof(TX_BYTE_POOL) == sizeof(TX_TIMER)) ||
+ (sizeof(TX_MUTEX) == sizeof(TX_TIMER)) ||
+ (sizeof(TX_BLOCK_POOL) == sizeof(TX_EVENT_FLAGS_GROUP))),
+ (ULONG) TX_TRUE);
+
+ test_expect("every case ran", (ULONG) (test_checks > ((ULONG) 100)), (ULONG) TX_TRUE);
+
+ if (test_failures == 0U)
+ {
+ printf("SUCCESS! %lu expectations\n", (unsigned long) test_checks);
+ return(0);
+ }
+
+ printf("ERROR: %u expectation(s) failed of %lu\n", test_failures, (unsigned long) test_checks);
+ return(1);
+}
diff --git a/test/tx/module_manager/threadx_module_manager_object_name_range_test.c b/test/tx/module_manager/threadx_module_manager_object_name_range_test.c
new file mode 100644
index 00000000..7e6ee5e5
--- /dev/null
+++ b/test/tx/module_manager/threadx_module_manager_object_name_range_test.c
@@ -0,0 +1,1608 @@
+/***************************************************************************
+ * 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
+ **************************************************************************/
+
+
+/**************************************************************************/
+/**************************************************************************/
+/** */
+/** ThreadX Test */
+/** */
+/** Module Manager object name range validation */
+/** */
+/**************************************************************************/
+/**************************************************************************/
+
+/* A memory-protected module asks the Module Manager to find a kernel object by name.
+ The dispatch layer decides how much of the module's name buffer the privileged
+ comparison may touch. It checked one byte.
+
+ _txm_module_manager_object_name_compare reads a character from each name at the top
+ of every iteration and tests the remaining search length at the bottom, so a declared
+ length N permits reads at offsets 0 through N -- N+1 bytes, the extra one being the
+ terminator a length excludes. The extended service receives that length in its extra
+ parameter array, validates the array correctly, and then never uses the length to
+ bound the buffer it describes. The deprecated service carries no length at all: its
+ manager wrapper calls the same implementation with the largest value a UINT can hold,
+ so it removes the bound rather than lacking one.
+
+ The comparison stops at the first differing character, so the walk past the end of
+ the module's memory is not free -- unless the module arranges for it to match. It can:
+ a create service stores the name pointer a module supplies in the control block
+ rather than copying the string, so a module can register an object whose name is the
+ very buffer it then searches for. Both sides of the comparison are then the same
+ address, every character matches by construction, and the walk continues until it
+ meets a terminator in memory the module does not own and cannot see.
+
+ This test drives both lookup dispatchers directly and measures two things rather than
+ sampling either side of a boundary, because there are two dials here and the finding
+ is about the relationship between them:
+
+ - the extent the dispatcher validates, found by anchoring the name buffer to the end
+ of one of the module's regions and walking it backwards a byte at a time until the
+ request is accepted; and
+
+ - the extent the comparison reads, found by filling memory with a filler character,
+ planting the only terminator at a chosen depth from the buffer, and asking for an
+ object whose name is exactly the bytes up to it. The lookup returns that object
+ only if the comparison read that deep, so the deepest depth it can be made to
+ return is the read extent.
+
+ The assertion is the relationship: the comparison never reads further than the
+ dispatcher validated, and the dispatcher validates the length the module declared plus
+ its terminator. Before the fix the read extent follows the declared length and the
+ validated extent stays at one byte, which is the defect stated as a measurement.
+
+ Two mechanics come from the queue message range test, which was the first test in the
+ tree to drive a dispatcher, and both are the reason this one is possible.
+
+ The dispatch table is a header of static functions, one per kernel service, each
+ wrapped in #ifndef TXM_<SERVICE>_CALL_NOT_USED. There are 96 of those guards. Defining
+ the 94 this test does not exercise compiles the header down to the two lookup services,
+ which keeps the link to the manager sources under test and a handful of kernel globals
+ instead of the hundred-odd kernel entry points the whole table reaches.
+
+ The extent has to reach a check that honours it, so this test compiles against a
+ Cortex-M module port's headers, whose inline data check is the shape the memory
+ protected module ports share. The Cortex-A7 port's data check takes the pointer alone,
+ so a test built on it would pass equally with and without this fix.
+
+ Unlike the queue test, this one cannot stub the service behind the dispatcher: the
+ comparison being measured is the service. It links the real lookup implementation and
+ the real comparison, and stands up the kernel created lists they walk. */
+
+#include <stdio.h>
+#include <string.h>
+
+#include "threadx_module_manager_host_test_port.h"
+
+#include "tx_thread.h"
+#include "tx_timer.h"
+#include "tx_queue.h"
+#include "tx_event_flags.h"
+#include "tx_semaphore.h"
+#include "tx_mutex.h"
+#include "tx_block_pool.h"
+#include "tx_byte_pool.h"
+#include "txm_module.h"
+#include "txm_module_manager_util.h"
+
+
+/* Refuse every service in the dispatch table except the two under test. The list is the
+ header's own guard names; a service added to the table appears here as a link error
+ naming the kernel entry point it reaches, which is the right way to find out. */
+
+#define TXM_BLOCK_ALLOCATE_CALL_NOT_USED
+#define TXM_BLOCK_POOL_CREATE_CALL_NOT_USED
+#define TXM_BLOCK_POOL_DELETE_CALL_NOT_USED
+#define TXM_BLOCK_POOL_INFO_GET_CALL_NOT_USED
+#define TXM_BLOCK_POOL_PERFORMANCE_INFO_GET_CALL_NOT_USED
+#define TXM_BLOCK_POOL_PERFORMANCE_SYSTEM_INFO_GET_CALL_NOT_USED
+#define TXM_BLOCK_POOL_PRIORITIZE_CALL_NOT_USED
+#define TXM_BLOCK_RELEASE_CALL_NOT_USED
+#define TXM_BYTE_ALLOCATE_CALL_NOT_USED
+#define TXM_BYTE_POOL_CREATE_CALL_NOT_USED
+#define TXM_BYTE_POOL_DELETE_CALL_NOT_USED
+#define TXM_BYTE_POOL_INFO_GET_CALL_NOT_USED
+#define TXM_BYTE_POOL_PERFORMANCE_INFO_GET_CALL_NOT_USED
+#define TXM_BYTE_POOL_PERFORMANCE_SYSTEM_INFO_GET_CALL_NOT_USED
+#define TXM_BYTE_POOL_PRIORITIZE_CALL_NOT_USED
+#define TXM_BYTE_RELEASE_CALL_NOT_USED
+#define TXM_EVENT_FLAGS_CREATE_CALL_NOT_USED
+#define TXM_EVENT_FLAGS_DELETE_CALL_NOT_USED
+#define TXM_EVENT_FLAGS_GET_CALL_NOT_USED
+#define TXM_EVENT_FLAGS_INFO_GET_CALL_NOT_USED
+#define TXM_EVENT_FLAGS_PERFORMANCE_INFO_GET_CALL_NOT_USED
+#define TXM_EVENT_FLAGS_PERFORMANCE_SYSTEM_INFO_GET_CALL_NOT_USED
+#define TXM_EVENT_FLAGS_SET_CALL_NOT_USED
+#define TXM_EVENT_FLAGS_SET_NOTIFY_CALL_NOT_USED
+#define TXM_MUTEX_CREATE_CALL_NOT_USED
+#define TXM_MUTEX_DELETE_CALL_NOT_USED
+#define TXM_MUTEX_GET_CALL_NOT_USED
+#define TXM_MUTEX_INFO_GET_CALL_NOT_USED
+#define TXM_MUTEX_PERFORMANCE_INFO_GET_CALL_NOT_USED
+#define TXM_MUTEX_PERFORMANCE_SYSTEM_INFO_GET_CALL_NOT_USED
+#define TXM_MUTEX_PRIORITIZE_CALL_NOT_USED
+#define TXM_MUTEX_PUT_CALL_NOT_USED
+#define TXM_QUEUE_CREATE_CALL_NOT_USED
+#define TXM_QUEUE_DELETE_CALL_NOT_USED
+#define TXM_QUEUE_FLUSH_CALL_NOT_USED
+#define TXM_QUEUE_FRONT_SEND_CALL_NOT_USED
+#define TXM_QUEUE_INFO_GET_CALL_NOT_USED
+#define TXM_QUEUE_PERFORMANCE_INFO_GET_CALL_NOT_USED
+#define TXM_QUEUE_PERFORMANCE_SYSTEM_INFO_GET_CALL_NOT_USED
+#define TXM_QUEUE_PRIORITIZE_CALL_NOT_USED
+#define TXM_QUEUE_RECEIVE_CALL_NOT_USED
+#define TXM_QUEUE_SEND_CALL_NOT_USED
+#define TXM_QUEUE_SEND_NOTIFY_CALL_NOT_USED
+#define TXM_SEMAPHORE_CEILING_PUT_CALL_NOT_USED
+#define TXM_SEMAPHORE_CREATE_CALL_NOT_USED
+#define TXM_SEMAPHORE_DELETE_CALL_NOT_USED
+#define TXM_SEMAPHORE_GET_CALL_NOT_USED
+#define TXM_SEMAPHORE_INFO_GET_CALL_NOT_USED
+#define TXM_SEMAPHORE_PERFORMANCE_INFO_GET_CALL_NOT_USED
+#define TXM_SEMAPHORE_PERFORMANCE_SYSTEM_INFO_GET_CALL_NOT_USED
+#define TXM_SEMAPHORE_PRIORITIZE_CALL_NOT_USED
+#define TXM_SEMAPHORE_PUT_CALL_NOT_USED
+#define TXM_SEMAPHORE_PUT_NOTIFY_CALL_NOT_USED
+#define TXM_THREAD_CREATE_CALL_NOT_USED
+#define TXM_THREAD_DELETE_CALL_NOT_USED
+#define TXM_THREAD_ENTRY_EXIT_NOTIFY_CALL_NOT_USED
+#define TXM_THREAD_IDENTIFY_CALL_NOT_USED
+#define TXM_THREAD_INFO_GET_CALL_NOT_USED
+#define TXM_THREAD_INTERRUPT_CONTROL_CALL_NOT_USED
+#define TXM_THREAD_PERFORMANCE_INFO_GET_CALL_NOT_USED
+#define TXM_THREAD_PERFORMANCE_SYSTEM_INFO_GET_CALL_NOT_USED
+#define TXM_THREAD_PREEMPTION_CHANGE_CALL_NOT_USED
+#define TXM_THREAD_PRIORITY_CHANGE_CALL_NOT_USED
+#define TXM_THREAD_RELINQUISH_CALL_NOT_USED
+#define TXM_THREAD_RESET_CALL_NOT_USED
+#define TXM_THREAD_RESUME_CALL_NOT_USED
+#define TXM_THREAD_SLEEP_CALL_NOT_USED
+#define TXM_THREAD_STACK_ERROR_NOTIFY_CALL_NOT_USED
+#define TXM_THREAD_SUSPEND_CALL_NOT_USED
+#define TXM_THREAD_SYSTEM_SUSPEND_CALL_NOT_USED
+#define TXM_THREAD_TERMINATE_CALL_NOT_USED
+#define TXM_THREAD_TIME_SLICE_CHANGE_CALL_NOT_USED
+#define TXM_THREAD_WAIT_ABORT_CALL_NOT_USED
+#define TXM_TIME_GET_CALL_NOT_USED
+#define TXM_TIME_SET_CALL_NOT_USED
+#define TXM_TIMER_ACTIVATE_CALL_NOT_USED
+#define TXM_TIMER_CHANGE_CALL_NOT_USED
+#define TXM_TIMER_CREATE_CALL_NOT_USED
+#define TXM_TIMER_DEACTIVATE_CALL_NOT_USED
+#define TXM_TIMER_DELETE_CALL_NOT_USED
+#define TXM_TIMER_INFO_GET_CALL_NOT_USED
+#define TXM_TIMER_PERFORMANCE_INFO_GET_CALL_NOT_USED
+#define TXM_TIMER_PERFORMANCE_SYSTEM_INFO_GET_CALL_NOT_USED
+#define TXM_TRACE_BUFFER_FULL_NOTIFY_CALL_NOT_USED
+#define TXM_TRACE_DISABLE_CALL_NOT_USED
+#define TXM_TRACE_ENABLE_CALL_NOT_USED
+#define TXM_TRACE_EVENT_FILTER_CALL_NOT_USED
+#define TXM_TRACE_EVENT_UNFILTER_CALL_NOT_USED
+#define TXM_TRACE_INTERRUPT_CONTROL_CALL_NOT_USED
+#define TXM_TRACE_ISR_ENTER_INSERT_CALL_NOT_USED
+#define TXM_TRACE_ISR_EXIT_INSERT_CALL_NOT_USED
+#define TXM_TRACE_USER_EVENT_INSERT_CALL_NOT_USED
+#define TXM_MODULE_OBJECT_ALLOCATE_CALL_NOT_USED
+#define TXM_MODULE_OBJECT_DEALLOCATE_CALL_NOT_USED
+
+#include "txm_module_manager_dispatch.h"
+
+/* The two under test have to have survived that, and the rest have to have gone. */
+
+#if defined(TXM_MODULE_OBJECT_POINTER_GET_CALL_NOT_USED) || defined(TXM_MODULE_OBJECT_POINTER_GET_EXTENDED_CALL_NOT_USED)
+#error "the lookup services under test must be compiled in"
+#endif
+
+#ifndef TXM_QUEUE_FLUSH_CALL_NOT_USED
+#error "the services this test does not exercise must be compiled out"
+#endif
+
+
+/* Define the stand-in interrupt lock the port shim counts through. */
+
+unsigned int test_interrupt_disable_depth;
+unsigned int test_interrupt_disable_max_depth;
+unsigned int test_interrupt_restore_underflows;
+
+
+/* Define the manager state the utility source under test links against. No case here
+ reaches the object pool: a lookup names no control block. */
+
+TX_BYTE_POOL _txm_module_manager_object_pool;
+UINT _txm_module_manager_object_pool_created;
+
+
+/* Define the kernel state the lookup reaches for. It walks a created list per object
+ type, holds off preemption while it does, and asks the current thread which module it
+ belongs to before it will search the two pool types. */
+
+TX_THREAD *_tx_thread_current_ptr;
+volatile UINT _tx_thread_preempt_disable;
+
+TX_BLOCK_POOL *_tx_block_pool_created_ptr;
+ULONG _tx_block_pool_created_count;
+TX_BYTE_POOL *_tx_byte_pool_created_ptr;
+ULONG _tx_byte_pool_created_count;
+TX_EVENT_FLAGS_GROUP *_tx_event_flags_created_ptr;
+ULONG _tx_event_flags_created_count;
+TX_MUTEX *_tx_mutex_created_ptr;
+ULONG _tx_mutex_created_count;
+TX_QUEUE *_tx_queue_created_ptr;
+ULONG _tx_queue_created_count;
+TX_SEMAPHORE *_tx_semaphore_created_ptr;
+ULONG _tx_semaphore_created_count;
+TX_THREAD *_tx_thread_created_ptr;
+ULONG _tx_thread_created_count;
+TX_TIMER *_tx_timer_created_ptr;
+ULONG _tx_timer_created_count;
+
+
+/* Count the lookup's final preemption check, and record what it left behind.
+
+ The lookup calls this once, on its way out, after restoring the preemption count it
+ raised on the way in. Counting it is how a case tells a request the dispatch layer
+ refused from one it let through: a refusal returns before the lookup is entered at
+ all. Recording the preemption count as the lookup leaves it is how the run as a whole
+ asserts that the hold is balanced. */
+
+static ULONG test_lookup_exits;
+static ULONG test_preempt_disable_on_exit;
+
+VOID _tx_thread_system_preempt_check(VOID)
+{
+ test_lookup_exits++;
+ test_preempt_disable_on_exit = (ULONG) _tx_thread_preempt_disable;
+}
+
+
+/* Stand in for the port primitive a manager source under test refers to but that no case
+ here exercises. */
+
+VOID _txm_module_manager_alignment_adjust(TXM_MODULE_PREAMBLE *module_preamble, ULONG *code_size,
+ ULONG *code_alignment, ULONG *data_size, ULONG *data_alignment)
+{
+ (VOID) module_preamble;
+ (VOID) code_size;
+ (VOID) code_alignment;
+ (VOID) data_size;
+ (VOID) data_alignment;
+}
+
+
+static UINT test_failures;
+static ULONG test_checks;
+
+
+/* Record the outcome of one expectation. */
+static VOID test_expect(const char *description, ULONG actual, ULONG expected)
+{
+ test_checks++;
+
+ if (actual != expected)
+ {
+ printf("FAIL: %s (expected %lu, got %lu)\n", description, (unsigned long) expected, (unsigned long) actual);
+ test_failures++;
+ }
+}
+
+
+/* Model the module's memory as one arena, so that the three regions a name buffer may
+ legitimately live in have known addresses and known gaps between them. The gaps are
+ memory the module may not reach: they are what a buffer anchored to the end of a
+ region overruns into, and they are inside the arena so that the overrun a case
+ describes is memory this test owns. */
+
+#define TEST_REGION_BYTES ((ULONG) 256)
+#define TEST_GAP_BYTES ((ULONG) 128)
+
+static union
+{
+ ALIGN_TYPE test_arena_alignment;
+ UCHAR test_arena_bytes[(((ULONG) 3) * TEST_REGION_BYTES) + (((ULONG) 4) * TEST_GAP_BYTES)];
+} test_arena;
+
+#define TEST_DATA_START (&test_arena.test_arena_bytes[TEST_GAP_BYTES])
+#define TEST_CODE_START (&test_arena.test_arena_bytes[(((ULONG) 2) * TEST_GAP_BYTES) + TEST_REGION_BYTES])
+#define TEST_SHARED_START (&test_arena.test_arena_bytes[(((ULONG) 3) * TEST_GAP_BYTES) + (((ULONG) 2) * TEST_REGION_BYTES)])
+
+
+/* The character memory is filled with. Any non-zero value will do: what matters is that
+ a case can plant the only terminator in memory where it wants one, and that the bytes
+ before it match a name the module chose. */
+
+#define TEST_FILLER ((UCHAR) 0x5A)
+
+
+/* The module's outgoing parameters live at the front of its data region, because the
+ extra parameter array must be inside module data and the result slot must be
+ writable by the module. Every name buffer is anchored to the end of a region, so the
+ two never meet. */
+
+#define TEST_EXTRA_PARAMETERS ((ALIGN_TYPE *) TEST_DATA_START)
+#define TEST_RESULT_SLOT ((VOID **) (TEST_DATA_START + (((ULONG) 2) * sizeof(ALIGN_TYPE))))
+
+
+/* Define the requesting modules. One has memory protection, and is the one every range
+ case below drives; the other does not, and is how the cases show that the dispatch
+ layer's checks are not reached for it at all. */
+
+static TXM_MODULE_INSTANCE test_protected_module;
+static TXM_MODULE_INSTANCE test_unprotected_module;
+
+
+/* Place a module's regions on the arena. */
+static VOID test_module_setup(TXM_MODULE_INSTANCE *module_instance, ULONG property_flags)
+{
+ memset((VOID *) module_instance, 0, sizeof(TXM_MODULE_INSTANCE));
+
+ module_instance -> txm_module_instance_property_flags = property_flags;
+
+ module_instance -> txm_module_instance_data_start = (VOID *) TEST_DATA_START;
+ module_instance -> txm_module_instance_data_end = (VOID *) (TEST_DATA_START + (TEST_REGION_BYTES - ((ULONG) 1)));
+
+ module_instance -> txm_module_instance_code_start = (VOID *) TEST_CODE_START;
+ module_instance -> txm_module_instance_code_end = (VOID *) (TEST_CODE_START + (TEST_REGION_BYTES - ((ULONG) 1)));
+
+ module_instance -> txm_module_instance_shared_memory_address = (ULONG) TEST_SHARED_START;
+ module_instance -> txm_module_instance_shared_memory_length = TEST_REGION_BYTES;
+}
+
+
+/* Define the thread the lookup asks which module is calling, and the objects the created
+ lists are built from. The objects are outside the arena, where a kernel object belongs. */
+
+static TX_THREAD test_current_thread;
+static TX_SEMAPHORE test_semaphore;
+static TX_SEMAPHORE test_decoy_semaphore;
+
+
+/* Define three control blocks of each type for the cases that search every list the
+ lookup knows: the match, and two ahead of it so that a list is walked rather than
+ looked at. */
+
+#define TEST_OBJECTS_PER_TYPE ((UINT) 3)
+
+static TX_THREAD test_thread_objects[TEST_OBJECTS_PER_TYPE];
+static TX_TIMER test_timer_objects[TEST_OBJECTS_PER_TYPE];
+static TX_QUEUE test_queue_objects[TEST_OBJECTS_PER_TYPE];
+static TX_EVENT_FLAGS_GROUP test_event_flags_objects[TEST_OBJECTS_PER_TYPE];
+static TX_SEMAPHORE test_semaphore_objects[TEST_OBJECTS_PER_TYPE];
+static TX_MUTEX test_mutex_objects[TEST_OBJECTS_PER_TYPE];
+static TX_BLOCK_POOL test_block_pool_objects[TEST_OBJECTS_PER_TYPE];
+static TX_BYTE_POOL test_byte_pool_objects[TEST_OBJECTS_PER_TYPE];
+
+
+/* Define the name a probing case gives an object. It is outside the arena too, because
+ an object's name is not the module's memory even when the module supplied the pointer;
+ the aliasing cases are the ones that make it so deliberately. */
+
+static CHAR test_probe_name[TEST_REGION_BYTES + TEST_GAP_BYTES];
+
+
+/* Put one object on the created list for its type, newest last, the way the kernel's
+ create service links a new object in ahead of the head of the ring. The name is stored
+ as the pointer the caller gave, because that is what a create service does with it and
+ it is the reason the aliasing cases below are possible at all.
+
+ Every list is built the same way and the eight blocks differ only in the type they
+ name, which is how the lookup itself is written. */
+static VOID test_object_create(UINT object_type, VOID *object_ptr, CHAR *name)
+{
+ switch (object_type)
+ {
+
+ case TXM_THREAD_OBJECT:
+ {
+ TX_THREAD *thread_ptr = (TX_THREAD *) object_ptr;
+ TX_THREAD *tail_ptr;
+
+ memset(object_ptr, 0, sizeof(TX_THREAD));
+ thread_ptr -> tx_thread_id = TX_THREAD_ID;
+ thread_ptr -> tx_thread_name = name;
+
+ if (_tx_thread_created_ptr == TX_NULL)
+ {
+ thread_ptr -> tx_thread_created_next = thread_ptr;
+ _tx_thread_created_ptr = thread_ptr;
+ }
+ else
+ {
+ tail_ptr = _tx_thread_created_ptr;
+ while (tail_ptr -> tx_thread_created_next != _tx_thread_created_ptr)
+ {
+ tail_ptr = tail_ptr -> tx_thread_created_next;
+ }
+ tail_ptr -> tx_thread_created_next = thread_ptr;
+ thread_ptr -> tx_thread_created_next = _tx_thread_created_ptr;
+ }
+ _tx_thread_created_count++;
+ break;
+ }
+
+ case TXM_TIMER_OBJECT:
+ {
+ TX_TIMER *timer_ptr = (TX_TIMER *) object_ptr;
+ TX_TIMER *tail_ptr;
+
+ memset(object_ptr, 0, sizeof(TX_TIMER));
+ timer_ptr -> tx_timer_id = TX_TIMER_ID;
+ timer_ptr -> tx_timer_name = name;
+
+ if (_tx_timer_created_ptr == TX_NULL)
+ {
+ timer_ptr -> tx_timer_created_next = timer_ptr;
+ _tx_timer_created_ptr = timer_ptr;
+ }
+ else
+ {
+ tail_ptr = _tx_timer_created_ptr;
+ while (tail_ptr -> tx_timer_created_next != _tx_timer_created_ptr)
+ {
+ tail_ptr = tail_ptr -> tx_timer_created_next;
+ }
+ tail_ptr -> tx_timer_created_next = timer_ptr;
+ timer_ptr -> tx_timer_created_next = _tx_timer_created_ptr;
+ }
+ _tx_timer_created_count++;
+ break;
+ }
+
+ case TXM_QUEUE_OBJECT:
+ {
+ TX_QUEUE *queue_ptr = (TX_QUEUE *) object_ptr;
+ TX_QUEUE *tail_ptr;
+
+ memset(object_ptr, 0, sizeof(TX_QUEUE));
+ queue_ptr -> tx_queue_id = TX_QUEUE_ID;
+ queue_ptr -> tx_queue_name = name;
+
+ if (_tx_queue_created_ptr == TX_NULL)
+ {
+ queue_ptr -> tx_queue_created_next = queue_ptr;
+ _tx_queue_created_ptr = queue_ptr;
+ }
+ else
+ {
+ tail_ptr = _tx_queue_created_ptr;
+ while (tail_ptr -> tx_queue_created_next != _tx_queue_created_ptr)
+ {
+ tail_ptr = tail_ptr -> tx_queue_created_next;
+ }
+ tail_ptr -> tx_queue_created_next = queue_ptr;
+ queue_ptr -> tx_queue_created_next = _tx_queue_created_ptr;
+ }
+ _tx_queue_created_count++;
+ break;
+ }
+
+ case TXM_EVENT_FLAGS_OBJECT:
+ {
+ TX_EVENT_FLAGS_GROUP *event_flags_ptr = (TX_EVENT_FLAGS_GROUP *) object_ptr;
+ TX_EVENT_FLAGS_GROUP *tail_ptr;
+
+ memset(object_ptr, 0, sizeof(TX_EVENT_FLAGS_GROUP));
+ event_flags_ptr -> tx_event_flags_group_id = TX_EVENT_FLAGS_ID;
+ event_flags_ptr -> tx_event_flags_group_name = name;
+
+ if (_tx_event_flags_created_ptr == TX_NULL)
+ {
+ event_flags_ptr -> tx_event_flags_group_created_next = event_flags_ptr;
+ _tx_event_flags_created_ptr = event_flags_ptr;
+ }
+ else
+ {
+ tail_ptr = _tx_event_flags_created_ptr;
+ while (tail_ptr -> tx_event_flags_group_created_next != _tx_event_flags_created_ptr)
+ {
+ tail_ptr = tail_ptr -> tx_event_flags_group_created_next;
+ }
+ tail_ptr -> tx_event_flags_group_created_next = event_flags_ptr;
+ event_flags_ptr -> tx_event_flags_group_created_next = _tx_event_flags_created_ptr;
+ }
+ _tx_event_flags_created_count++;
+ break;
+ }
+
+ case TXM_SEMAPHORE_OBJECT:
+ {
+ TX_SEMAPHORE *semaphore_ptr = (TX_SEMAPHORE *) object_ptr;
+ TX_SEMAPHORE *tail_ptr;
+
+ memset(object_ptr, 0, sizeof(TX_SEMAPHORE));
+ semaphore_ptr -> tx_semaphore_id = TX_SEMAPHORE_ID;
+ semaphore_ptr -> tx_semaphore_name = name;
+
+ if (_tx_semaphore_created_ptr == TX_NULL)
+ {
+ semaphore_ptr -> tx_semaphore_created_next = semaphore_ptr;
+ _tx_semaphore_created_ptr = semaphore_ptr;
+ }
+ else
+ {
+ tail_ptr = _tx_semaphore_created_ptr;
+ while (tail_ptr -> tx_semaphore_created_next != _tx_semaphore_created_ptr)
+ {
+ tail_ptr = tail_ptr -> tx_semaphore_created_next;
+ }
+ tail_ptr -> tx_semaphore_created_next = semaphore_ptr;
+ semaphore_ptr -> tx_semaphore_created_next = _tx_semaphore_created_ptr;
+ }
+ _tx_semaphore_created_count++;
+ break;
+ }
+
+ case TXM_MUTEX_OBJECT:
+ {
+ TX_MUTEX *mutex_ptr = (TX_MUTEX *) object_ptr;
+ TX_MUTEX *tail_ptr;
+
+ memset(object_ptr, 0, sizeof(TX_MUTEX));
+ mutex_ptr -> tx_mutex_id = TX_MUTEX_ID;
+ mutex_ptr -> tx_mutex_name = name;
+
+ if (_tx_mutex_created_ptr == TX_NULL)
+ {
+ mutex_ptr -> tx_mutex_created_next = mutex_ptr;
+ _tx_mutex_created_ptr = mutex_ptr;
+ }
+ else
+ {
+ tail_ptr = _tx_mutex_created_ptr;
+ while (tail_ptr -> tx_mutex_created_next != _tx_mutex_created_ptr)
+ {
+ tail_ptr = tail_ptr -> tx_mutex_created_next;
+ }
+ tail_ptr -> tx_mutex_created_next = mutex_ptr;
+ mutex_ptr -> tx_mutex_created_next = _tx_mutex_created_ptr;
+ }
+ _tx_mutex_created_count++;
+ break;
+ }
+
+ case TXM_BLOCK_POOL_OBJECT:
+ {
+ TX_BLOCK_POOL *block_pool_ptr = (TX_BLOCK_POOL *) object_ptr;
+ TX_BLOCK_POOL *tail_ptr;
+
+ memset(object_ptr, 0, sizeof(TX_BLOCK_POOL));
+ block_pool_ptr -> tx_block_pool_id = TX_BLOCK_POOL_ID;
+ block_pool_ptr -> tx_block_pool_name = name;
+
+ if (_tx_block_pool_created_ptr == TX_NULL)
+ {
+ block_pool_ptr -> tx_block_pool_created_next = block_pool_ptr;
+ _tx_block_pool_created_ptr = block_pool_ptr;
+ }
+ else
+ {
+ tail_ptr = _tx_block_pool_created_ptr;
+ while (tail_ptr -> tx_block_pool_created_next != _tx_block_pool_created_ptr)
+ {
+ tail_ptr = tail_ptr -> tx_block_pool_created_next;
+ }
+ tail_ptr -> tx_block_pool_created_next = block_pool_ptr;
+ block_pool_ptr -> tx_block_pool_created_next = _tx_block_pool_created_ptr;
+ }
+ _tx_block_pool_created_count++;
+ break;
+ }
+
+ case TXM_BYTE_POOL_OBJECT:
+ default:
+ {
+ TX_BYTE_POOL *byte_pool_ptr = (TX_BYTE_POOL *) object_ptr;
+ TX_BYTE_POOL *tail_ptr;
+
+ memset(object_ptr, 0, sizeof(TX_BYTE_POOL));
+ byte_pool_ptr -> tx_byte_pool_id = TX_BYTE_POOL_ID;
+ byte_pool_ptr -> tx_byte_pool_name = name;
+
+ if (_tx_byte_pool_created_ptr == TX_NULL)
+ {
+ byte_pool_ptr -> tx_byte_pool_created_next = byte_pool_ptr;
+ _tx_byte_pool_created_ptr = byte_pool_ptr;
+ }
+ else
+ {
+ tail_ptr = _tx_byte_pool_created_ptr;
+ while (tail_ptr -> tx_byte_pool_created_next != _tx_byte_pool_created_ptr)
+ {
+ tail_ptr = tail_ptr -> tx_byte_pool_created_next;
+ }
+ tail_ptr -> tx_byte_pool_created_next = byte_pool_ptr;
+ byte_pool_ptr -> tx_byte_pool_created_next = _tx_byte_pool_created_ptr;
+ }
+ _tx_byte_pool_created_count++;
+ break;
+ }
+ }
+}
+
+
+/* Empty every created list. */
+static VOID test_lists_clear(VOID)
+{
+ _tx_block_pool_created_ptr = TX_NULL;
+ _tx_block_pool_created_count = ((ULONG) 0);
+ _tx_byte_pool_created_ptr = TX_NULL;
+ _tx_byte_pool_created_count = ((ULONG) 0);
+ _tx_event_flags_created_ptr = TX_NULL;
+ _tx_event_flags_created_count = ((ULONG) 0);
+ _tx_mutex_created_ptr = TX_NULL;
+ _tx_mutex_created_count = ((ULONG) 0);
+ _tx_queue_created_ptr = TX_NULL;
+ _tx_queue_created_count = ((ULONG) 0);
+ _tx_semaphore_created_ptr = TX_NULL;
+ _tx_semaphore_created_count = ((ULONG) 0);
+ _tx_thread_created_ptr = TX_NULL;
+ _tx_thread_created_count = ((ULONG) 0);
+ _tx_timer_created_ptr = TX_NULL;
+ _tx_timer_created_count = ((ULONG) 0);
+}
+
+
+/* Fill the arena, then restore the module's outgoing parameters, which live in it. */
+static VOID test_arena_fill(VOID)
+{
+ memset((VOID *) &test_arena, (int) TEST_FILLER, sizeof(test_arena));
+
+ TEST_EXTRA_PARAMETERS[0] = ((ALIGN_TYPE) 0);
+ TEST_EXTRA_PARAMETERS[1] = (ALIGN_TYPE) TEST_RESULT_SLOT;
+ *TEST_RESULT_SLOT = TX_NULL;
+}
+
+
+/* What one request came back with. */
+
+#define TEST_RESULT_POISON ((VOID *) (ALIGN_TYPE) 0xD0D0D0D0)
+
+static ALIGN_TYPE test_return_value;
+static VOID *test_result;
+static ULONG test_reached_lookup;
+
+
+/* Make one request of the extended dispatcher, the way a module makes it: the declared
+ length and the result slot go in the extra parameter array, which is in the module's
+ own data. */
+static VOID test_lookup_extended_result_slot(TXM_MODULE_INSTANCE *module_instance, ALIGN_TYPE object_type,
+ UCHAR *name, UINT name_length, ALIGN_TYPE *extra_parameters,
+ VOID **result_slot)
+{
+ *TEST_RESULT_SLOT = TEST_RESULT_POISON;
+ test_lookup_exits = ((ULONG) 0);
+
+ /* The lookup asks the running thread which module is calling before it will search
+ the two pool types, so the request has to come from one. */
+ test_current_thread.tx_thread_module_instance_ptr = (VOID *) module_instance;
+ _tx_thread_current_ptr = &test_current_thread;
+
+ extra_parameters[0] = (ALIGN_TYPE) name_length;
+ extra_parameters[1] = (ALIGN_TYPE) result_slot;
+
+ test_return_value = _txm_module_manager_txm_module_object_pointer_get_extended_dispatch(module_instance, object_type,
+ (ALIGN_TYPE) name, extra_parameters);
+ test_result = *TEST_RESULT_SLOT;
+ test_reached_lookup = test_lookup_exits;
+}
+
+
+/* Make one request without writing to the extra parameter array first, for the cases
+ whose whole point is an array the module could not have written to either. */
+static VOID test_lookup_extended_raw(TXM_MODULE_INSTANCE *module_instance, ALIGN_TYPE object_type,
+ UCHAR *name, ALIGN_TYPE *extra_parameters)
+{
+ *TEST_RESULT_SLOT = TEST_RESULT_POISON;
+ test_lookup_exits = ((ULONG) 0);
+
+ test_current_thread.tx_thread_module_instance_ptr = (VOID *) module_instance;
+ _tx_thread_current_ptr = &test_current_thread;
+
+ test_return_value = _txm_module_manager_txm_module_object_pointer_get_extended_dispatch(module_instance, object_type,
+ (ALIGN_TYPE) name, extra_parameters);
+ test_result = *TEST_RESULT_SLOT;
+ test_reached_lookup = test_lookup_exits;
+}
+
+
+/* The same request with the result slot the module normally uses. */
+static VOID test_lookup_extended(TXM_MODULE_INSTANCE *module_instance, ALIGN_TYPE object_type,
+ UCHAR *name, UINT name_length, ALIGN_TYPE *extra_parameters)
+{
+ test_lookup_extended_result_slot(module_instance, object_type, name, name_length, extra_parameters, TEST_RESULT_SLOT);
+}
+
+
+/* Make one request of the deprecated dispatcher, which carries no length. */
+static VOID test_lookup_deprecated(TXM_MODULE_INSTANCE *module_instance, ALIGN_TYPE object_type, UCHAR *name)
+{
+ *TEST_RESULT_SLOT = TEST_RESULT_POISON;
+ test_lookup_exits = ((ULONG) 0);
+
+ test_current_thread.tx_thread_module_instance_ptr = (VOID *) module_instance;
+ _tx_thread_current_ptr = &test_current_thread;
+
+ test_return_value = _txm_module_manager_txm_module_object_pointer_get_dispatch(module_instance, object_type,
+ (ALIGN_TYPE) name, (ALIGN_TYPE) TEST_RESULT_SLOT);
+ test_result = *TEST_RESULT_SLOT;
+ test_reached_lookup = test_lookup_exits;
+}
+
+
+/* Report whether the dispatch layer let the last request through to the lookup, checking
+ as it goes that a refusal is a whole refusal: TXM_MODULE_INVALID_MEMORY, the lookup not
+ entered, and the module's result slot not written. */
+static UINT test_accepted(VOID)
+{
+
+UINT accepted;
+
+
+ if (test_reached_lookup == ((ULONG) 0))
+ {
+ accepted = TX_FALSE;
+
+ test_expect("a refused request returns TXM_MODULE_INVALID_MEMORY",
+ (ULONG) test_return_value, (ULONG) TXM_MODULE_INVALID_MEMORY);
+ test_expect("...and writes nothing to the module's result slot",
+ (ULONG) (test_result == TEST_RESULT_POISON), (ULONG) TX_TRUE);
+ }
+ else
+ {
+ accepted = TX_TRUE;
+
+ test_expect("an accepted request enters the lookup once", test_reached_lookup, ((ULONG) 1));
+ test_expect("...and the lookup gives back the preemption it took",
+ test_preempt_disable_on_exit, ((ULONG) 0));
+ }
+
+ return(accepted);
+}
+
+
+/* Measure the extent the dispatcher validates.
+
+ The name buffer is anchored to the end of one of the module's regions and walked
+ backwards into it a byte at a time, so that the room left between the buffer and the
+ end of the region grows by one on each step. The smallest amount of room the
+ dispatcher accepts is the extent it validated. TEST_NOT_ACCEPTED means it accepted
+ nothing within the region. */
+
+#define TEST_NOT_ACCEPTED ((ULONG) 0xFFFFFFFF)
+
+static ULONG test_measure_validated_extent(TXM_MODULE_INSTANCE *module_instance, UCHAR *region_start, UINT name_length)
+{
+
+ULONG room;
+ULONG walk_limit;
+ULONG extent;
+UCHAR *region_end_plus_one;
+
+
+ region_end_plus_one = region_start + TEST_REGION_BYTES;
+
+ /* Walking past the length plus its terminator is enough to establish that nothing
+ validates more than that. */
+ walk_limit = ((ULONG) name_length) + ((ULONG) 4);
+
+ extent = TEST_NOT_ACCEPTED;
+
+ for (room = ((ULONG) 0); room <= walk_limit; room++)
+ {
+ test_arena_fill();
+ test_lists_clear();
+
+ test_lookup_extended(module_instance, (ALIGN_TYPE) TXM_SEMAPHORE_OBJECT,
+ region_end_plus_one - room, name_length, TEST_EXTRA_PARAMETERS);
+
+ if (test_accepted() == TX_TRUE)
+ {
+ extent = room;
+ break;
+ }
+ }
+
+ return(extent);
+}
+
+
+/* Measure the extent the comparison reads.
+
+ Memory is filled with a filler character and the only terminator in it is planted at a
+ chosen depth from the name buffer. The object the module asks for is named with
+ exactly the bytes up to that terminator, so the lookup returns it if and only if the
+ comparison read that deep. Scanning the depth and keeping the deepest one that comes
+ back is therefore the read extent, measured rather than inferred.
+
+ TEST_NOT_READ means no depth came back, which is what a refused request looks like. */
+
+#define TEST_NOT_READ ((ULONG) 0xFFFFFFFF)
+
+static ULONG test_measure_read_depth(TXM_MODULE_INSTANCE *module_instance, UCHAR *region_start,
+ ULONG room, UINT name_length, ULONG max_depth)
+{
+
+ULONG depth;
+ULONG deepest;
+UCHAR *name;
+
+
+ deepest = TEST_NOT_READ;
+
+ for (depth = ((ULONG) 0); depth <= max_depth; depth++)
+ {
+ test_arena_fill();
+
+ name = (region_start + TEST_REGION_BYTES) - room;
+ name[depth] = ((UCHAR) 0);
+
+ memcpy((VOID *) test_probe_name, (VOID *) name, (size_t) depth);
+ test_probe_name[depth] = ((CHAR) 0);
+
+ test_lists_clear();
+ test_object_create((UINT) TXM_SEMAPHORE_OBJECT, (VOID *) &test_semaphore, test_probe_name);
+
+ test_lookup_extended(module_instance, (ALIGN_TYPE) TXM_SEMAPHORE_OBJECT, name, name_length, TEST_EXTRA_PARAMETERS);
+
+ if ((test_reached_lookup != ((ULONG) 0)) &&
+ (test_return_value == ((ALIGN_TYPE) TX_SUCCESS)) &&
+ (test_result == (VOID *) &test_semaphore))
+ {
+ deepest = depth;
+ }
+ }
+
+ return(deepest);
+}
+
+
+/* Measure how far the comparison walks when the object's name is the module's own search
+ buffer.
+
+ A create service stores the name pointer a module hands it rather than copying the
+ string, so a module can register an object whose name is the buffer it then searches
+ for. Both sides of the comparison are then the same address and every character
+ matches whatever it is, so the walk is bounded by nothing the module knows or supplied:
+ it runs to the first terminator in memory. This returns the deepest depth at which a
+ terminator is still reached, which is that walk length. */
+static ULONG test_measure_alias_walk(TXM_MODULE_INSTANCE *module_instance, UCHAR *region_start,
+ ULONG room, UINT name_length, ULONG max_depth, UINT use_deprecated)
+{
+
+ULONG depth;
+ULONG deepest;
+UCHAR *name;
+
+
+ deepest = TEST_NOT_READ;
+
+ for (depth = ((ULONG) 0); depth <= max_depth; depth++)
+ {
+ test_arena_fill();
+
+ name = (region_start + TEST_REGION_BYTES) - room;
+ name[depth] = ((UCHAR) 0);
+
+ test_lists_clear();
+ test_object_create((UINT) TXM_SEMAPHORE_OBJECT, (VOID *) &test_semaphore, (CHAR *) name);
+
+ if (use_deprecated == TX_TRUE)
+ {
+ test_lookup_deprecated(module_instance, (ALIGN_TYPE) TXM_SEMAPHORE_OBJECT, name);
+ }
+ else
+ {
+ test_lookup_extended(module_instance, (ALIGN_TYPE) TXM_SEMAPHORE_OBJECT, name, name_length, TEST_EXTRA_PARAMETERS);
+ }
+
+ if ((test_reached_lookup != ((ULONG) 0)) &&
+ (test_return_value == ((ALIGN_TYPE) TX_SUCCESS)) &&
+ (test_result == (VOID *) &test_semaphore))
+ {
+ deepest = depth;
+ }
+ }
+
+ return(deepest);
+}
+
+
+/* The declared lengths the cases run through: none, one character, and lengths either
+ side of the room a case can leave in a region. */
+
+static const UINT test_name_lengths[] =
+{
+ ((UINT) 0),
+ ((UINT) 1),
+ ((UINT) 2),
+ ((UINT) 3),
+ ((UINT) 7),
+ ((UINT) 16),
+ ((UINT) 31)
+};
+
+#define TEST_NAME_LENGTH_COUNT (sizeof(test_name_lengths) / sizeof(test_name_lengths[0]))
+
+
+/* The three regions a name buffer may legitimately be in. A search name is read, never
+ written, so all three are permitted -- a module searching for a literal searches for
+ one out of its own read-only image. */
+
+#define TEST_REGION_COUNT ((UINT) 3)
+
+static const char *test_region_names[TEST_REGION_COUNT] =
+{
+ "the module's data",
+ "a shared region",
+ "the module's read-only image"
+};
+
+static UCHAR *test_region_start(UINT region)
+{
+
+UCHAR *start;
+
+
+ if (region == ((UINT) 0))
+ {
+ start = TEST_DATA_START;
+ }
+ else if (region == ((UINT) 1))
+ {
+ start = TEST_SHARED_START;
+ }
+ else
+ {
+ start = TEST_CODE_START;
+ }
+
+ return(start);
+}
+
+
+/* The eight object types the lookup knows, and one it does not. */
+
+#define TEST_OBJECT_TYPE_COUNT ((UINT) 8)
+
+static const ALIGN_TYPE test_object_types[TEST_OBJECT_TYPE_COUNT] =
+{
+ (ALIGN_TYPE) TXM_THREAD_OBJECT,
+ (ALIGN_TYPE) TXM_TIMER_OBJECT,
+ (ALIGN_TYPE) TXM_QUEUE_OBJECT,
+ (ALIGN_TYPE) TXM_EVENT_FLAGS_OBJECT,
+ (ALIGN_TYPE) TXM_SEMAPHORE_OBJECT,
+ (ALIGN_TYPE) TXM_MUTEX_OBJECT,
+ (ALIGN_TYPE) TXM_BLOCK_POOL_OBJECT,
+ (ALIGN_TYPE) TXM_BYTE_POOL_OBJECT
+};
+
+static const char *test_object_type_names[TEST_OBJECT_TYPE_COUNT] =
+{
+ "thread",
+ "timer",
+ "queue",
+ "event flags group",
+ "semaphore",
+ "mutex",
+ "block pool",
+ "byte pool"
+};
+
+#define TEST_INVALID_OBJECT_TYPE ((ALIGN_TYPE) 0x7EU)
+
+
+/* One of the two control blocks held for each type, in the order the type table lists
+ them. */
+static VOID *test_object_block(UINT type_index, UINT which)
+{
+
+VOID *object_ptr;
+
+
+ switch (type_index)
+ {
+ case ((UINT) 0): object_ptr = (VOID *) &test_thread_objects[which]; break;
+ case ((UINT) 1): object_ptr = (VOID *) &test_timer_objects[which]; break;
+ case ((UINT) 2): object_ptr = (VOID *) &test_queue_objects[which]; break;
+ case ((UINT) 3): object_ptr = (VOID *) &test_event_flags_objects[which]; break;
+ case ((UINT) 4): object_ptr = (VOID *) &test_semaphore_objects[which]; break;
+ case ((UINT) 5): object_ptr = (VOID *) &test_mutex_objects[which]; break;
+ case ((UINT) 6): object_ptr = (VOID *) &test_block_pool_objects[which]; break;
+ default: object_ptr = (VOID *) &test_byte_pool_objects[which]; break;
+ }
+
+ return(object_ptr);
+}
+
+
+int main(void)
+{
+
+UINT length_index;
+UINT region;
+UINT type_index;
+UINT name_length;
+UCHAR *region_start;
+UCHAR *name;
+ULONG room;
+ULONG depth;
+ULONG expected_extent;
+char description[192];
+
+
+ memset((VOID *) &test_current_thread, 0, sizeof(TX_THREAD));
+ test_current_thread.tx_thread_id = TX_THREAD_ID;
+
+ test_module_setup(&test_protected_module, (ULONG) TXM_MODULE_MEMORY_PROTECTION);
+ test_module_setup(&test_unprotected_module, ((ULONG) 0));
+
+ printf("Module Manager object name range validation test\n");
+
+ /**********************************************************************/
+ /* The extent the extended dispatcher validates. */
+ /**********************************************************************/
+
+ /* A declared length excludes the terminator and the comparison reads it, so the range
+ the module has to have made readable is the length plus one byte, in whichever of
+ the three regions a search name may live in. */
+
+ for (length_index = ((UINT) 0); length_index < ((UINT) TEST_NAME_LENGTH_COUNT); length_index++)
+ {
+ name_length = test_name_lengths[length_index];
+ expected_extent = ((ULONG) name_length) + ((ULONG) 1);
+
+ for (region = ((UINT) 0); region < TEST_REGION_COUNT; region++)
+ {
+ snprintf(description, sizeof(description),
+ "a declared length of %u is checked over %lu bytes of a name in %s",
+ name_length, (unsigned long) expected_extent, test_region_names[region]);
+ test_expect(description,
+ test_measure_validated_extent(&test_protected_module, test_region_start(region), name_length),
+ expected_extent);
+ }
+ }
+
+ /**********************************************************************/
+ /* The extent the comparison reads, against the extent validated. */
+ /**********************************************************************/
+
+ /* The two dials are the declared length and the object's name, and the finding is the
+ relationship between them: the comparison reads through the declared length, so a
+ request must not be accepted unless that whole range is inside the module. For every
+ length and every amount of room, the request is accepted exactly when the room
+ covers the length and its terminator, and when it is accepted the deepest byte the
+ comparison can be made to read is the last byte of the declared name -- which the
+ room then guarantees is inside the module. */
+
+ for (length_index = ((UINT) 0); length_index < ((UINT) TEST_NAME_LENGTH_COUNT); length_index++)
+ {
+ name_length = test_name_lengths[length_index];
+
+ for (region = ((UINT) 0); region < TEST_REGION_COUNT; region++)
+ {
+ region_start = test_region_start(region);
+
+ for (room = ((ULONG) 1); room <= (((ULONG) name_length) + ((ULONG) 2)); room++)
+ {
+ depth = test_measure_read_depth(&test_protected_module, region_start, room,
+ name_length, ((ULONG) name_length) + ((ULONG) 2));
+
+ if (room >= (((ULONG) name_length) + ((ULONG) 1)))
+ {
+ snprintf(description, sizeof(description),
+ "a %u-character name with %lu bytes of room in %s is read to its declared end",
+ name_length, (unsigned long) room, test_region_names[region]);
+ test_expect(description, depth, (ULONG) name_length);
+
+ snprintf(description, sizeof(description),
+ "...and every byte it read was inside the module (%u characters, %lu bytes of room)",
+ name_length, (unsigned long) room);
+ test_expect(description, (ULONG) (depth < room), (ULONG) TX_TRUE);
+ }
+ else
+ {
+ snprintf(description, sizeof(description),
+ "a %u-character name with only %lu bytes of room in %s is not read at all",
+ name_length, (unsigned long) room, test_region_names[region]);
+ test_expect(description, depth, TEST_NOT_READ);
+ }
+ }
+ }
+ }
+
+ /**********************************************************************/
+ /* The name a module registered for an object can be the name it */
+ /* searches for. */
+ /**********************************************************************/
+
+ /* A create service stores the name pointer rather than the string, so a module can
+ give an object the address of the buffer it then searches for. Both sides of the
+ comparison are the same address, so it matches whatever is there and the walk is
+ bounded by nothing the module supplied. That is the case the one-byte check could
+ not survive, and it is why the length has to bound the buffer rather than the
+ comparison having to discover the end of it.
+
+ With the range checked, the walk cannot start: the only placements accepted are the
+ ones whose whole declared range is inside the module, and there the walk ends inside
+ it too. */
+
+ for (region = ((UINT) 0); region < TEST_REGION_COUNT; region++)
+ {
+ region_start = test_region_start(region);
+
+ snprintf(description, sizeof(description),
+ "an aliased name with one byte of room in %s reaches no depth at all", test_region_names[region]);
+ test_expect(description,
+ test_measure_alias_walk(&test_protected_module, region_start, ((ULONG) 1),
+ ((UINT) 31), ((ULONG) 31), TX_FALSE),
+ TEST_NOT_READ);
+
+ snprintf(description, sizeof(description),
+ "an aliased name with room for its whole declared range in %s walks no further than that range",
+ test_region_names[region]);
+ test_expect(description,
+ test_measure_alias_walk(&test_protected_module, region_start, ((ULONG) 32),
+ ((UINT) 31), ((ULONG) 63), TX_FALSE),
+ ((ULONG) 31));
+ }
+
+ /**********************************************************************/
+ /* The deprecated service, which carries no length. */
+ /**********************************************************************/
+
+ /* Its manager wrapper searches with the largest length a UINT can hold, so there is no
+ range to check and none can be derived from a pointer. A memory-protected module is
+ refused it; the extended service is what such a module uses. */
+
+ for (region = ((UINT) 0); region < TEST_REGION_COUNT; region++)
+ {
+ region_start = test_region_start(region);
+
+ for (room = ((ULONG) 1); room <= ((ULONG) 64); room++)
+ {
+ test_arena_fill();
+ test_lists_clear();
+
+ test_lookup_deprecated(&test_protected_module, (ALIGN_TYPE) TXM_SEMAPHORE_OBJECT,
+ (region_start + TEST_REGION_BYTES) - room);
+
+ if (room == ((ULONG) 1))
+ {
+ snprintf(description, sizeof(description),
+ "a memory-protected module's lookup without a length in %s is refused",
+ test_region_names[region]);
+ test_expect(description, (ULONG) test_accepted(), (ULONG) TX_FALSE);
+ }
+ else
+ {
+ test_expect("a memory-protected module's lookup without a length is refused at every placement",
+ (ULONG) test_accepted(), (ULONG) TX_FALSE);
+ }
+ }
+
+ snprintf(description, sizeof(description),
+ "...including an aliased name in %s, which is the walk it cannot bound", test_region_names[region]);
+ test_expect(description,
+ test_measure_alias_walk(&test_protected_module, region_start, ((ULONG) 1),
+ ((UINT) 0), ((ULONG) 63), TX_TRUE),
+ TEST_NOT_READ);
+ }
+
+ /* A module without memory protection still has it, and there it shows what the absent
+ length means: with the object's name aliased to the search buffer the walk runs to
+ whatever terminator memory happens to hold, however far away, because no length
+ stops it. The extended service on the same module stops exactly where the module
+ said to. */
+
+ test_expect("an unprotected module's lookup without a length walks to any depth memory puts a terminator at",
+ test_measure_alias_walk(&test_unprotected_module, TEST_DATA_START, ((ULONG) 1),
+ ((UINT) 0), ((ULONG) 100), TX_TRUE),
+ ((ULONG) 100));
+
+ test_expect("an unprotected module's extended lookup walks exactly as far as the length it declared",
+ test_measure_alias_walk(&test_unprotected_module, TEST_DATA_START, ((ULONG) 1),
+ ((UINT) 40), ((ULONG) 100), TX_FALSE),
+ ((ULONG) 40));
+
+ /**********************************************************************/
+ /* A null name, and a null result slot. */
+ /**********************************************************************/
+
+ /* The range check accepts a null pointer outright, as the one-byte check it replaces
+ did and as the buffer checks do, because passing null is how a module declines an
+ optional argument. Neither of these is optional, and it is the lookup that refuses
+ them, before it dereferences either. These cases assert that sequence rather than
+ the macro, so that neither side can change quietly. */
+
+ test_arena_fill();
+ test_lists_clear();
+
+ test_lookup_extended(&test_protected_module, (ALIGN_TYPE) TXM_SEMAPHORE_OBJECT, TX_NULL, ((UINT) 31), TEST_EXTRA_PARAMETERS);
+ test_expect("the extended dispatcher passes a null name through", (ULONG) test_return_value, (ULONG) TX_PTR_ERROR);
+ test_expect("...and the lookup refuses it before dereferencing it", test_reached_lookup, ((ULONG) 0));
+
+ test_arena_fill();
+ test_lists_clear();
+ TEST_EXTRA_PARAMETERS[0] = ((ALIGN_TYPE) 31);
+ TEST_EXTRA_PARAMETERS[1] = ((ALIGN_TYPE) 0);
+ test_return_value = _txm_module_manager_txm_module_object_pointer_get_extended_dispatch(&test_protected_module,
+ (ALIGN_TYPE) TXM_SEMAPHORE_OBJECT,
+ (ALIGN_TYPE) TEST_DATA_START,
+ TEST_EXTRA_PARAMETERS);
+ test_expect("the extended dispatcher passes a null result slot through", (ULONG) test_return_value, (ULONG) TX_PTR_ERROR);
+
+ /* The deprecated service refuses a memory-protected module before it gets that far,
+ which is a change from the one-byte check and is recorded here as one. */
+
+ test_arena_fill();
+ test_lists_clear();
+ test_lookup_deprecated(&test_protected_module, (ALIGN_TYPE) TXM_SEMAPHORE_OBJECT, TX_NULL);
+ test_expect("a memory-protected module's null name is refused by the deprecated dispatcher",
+ (ULONG) test_return_value, (ULONG) TXM_MODULE_INVALID_MEMORY);
+
+ test_arena_fill();
+ test_lists_clear();
+ test_lookup_deprecated(&test_unprotected_module, (ALIGN_TYPE) TXM_SEMAPHORE_OBJECT, TX_NULL);
+ test_expect("an unprotected module's null name still reaches the lookup", (ULONG) test_return_value, (ULONG) TX_PTR_ERROR);
+
+ /**********************************************************************/
+ /* The extra parameter array itself. */
+ /**********************************************************************/
+
+ /* The length is read out of that array, so checking the name's range depends on the
+ array having been checked first. Reordering the two checks is what the fix required,
+ and a request that fails both still fails the same way. */
+
+ test_arena_fill();
+ test_lists_clear();
+ test_lookup_extended(&test_protected_module, (ALIGN_TYPE) TXM_SEMAPHORE_OBJECT, TEST_DATA_START,
+ ((UINT) 1), (ALIGN_TYPE *) TEST_CODE_START);
+ test_expect("an extra parameter array in the module's read-only image is refused",
+ (ULONG) test_accepted(), (ULONG) TX_FALSE);
+
+ test_arena_fill();
+ test_lists_clear();
+ test_lookup_extended(&test_protected_module, (ALIGN_TYPE) TXM_SEMAPHORE_OBJECT,
+ (TEST_DATA_START + TEST_REGION_BYTES) - ((ULONG) 1),
+ ((UINT) 31), (ALIGN_TYPE *) TEST_CODE_START);
+ test_expect("an unreadable name and an unreadable array are refused the same way",
+ (ULONG) test_accepted(), (ULONG) TX_FALSE);
+
+ test_arena_fill();
+ test_lists_clear();
+ test_lookup_extended(&test_protected_module, (ALIGN_TYPE) TXM_SEMAPHORE_OBJECT, TEST_DATA_START,
+ ((UINT) 1), (ALIGN_TYPE *) ((TEST_DATA_START + TEST_REGION_BYTES) - sizeof(ALIGN_TYPE)));
+ test_expect("an extra parameter array with room for one word of two is refused",
+ (ULONG) test_accepted(), (ULONG) TX_FALSE);
+
+ /* A shared region registered to the module is writable by it, so an array there is
+ as good as one in its own data, and so is a result slot. */
+
+ test_arena_fill();
+ test_lists_clear();
+ test_lookup_extended_result_slot(&test_protected_module, (ALIGN_TYPE) TXM_SEMAPHORE_OBJECT, TEST_DATA_START,
+ ((UINT) 1), (ALIGN_TYPE *) TEST_SHARED_START, (VOID **) TEST_SHARED_START);
+ test_expect("an extra parameter array and result slot in a shared region are accepted",
+ (ULONG) test_accepted(), (ULONG) TX_TRUE);
+
+ test_arena_fill();
+ test_lists_clear();
+ test_lookup_extended(&test_protected_module, (ALIGN_TYPE) TXM_SEMAPHORE_OBJECT, TEST_DATA_START,
+ ((UINT) 1), (ALIGN_TYPE *) ((TEST_SHARED_START + TEST_REGION_BYTES) - sizeof(ALIGN_TYPE)));
+ test_expect("an extra parameter array running off the end of a shared region is refused",
+ (ULONG) test_accepted(), (ULONG) TX_FALSE);
+
+ test_arena_fill();
+ test_lists_clear();
+ test_lookup_extended(&test_protected_module, (ALIGN_TYPE) TXM_SEMAPHORE_OBJECT, TEST_DATA_START,
+ ((UINT) 1), (ALIGN_TYPE *) &test_arena.test_arena_bytes[0]);
+ test_expect("an extra parameter array below the module's data is refused",
+ (ULONG) test_accepted(), (ULONG) TX_FALSE);
+
+ test_arena_fill();
+ test_lists_clear();
+ test_lookup_extended_raw(&test_protected_module, (ALIGN_TYPE) TXM_SEMAPHORE_OBJECT, TEST_DATA_START,
+ (ALIGN_TYPE *) (~((ALIGN_TYPE) 0) - ((ALIGN_TYPE) 3)));
+ test_expect("an extra parameter array whose two words wrap the address space is refused",
+ (ULONG) test_accepted(), (ULONG) TX_FALSE);
+
+ /* The module's read-only image is not, and a result slot the module cannot write is
+ refused whatever else is right about the request. */
+
+ test_arena_fill();
+ test_lists_clear();
+ test_lookup_extended_result_slot(&test_protected_module, (ALIGN_TYPE) TXM_SEMAPHORE_OBJECT, TEST_DATA_START,
+ ((UINT) 1), TEST_EXTRA_PARAMETERS, (VOID **) TEST_CODE_START);
+ test_expect("a result slot in the module's read-only image is refused",
+ (ULONG) test_accepted(), (ULONG) TX_FALSE);
+
+ test_arena_fill();
+ test_lists_clear();
+ test_lookup_extended_result_slot(&test_protected_module, (ALIGN_TYPE) TXM_SEMAPHORE_OBJECT, TEST_DATA_START,
+ ((UINT) 1), TEST_EXTRA_PARAMETERS, (VOID **) test_probe_name);
+ test_expect("a result slot outside every region the module owns is refused",
+ (ULONG) test_accepted(), (ULONG) TX_FALSE);
+
+ test_arena_fill();
+ test_lists_clear();
+ test_lookup_extended_result_slot(&test_protected_module, (ALIGN_TYPE) TXM_SEMAPHORE_OBJECT, TEST_DATA_START,
+ ((UINT) 1), TEST_EXTRA_PARAMETERS, (VOID **) (~((ALIGN_TYPE) 0) - ((ALIGN_TYPE) 1)));
+ test_expect("a result slot whose word wraps the address space is refused",
+ (ULONG) test_accepted(), (ULONG) TX_FALSE);
+
+ /**********************************************************************/
+ /* Lengths that cannot describe a range. */
+ /**********************************************************************/
+
+ /* The length plus its terminator has to be expressible, and the range has to fit
+ under the top of memory. Neither is a range a module can be given. */
+
+ test_arena_fill();
+ test_lists_clear();
+ test_lookup_extended(&test_protected_module, (ALIGN_TYPE) TXM_SEMAPHORE_OBJECT, TEST_DATA_START,
+ (UINT) TXM_MODULE_MANAGER_UTIL_MAX_VALUE_OF_TYPE_UNSIGNED(UINT), TEST_EXTRA_PARAMETERS);
+ test_expect("the largest length a UINT can hold is refused", (ULONG) test_accepted(), (ULONG) TX_FALSE);
+
+ test_arena_fill();
+ test_lists_clear();
+ test_lookup_extended(&test_protected_module, (ALIGN_TYPE) TXM_SEMAPHORE_OBJECT, TEST_DATA_START,
+ ((UINT) TXM_MODULE_MANAGER_UTIL_MAX_VALUE_OF_TYPE_UNSIGNED(UINT)) - ((UINT) 1), TEST_EXTRA_PARAMETERS);
+ test_expect("a length one below it is refused too", (ULONG) test_accepted(), (ULONG) TX_FALSE);
+
+ test_arena_fill();
+ test_lists_clear();
+ test_lookup_extended(&test_protected_module, (ALIGN_TYPE) TXM_SEMAPHORE_OBJECT,
+ (UCHAR *) (~((ALIGN_TYPE) 0) - ((ALIGN_TYPE) 3)), ((UINT) 31), TEST_EXTRA_PARAMETERS);
+ test_expect("a name whose range wraps the address space is refused", (ULONG) test_accepted(), (ULONG) TX_FALSE);
+
+ /**********************************************************************/
+ /* The range check is the dispatcher's, not the object type's. */
+ /**********************************************************************/
+
+ /* It runs before the lookup and does not depend on what is being looked up, so the
+ boundary is in the same place for all eight types the lookup knows and for one it
+ does not. What the lookup then does with the type is its own business, and the two
+ pool types it refuses a memory-protected module outright. */
+
+ for (type_index = ((UINT) 0); type_index < TEST_OBJECT_TYPE_COUNT; type_index++)
+ {
+ test_arena_fill();
+ test_lists_clear();
+ test_lookup_extended(&test_protected_module, test_object_types[type_index],
+ (TEST_DATA_START + TEST_REGION_BYTES) - ((ULONG) 4), ((UINT) 4), TEST_EXTRA_PARAMETERS);
+
+ snprintf(description, sizeof(description), "a %s name one byte short of its range is refused",
+ test_object_type_names[type_index]);
+ test_expect(description, (ULONG) test_accepted(), (ULONG) TX_FALSE);
+
+ test_arena_fill();
+ test_lists_clear();
+ test_lookup_extended(&test_protected_module, test_object_types[type_index],
+ (TEST_DATA_START + TEST_REGION_BYTES) - ((ULONG) 5), ((UINT) 4), TEST_EXTRA_PARAMETERS);
+
+ snprintf(description, sizeof(description), "a %s name with its whole range is accepted",
+ test_object_type_names[type_index]);
+ test_expect(description, (ULONG) test_accepted(), (ULONG) TX_TRUE);
+
+ if ((test_object_types[type_index] == ((ALIGN_TYPE) TXM_BLOCK_POOL_OBJECT)) ||
+ (test_object_types[type_index] == ((ALIGN_TYPE) TXM_BYTE_POOL_OBJECT)))
+ {
+ snprintf(description, sizeof(description), "...and the lookup refuses a memory-protected module a %s",
+ test_object_type_names[type_index]);
+ test_expect(description, (ULONG) test_return_value, (ULONG) TXM_MODULE_INVALID);
+ }
+ else
+ {
+ snprintf(description, sizeof(description), "...and the lookup searches for the %s and finds none",
+ test_object_type_names[type_index]);
+ test_expect(description, (ULONG) test_return_value, (ULONG) TX_NO_INSTANCE);
+ }
+ }
+
+ test_arena_fill();
+ test_lists_clear();
+ test_lookup_extended(&test_protected_module, TEST_INVALID_OBJECT_TYPE,
+ (TEST_DATA_START + TEST_REGION_BYTES) - ((ULONG) 4), ((UINT) 4), TEST_EXTRA_PARAMETERS);
+ test_expect("an unknown object type is refused for its range first", (ULONG) test_accepted(), (ULONG) TX_FALSE);
+
+ test_arena_fill();
+ test_lists_clear();
+ test_lookup_extended(&test_protected_module, TEST_INVALID_OBJECT_TYPE,
+ (TEST_DATA_START + TEST_REGION_BYTES) - ((ULONG) 5), ((UINT) 4), TEST_EXTRA_PARAMETERS);
+ test_expect("...and once its range is right, by the lookup for being unknown",
+ (ULONG) test_return_value, (ULONG) TX_OPTION_ERROR);
+
+ /**********************************************************************/
+ /* Every created list the lookup walks. */
+ /**********************************************************************/
+
+ /* The comparison the range check protects runs once per object on the list for the
+ requested type, and there are eight such lists. These cases put two objects on each
+ of them and search for the second, so that every list is walked, every comparison
+ is reached, and the name pointer the lookup dereferences out of each kind of control
+ block is the one a create service stored there.
+
+ The two pool types are refused to a memory-protected module by the lookup itself, so
+ these run as the module without protection, which is the caller that can reach them. */
+
+ for (type_index = ((UINT) 0); type_index < TEST_OBJECT_TYPE_COUNT; type_index++)
+ {
+ test_arena_fill();
+ test_lists_clear();
+
+ name = (TEST_DATA_START + TEST_REGION_BYTES) - ((ULONG) 8);
+ memcpy((VOID *) name, "target", (size_t) 7);
+ memcpy((VOID *) test_probe_name, "target", (size_t) 7);
+
+ test_object_create((UINT) test_object_types[type_index], test_object_block(type_index, ((UINT) 0)), "other");
+ test_object_create((UINT) test_object_types[type_index], test_object_block(type_index, ((UINT) 1)), "another");
+ test_object_create((UINT) test_object_types[type_index], test_object_block(type_index, ((UINT) 2)), test_probe_name);
+
+ test_lookup_extended(&test_unprotected_module, test_object_types[type_index], name, ((UINT) 6), TEST_EXTRA_PARAMETERS);
+
+ snprintf(description, sizeof(description), "the %s list is walked past its earlier members to the match",
+ test_object_type_names[type_index]);
+ test_expect(description, (ULONG) test_return_value, (ULONG) TX_SUCCESS);
+
+ snprintf(description, sizeof(description), "...and the %s that comes back is the one whose name matched",
+ test_object_type_names[type_index]);
+ test_expect(description,
+ (ULONG) (test_result == test_object_block(type_index, ((UINT) 2))), (ULONG) TX_TRUE);
+
+ /* A name on no list walks the whole of it and comes back empty, which is the
+ traversal running to the created count rather than to a match. */
+
+ memcpy((VOID *) name, "absent", (size_t) 7);
+
+ test_lookup_extended(&test_unprotected_module, test_object_types[type_index], name, ((UINT) 6), TEST_EXTRA_PARAMETERS);
+
+ snprintf(description, sizeof(description), "a name on no %s finds none of them", test_object_type_names[type_index]);
+ test_expect(description, (ULONG) test_return_value, (ULONG) TX_NO_INSTANCE);
+ }
+
+ /* The two pool lists are also reachable from application code, which is not a module
+ at all. That caller does not come through the dispatch layer, so these two cases
+ call the lookup the dispatchers call. */
+
+ test_arena_fill();
+ test_lists_clear();
+ memcpy((VOID *) test_probe_name, "target", (size_t) 7);
+ test_object_create((UINT) TXM_BLOCK_POOL_OBJECT, (VOID *) &test_block_pool_objects[0], test_probe_name);
+ test_object_create((UINT) TXM_BYTE_POOL_OBJECT, (VOID *) &test_byte_pool_objects[0], test_probe_name);
+ test_current_thread.tx_thread_module_instance_ptr = TX_NULL;
+ _tx_thread_current_ptr = &test_current_thread;
+
+ test_result = TEST_RESULT_POISON;
+ test_expect("a caller that is not a module may look a block pool up by name",
+ (ULONG) _txm_module_manager_object_pointer_get_extended((UINT) TXM_BLOCK_POOL_OBJECT, test_probe_name,
+ ((UINT) 6), &test_result),
+ (ULONG) TX_SUCCESS);
+ test_expect("...and gets the block pool it named",
+ (ULONG) (test_result == (VOID *) &test_block_pool_objects[0]), (ULONG) TX_TRUE);
+
+ test_result = TEST_RESULT_POISON;
+ test_expect("a caller that is not a module may look a byte pool up by name",
+ (ULONG) _txm_module_manager_object_pointer_get_extended((UINT) TXM_BYTE_POOL_OBJECT, test_probe_name,
+ ((UINT) 6), &test_result),
+ (ULONG) TX_SUCCESS);
+ test_expect("...and gets the byte pool it named",
+ (ULONG) (test_result == (VOID *) &test_byte_pool_objects[0]), (ULONG) TX_TRUE);
+
+ /* An object with no name at all is on the list like any other, and the comparison has
+ to refuse it without dereferencing the null. */
+
+ test_arena_fill();
+ test_lists_clear();
+ memcpy((VOID *) test_probe_name, "target", (size_t) 7);
+ test_object_create((UINT) TXM_SEMAPHORE_OBJECT, (VOID *) &test_semaphore_objects[0], TX_NULL);
+ test_object_create((UINT) TXM_SEMAPHORE_OBJECT, (VOID *) &test_semaphore_objects[1], test_probe_name);
+
+ name = (TEST_DATA_START + TEST_REGION_BYTES) - ((ULONG) 8);
+ memcpy((VOID *) name, "target", (size_t) 7);
+
+ test_lookup_extended(&test_protected_module, (ALIGN_TYPE) TXM_SEMAPHORE_OBJECT, name, ((UINT) 6), TEST_EXTRA_PARAMETERS);
+ test_expect("an unnamed object on the list is passed over", (ULONG) test_return_value, (ULONG) TX_SUCCESS);
+ test_expect("...and the named one behind it is found",
+ (ULONG) (test_result == (VOID *) &test_semaphore_objects[1]), (ULONG) TX_TRUE);
+
+ /**********************************************************************/
+ /* The object the module was looking for is still found. */
+ /**********************************************************************/
+
+ /* A name that fits, matching an object whose name is a string of that length, comes
+ back -- and the first match on the list is the one that comes back, which is the
+ property the probing cases above rely on. */
+
+ test_arena_fill();
+ test_lists_clear();
+
+ name = (TEST_DATA_START + TEST_REGION_BYTES) - ((ULONG) 8);
+ memcpy((VOID *) name, "object", (size_t) 7);
+ memcpy((VOID *) test_probe_name, "object", (size_t) 7);
+
+ test_object_create((UINT) TXM_SEMAPHORE_OBJECT, (VOID *) &test_decoy_semaphore, "objec");
+ test_object_create((UINT) TXM_SEMAPHORE_OBJECT, (VOID *) &test_semaphore, test_probe_name);
+
+ test_lookup_extended(&test_protected_module, (ALIGN_TYPE) TXM_SEMAPHORE_OBJECT, name, ((UINT) 6), TEST_EXTRA_PARAMETERS);
+ test_expect("a name that fits finds the object it names", (ULONG) test_return_value, (ULONG) TX_SUCCESS);
+ test_expect("...and it is the matching object, not the one whose name is a prefix of it",
+ (ULONG) (test_result == (VOID *) &test_semaphore), (ULONG) TX_TRUE);
+
+ /* The same search declared one character short stops before the end of the name and
+ finds nothing, which is the comparison obeying the length rather than the string. */
+
+ test_lookup_extended(&test_protected_module, (ALIGN_TYPE) TXM_SEMAPHORE_OBJECT, name, ((UINT) 5), TEST_EXTRA_PARAMETERS);
+ test_expect("the same search one character short finds nothing", (ULONG) test_return_value, (ULONG) TX_NO_INSTANCE);
+ test_expect("...and leaves the module's result slot null", (ULONG) (test_result == TX_NULL), (ULONG) TX_TRUE);
+
+ /**********************************************************************/
+ /* A module without memory protection is unaffected. */
+ /**********************************************************************/
+
+ /* Every check the fix touches is inside the dispatchers' memory protection block. A
+ module loaded without protection reaches the lookup with no range check of any kind,
+ before the fix and after it, and these cases are here so that a later change cannot
+ quietly start refusing its requests. */
+
+ for (length_index = ((UINT) 0); length_index < ((UINT) TEST_NAME_LENGTH_COUNT); length_index++)
+ {
+ name_length = test_name_lengths[length_index];
+
+ snprintf(description, sizeof(description),
+ "an unprotected module's %u-character name is accepted with one byte of room", name_length);
+ test_expect(description,
+ test_measure_validated_extent(&test_unprotected_module, TEST_DATA_START, name_length),
+ ((ULONG) 0));
+
+ snprintf(description, sizeof(description),
+ "an unprotected module's %u-character name is read to its declared end wherever it sits", name_length);
+ test_expect(description,
+ test_measure_read_depth(&test_unprotected_module, TEST_DATA_START, ((ULONG) 1),
+ name_length, ((ULONG) name_length) + ((ULONG) 2)),
+ (ULONG) name_length);
+ }
+
+ test_arena_fill();
+ test_lists_clear();
+ test_lookup_extended(&test_unprotected_module, (ALIGN_TYPE) TXM_SEMAPHORE_OBJECT, TEST_DATA_START,
+ ((UINT) 31), (ALIGN_TYPE *) test_probe_name);
+ test_expect("an unprotected module's extra parameter array is accepted outside every region it owns",
+ (ULONG) test_accepted(), (ULONG) TX_TRUE);
+
+ /**********************************************************************/
+ /* What the whole run has to have held. */
+ /**********************************************************************/
+
+ /* The lookup raises the preemption hold and lowers it again around every search, and
+ the port shim counts the interrupt lock it takes to do so. A search that returned
+ without giving either back is a fault path this test cannot take, and one that never
+ took them is a search that did not happen. */
+
+ test_expect("the interrupt lock is balanced", (ULONG) test_interrupt_disable_depth, ((ULONG) 0));
+ test_expect("the interrupt lock never underflowed", (ULONG) test_interrupt_restore_underflows, ((ULONG) 0));
+ test_expect("the interrupt lock was never nested", (ULONG) test_interrupt_disable_max_depth, ((ULONG) 1));
+ test_expect("the preemption hold is balanced", (ULONG) _tx_thread_preempt_disable, ((ULONG) 0));
+
+ test_expect("every case ran", (ULONG) (test_checks > ((ULONG) 1000)), (ULONG) TX_TRUE);
+
+ if (test_failures == 0U)
+ {
+ printf("SUCCESS! %lu expectations\n", (unsigned long) test_checks);
+ return(0);
+ }
+
+ printf("ERROR: %u expectation(s) failed of %lu\n", test_failures, (unsigned long) test_checks);
+ return(1);
+}
diff --git a/test/tx/module_manager/threadx_module_manager_queue_message_range_test.c b/test/tx/module_manager/threadx_module_manager_queue_message_range_test.c
new file mode 100644
index 00000000..a4f55920
--- /dev/null
+++ b/test/tx/module_manager/threadx_module_manager_queue_message_range_test.c
@@ -0,0 +1,836 @@
+/***************************************************************************
+ * 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
+ **************************************************************************/
+
+
+/**************************************************************************/
+/**************************************************************************/
+/** */
+/** ThreadX Test */
+/** */
+/** Module Manager queue message range validation */
+/** */
+/**************************************************************************/
+/**************************************************************************/
+
+/* A memory-protected module's queue request arrives at the Module Manager's dispatch
+ layer, which decides how much of the module's buffer the privileged queue copy is
+ allowed to touch and hands that extent to the port's range check. A queue's
+ tx_queue_message_size is a count of ULONGs, and the copy moves that many words, so
+ the extent is message_size * sizeof(ULONG) bytes. Front-send passed the word count
+ itself, which is a quarter of that on every target ThreadX supports, so a buffer
+ ending message_size bytes into the module's permitted memory was accepted and then
+ read message_size * sizeof(ULONG) bytes -- 3 * message_size bytes past the end of
+ what the module may reach. Send and receive have always multiplied.
+
+ This test drives the three queue dispatchers directly and measures the extent each
+ one validates, rather than checking a handful of cases either side of it. For a
+ given message size it walks the number of bytes left between the buffer and the end
+ of the module's region and finds the smallest number the dispatcher accepts. That
+ number is the extent, and it must be message_size * sizeof(ULONG) for all three
+ services and for every region a queue buffer may legitimately live in.
+
+ Two things make a dispatcher testable on the host at all.
+
+ The dispatch table is a header of static functions, one per kernel service, each
+ wrapped in #ifndef TXM_<SERVICE>_CALL_NOT_USED so that an integrator can refuse a
+ service to modules. There are 96 of those guards. Defining the 93 this test does not
+ exercise compiles the header down to the three queue services under test, which is
+ what keeps the link to a handful of stubs instead of the hundred-odd kernel entry
+ points the whole table reaches. It also exercises that conditional compilation,
+ which nothing else in the tree does.
+
+ The extent has to reach a check that honours it. The manager's portable checks do,
+ and so does the inline data check of the Cortex-M module ports, which is the shape
+ the memory-protected module ports share: an address range is inside the module's
+ data, or inside a shared memory region registered to it, or it is not accepted. This
+ test therefore compiles against a Cortex-M module port's headers. The Cortex-A7
+ port's data check is a function that takes the pointer alone and translates one
+ address, so on that port no extent -- right or wrong -- reaches anything, and a test
+ built on it would pass equally before and after this fix. That check is being
+ corrected separately; the defect this test covers is in the portable dispatch layer
+ and was live on every other memory-protected module port.
+
+ The three regions the cases below use are the three a queue buffer can be in. A read
+ source may be in the module's writable data, in a shared region, or in the module's
+ read-only image, since a module sending a constant message sends it out of its own
+ .rodata. A receive destination may not be in the read-only image, and the case that
+ asks for that must be refused however much room is left. */
+
+#include <stdio.h>
+#include <string.h>
+
+#include "threadx_module_manager_host_test_port.h"
+
+#include "tx_thread.h"
+#include "tx_timer.h"
+#include "tx_queue.h"
+#include "tx_event_flags.h"
+#include "tx_semaphore.h"
+#include "tx_mutex.h"
+#include "tx_block_pool.h"
+#include "tx_byte_pool.h"
+#include "txm_module.h"
+#include "txm_module_manager_util.h"
+
+
+/* Refuse every service in the dispatch table except the three under test. The list is
+ the header's own guard names; a service added to the table appears here as a link
+ error naming the kernel entry point it reaches, which is the right way to find out. */
+
+#define TXM_BLOCK_ALLOCATE_CALL_NOT_USED
+#define TXM_BLOCK_POOL_CREATE_CALL_NOT_USED
+#define TXM_BLOCK_POOL_DELETE_CALL_NOT_USED
+#define TXM_BLOCK_POOL_INFO_GET_CALL_NOT_USED
+#define TXM_BLOCK_POOL_PERFORMANCE_INFO_GET_CALL_NOT_USED
+#define TXM_BLOCK_POOL_PERFORMANCE_SYSTEM_INFO_GET_CALL_NOT_USED
+#define TXM_BLOCK_POOL_PRIORITIZE_CALL_NOT_USED
+#define TXM_BLOCK_RELEASE_CALL_NOT_USED
+#define TXM_BYTE_ALLOCATE_CALL_NOT_USED
+#define TXM_BYTE_POOL_CREATE_CALL_NOT_USED
+#define TXM_BYTE_POOL_DELETE_CALL_NOT_USED
+#define TXM_BYTE_POOL_INFO_GET_CALL_NOT_USED
+#define TXM_BYTE_POOL_PERFORMANCE_INFO_GET_CALL_NOT_USED
+#define TXM_BYTE_POOL_PERFORMANCE_SYSTEM_INFO_GET_CALL_NOT_USED
+#define TXM_BYTE_POOL_PRIORITIZE_CALL_NOT_USED
+#define TXM_BYTE_RELEASE_CALL_NOT_USED
+#define TXM_EVENT_FLAGS_CREATE_CALL_NOT_USED
+#define TXM_EVENT_FLAGS_DELETE_CALL_NOT_USED
+#define TXM_EVENT_FLAGS_GET_CALL_NOT_USED
+#define TXM_EVENT_FLAGS_INFO_GET_CALL_NOT_USED
+#define TXM_EVENT_FLAGS_PERFORMANCE_INFO_GET_CALL_NOT_USED
+#define TXM_EVENT_FLAGS_PERFORMANCE_SYSTEM_INFO_GET_CALL_NOT_USED
+#define TXM_EVENT_FLAGS_SET_CALL_NOT_USED
+#define TXM_EVENT_FLAGS_SET_NOTIFY_CALL_NOT_USED
+#define TXM_MUTEX_CREATE_CALL_NOT_USED
+#define TXM_MUTEX_DELETE_CALL_NOT_USED
+#define TXM_MUTEX_GET_CALL_NOT_USED
+#define TXM_MUTEX_INFO_GET_CALL_NOT_USED
+#define TXM_MUTEX_PERFORMANCE_INFO_GET_CALL_NOT_USED
+#define TXM_MUTEX_PERFORMANCE_SYSTEM_INFO_GET_CALL_NOT_USED
+#define TXM_MUTEX_PRIORITIZE_CALL_NOT_USED
+#define TXM_MUTEX_PUT_CALL_NOT_USED
+#define TXM_QUEUE_CREATE_CALL_NOT_USED
+#define TXM_QUEUE_DELETE_CALL_NOT_USED
+#define TXM_QUEUE_FLUSH_CALL_NOT_USED
+#define TXM_QUEUE_INFO_GET_CALL_NOT_USED
+#define TXM_QUEUE_PERFORMANCE_INFO_GET_CALL_NOT_USED
+#define TXM_QUEUE_PERFORMANCE_SYSTEM_INFO_GET_CALL_NOT_USED
+#define TXM_QUEUE_PRIORITIZE_CALL_NOT_USED
+#define TXM_QUEUE_SEND_NOTIFY_CALL_NOT_USED
+#define TXM_SEMAPHORE_CEILING_PUT_CALL_NOT_USED
+#define TXM_SEMAPHORE_CREATE_CALL_NOT_USED
+#define TXM_SEMAPHORE_DELETE_CALL_NOT_USED
+#define TXM_SEMAPHORE_GET_CALL_NOT_USED
+#define TXM_SEMAPHORE_INFO_GET_CALL_NOT_USED
+#define TXM_SEMAPHORE_PERFORMANCE_INFO_GET_CALL_NOT_USED
+#define TXM_SEMAPHORE_PERFORMANCE_SYSTEM_INFO_GET_CALL_NOT_USED
+#define TXM_SEMAPHORE_PRIORITIZE_CALL_NOT_USED
+#define TXM_SEMAPHORE_PUT_CALL_NOT_USED
+#define TXM_SEMAPHORE_PUT_NOTIFY_CALL_NOT_USED
+#define TXM_THREAD_CREATE_CALL_NOT_USED
+#define TXM_THREAD_DELETE_CALL_NOT_USED
+#define TXM_THREAD_ENTRY_EXIT_NOTIFY_CALL_NOT_USED
+#define TXM_THREAD_IDENTIFY_CALL_NOT_USED
+#define TXM_THREAD_INFO_GET_CALL_NOT_USED
+#define TXM_THREAD_INTERRUPT_CONTROL_CALL_NOT_USED
+#define TXM_THREAD_PERFORMANCE_INFO_GET_CALL_NOT_USED
+#define TXM_THREAD_PERFORMANCE_SYSTEM_INFO_GET_CALL_NOT_USED
+#define TXM_THREAD_PREEMPTION_CHANGE_CALL_NOT_USED
+#define TXM_THREAD_PRIORITY_CHANGE_CALL_NOT_USED
+#define TXM_THREAD_RELINQUISH_CALL_NOT_USED
+#define TXM_THREAD_RESET_CALL_NOT_USED
+#define TXM_THREAD_RESUME_CALL_NOT_USED
+#define TXM_THREAD_SLEEP_CALL_NOT_USED
+#define TXM_THREAD_STACK_ERROR_NOTIFY_CALL_NOT_USED
+#define TXM_THREAD_SUSPEND_CALL_NOT_USED
+#define TXM_THREAD_SYSTEM_SUSPEND_CALL_NOT_USED
+#define TXM_THREAD_TERMINATE_CALL_NOT_USED
+#define TXM_THREAD_TIME_SLICE_CHANGE_CALL_NOT_USED
+#define TXM_THREAD_WAIT_ABORT_CALL_NOT_USED
+#define TXM_TIME_GET_CALL_NOT_USED
+#define TXM_TIME_SET_CALL_NOT_USED
+#define TXM_TIMER_ACTIVATE_CALL_NOT_USED
+#define TXM_TIMER_CHANGE_CALL_NOT_USED
+#define TXM_TIMER_CREATE_CALL_NOT_USED
+#define TXM_TIMER_DEACTIVATE_CALL_NOT_USED
+#define TXM_TIMER_DELETE_CALL_NOT_USED
+#define TXM_TIMER_INFO_GET_CALL_NOT_USED
+#define TXM_TIMER_PERFORMANCE_INFO_GET_CALL_NOT_USED
+#define TXM_TIMER_PERFORMANCE_SYSTEM_INFO_GET_CALL_NOT_USED
+#define TXM_TRACE_BUFFER_FULL_NOTIFY_CALL_NOT_USED
+#define TXM_TRACE_DISABLE_CALL_NOT_USED
+#define TXM_TRACE_ENABLE_CALL_NOT_USED
+#define TXM_TRACE_EVENT_FILTER_CALL_NOT_USED
+#define TXM_TRACE_EVENT_UNFILTER_CALL_NOT_USED
+#define TXM_TRACE_INTERRUPT_CONTROL_CALL_NOT_USED
+#define TXM_TRACE_ISR_ENTER_INSERT_CALL_NOT_USED
+#define TXM_TRACE_ISR_EXIT_INSERT_CALL_NOT_USED
+#define TXM_TRACE_USER_EVENT_INSERT_CALL_NOT_USED
+#define TXM_MODULE_OBJECT_ALLOCATE_CALL_NOT_USED
+#define TXM_MODULE_OBJECT_DEALLOCATE_CALL_NOT_USED
+#define TXM_MODULE_OBJECT_POINTER_GET_CALL_NOT_USED
+#define TXM_MODULE_OBJECT_POINTER_GET_EXTENDED_CALL_NOT_USED
+
+#include "txm_module_manager_dispatch.h"
+
+/* The three under test have to have survived that, and the rest have to have gone. */
+
+#if defined(TXM_QUEUE_FRONT_SEND_CALL_NOT_USED) || defined(TXM_QUEUE_SEND_CALL_NOT_USED) || defined(TXM_QUEUE_RECEIVE_CALL_NOT_USED)
+#error "the queue services under test must be compiled in"
+#endif
+
+#ifndef TXM_QUEUE_FLUSH_CALL_NOT_USED
+#error "the services this test does not exercise must be compiled out"
+#endif
+
+
+/* Define the stand-in interrupt lock the port shim counts through. */
+
+unsigned int test_interrupt_disable_depth;
+unsigned int test_interrupt_disable_max_depth;
+unsigned int test_interrupt_restore_underflows;
+
+
+/* Define the manager state the utility source under test links against. The object
+ pool plays no part in a queue request: the object check these dispatchers run asks
+ only that the queue lie outside the module. */
+
+TX_BYTE_POOL _txm_module_manager_object_pool;
+UINT _txm_module_manager_object_pool_created;
+
+
+/* The kernel's created lists. A created object lives on the list for its type, and
+ the manager reads those lists, so a test that stands in for the kernel has to
+ provide them. All stay empty here, because this test creates no kernel object; it
+ exercises the range a queue request hands the port's buffer check. */
+
+TX_BLOCK_POOL *_tx_block_pool_created_ptr;
+ULONG _tx_block_pool_created_count;
+TX_BYTE_POOL *_tx_byte_pool_created_ptr;
+ULONG _tx_byte_pool_created_count;
+TX_EVENT_FLAGS_GROUP *_tx_event_flags_created_ptr;
+ULONG _tx_event_flags_created_count;
+TX_MUTEX *_tx_mutex_created_ptr;
+ULONG _tx_mutex_created_count;
+TX_QUEUE *_tx_queue_created_ptr;
+ULONG _tx_queue_created_count;
+TX_SEMAPHORE *_tx_semaphore_created_ptr;
+ULONG _tx_semaphore_created_count;
+TX_THREAD *_tx_thread_created_ptr;
+ULONG _tx_thread_created_count;
+TX_TIMER *_tx_timer_created_ptr;
+ULONG _tx_timer_created_count;
+
+
+/* Stand in for the module port's load-time alignment adjustment, which the utility
+ source declares and no case here reaches. */
+
+VOID _txm_module_manager_alignment_adjust(TXM_MODULE_PREAMBLE *module_preamble, ULONG *code_size,
+ ULONG *code_alignment, ULONG *data_size, ULONG *data_alignment)
+{
+ (VOID) module_preamble;
+ (VOID) code_size;
+ (VOID) code_alignment;
+ (VOID) data_size;
+ (VOID) data_alignment;
+}
+
+
+static UINT test_failures;
+static ULONG test_checks;
+
+
+/* Record the outcome of one expectation. */
+static VOID test_expect(const char *description, ULONG actual, ULONG expected)
+{
+ test_checks++;
+
+ if (actual != expected)
+ {
+ printf("FAIL: %s (expected %lu, got %lu)\n", description, (unsigned long) expected, (unsigned long) actual);
+ test_failures++;
+ }
+}
+
+
+/* Model the module's memory as one arena, so that the three regions a queue buffer may
+ legitimately live in have known addresses and known gaps between them. The gaps are
+ memory the module may not reach: they are what a buffer anchored to the end of a
+ region overruns into, and they are inside the arena so that the overrun a case
+ describes is memory this test owns. */
+
+#define TEST_REGION_BYTES ((ULONG) 256)
+#define TEST_GAP_BYTES ((ULONG) 64)
+
+static union
+{
+ ALIGN_TYPE test_arena_alignment;
+ UCHAR test_arena_bytes[(((ULONG) 3) * TEST_REGION_BYTES) + (((ULONG) 4) * TEST_GAP_BYTES)];
+} test_arena;
+
+#define TEST_DATA_START (&test_arena.test_arena_bytes[TEST_GAP_BYTES])
+#define TEST_CODE_START (&test_arena.test_arena_bytes[(((ULONG) 2) * TEST_GAP_BYTES) + TEST_REGION_BYTES])
+#define TEST_SHARED_START (&test_arena.test_arena_bytes[(((ULONG) 3) * TEST_GAP_BYTES) + (((ULONG) 2) * TEST_REGION_BYTES)])
+
+
+/* Define the queue the requests name. It is outside the arena, which is what the
+ object check requires of a kernel object a module uses. */
+
+static TX_QUEUE test_queue;
+
+
+/* Define the requesting modules. One has memory protection, and is the one every
+ range case below drives; the other does not, and is how the cases show that the
+ dispatch layer's buffer checks are not reached for it at all. */
+
+static TXM_MODULE_INSTANCE test_protected_module;
+static TXM_MODULE_INSTANCE test_unprotected_module;
+
+
+/* Place a module's regions on the arena. */
+static VOID test_module_setup(TXM_MODULE_INSTANCE *module_instance, ULONG property_flags)
+{
+ memset((VOID *) module_instance, 0, sizeof(TXM_MODULE_INSTANCE));
+
+ module_instance -> txm_module_instance_property_flags = property_flags;
+
+ module_instance -> txm_module_instance_data_start = (VOID *) TEST_DATA_START;
+ module_instance -> txm_module_instance_data_end = (VOID *) (TEST_DATA_START + (TEST_REGION_BYTES - ((ULONG) 1)));
+
+ module_instance -> txm_module_instance_code_start = (VOID *) TEST_CODE_START;
+ module_instance -> txm_module_instance_code_end = (VOID *) (TEST_CODE_START + (TEST_REGION_BYTES - ((ULONG) 1)));
+
+ module_instance -> txm_module_instance_shared_memory_address = (ULONG) TEST_SHARED_START;
+ module_instance -> txm_module_instance_shared_memory_length = TEST_REGION_BYTES;
+}
+
+
+/* Record what the privileged queue services were handed.
+
+ Whether the service was reached at all is half of every expectation below: a refusal
+ that still calls the service has refused nothing, and it is the service that reads or
+ writes the caller's buffer. */
+
+static ULONG test_service_calls;
+static ALIGN_TYPE test_service_queue_ptr;
+static ALIGN_TYPE test_service_buffer_ptr;
+static ULONG test_service_wait_option;
+
+
+/* Stand in for the three privileged queue services.
+
+ They keep the two checks the real ones make before they touch the caller's buffer: a
+ queue pointer that is null or carries the wrong ID is refused with TX_QUEUE_ERROR,
+ and a null buffer pointer is refused with TX_PTR_ERROR. The second one matters here.
+ The dispatch layer's buffer checks accept a null buffer pointer -- passing one is how
+ a module asks for an optional output it does not want -- and for these three services
+ nothing is optional, so it is the service's own null check that keeps the extent
+ question from arising at all. A case below drives that. */
+
+static UINT test_service_record(TX_QUEUE *queue_ptr, VOID *buffer_ptr, ULONG wait_option)
+{
+
+UINT status;
+
+
+ test_service_calls++;
+ test_service_queue_ptr = (ALIGN_TYPE) queue_ptr;
+ test_service_buffer_ptr = (ALIGN_TYPE) buffer_ptr;
+ test_service_wait_option = wait_option;
+
+ if (queue_ptr == TX_NULL)
+ {
+ status = TX_QUEUE_ERROR;
+ }
+ else if (queue_ptr -> tx_queue_id != TX_QUEUE_ID)
+ {
+ status = TX_QUEUE_ERROR;
+ }
+ else if (buffer_ptr == TX_NULL)
+ {
+ status = TX_PTR_ERROR;
+ }
+ else
+ {
+ status = TX_SUCCESS;
+ }
+
+ return(status);
+}
+
+
+UINT _txe_queue_front_send(TX_QUEUE *queue_ptr, VOID *source_ptr, ULONG wait_option)
+{
+ return(test_service_record(queue_ptr, source_ptr, wait_option));
+}
+
+
+UINT _txe_queue_send(TX_QUEUE *queue_ptr, VOID *source_ptr, ULONG wait_option)
+{
+ return(test_service_record(queue_ptr, source_ptr, wait_option));
+}
+
+
+UINT _txe_queue_receive(TX_QUEUE *queue_ptr, VOID *destination_ptr, ULONG wait_option)
+{
+ return(test_service_record(queue_ptr, destination_ptr, wait_option));
+}
+
+
+/* Name the three services under test. */
+
+#define TEST_SERVICE_FRONT_SEND ((UINT) 0)
+#define TEST_SERVICE_SEND ((UINT) 1)
+#define TEST_SERVICE_RECEIVE ((UINT) 2)
+#define TEST_SERVICE_COUNT ((UINT) 3)
+
+static const char *test_service_names[TEST_SERVICE_COUNT] =
+{
+ "front-send",
+ "send",
+ "receive"
+};
+
+#define TEST_WAIT_OPTION ((ALIGN_TYPE) 27)
+
+
+/* Make one request of one dispatcher, resetting the record of the service behind it
+ first so that a caller can tell a refusal from an acceptance. */
+static ALIGN_TYPE test_dispatch(UINT service, TXM_MODULE_INSTANCE *module_instance, ALIGN_TYPE queue_ptr, ALIGN_TYPE buffer_ptr)
+{
+
+ALIGN_TYPE return_value;
+
+
+ test_service_calls = ((ULONG) 0);
+ test_service_queue_ptr = ((ALIGN_TYPE) 0);
+ test_service_buffer_ptr = ((ALIGN_TYPE) 0);
+ test_service_wait_option = ((ULONG) 0);
+
+ if (service == TEST_SERVICE_FRONT_SEND)
+ {
+ return_value = _txm_module_manager_tx_queue_front_send_dispatch(module_instance, queue_ptr, buffer_ptr, TEST_WAIT_OPTION);
+ }
+ else if (service == TEST_SERVICE_SEND)
+ {
+ return_value = _txm_module_manager_tx_queue_send_dispatch(module_instance, queue_ptr, buffer_ptr, TEST_WAIT_OPTION);
+ }
+ else
+ {
+ return_value = _txm_module_manager_tx_queue_receive_dispatch(module_instance, queue_ptr, buffer_ptr, TEST_WAIT_OPTION);
+ }
+
+ return(return_value);
+}
+
+
+/* Report whether one request was accepted, checking as it goes that acceptance and
+ refusal are each consistent: a refused request returns TXM_MODULE_INVALID_MEMORY and
+ does not reach the service, and an accepted one reaches it exactly once and is given
+ the caller's own arguments. */
+static UINT test_accepted(UINT service, TXM_MODULE_INSTANCE *module_instance, ALIGN_TYPE queue_ptr, ALIGN_TYPE buffer_ptr)
+{
+
+ALIGN_TYPE return_value;
+UINT accepted;
+
+
+ return_value = test_dispatch(service, module_instance, queue_ptr, buffer_ptr);
+
+ if (test_service_calls == ((ULONG) 0))
+ {
+ accepted = TX_FALSE;
+
+ test_expect("a refused request returns TXM_MODULE_INVALID_MEMORY",
+ (ULONG) return_value, (ULONG) TXM_MODULE_INVALID_MEMORY);
+ }
+ else
+ {
+ accepted = TX_TRUE;
+
+ test_expect("an accepted request reaches the service once", test_service_calls, ((ULONG) 1));
+ test_expect("...with the queue the caller named", (ULONG) test_service_queue_ptr, (ULONG) queue_ptr);
+ test_expect("...the buffer the caller supplied", (ULONG) test_service_buffer_ptr, (ULONG) buffer_ptr);
+ test_expect("...and the wait option the caller chose", test_service_wait_option, (ULONG) TEST_WAIT_OPTION);
+ }
+
+ return(accepted);
+}
+
+
+/* Measure the extent a dispatcher validates.
+
+ The buffer is anchored to the end of one of the module's regions and walked backwards
+ into it a byte at a time, so that the room left between the buffer and the end of the
+ region grows by one on each step. The smallest amount of room the dispatcher accepts
+ is the extent it validated. TEST_NOT_ACCEPTED means it accepted nothing within the
+ region, which is the right answer for a service that may not use that region at all. */
+
+#define TEST_NOT_ACCEPTED ((ULONG) 0xFFFFFFFF)
+
+static ULONG test_measure_extent(UINT service, TXM_MODULE_INSTANCE *module_instance, ULONG message_size, UCHAR *region_start)
+{
+
+ULONG room;
+ULONG walk_limit;
+ULONG extent;
+UCHAR *region_end_plus_one;
+
+
+ test_queue.tx_queue_message_size = message_size;
+
+ region_end_plus_one = region_start + TEST_REGION_BYTES;
+
+ /* Walking past the extent send and receive validate is enough to establish that
+ nothing validates more than they do. */
+ walk_limit = (message_size * ((ULONG) sizeof(ULONG))) + ((ULONG) sizeof(ULONG));
+
+ extent = TEST_NOT_ACCEPTED;
+
+ for (room = ((ULONG) 0); room <= walk_limit; room++)
+ {
+ if (test_accepted(service, module_instance, (ALIGN_TYPE) &test_queue,
+ (ALIGN_TYPE) (region_end_plus_one - room)) == TX_TRUE)
+ {
+ extent = room;
+ break;
+ }
+ }
+
+ return(extent);
+}
+
+
+/* Define a buffer outside every region the module owns, for the cases about a module
+ that has no memory protection and is therefore range-checked nowhere. */
+
+static ULONG test_outside_buffer[TX_QUEUE_MESSAGE_MAX_SIZE];
+
+
+/* The message sizes the cases run through. One word is the smallest a queue can carry
+ and the case where the wrong extent is smallest; the configured maximum is the case
+ where it is largest. TX_QUEUE_MESSAGE_MAX_SIZE is what an integrator sets, so the
+ largest case follows the build rather than a number written here. */
+
+static const ULONG test_message_sizes[] =
+{
+ ((ULONG) 1),
+ ((ULONG) 2),
+ ((ULONG) 3),
+ ((ULONG) TX_QUEUE_MESSAGE_MAX_SIZE)
+};
+
+#define TEST_MESSAGE_SIZE_COUNT (sizeof(test_message_sizes) / sizeof(test_message_sizes[0]))
+
+
+int main(void)
+{
+
+UINT service;
+UINT size_index;
+ULONG message_size;
+ULONG expected_extent;
+ALIGN_TYPE return_value;
+UCHAR *buffer_ptr;
+TX_QUEUE saved_queue;
+char description[160];
+
+
+ memset((VOID *) &test_arena, 0, sizeof(test_arena));
+
+ memset((VOID *) &test_queue, 0, sizeof(test_queue));
+ test_queue.tx_queue_id = TX_QUEUE_ID;
+
+ /* Put the queue on the kernel's created list. The dispatchers authenticate the
+ object a module names against that list before they validate anything about
+ the buffer, so a queue that is not on it is refused before the range check
+ this test is about is ever reached. */
+ test_queue.tx_queue_created_next = &test_queue;
+ _tx_queue_created_ptr = &test_queue;
+ _tx_queue_created_count = ((ULONG) 1);
+
+ test_module_setup(&test_protected_module, (ULONG) TXM_MODULE_MEMORY_PROTECTION);
+ test_module_setup(&test_unprotected_module, ((ULONG) 0));
+
+ printf("Module Manager queue message range validation test\n");
+
+ /**********************************************************************/
+ /* The extent each service validates. */
+ /**********************************************************************/
+
+ /* A queue's message size is a count of ULONGs and the copy moves that many words,
+ so the extent every one of the three services must validate is the message size
+ times the width of a word -- in the module's data, in a shared region registered
+ to it, and, for the two services that read the buffer, in the module's read-only
+ image. */
+
+ for (size_index = ((UINT) 0); size_index < ((UINT) TEST_MESSAGE_SIZE_COUNT); size_index++)
+ {
+ message_size = test_message_sizes[size_index];
+ expected_extent = message_size * ((ULONG) sizeof(ULONG));
+
+ for (service = ((UINT) 0); service < TEST_SERVICE_COUNT; service++)
+ {
+ snprintf(description, sizeof(description),
+ "%s validates %lu * sizeof(ULONG) bytes of a buffer in the module's data",
+ test_service_names[service], (unsigned long) message_size);
+ test_expect(description,
+ test_measure_extent(service, &test_protected_module, message_size, TEST_DATA_START),
+ expected_extent);
+
+ snprintf(description, sizeof(description),
+ "%s validates %lu * sizeof(ULONG) bytes of a buffer in a shared region",
+ test_service_names[service], (unsigned long) message_size);
+ test_expect(description,
+ test_measure_extent(service, &test_protected_module, message_size, TEST_SHARED_START),
+ expected_extent);
+ }
+
+ /* A module sending a constant message sends it out of its own read-only image,
+ which a read source may be in and a receive destination may not. */
+
+ snprintf(description, sizeof(description),
+ "front-send validates %lu * sizeof(ULONG) bytes of a source in the module's read-only image",
+ (unsigned long) message_size);
+ test_expect(description,
+ test_measure_extent(TEST_SERVICE_FRONT_SEND, &test_protected_module, message_size, TEST_CODE_START),
+ expected_extent);
+
+ snprintf(description, sizeof(description),
+ "send validates %lu * sizeof(ULONG) bytes of a source in the module's read-only image",
+ (unsigned long) message_size);
+ test_expect(description,
+ test_measure_extent(TEST_SERVICE_SEND, &test_protected_module, message_size, TEST_CODE_START),
+ expected_extent);
+
+ snprintf(description, sizeof(description),
+ "receive refuses a destination in the module's read-only image at any extent (%lu words)",
+ (unsigned long) message_size);
+ test_expect(description,
+ test_measure_extent(TEST_SERVICE_RECEIVE, &test_protected_module, message_size, TEST_CODE_START),
+ TEST_NOT_ACCEPTED);
+ }
+
+ /**********************************************************************/
+ /* The word count is not the byte count. */
+ /**********************************************************************/
+
+ /* The same statement as the measurements above, made once at the anchor point the
+ defect turned on: a buffer with exactly as many bytes left as the message has
+ words. Front-send accepted that and then read four times as far. */
+
+ for (size_index = ((UINT) 0); size_index < ((UINT) TEST_MESSAGE_SIZE_COUNT); size_index++)
+ {
+ message_size = test_message_sizes[size_index];
+ test_queue.tx_queue_message_size = message_size;
+
+ for (service = ((UINT) 0); service < TEST_SERVICE_COUNT; service++)
+ {
+ buffer_ptr = (TEST_DATA_START + TEST_REGION_BYTES) - message_size;
+
+ snprintf(description, sizeof(description),
+ "%s refuses a %lu-word message given room for the word count in bytes",
+ test_service_names[service], (unsigned long) message_size);
+ test_expect(description,
+ (ULONG) test_accepted(service, &test_protected_module, (ALIGN_TYPE) &test_queue, (ALIGN_TYPE) buffer_ptr),
+ (ULONG) TX_FALSE);
+
+ buffer_ptr = (TEST_DATA_START + TEST_REGION_BYTES) - ((message_size * ((ULONG) sizeof(ULONG))) - ((ULONG) 1));
+
+ snprintf(description, sizeof(description),
+ "%s refuses a buffer one byte short of the whole message", test_service_names[service]);
+ test_expect(description,
+ (ULONG) test_accepted(service, &test_protected_module, (ALIGN_TYPE) &test_queue, (ALIGN_TYPE) buffer_ptr),
+ (ULONG) TX_FALSE);
+
+ buffer_ptr = (TEST_DATA_START + TEST_REGION_BYTES) - (message_size * ((ULONG) sizeof(ULONG)));
+
+ snprintf(description, sizeof(description),
+ "%s accepts a buffer that reaches exactly the end of the region", test_service_names[service]);
+ test_expect(description,
+ (ULONG) test_accepted(service, &test_protected_module, (ALIGN_TYPE) &test_queue, (ALIGN_TYPE) buffer_ptr),
+ (ULONG) TX_TRUE);
+ }
+ }
+
+ /**********************************************************************/
+ /* A refusal changes nothing. */
+ /**********************************************************************/
+
+ test_queue.tx_queue_message_size = (ULONG) TX_QUEUE_MESSAGE_MAX_SIZE;
+ memcpy((VOID *) &saved_queue, (VOID *) &test_queue, sizeof(TX_QUEUE));
+
+ buffer_ptr = (TEST_DATA_START + TEST_REGION_BYTES) - test_queue.tx_queue_message_size;
+
+ for (service = ((UINT) 0); service < TEST_SERVICE_COUNT; service++)
+ {
+ return_value = test_dispatch(service, &test_protected_module, (ALIGN_TYPE) &test_queue, (ALIGN_TYPE) buffer_ptr);
+
+ snprintf(description, sizeof(description), "%s refuses with TXM_MODULE_INVALID_MEMORY", test_service_names[service]);
+ test_expect(description, (ULONG) return_value, (ULONG) TXM_MODULE_INVALID_MEMORY);
+
+ snprintf(description, sizeof(description), "...%s does not reach the queue service", test_service_names[service]);
+ test_expect(description, test_service_calls, ((ULONG) 0));
+
+ snprintf(description, sizeof(description), "...and %s leaves the queue control block untouched", test_service_names[service]);
+ test_expect(description,
+ (ULONG) (memcmp((VOID *) &saved_queue, (VOID *) &test_queue, sizeof(TX_QUEUE)) == 0),
+ (ULONG) TX_TRUE);
+ }
+
+ /**********************************************************************/
+ /* The queue itself still has to be a kernel object outside the module.*/
+ /**********************************************************************/
+
+ /* The message size is read out of the queue, so the extent check depends on the
+ object check having run first. A buffer with all the room in the world does not
+ excuse a queue pointer aimed into the module or at nothing at all. */
+
+ buffer_ptr = TEST_DATA_START;
+
+ for (service = ((UINT) 0); service < TEST_SERVICE_COUNT; service++)
+ {
+ snprintf(description, sizeof(description), "%s refuses a queue inside the module's data", test_service_names[service]);
+ test_expect(description,
+ (ULONG) test_accepted(service, &test_protected_module, (ALIGN_TYPE) TEST_DATA_START, (ALIGN_TYPE) buffer_ptr),
+ (ULONG) TX_FALSE);
+
+ snprintf(description, sizeof(description), "%s refuses a queue inside the module's read-only image", test_service_names[service]);
+ test_expect(description,
+ (ULONG) test_accepted(service, &test_protected_module, (ALIGN_TYPE) TEST_CODE_START, (ALIGN_TYPE) buffer_ptr),
+ (ULONG) TX_FALSE);
+
+ snprintf(description, sizeof(description), "%s refuses a null queue", test_service_names[service]);
+ test_expect(description,
+ (ULONG) test_accepted(service, &test_protected_module, ((ALIGN_TYPE) 0), (ALIGN_TYPE) buffer_ptr),
+ (ULONG) TX_FALSE);
+ }
+
+ /**********************************************************************/
+ /* The null buffer the dispatch layer lets through. */
+ /**********************************************************************/
+
+ /* The dispatch layer's buffer checks accept a null pointer whatever extent they are
+ given, so correcting the extent cannot be what refuses one. For these three
+ services the buffer is not optional and the service refuses it itself, which is
+ why the extent question never arises for a null buffer. */
+
+ test_queue.tx_queue_message_size = (ULONG) TX_QUEUE_MESSAGE_MAX_SIZE;
+
+ for (service = ((UINT) 0); service < TEST_SERVICE_COUNT; service++)
+ {
+ return_value = test_dispatch(service, &test_protected_module, (ALIGN_TYPE) &test_queue, ((ALIGN_TYPE) 0));
+
+ snprintf(description, sizeof(description), "%s passes a null buffer to the queue service", test_service_names[service]);
+ test_expect(description, test_service_calls, ((ULONG) 1));
+
+ snprintf(description, sizeof(description), "...and the %s service refuses it with TX_PTR_ERROR", test_service_names[service]);
+ test_expect(description, (ULONG) return_value, (ULONG) TX_PTR_ERROR);
+ }
+
+ /**********************************************************************/
+ /* A buffer whose extent runs off the end of the address space. */
+ /**********************************************************************/
+
+ /* The manager's checks test for that before comparing anything, so a module cannot
+ reach past the top of memory by wrapping the sum. */
+
+ test_queue.tx_queue_message_size = (ULONG) TX_QUEUE_MESSAGE_MAX_SIZE;
+
+ for (service = ((UINT) 0); service < TEST_SERVICE_COUNT; service++)
+ {
+ snprintf(description, sizeof(description), "%s refuses a buffer whose extent wraps the address space", test_service_names[service]);
+ test_expect(description,
+ (ULONG) test_accepted(service, &test_protected_module, (ALIGN_TYPE) &test_queue,
+ (ALIGN_TYPE) (~((ALIGN_TYPE) 0) - ((ALIGN_TYPE) 3))),
+ (ULONG) TX_FALSE);
+ }
+
+ /**********************************************************************/
+ /* A module without memory protection is unaffected. */
+ /**********************************************************************/
+
+ /* Every check the fix touches sits inside the dispatchers' memory-protection block.
+ A module that was loaded without protection reaches the kernel service with no
+ range check of any kind, before the fix and after it, and these cases are here so
+ that a later change to the extent cannot quietly start refusing its requests. */
+
+ for (size_index = ((UINT) 0); size_index < ((UINT) TEST_MESSAGE_SIZE_COUNT); size_index++)
+ {
+ message_size = test_message_sizes[size_index];
+ test_queue.tx_queue_message_size = message_size;
+
+ for (service = ((UINT) 0); service < TEST_SERVICE_COUNT; service++)
+ {
+ buffer_ptr = (TEST_DATA_START + TEST_REGION_BYTES) - message_size;
+
+ snprintf(description, sizeof(description),
+ "an unprotected module's %lu-word %s is accepted with room for the word count in bytes",
+ (unsigned long) message_size, test_service_names[service]);
+ test_expect(description,
+ (ULONG) test_accepted(service, &test_unprotected_module, (ALIGN_TYPE) &test_queue, (ALIGN_TYPE) buffer_ptr),
+ (ULONG) TX_TRUE);
+
+ snprintf(description, sizeof(description),
+ "an unprotected module's %s is accepted with a buffer outside every region it owns",
+ test_service_names[service]);
+ test_expect(description,
+ (ULONG) test_accepted(service, &test_unprotected_module, (ALIGN_TYPE) &test_queue, (ALIGN_TYPE) test_outside_buffer),
+ (ULONG) TX_TRUE);
+
+ snprintf(description, sizeof(description),
+ "an unprotected module's %s is accepted with a queue inside its own data",
+ test_service_names[service]);
+ test_expect(description,
+ (ULONG) test_accepted(service, &test_unprotected_module, (ALIGN_TYPE) TEST_DATA_START, (ALIGN_TYPE) test_outside_buffer),
+ (ULONG) TX_TRUE);
+
+ /* Nothing screens an unprotected module's arguments before the service, so
+ the service is where a null queue is refused. It is the same for a null
+ buffer, and for a protected module the object check is what refuses the
+ queue first -- which is why these two cases are here and not above. */
+ return_value = test_dispatch(service, &test_unprotected_module, ((ALIGN_TYPE) 0), (ALIGN_TYPE) test_outside_buffer);
+
+ snprintf(description, sizeof(description),
+ "an unprotected module's %s reaches the service with a null queue",
+ test_service_names[service]);
+ test_expect(description, test_service_calls, ((ULONG) 1));
+
+ snprintf(description, sizeof(description),
+ "...and the %s service refuses it with TX_QUEUE_ERROR", test_service_names[service]);
+ test_expect(description, (ULONG) return_value, (ULONG) TX_QUEUE_ERROR);
+ }
+ }
+
+ /**********************************************************************/
+ /* What the whole run has to have held. */
+ /**********************************************************************/
+
+ /* Nothing on these paths disables interrupts, and the shim counts them, so the
+ lock has to have been left exactly as it was found. */
+ test_expect("the interrupt lock is balanced", test_interrupt_disable_depth, ((ULONG) 0));
+ test_expect("the interrupt lock never underflowed", (ULONG) test_interrupt_restore_underflows, ((ULONG) 0));
+
+ test_expect("every case ran", (ULONG) (test_checks > ((ULONG) 500)), (ULONG) TX_TRUE);
+
+ if (test_failures == 0U)
+ {
+ printf("SUCCESS! %lu expectations\n", (unsigned long) test_checks);
+ return(0);
+ }
+
+ printf("ERROR: %u expectation(s) failed of %lu\n", test_failures, (unsigned long) test_checks);
+ return(1);
+}
diff --git a/test/tx/module_manager/threadx_module_manager_thread_kernel_stack_test.c b/test/tx/module_manager/threadx_module_manager_thread_kernel_stack_test.c
new file mode 100644
index 00000000..1ab91145
--- /dev/null
+++ b/test/tx/module_manager/threadx_module_manager_thread_kernel_stack_test.c
@@ -0,0 +1,589 @@
+/***************************************************************************
+ * 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
+ **************************************************************************/
+
+
+/**************************************************************************/
+/**************************************************************************/
+/** */
+/** ThreadX Test */
+/** */
+/** Module thread kernel stack lifetime */
+/** */
+/**************************************************************************/
+/**************************************************************************/
+
+/* One logical thread of a user-mode module costs the object pool two allocations:
+ the TX_THREAD control block the module asks for, and the privileged kernel stack
+ the manager takes on the module's behalf and records in that control block. Both
+ are linked onto the module's allocation list, and only the module's own request
+ is visible to it.
+
+ What this asserts is an equality rather than a bound: the pool's available bytes
+ and the module's allocation-list count must come back to exactly what they were
+ before the thread existed. A bound would pass while one of the two allocations
+ was left behind on every cycle. */
+
+#include <stdio.h>
+
+#include "tx_api.h"
+#include "tx_thread.h"
+#include "tx_timer.h"
+#include "tx_queue.h"
+#include "tx_semaphore.h"
+#include "tx_mutex.h"
+#include "tx_event_flags.h"
+#include "tx_block_pool.h"
+#include "tx_byte_pool.h"
+#include "txm_module.h"
+#include "txm_module_manager_util.h"
+#include "txm_module_manager_dispatch.h"
+
+
+/* A pool small enough that a leak of one kernel stack per cycle exhausts it well
+ inside the cycle count below. */
+#define TEST_POOL_BYTES 12280
+
+/* How many create and delete cycles to run. Far more than the pool can absorb if
+ anything is left behind. */
+#define TEST_CYCLES 200
+
+
+/* The arena the stand-in byte pool hands out of, aligned as the real pool is. */
+static union
+{
+ ULONG test_pool_alignment;
+ UCHAR test_pool_bytes[TEST_POOL_BYTES];
+} test_pool_arena;
+
+/* Bump position, and a free list of blocks given back. A released block is offered
+ again only for a request of exactly its size, which is all the cycles below need
+ and keeps the arena from growing when nothing leaks. */
+static ULONG test_pool_offset;
+
+typedef struct TEST_FREE_BLOCK_STRUCT
+{
+ UCHAR *test_free_start;
+ ULONG test_free_size;
+} TEST_FREE_BLOCK;
+
+#define TEST_FREE_BLOCKS 64
+
+static TEST_FREE_BLOCK test_free_list[TEST_FREE_BLOCKS];
+static ULONG test_free_count;
+
+/* Counts of what the manager asked the pool to do. */
+static ULONG test_allocations;
+static ULONG test_releases;
+
+/* Expectation accounting. */
+static ULONG test_checks;
+static ULONG test_failures;
+
+/* Interrupt lock accounting, referenced by the force-included port shim. */
+unsigned int test_interrupt_disable_depth;
+unsigned int test_interrupt_disable_max_depth;
+unsigned int test_interrupt_restore_underflows;
+
+/* The kernel's created lists. A created object lives on the list for its type, and
+ the manager reads those lists, so a test that stands in for the kernel has to
+ provide them. Only the thread list is populated here, by the thread this test
+ creates; the rest stay empty because nothing here creates an object of that type. */
+TX_BLOCK_POOL *_tx_block_pool_created_ptr;
+ULONG _tx_block_pool_created_count;
+TX_BYTE_POOL *_tx_byte_pool_created_ptr;
+ULONG _tx_byte_pool_created_count;
+TX_EVENT_FLAGS_GROUP *_tx_event_flags_created_ptr;
+ULONG _tx_event_flags_created_count;
+TX_MUTEX *_tx_mutex_created_ptr;
+ULONG _tx_mutex_created_count;
+TX_QUEUE *_tx_queue_created_ptr;
+ULONG _tx_queue_created_count;
+TX_SEMAPHORE *_tx_semaphore_created_ptr;
+ULONG _tx_semaphore_created_count;
+TX_THREAD *_tx_thread_created_ptr;
+ULONG _tx_thread_created_count;
+TX_TIMER *_tx_timer_created_ptr;
+ULONG _tx_timer_created_count;
+
+/* The manager globals the sources under test refer to. */
+TX_BYTE_POOL _txm_module_manager_object_pool;
+UINT _txm_module_manager_object_pool_created;
+TX_MUTEX _txm_module_manager_mutex;
+TX_THREAD *_tx_thread_current_ptr;
+
+/* The thread the requests are made on behalf of. */
+static TX_THREAD test_current_thread;
+
+/* What the stand-in thread delete was told, and what it should report. */
+static TX_THREAD *test_deleted_thread;
+static UINT test_delete_status;
+
+
+/* Record the outcome of one expectation. */
+static VOID test_expect(const char *description, ULONG actual, ULONG expected)
+{
+
+ test_checks++;
+
+ if (actual != expected)
+ {
+ printf("FAIL: %s (expected %lu, got %lu)\n", description,
+ (unsigned long) expected, (unsigned long) actual);
+ test_failures++;
+ }
+}
+
+
+/* Stand in for the byte pool. Requests come only from the manager's allocator, and
+ the accounting mirrors the real one closely enough for an equality assertion:
+ available falls by exactly what was handed out and rises by exactly what came
+ back. */
+UINT _txe_byte_allocate(TX_BYTE_POOL *pool_ptr, VOID **memory_ptr, ULONG memory_size, ULONG wait_option)
+{
+
+ULONG index;
+UCHAR *block_start;
+
+
+ (VOID) wait_option;
+
+ if ((pool_ptr != &_txm_module_manager_object_pool) || (memory_ptr == TX_NULL))
+ {
+ return(TX_PTR_ERROR);
+ }
+
+ block_start = TX_NULL;
+
+ /* Offer a block of exactly this size back before taking new arena. */
+ for (index = 0; index < test_free_count; index++)
+ {
+ if (test_free_list[index].test_free_size == memory_size)
+ {
+ block_start = test_free_list[index].test_free_start;
+
+ test_free_count--;
+ test_free_list[index] = test_free_list[test_free_count];
+ break;
+ }
+ }
+
+ if (block_start == TX_NULL)
+ {
+
+ /* Keep every block aligned the way the real pool does. */
+ ULONG rounded_size = (memory_size + (sizeof(ULONG) - 1U)) & (~(sizeof(ULONG) - 1U));
+
+ if ((test_pool_offset + rounded_size) > ((ULONG) TEST_POOL_BYTES))
+ {
+ return(TX_NO_MEMORY);
+ }
+
+ block_start = &test_pool_arena.test_pool_bytes[test_pool_offset];
+ test_pool_offset = test_pool_offset + rounded_size;
+ }
+
+ _txm_module_manager_object_pool.tx_byte_pool_available =
+ _txm_module_manager_object_pool.tx_byte_pool_available - memory_size;
+
+ test_allocations++;
+
+ *memory_ptr = (VOID *) block_start;
+
+ return(TX_SUCCESS);
+}
+
+
+/* Stand in for the release side. An address outside the arena is refused, which is
+ what makes a release of memory the pool never handed out visible as a failure
+ rather than as silent bookkeeping. */
+UINT _txe_byte_release(VOID *memory_ptr)
+{
+
+UCHAR *block_start;
+ULONG index;
+
+
+ block_start = (UCHAR *) memory_ptr;
+
+ if ((block_start < test_pool_arena.test_pool_bytes) ||
+ (block_start >= &test_pool_arena.test_pool_bytes[TEST_POOL_BYTES]))
+ {
+ return(TX_PTR_ERROR);
+ }
+
+ /* The size is the one recorded in the manager's header, which sits at the start
+ of the block, plus the header itself. */
+ {
+ TXM_MODULE_ALLOCATED_OBJECT *header_ptr;
+ ULONG block_size;
+
+ header_ptr = (TXM_MODULE_ALLOCATED_OBJECT *) memory_ptr;
+ block_size = header_ptr -> txm_module_object_size + (ULONG) sizeof(TXM_MODULE_ALLOCATED_OBJECT);
+
+ _txm_module_manager_object_pool.tx_byte_pool_available =
+ _txm_module_manager_object_pool.tx_byte_pool_available + block_size;
+
+ if (test_free_count < ((ULONG) TEST_FREE_BLOCKS))
+ {
+ index = test_free_count;
+ test_free_list[index].test_free_start = block_start;
+ test_free_list[index].test_free_size = block_size;
+ test_free_count++;
+ }
+ }
+
+ test_releases++;
+
+ return(TX_SUCCESS);
+}
+
+
+/* Stand in for the module port's data-range check. Nothing here is inside a module's
+ own data, so every address the manager asks about is outside it. */
+UINT _txm_module_manager_inside_data_check(TXM_MODULE_INSTANCE *module_instance, ALIGN_TYPE obj_ptr,
+ ULONG obj_size, UINT write_request)
+{
+ (VOID) module_instance;
+ (VOID) obj_ptr;
+ (VOID) obj_size;
+ (VOID) write_request;
+
+ return(TX_FALSE);
+}
+
+
+/* No module data is modelled here, so every range lies outside it. */
+UINT _txm_module_manager_outside_data_check(TXM_MODULE_INSTANCE *module_instance, ALIGN_TYPE obj_ptr,
+ ULONG obj_size)
+{
+ (VOID) module_instance;
+ (VOID) obj_ptr;
+ (VOID) obj_size;
+
+ return(TX_TRUE);
+}
+
+
+/* Stand in for the module port's alignment adjustment. It is referenced by a
+ loading path this test does not drive, and is never reached from here. */
+VOID _txm_module_manager_alignment_adjust(TXM_MODULE_PREAMBLE *module_preamble, ULONG *code_size,
+ ULONG *code_alignment, ULONG *data_size, ULONG *data_alignment)
+{
+
+ (VOID) module_preamble;
+ (VOID) code_size;
+ (VOID) code_alignment;
+ (VOID) data_size;
+ (VOID) data_alignment;
+}
+
+
+UINT _txe_mutex_get(TX_MUTEX *mutex_ptr, ULONG wait_option)
+{
+
+ (VOID) mutex_ptr;
+ (VOID) wait_option;
+
+ return(TX_SUCCESS);
+}
+
+
+UINT _txe_mutex_put(TX_MUTEX *mutex_ptr)
+{
+
+ (VOID) mutex_ptr;
+
+ return(TX_SUCCESS);
+}
+
+
+/* Stand in for the thread delete the dispatcher calls. It records what it was given
+ and reports what the case under test wants, so that the dispatcher's behaviour
+ either side of a refusal can be asserted. */
+UINT _txe_thread_delete(TX_THREAD *thread_ptr)
+{
+
+ test_deleted_thread = thread_ptr;
+
+ /* A successful delete takes the thread off the kernel's created list. */
+ if (test_delete_status == TX_SUCCESS)
+ {
+ _tx_thread_created_ptr = TX_NULL;
+ _tx_thread_created_count = (ULONG) 0;
+ }
+
+ return(test_delete_status);
+}
+
+
+/* Put the pool, the module and the counters back to a known state. */
+static VOID test_reset(TXM_MODULE_INSTANCE *module_ptr, ULONG property_flags)
+{
+
+ULONG index;
+
+
+ for (index = 0; index < ((ULONG) TEST_POOL_BYTES); index++)
+ {
+ test_pool_arena.test_pool_bytes[index] = (UCHAR) 0;
+ }
+
+ test_pool_offset = 0;
+ test_free_count = 0;
+ test_allocations = 0;
+ test_releases = 0;
+
+ test_deleted_thread = TX_NULL;
+ test_delete_status = TX_SUCCESS;
+
+ _txm_module_manager_object_pool.tx_byte_pool_id = TX_BYTE_POOL_ID;
+ _txm_module_manager_object_pool.tx_byte_pool_start = test_pool_arena.test_pool_bytes;
+ _txm_module_manager_object_pool.tx_byte_pool_size = TEST_POOL_BYTES;
+ _txm_module_manager_object_pool.tx_byte_pool_available = TEST_POOL_BYTES;
+ _txm_module_manager_object_pool_created = TX_TRUE;
+
+ TX_MEMSET(module_ptr, 0, sizeof(TXM_MODULE_INSTANCE));
+ module_ptr -> txm_module_instance_property_flags = property_flags;
+
+ TX_MEMSET(&test_current_thread, 0, sizeof(TX_THREAD));
+ test_current_thread.tx_thread_module_instance_ptr = module_ptr;
+ _tx_thread_current_ptr = &test_current_thread;
+}
+
+
+/* Create a module thread the way the manager does: the control block the module
+ asks for, and then, for a user-mode module, the kernel stack the manager takes on
+ its behalf and records in the control block. The stack allocation is the one the
+ module never sees and never asks to have back. */
+static TX_THREAD *test_thread_create(TXM_MODULE_INSTANCE *module_ptr)
+{
+
+TX_THREAD *thread_ptr;
+VOID *stack_ptr;
+
+
+ thread_ptr = TX_NULL;
+
+ if (_txm_module_manager_object_allocate((VOID **) &thread_ptr, (ULONG) sizeof(TX_THREAD),
+ module_ptr) != TX_SUCCESS)
+ {
+ return(TX_NULL);
+ }
+
+ TX_MEMSET(thread_ptr, 0, sizeof(TX_THREAD));
+
+ thread_ptr -> tx_thread_id = TX_THREAD_ID;
+ thread_ptr -> tx_thread_module_instance_ptr = module_ptr;
+
+ /* Put it on the kernel's created list, which is where a created thread lives.
+ One thread exists at a time here, so the list is that thread alone. */
+ thread_ptr -> tx_thread_created_next = thread_ptr;
+ thread_ptr -> tx_thread_created_previous = thread_ptr;
+ _tx_thread_created_ptr = thread_ptr;
+ _tx_thread_created_count = (ULONG) 1;
+
+ if ((module_ptr -> txm_module_instance_property_flags & TXM_MODULE_USER_MODE) != 0U)
+ {
+
+ stack_ptr = TX_NULL;
+
+ if (_txm_module_manager_object_allocate(&stack_ptr, (ULONG) TXM_MODULE_KERNEL_STACK_SIZE,
+ module_ptr) != TX_SUCCESS)
+ {
+ return(TX_NULL);
+ }
+
+ thread_ptr -> tx_thread_module_kernel_stack_start = stack_ptr;
+ }
+
+ return(thread_ptr);
+}
+
+
+int main(void)
+{
+
+TXM_MODULE_INSTANCE module_instance;
+TX_THREAD *thread_ptr;
+ULONG baseline_available;
+ULONG baseline_count;
+ULONG cycle;
+ULONG status;
+
+
+ /**********************************************************************/
+ /* One user-mode thread, created and deleted. */
+ /**********************************************************************/
+
+ test_reset(&module_instance, (ULONG) (TXM_MODULE_USER_MODE | TXM_MODULE_MEMORY_PROTECTION));
+
+ baseline_available = _txm_module_manager_object_pool.tx_byte_pool_available;
+ baseline_count = module_instance.txm_module_instance_object_list_count;
+
+ thread_ptr = test_thread_create(&module_instance);
+
+ test_expect("a user-mode module thread was created",
+ (ULONG) (thread_ptr != TX_NULL), (ULONG) TX_TRUE);
+ test_expect("...costing the pool two allocations", test_allocations, ((ULONG) 2));
+ test_expect("...both on the module's allocation list",
+ module_instance.txm_module_instance_object_list_count, (baseline_count + ((ULONG) 2)));
+ test_expect("...and it holds a kernel stack",
+ (ULONG) (thread_ptr -> tx_thread_module_kernel_stack_start != TX_NULL), (ULONG) TX_TRUE);
+
+ status = (ULONG) _txm_module_manager_tx_thread_delete_dispatch(&module_instance,
+ (ALIGN_TYPE) thread_ptr);
+
+ test_expect("deleting it succeeds", status, (ULONG) TX_SUCCESS);
+ test_expect("...and gives both allocations back", test_releases, ((ULONG) 2));
+ test_expect("...leaving the allocation list as it was",
+ module_instance.txm_module_instance_object_list_count, baseline_count);
+ test_expect("...and the pool's available bytes exactly as they were",
+ _txm_module_manager_object_pool.tx_byte_pool_available, baseline_available);
+
+ /**********************************************************************/
+ /* Many cycles. A leak of one kernel stack per cycle would exhaust */
+ /* this pool long before the count below is reached. */
+ /**********************************************************************/
+
+ test_reset(&module_instance, (ULONG) (TXM_MODULE_USER_MODE | TXM_MODULE_MEMORY_PROTECTION));
+
+ baseline_available = _txm_module_manager_object_pool.tx_byte_pool_available;
+ baseline_count = module_instance.txm_module_instance_object_list_count;
+
+ for (cycle = 0; cycle < ((ULONG) TEST_CYCLES); cycle++)
+ {
+
+ thread_ptr = test_thread_create(&module_instance);
+
+ if (thread_ptr == TX_NULL)
+ {
+ printf("FAIL: the pool was exhausted at cycle %lu\n", (unsigned long) cycle);
+ test_failures++;
+ test_checks++;
+ break;
+ }
+
+ if (_txm_module_manager_tx_thread_delete_dispatch(&module_instance,
+ (ALIGN_TYPE) thread_ptr) != TX_SUCCESS)
+ {
+ printf("FAIL: the delete was refused at cycle %lu\n", (unsigned long) cycle);
+ test_failures++;
+ test_checks++;
+ break;
+ }
+ }
+
+ test_expect("every cycle completed", cycle, ((ULONG) TEST_CYCLES));
+ test_expect("...with the allocation list back where it started",
+ module_instance.txm_module_instance_object_list_count, baseline_count);
+ test_expect("...and the pool's available bytes back where they started",
+ _txm_module_manager_object_pool.tx_byte_pool_available, baseline_available);
+ test_expect("...having released exactly what it allocated", test_releases, test_allocations);
+
+ /**********************************************************************/
+ /* A thread that carries no kernel stack the manager allocated. */
+ /**********************************************************************/
+
+ test_reset(&module_instance, (ULONG) (TXM_MODULE_USER_MODE | TXM_MODULE_MEMORY_PROTECTION));
+
+ baseline_available = _txm_module_manager_object_pool.tx_byte_pool_available;
+
+ thread_ptr = TX_NULL;
+
+ status = (ULONG) _txm_module_manager_object_allocate((VOID **) &thread_ptr,
+ (ULONG) sizeof(TX_THREAD), &module_instance);
+
+ test_expect("a control block with no kernel stack can be allocated", status, (ULONG) TX_SUCCESS);
+
+ TX_MEMSET(thread_ptr, 0, sizeof(TX_THREAD));
+ thread_ptr -> tx_thread_id = TX_THREAD_ID;
+ thread_ptr -> tx_thread_module_instance_ptr = &module_instance;
+ thread_ptr -> tx_thread_created_next = thread_ptr;
+ thread_ptr -> tx_thread_created_previous = thread_ptr;
+ _tx_thread_created_ptr = thread_ptr;
+ _tx_thread_created_count = (ULONG) 1;
+
+ test_expect("...and holds no kernel stack",
+ (ULONG) (thread_ptr -> tx_thread_module_kernel_stack_start == TX_NULL), (ULONG) TX_TRUE);
+
+ test_releases = 0;
+
+ status = (ULONG) _txm_module_manager_tx_thread_delete_dispatch(&module_instance,
+ (ALIGN_TYPE) thread_ptr);
+
+ test_expect("a user-mode module deleting it succeeds", status, (ULONG) TX_SUCCESS);
+ test_expect("...giving back the control block and nothing else", test_releases, ((ULONG) 1));
+ test_expect("...and the pool's available bytes exactly as they were",
+ _txm_module_manager_object_pool.tx_byte_pool_available, baseline_available);
+
+ /**********************************************************************/
+ /* A module that is not in user mode. */
+ /**********************************************************************/
+
+ test_reset(&module_instance, (ULONG) TXM_MODULE_MEMORY_PROTECTION);
+
+ baseline_available = _txm_module_manager_object_pool.tx_byte_pool_available;
+
+ thread_ptr = test_thread_create(&module_instance);
+
+ test_expect("a thread of a module that is not in user mode was created",
+ (ULONG) (thread_ptr != TX_NULL), (ULONG) TX_TRUE);
+ test_expect("...costing the pool one allocation", test_allocations, ((ULONG) 1));
+
+ status = (ULONG) _txm_module_manager_tx_thread_delete_dispatch(&module_instance,
+ (ALIGN_TYPE) thread_ptr);
+
+ test_expect("deleting it succeeds", status, (ULONG) TX_SUCCESS);
+ test_expect("...giving back that one allocation", test_releases, ((ULONG) 1));
+ test_expect("...and the pool's available bytes exactly as they were",
+ _txm_module_manager_object_pool.tx_byte_pool_available, baseline_available);
+
+ /**********************************************************************/
+ /* A refused delete releases nothing. */
+ /**********************************************************************/
+
+ test_reset(&module_instance, (ULONG) (TXM_MODULE_USER_MODE | TXM_MODULE_MEMORY_PROTECTION));
+
+ thread_ptr = test_thread_create(&module_instance);
+ baseline_count = module_instance.txm_module_instance_object_list_count;
+ test_delete_status = TX_DELETE_ERROR;
+
+ status = (ULONG) _txm_module_manager_tx_thread_delete_dispatch(&module_instance,
+ (ALIGN_TYPE) thread_ptr);
+
+ test_expect("a refused delete is reported", status, (ULONG) TX_DELETE_ERROR);
+ test_expect("...and releases nothing", test_releases, ((ULONG) 0));
+ test_expect("...leaving both allocations on the module's list",
+ module_instance.txm_module_instance_object_list_count, baseline_count);
+
+ /**********************************************************************/
+ /* What the whole run has to have held. */
+ /**********************************************************************/
+
+ test_expect("the interrupt lock was left balanced",
+ (ULONG) test_interrupt_disable_depth, ((ULONG) 0));
+ test_expect("...and was never restored more often than it was taken",
+ (ULONG) test_interrupt_restore_underflows, ((ULONG) 0));
+
+ if (test_failures == 0U)
+ {
+ printf("SUCCESS! %lu expectations\n", (unsigned long) test_checks);
+ return(0);
+ }
+
+ printf("ERROR: %lu expectation(s) failed of %lu\n",
+ (unsigned long) test_failures, (unsigned long) test_checks);
+ return(1);
+}
diff --git a/test/tx/regression/testcontrol.c b/test/tx/regression/testcontrol.c
index e2b3f725..bec50ee7 100644
--- a/test/tx/regression/testcontrol.c
+++ b/test/tx/regression/testcontrol.c
@@ -10,7 +10,7 @@
/***************************************************************************/
/* This is the test control routine of the ThreadX kernel. All tests are dispatched from this routine. */
-// Some portions generated by Codex (gpt 5.4).
+// Portions of this file were generated with AI assistance.
#include "tx_api.h"
#include <stdio.h>
@@ -69,6 +69,7 @@ TX_BLOCK_POOL init_block_pool;
ULONG init_block_pool_area[50];
TX_EVENT_FLAGS_GROUP init_event_flags;
TX_TIMER init_timer;
+TX_TIMER init_timer_change;
TX_THREAD init_test_thread;
TX_THREAD second_test_thread;
#ifndef TX_TIMER_PROCESS_IN_ISR
@@ -218,6 +219,7 @@ void threadx_thread_simple_sleep_non_clear_application_define(void *);
void threadx_thread_sleep_for_100ticks_application_define(void *);
void threadx_thread_multiple_sleep_application_define(void *);
void threadx_thread_terminate_delete_application_define(void *);
+void threadx_thread_exit_callback_transition_application_define(void *);
void threadx_thread_preemption_change_application_define(void *);
void threadx_thread_priority_change_application_define(void *);
void threadx_thread_time_slice_change_application_define(void *);
@@ -229,6 +231,7 @@ void threadx_thread_create_preemption_threshold_application_define(void *);
void threadx_thread_information_application_define(void *);
void threadx_thread_reset_application_define(void *);
void threadx_thread_stack_checking_application_define(void *);
+void threadx_thread_stack_fill_value_application_define(void *);
void threadx_time_get_set_application_define(void *);
@@ -335,6 +338,7 @@ TEST_ENTRY test_control_tests[] =
threadx_thread_sleep_for_100ticks_application_define,
threadx_thread_multiple_sleep_application_define,
threadx_thread_terminate_delete_application_define,
+ threadx_thread_exit_callback_transition_application_define,
threadx_thread_priority_change_application_define,
@@ -348,6 +352,7 @@ TEST_ENTRY test_control_tests[] =
threadx_thread_information_application_define,
threadx_thread_reset_application_define,
threadx_thread_stack_checking_application_define,
+ threadx_thread_stack_fill_value_application_define,
threadx_time_get_set_application_define,
@@ -565,6 +570,11 @@ TX_THREAD *thread_ptr;
test_timer_create_init = tx_timer_create(&init_timer, "init timer", init_timer_entry, 0x5678,
100, 200, TX_AUTO_ACTIVATE);
+ /* Test timer change from initialization. */
+ test_timer_create_init += tx_timer_create(&init_timer_change, "init timer change", init_timer_entry, 0x5678,
+ 100, 200, TX_NO_ACTIVATE);
+ test_timer_create_init += tx_timer_change(&init_timer_change, 50, 100);
+
/* Test calling tx_thread_relinquish to see if the error checking throws it out. */
tx_thread_relinquish();
#endif
diff --git a/test/tx/regression/threadx_block_memory_basic_test.c b/test/tx/regression/threadx_block_memory_basic_test.c
index 94cc8793..d7c609d2 100644
--- a/test/tx/regression/threadx_block_memory_basic_test.c
+++ b/test/tx/regression/threadx_block_memory_basic_test.c
@@ -9,6 +9,8 @@
/* SPDX-License-Identifier: MIT */
/***************************************************************************/
+/* Portions of this file were generated with AI assistance. */
+
/* This test is designed to test simple memory block pool creation, deletion, and
allocates and releases. */
@@ -72,7 +74,7 @@ void test_control_return(UINT status);
/* Prototype direct call to block pool core service. */
-UINT _tx_block_pool_create(TX_BLOCK_POOL *pool_ptr, CHAR *name_ptr, ULONG block_size,
+UINT _tx_block_pool_create(TX_BLOCK_POOL *pool_ptr, TX_NAME_CONST CHAR *name_ptr, ULONG block_size,
VOID *pool_start, ULONG pool_size);
diff --git a/test/tx/regression/threadx_block_memory_error_detection_test.c b/test/tx/regression/threadx_block_memory_error_detection_test.c
index 41148120..b0303b76 100644
--- a/test/tx/regression/threadx_block_memory_error_detection_test.c
+++ b/test/tx/regression/threadx_block_memory_error_detection_test.c
@@ -9,6 +9,8 @@
/* SPDX-License-Identifier: MIT */
/***************************************************************************/
+/* Portions of this file were generated with AI assistance. */
+
/* This test is designed to test error detection for simple memory block operations. */
#include <stdio.h>
@@ -29,7 +31,7 @@ static CHAR *pointer;
static void thread_0_entry(ULONG thread_input);
-UINT _txe_block_pool_create(TX_BLOCK_POOL *pool_ptr, CHAR *name_ptr, ULONG block_size,
+UINT _txe_block_pool_create(TX_BLOCK_POOL *pool_ptr, TX_NAME_CONST CHAR *name_ptr, ULONG block_size,
VOID *pool_start, ULONG pool_size, UINT pool_control_block_size);
diff --git a/test/tx/regression/threadx_block_memory_information_test.c b/test/tx/regression/threadx_block_memory_information_test.c
index 82d57f40..0c04cb7e 100644
--- a/test/tx/regression/threadx_block_memory_information_test.c
+++ b/test/tx/regression/threadx_block_memory_information_test.c
@@ -9,6 +9,8 @@
/* SPDX-License-Identifier: MIT */
/***************************************************************************/
+/* Portions of this file were generated with AI assistance. */
+
/* This test is designed to test block memory information services. */
#include <stdio.h>
@@ -68,7 +70,7 @@ static void thread_5_entry(ULONG thread_input);
static void thread_6_entry(ULONG thread_input);
/* Direct core function to bypass the error checking shell. */
-UINT _tx_block_pool_info_get(TX_BLOCK_POOL *pool_ptr, CHAR **name, ULONG *available_blocks,
+UINT _tx_block_pool_info_get(TX_BLOCK_POOL *pool_ptr, TX_NAME_CONST CHAR **name, ULONG *available_blocks,
ULONG *total_blocks, TX_THREAD **first_suspended,
ULONG *suspended_count, TX_BLOCK_POOL **next_pool);
@@ -227,7 +229,7 @@ static void thread_0_entry(ULONG thread_input)
UINT status;
VOID *pointer;
-CHAR *name;
+TX_NAME_CONST CHAR *name;
ULONG available;
ULONG total_blocks;
TX_THREAD *first_suspended;
@@ -369,7 +371,7 @@ ULONG timeouts;
status += tx_block_pool_info_get(&block_pool_0, &name, &available, &total_blocks, &first_suspended, &suspended_count, &next_pool);
/* Check for an error condition. */
- if ((status) || (available != block_pool_0.tx_block_pool_available) || (total_blocks != block_pool_0.tx_block_pool_total) ||
+ if ((status) || (name != block_pool_0.tx_block_pool_name) || (available != block_pool_0.tx_block_pool_available) || (total_blocks != block_pool_0.tx_block_pool_total) ||
(first_suspended != &thread_4) || (suspended_count != block_pool_0.tx_block_pool_suspended_count) || (next_pool != &block_pool_0))
{
diff --git a/test/tx/regression/threadx_byte_memory_basic_test.c b/test/tx/regression/threadx_byte_memory_basic_test.c
index 45f92e12..6b916f67 100644
--- a/test/tx/regression/threadx_byte_memory_basic_test.c
+++ b/test/tx/regression/threadx_byte_memory_basic_test.c
@@ -9,6 +9,8 @@
/* SPDX-License-Identifier: MIT */
/***************************************************************************/
+/* Portions of this file were generated with AI assistance. */
+
/* This test is designed to test simple memory byte pool creation, deletion, and
allocates and releases. */
@@ -51,7 +53,7 @@ extern UINT test_byte_pool_create_init;
extern VOID (*test_isr_dispatch)(void);
-UINT _txe_byte_pool_create(TX_BYTE_POOL *pool_ptr, CHAR *name_ptr, VOID *pool_start,
+UINT _txe_byte_pool_create(TX_BYTE_POOL *pool_ptr, TX_NAME_CONST CHAR *name_ptr, VOID *pool_start,
ULONG pool_size, UINT pool_control_block_size);
diff --git a/test/tx/regression/threadx_byte_memory_information_test.c b/test/tx/regression/threadx_byte_memory_information_test.c
index f1624a65..f65efac3 100644
--- a/test/tx/regression/threadx_byte_memory_information_test.c
+++ b/test/tx/regression/threadx_byte_memory_information_test.c
@@ -9,6 +9,8 @@
/* SPDX-License-Identifier: MIT */
/***************************************************************************/
+/* Portions of this file were generated with AI assistance. */
+
/* This test is designed to test byte memory information. */
#include <stdio.h>
@@ -226,7 +228,7 @@ static void thread_0_entry(ULONG thread_input)
UINT status;
VOID *pointer;
-CHAR *name;
+TX_NAME_CONST CHAR *name;
ULONG available;
ULONG fragments;
TX_THREAD *first_suspended;
@@ -369,7 +371,7 @@ ULONG timeouts;
status += tx_byte_pool_info_get(&byte_pool_0, &name, &available, &fragments, &first_suspended, &suspended_count, &next_pool);
/* Check the status. */
- if ((status != TX_SUCCESS) || (available != byte_pool_0.tx_byte_pool_available) || (fragments != byte_pool_0.tx_byte_pool_fragments) ||
+ if ((status != TX_SUCCESS) || (name != byte_pool_0.tx_byte_pool_name) || (available != byte_pool_0.tx_byte_pool_available) || (fragments != byte_pool_0.tx_byte_pool_fragments) ||
(first_suspended != &thread_4) || (suspended_count != byte_pool_0.tx_byte_pool_suspended_count) || (next_pool != &byte_pool_0))
{
diff --git a/test/tx/regression/threadx_event_flag_basic_test.c b/test/tx/regression/threadx_event_flag_basic_test.c
index 8ab990f7..fed424a6 100644
--- a/test/tx/regression/threadx_event_flag_basic_test.c
+++ b/test/tx/regression/threadx_event_flag_basic_test.c
@@ -9,6 +9,8 @@
/* SPDX-License-Identifier: MIT */
/***************************************************************************/
+/* Portions of this file were generated with AI assistance. */
+
/* This test is designed to test simple event flag group creation, deletion, gets and
sets. */
@@ -57,7 +59,7 @@ static TX_EVENT_FLAGS_GROUP group_3;
static void thread_0_entry(ULONG thread_input);
static void thread_1_entry(ULONG thread_input);
-UINT _txe_event_flags_create(TX_EVENT_FLAGS_GROUP *group_ptr, CHAR *name_ptr, UINT event_control_block_size);
+UINT _txe_event_flags_create(TX_EVENT_FLAGS_GROUP *group_ptr, TX_NAME_CONST CHAR *name_ptr, UINT event_control_block_size);
/* Prototype for test control return. */
diff --git a/test/tx/regression/threadx_event_flag_information_test.c b/test/tx/regression/threadx_event_flag_information_test.c
index 45e547f9..1c7cfd2d 100644
--- a/test/tx/regression/threadx_event_flag_information_test.c
+++ b/test/tx/regression/threadx_event_flag_information_test.c
@@ -9,6 +9,8 @@
/* SPDX-License-Identifier: MIT */
/***************************************************************************/
+/* Portions of this file were generated with AI assistance. */
+
/* This test is designed to test the event flag group information gathering services. */
#include <stdio.h>
@@ -134,7 +136,7 @@ static void thread_0_entry(ULONG thread_input)
UINT status;
ULONG actual_events;
-CHAR *name;
+TX_NAME_CONST CHAR *name;
ULONG current_flags;
TX_THREAD *first_suspended;
ULONG suspended_count;
@@ -356,7 +358,7 @@ ULONG timeouts;
status += tx_event_flags_info_get(&group_0, &name, &current_flags, &first_suspended, &suspended_count, &next_group);
/* Check the status. */
- if ((status != TX_SUCCESS) || (current_flags != group_0.tx_event_flags_group_current) || (first_suspended != TX_NULL) || (suspended_count != 0) || (next_group != &group_1))
+ if ((status != TX_SUCCESS) || (name != group_0.tx_event_flags_group_name) || (current_flags != group_0.tx_event_flags_group_current) || (first_suspended != TX_NULL) || (suspended_count != 0) || (next_group != &group_1))
{
/* Event flag error. */
diff --git a/test/tx/regression/threadx_initialize_kernel_setup_test.c b/test/tx/regression/threadx_initialize_kernel_setup_test.c
index 057ac223..b69a26bd 100644
--- a/test/tx/regression/threadx_initialize_kernel_setup_test.c
+++ b/test/tx/regression/threadx_initialize_kernel_setup_test.c
@@ -10,7 +10,7 @@
/***************************************************************************/
/* This test is designed to test kernel setup functionality in ThreadX. */
-// Some portions generated by Codex (gpt 5.4).
+// Portions of this file were generated with AI assistance.
#include <stdio.h>
#include <stdlib.h>
diff --git a/test/tx/regression/threadx_mutex_basic_test.c b/test/tx/regression/threadx_mutex_basic_test.c
index bb81eb9a..5a652843 100644
--- a/test/tx/regression/threadx_mutex_basic_test.c
+++ b/test/tx/regression/threadx_mutex_basic_test.c
@@ -9,6 +9,8 @@
/* SPDX-License-Identifier: MIT */
/***************************************************************************/
+/* Portions of this file were generated with AI assistance. */
+
/* This test is designed to test the mutex create/delete and immediate
return gets and puts. */
@@ -69,7 +71,7 @@ static void thread_2_entry(ULONG thread_input);
static void thread_3_entry(ULONG thread_input);
static void thread_4_entry(ULONG thread_input);
-UINT _txe_mutex_create(TX_MUTEX *mutex_ptr, CHAR *name_ptr, UINT inherit, UINT mutex_control_block_size);
+UINT _txe_mutex_create(TX_MUTEX *mutex_ptr, TX_NAME_CONST CHAR *name_ptr, UINT inherit, UINT mutex_control_block_size);
/* Prototype for test control return. */
diff --git a/test/tx/regression/threadx_mutex_information_test.c b/test/tx/regression/threadx_mutex_information_test.c
index 3829122c..fea78c04 100644
--- a/test/tx/regression/threadx_mutex_information_test.c
+++ b/test/tx/regression/threadx_mutex_information_test.c
@@ -9,6 +9,8 @@
/* SPDX-License-Identifier: MIT */
/***************************************************************************/
+/* Portions of this file were generated with AI assistance. */
+
/* This test is designed to test the mutex information services. */
#include <stdio.h>
@@ -134,7 +136,7 @@ static void thread_0_entry(ULONG thread_input)
{
UINT status;
-CHAR *name;
+TX_NAME_CONST CHAR *name;
ULONG count;
TX_THREAD *owner;
TX_THREAD *first_suspended;
@@ -300,7 +302,7 @@ ULONG inheritances;
status += tx_mutex_info_get(&mutex_2, &name, &count, &owner, &first_suspended, &suspended_count, &next_mutex);
/* Check status. */
- if ((status != TX_SUCCESS) || (count != mutex_2.tx_mutex_ownership_count) || (owner != mutex_2.tx_mutex_owner) ||
+ if ((status != TX_SUCCESS) || (name != mutex_2.tx_mutex_name) || (count != mutex_2.tx_mutex_ownership_count) || (owner != mutex_2.tx_mutex_owner) ||
(first_suspended != mutex_2.tx_mutex_suspension_list) || (suspended_count != mutex_2.tx_mutex_suspended_count) || (next_mutex != mutex_2.tx_mutex_created_next))
{
diff --git a/test/tx/regression/threadx_queue_basic_one_word_test.c b/test/tx/regression/threadx_queue_basic_one_word_test.c
index 1abb6986..58142471 100644
--- a/test/tx/regression/threadx_queue_basic_one_word_test.c
+++ b/test/tx/regression/threadx_queue_basic_one_word_test.c
@@ -9,6 +9,8 @@
/* SPDX-License-Identifier: MIT */
/***************************************************************************/
+/* Portions of this file were generated with AI assistance. */
+
/* This test is designed to test immediate response queue services including create
and delete. This test is for queue sizes of 1 ULONG. Two queues are used one with
a capacity of 1 message and another with a capacity of 3 messages. */
@@ -51,7 +53,7 @@ static void thread_0_entry(ULONG thread_input);
static void thread_1_entry(ULONG thread_input);
-UINT _txe_queue_create(TX_QUEUE *queue_ptr, CHAR *name_ptr, UINT message_size,
+UINT _txe_queue_create(TX_QUEUE *queue_ptr, TX_NAME_CONST CHAR *name_ptr, UINT message_size,
VOID *queue_start, ULONG queue_size, UINT queue_control_block_size);
diff --git a/test/tx/regression/threadx_queue_information_test.c b/test/tx/regression/threadx_queue_information_test.c
index f884bd0d..8929c465 100644
--- a/test/tx/regression/threadx_queue_information_test.c
+++ b/test/tx/regression/threadx_queue_information_test.c
@@ -9,6 +9,8 @@
/* SPDX-License-Identifier: MIT */
/***************************************************************************/
+/* Portions of this file were generated with AI assistance. */
+
/* This test is designed to test the queue information services. */
#include <stdio.h>
@@ -102,7 +104,7 @@ UINT status;
ULONG source_message = 0x12345678UL;
ULONG dest_message;
ULONG expected_message;
-CHAR *name;
+TX_NAME_CONST CHAR *name;
ULONG enqueued;
ULONG available_storage;
TX_THREAD *first_suspended;
@@ -409,7 +411,7 @@ ULONG timeouts;
status += tx_queue_info_get(&queue_0, &name, &enqueued, &available_storage, &first_suspended, &suspended_count, &next_queue);
/* Check for errors. */
- if ((status != TX_SUCCESS) || (enqueued != queue_0.tx_queue_enqueued) || (available_storage != queue_0.tx_queue_available_storage) ||
+ if ((status != TX_SUCCESS) || (name != queue_0.tx_queue_name) || (enqueued != queue_0.tx_queue_enqueued) || (available_storage != queue_0.tx_queue_available_storage) ||
(first_suspended != queue_0.tx_queue_suspension_list) || (suspended_count != queue_0.tx_queue_suspended_count) ||
(next_queue != queue_0.tx_queue_created_next))
{
diff --git a/test/tx/regression/threadx_semaphore_basic_test.c b/test/tx/regression/threadx_semaphore_basic_test.c
index 3209b586..a1a3d340 100644
--- a/test/tx/regression/threadx_semaphore_basic_test.c
+++ b/test/tx/regression/threadx_semaphore_basic_test.c
@@ -9,6 +9,8 @@
/* SPDX-License-Identifier: MIT */
/***************************************************************************/
+/* Portions of this file were generated with AI assistance. */
+
/* This test is designed to test the semaphore create/delete and immediate return gets and puts. */
#include <stdio.h>
@@ -62,7 +64,7 @@ static TX_SEMAPHORE semaphore_3;
static void thread_0_entry(ULONG thread_input);
static void thread_1_entry(ULONG thread_input);
-UINT _txe_semaphore_create(TX_SEMAPHORE *semaphore_ptr, CHAR *name_ptr, ULONG initial_count, UINT semaphore_control_block_size);
+UINT _txe_semaphore_create(TX_SEMAPHORE *semaphore_ptr, TX_NAME_CONST CHAR *name_ptr, ULONG initial_count, UINT semaphore_control_block_size);
/* Prototype for test control return. */
diff --git a/test/tx/regression/threadx_semaphore_information_test.c b/test/tx/regression/threadx_semaphore_information_test.c
index ede276e9..c0bb3c72 100644
--- a/test/tx/regression/threadx_semaphore_information_test.c
+++ b/test/tx/regression/threadx_semaphore_information_test.c
@@ -9,6 +9,8 @@
/* SPDX-License-Identifier: MIT */
/***************************************************************************/
+/* Portions of this file were generated with AI assistance. */
+
/* This test is designed to test the semaphore information services. */
#include <stdio.h>
@@ -100,7 +102,7 @@ static void thread_0_entry(ULONG thread_input)
{
UINT status;
-CHAR *name;
+TX_NAME_CONST CHAR *name;
ULONG current_value;
TX_THREAD *first_suspended;
ULONG suspended_count;
@@ -211,7 +213,7 @@ ULONG timeouts;
status += tx_semaphore_info_get(&semaphore_0, &name, &current_value, &first_suspended, &suspended_count, &next_semaphore);
/* Check status. */
- if ((status != TX_SUCCESS) || (current_value != semaphore_0.tx_semaphore_count) ||
+ if ((status != TX_SUCCESS) || (name != semaphore_0.tx_semaphore_name) || (current_value != semaphore_0.tx_semaphore_count) ||
(first_suspended != semaphore_0.tx_semaphore_suspension_list) || (suspended_count != semaphore_0.tx_semaphore_suspended_count) ||
(next_semaphore != semaphore_0.tx_semaphore_created_next))
{
diff --git a/test/tx/regression/threadx_test_port.h b/test/tx/regression/threadx_test_port.h
index b8502b4d..5d79c8e8 100644
--- a/test/tx/regression/threadx_test_port.h
+++ b/test/tx/regression/threadx_test_port.h
@@ -32,6 +32,11 @@
typedef ALIGN_TYPE TX_TEST_POINTER_WORD;
-#define TX_TEST_STORE_POINTER(a, b) (a) = ((TX_TEST_POINTER_WORD) TX_POINTER_TO_ALIGN_TYPE_CONVERT(b))
+/* TX_POINTER_TO_ALIGN_TYPE_CONVERT is defined only when TX_MISRA_ENABLE is absent,
+ so the conversion is written out here rather than taken from the API. This is
+ test scaffolding storing a pointer in a word wide enough to hold one; it is not
+ kernel source and carries no MISRA obligation of its own. */
+
+#define TX_TEST_STORE_POINTER(a, b) (a) = ((TX_TEST_POINTER_WORD) (ALIGN_TYPE) (b))
#endif
diff --git a/test/tx/regression/threadx_thread_basic_execution_test.c b/test/tx/regression/threadx_thread_basic_execution_test.c
index c51a677c..ea9b1177 100644
--- a/test/tx/regression/threadx_thread_basic_execution_test.c
+++ b/test/tx/regression/threadx_thread_basic_execution_test.c
@@ -1,4 +1,6 @@
/***************************************************************************/
+
+// Portions of this file were generated with AI assistance.
/* Copyright (c) 2024 Microsoft Corporation */
/* Copyright (c) 2026 Eclipse ThreadX contributors */
/* */
@@ -71,7 +73,7 @@ extern TX_TIMER_INTERNAL *_tx_timer_expired_timer_ptr;
static void thread_0_entry(ULONG task_input);
-UINT _txe_thread_create(TX_THREAD *thread_ptr, CHAR *name_ptr,
+UINT _txe_thread_create(TX_THREAD *thread_ptr, TX_NAME_CONST CHAR *name_ptr,
VOID (*entry_function)(ULONG), ULONG entry_input,
VOID *stack_start, ULONG stack_size,
UINT priority, UINT preempt_threshold,
@@ -401,7 +403,7 @@ VOID (*temp_mutex_release)(TX_THREAD *thread_ptr);
test_thread.tx_thread_timer.tx_timer_internal_list_head = TX_NULL;
test_thread.tx_thread_suspending = TX_TRUE;
test_thread.tx_thread_delayed_suspend = TX_TRUE;
-#if defined(_WIN64) || defined(TX_TIMER_EXTENSION_PTR_DEFINED)
+#if defined(_WIN32) || defined(TX_TIMER_EXTENSION_PTR_DEFINED)
{
TX_TIMER_INTERNAL timeout_timer;
TX_TIMER_INTERNAL *saved_expired_timer_ptr;
diff --git a/test/tx/regression/threadx_thread_delayed_suspension_test.c b/test/tx/regression/threadx_thread_delayed_suspension_test.c
index 2bf26421..1841458a 100644
--- a/test/tx/regression/threadx_thread_delayed_suspension_test.c
+++ b/test/tx/regression/threadx_thread_delayed_suspension_test.c
@@ -12,6 +12,7 @@
/* This test checks out the delayed suspension clear from tx_thread_resume. */
#include <stdio.h>
+#include <time.h>
#include "tx_api.h"
#include "tx_thread.h"
#include "tx_timer.h"
@@ -235,6 +236,12 @@ static void thread_0_entry(ULONG thread_input)
UINT status;
+#ifndef TX_NOT_INTERRUPTABLE
+#define DELAYED_SUSPENSION_SECOND_BUDGET ((ULONG) 120)
+
+time_t start_wall;
+#endif
+
/* Inform user. */
printf("Running Thread Delayed Suspension Clearing Test..................... ");
@@ -278,8 +285,23 @@ UINT status;
/* Resume the test thread. */
tx_thread_resume(&thread_2);
- /* Wait until we see the delayed suspension set flag. */
- while(delayed_suspend_set == 0)
+ /* Wait until we see the delayed suspension set flag.
+
+ The flag is set by the interrupt above, and only when that interrupt lands
+ while thread 2 is part way through suspending. That is a narrow window,
+ and waiting for it without a bound leaves the test no way out when it is
+ not reached. How narrow it is depends on the build: this loop finishes in
+ between a tenth of a second and three seconds in four of the five ThreadX
+ configurations, and took 490 seconds in trace_build, where it was the most
+ expensive thing in the suite by a wide margin.
+
+ The budget is in wall clock seconds rather than ticks. A tick arrives only
+ when the port's timer thread runs, so the tick clock falls behind real
+ time under load or instrumentation, which is exactly the configuration
+ that needs bounding here. */
+ start_wall = time(TX_NULL);
+ while ((delayed_suspend_set == 0) &&
+ (((ULONG) (time(TX_NULL) - start_wall)) <= DELAYED_SUSPENSION_SECOND_BUDGET))
{
/* Abort the suspension for thread 2. */
@@ -293,7 +315,18 @@ UINT status;
tx_thread_relinquish();
/* At this point, check for an error. */
- if (thread_2_counter != thread_2_counter_capture)
+ if (delayed_suspend_set == 0)
+ {
+
+ /* The window was never reached, so thread_2_counter_capture was never
+ taken and the comparison below would be against a value that means
+ nothing. Not reaching the window is a gap in what this run covered
+ rather than a fault in the code under test, so say so and skip the
+ check that depends on it. */
+ printf("(delayed suspension window not reached in %lu seconds) ",
+ (ULONG) (time(TX_NULL) - start_wall));
+ }
+ else if (thread_2_counter != thread_2_counter_capture)
{
/* Delayed suspension error... thread kept running! */
diff --git a/test/tx/regression/threadx_thread_exit_callback_transition_test.c b/test/tx/regression/threadx_thread_exit_callback_transition_test.c
new file mode 100644
index 00000000..c931085c
--- /dev/null
+++ b/test/tx/regression/threadx_thread_exit_callback_transition_test.c
@@ -0,0 +1,1375 @@
+/***************************************************************************
+ * 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
+ **************************************************************************/
+
+/* This test covers the completion and termination transitions that a thread's
+ TX_THREAD_EXIT notification callback runs inside.
+
+ _tx_thread_shell_entry and _tx_thread_terminate both publish the terminal
+ state -- TX_COMPLETED or TX_TERMINATED -- and then call the thread's exit
+ notification callback, before the thread has been detached from the ready
+ list and before either service has finished with the pointer it holds to
+ the control block. That terminal state is the same state _tx_thread_delete
+ and _tx_thread_reset accept as authorization to invalidate or rebuild the
+ control block, so a callback used to be able to delete the object -- and
+ then lawfully recreate it over the same memory -- while the scheduler was
+ still linked to the old incarnation, or to reset a terminated thread and
+ resume it back onto the ready list after termination had reported success.
+
+ The tx_thread_suspending flag marks the transition for exactly its
+ duration, and delete and reset now refuse a target whose flag is still set.
+ The refusal is transient: TX_DELETE_ERROR and TX_NOT_DONE are errors those
+ two services already document, and the same call succeeds once the
+ transition has finished.
+
+ What the test measures, in every scenario:
+
+ 1. what the callback observes -- the published state, the transition
+ flag, and the status of each lifecycle service it can reach,
+ 2. that the scheduler's ready lists, priority map and execute pointer
+ stay consistent with the control blocks they point at, taken as one
+ snapshot under interrupt lockout so that the reading is not itself a
+ race,
+ 3. that the documented behaviour still works afterwards: the same delete
+ and reset succeed once the transition has completed, so nothing has
+ become permanently undeletable.
+
+ The consequences are demonstrated rather than described, and deliberately
+ from the terminator's context rather than the completing thread's:
+
+ - scenario A has the callback recreate the control block if its delete
+ unexpectedly succeeded, at a lower priority, so that the outcome is an
+ assertable disagreement -- the old priority list still heading at a
+ control block whose own priority field names a different list -- rather
+ than a fault;
+ - scenario B has the callback reset and then resume the target, so that a
+ kernel which allows the reset is caught reporting a successful
+ termination for a thread that is runnable again.
+
+ Both run while the victim is a ready thread that is not the current thread.
+ That ordering is not cosmetic. On the Linux, Win32 and Win64 host
+ simulation ports TX_THREAD_DELETE_PORT_COMPLETION cancels and joins the
+ host thread backing the deleted thread, so a callback-side delete of the
+ *completing* thread destroys the host thread the callback is running on and
+ wedges the simulator instead of reaching an assertion. The completion
+ scenarios therefore come last, and the scenario gate below stops the run
+ before them if anything earlier has already failed.
+
+ Every list walk is bounded, so a corrupted ring costs an assertion and not
+ a hang. Expectations are counted rather than short-circuited within a
+ scenario, so a failing kernel reports how much it failed by; progression
+ between scenarios is gated on there being no failures yet, so a kernel that
+ has already corrupted the scheduler is not driven further into it. */
+
+#include <stdio.h>
+#include "tx_api.h"
+#include "tx_thread.h"
+
+
+/* Priorities. The controller sits above every thread it drives. The victim
+ shares its priority with two peers, so that both the multi-member and the
+ single-member ready-ring unlink paths are reached. RECREATE_PRIORITY is
+ below VICTIM_PRIORITY on purpose: see the note in the callback. */
+
+#define CONTROLLER_PRIORITY 10
+#define VICTIM_PRIORITY 20
+#define RECREATE_PRIORITY 21
+
+/* Upper bound on a ready-ring walk. The suite never has this many threads at
+ one priority, so exceeding it means the ring is broken rather than long. */
+
+#define READY_RING_LIMIT 32
+
+/* Sentinel for a probe slot the callback did not reach. */
+
+#define PROBE_NOT_ATTEMPTED 0xFFFFFFFFUL
+
+
+static unsigned long controller_counter = 0;
+static unsigned long victim_counter = 0;
+static unsigned long peer_a_counter = 0;
+static unsigned long peer_b_counter = 0;
+static unsigned long replacement_counter = 0;
+static unsigned long callback_entry_counter = 0;
+static unsigned long callback_exit_counter = 0;
+
+static TX_THREAD controller;
+static TX_THREAD victim;
+static TX_THREAD peer_a;
+static TX_THREAD peer_b;
+static TX_SEMAPHORE victim_semaphore;
+static TX_MUTEX victim_mutex;
+
+static CHAR *victim_stack;
+static CHAR *replacement_stack;
+
+/* What the victim's entry function should do before it returns. */
+
+static volatile UINT victim_hold = TX_FALSE;
+static volatile UINT victim_self_terminate = TX_FALSE;
+static volatile UINT victim_take_mutex = TX_FALSE;
+static volatile UINT victim_wait_semaphore = TX_FALSE;
+static volatile UINT peers_run = TX_FALSE;
+
+/* What the exit callback should attempt the next time it runs. Independent
+ flags rather than one mode, because a delete that unexpectedly succeeds
+ destroys the object and would mask the reset measurement behind it. */
+
+static volatile UINT probe_delete_enabled = TX_FALSE;
+static volatile UINT probe_reset_enabled = TX_FALSE;
+static volatile UINT probe_misc_enabled = TX_FALSE;
+static volatile UINT probe_reuse_enabled = TX_FALSE;
+
+/* What the callback saw the last time it probed. */
+
+static UINT probe_state;
+static UINT probe_suspending;
+static UINT probe_delete_status;
+static UINT probe_core_delete_status;
+static UINT probe_reset_status;
+static UINT probe_core_reset_status;
+static UINT probe_resume_status;
+static UINT probe_suspend_status;
+static UINT probe_terminate_status;
+static UINT probe_create_status;
+static UINT probe_consistent;
+static UINT probe_ran;
+static UINT delete_succeeded;
+
+/* Ordering witness for the notification pair. Entry and exit counts cannot
+ simply be compared: a victim created with TX_DONT_START never reports an
+ entry, and an incarnation that is reset and terminated again reports a
+ second exit with no second entry. What must hold is that an exit for a
+ thread that ran is preceded by that thread's entry. */
+
+static UINT last_notify_type = PROBE_NOT_ATTEMPTED;
+static UINT exit_saw_entry_first = TX_FALSE;
+
+/* Expectation accounting. */
+
+#define FAILED_TAG_LIMIT 32
+
+static unsigned long expectations_checked = 0;
+static unsigned long expectations_failed = 0;
+static UINT first_failure = 0;
+static UINT failed_tags[FAILED_TAG_LIMIT];
+static UINT failed_tag_count = 0;
+
+
+/* Define task prototypes. */
+
+static void controller_entry(ULONG thread_input);
+static void victim_entry(ULONG thread_input);
+static void peer_entry(ULONG thread_input);
+static void replacement_entry(ULONG thread_input);
+
+
+/* Prototype for test control return. */
+
+void test_control_return(UINT status);
+
+
+/* Record one expectation. tag identifies it in the failure report; the first
+ failing tag is the number the suite prints, so tags are allocated in the
+ order the expectations are written. */
+
+static void expect(UINT condition, UINT tag)
+{
+
+ expectations_checked++;
+
+ if (condition != TX_TRUE)
+ {
+
+ expectations_failed++;
+
+ if (first_failure == 0)
+ {
+ first_failure = tag;
+ }
+
+ /* Keep the tags, so that a failing kernel reports which expectations
+ it failed rather than only the first and a total. */
+ if (failed_tag_count < FAILED_TAG_LIMIT)
+ {
+ failed_tags[failed_tag_count] = tag;
+ failed_tag_count++;
+ }
+ }
+}
+
+
+static void expect_status(UINT actual, UINT wanted, UINT tag)
+{
+
+ if (actual == wanted)
+ {
+ expect(TX_TRUE, tag);
+ }
+ else
+ {
+ expect(TX_FALSE, tag);
+ }
+}
+
+
+static void expect_pointer(void *actual, void *wanted, UINT tag)
+{
+
+ if (actual == wanted)
+ {
+ expect(TX_TRUE, tag);
+ }
+ else
+ {
+ expect(TX_FALSE, tag);
+ }
+}
+
+
+static void expect_count(unsigned long actual, unsigned long wanted, UINT tag)
+{
+
+ if (actual == wanted)
+ {
+ expect(TX_TRUE, tag);
+ }
+ else
+ {
+ expect(TX_FALSE, tag);
+ }
+}
+
+
+/* Walk one ready ring and report whether thread_ptr is a member of it. The
+ walk is bounded, so a ring that does not close is reported as a
+ non-membership rather than spun on for ever. */
+
+static UINT ready_ring_contains(UINT priority, TX_THREAD *thread_ptr)
+{
+
+TX_THREAD *walk_ptr;
+TX_THREAD *head_ptr;
+UINT steps;
+UINT found;
+
+
+ found = TX_FALSE;
+ head_ptr = _tx_thread_priority_list[priority];
+ walk_ptr = head_ptr;
+ steps = 0;
+
+ while ((walk_ptr != TX_NULL) && (steps < READY_RING_LIMIT) && (found == TX_FALSE))
+ {
+
+ if (walk_ptr == thread_ptr)
+ {
+ found = TX_TRUE;
+ }
+
+ walk_ptr = walk_ptr -> tx_thread_ready_next;
+ steps++;
+
+ /* The ring has closed. */
+ if (walk_ptr == head_ptr)
+ {
+ walk_ptr = TX_NULL;
+ }
+ }
+
+ return(found);
+}
+
+
+/* Take one snapshot of the scheduler's ready structures and check that they
+ agree with the control blocks they point at.
+
+ This is the assertion the pre-fix behaviour fails. A callback that deletes
+ the victim and recreates it at another priority leaves the old priority list
+ heading at that control block while the block's own priority field names the
+ new list, and leaves the old priority's map bit set with nothing behind it.
+ Neither disagreement needs a fault to be visible.
+
+ The whole snapshot is taken under interrupt lockout -- which is the global
+ SMP protection on an SMP port -- so that reading the scheduler's state is
+ not itself a race against another core.
+
+ transitioning names the one thread that is allowed to be on a ready list
+ without being ready, which is precisely the thread whose completion or
+ termination transition is in progress: the terminal state is published
+ before the detachment, so inside the callback the victim is legitimately a
+ TX_COMPLETED or TX_TERMINATED member of its ready ring. That single
+ exemption is from the state clause only. Every other clause, and in
+ particular the clause that requires a ready-list member to agree that this
+ is its priority -- the one a callback-side delete and recreate breaks --
+ still applies to it. */
+
+static UINT scheduler_consistent_except(TX_THREAD *transitioning)
+{
+
+TX_INTERRUPT_SAVE_AREA
+
+TX_THREAD *head_ptr;
+TX_THREAD *walk_ptr;
+TX_THREAD *execute_ptr;
+UINT priority;
+UINT steps;
+UINT consistent;
+#ifdef TX_THREAD_SMP_MAX_CORES
+UINT core;
+#endif
+
+
+ consistent = TX_TRUE;
+
+ TX_DISABLE
+
+ for (priority = 0; priority < ((UINT) TX_MAX_PRIORITIES); priority++)
+ {
+
+ head_ptr = _tx_thread_priority_list[priority];
+
+#if TX_MAX_PRIORITIES <= 32
+
+ /* A priority with a ready list must have its map bit set, and a
+ priority with no ready list must not. */
+ if (head_ptr == TX_NULL)
+ {
+ if ((_tx_thread_priority_maps[0] & (((ULONG) 1) << priority)) != ((ULONG) 0))
+ {
+ consistent = TX_FALSE;
+ }
+ }
+ else
+ {
+ if ((_tx_thread_priority_maps[0] & (((ULONG) 1) << priority)) == ((ULONG) 0))
+ {
+ consistent = TX_FALSE;
+ }
+ }
+#endif
+
+ walk_ptr = head_ptr;
+ steps = 0;
+
+ while ((walk_ptr != TX_NULL) && (steps < READY_RING_LIMIT))
+ {
+
+ /* Every thread on a ready list must be a live thread, must be
+ ready, and must agree that this is its priority. */
+ if (walk_ptr -> tx_thread_id != TX_THREAD_ID)
+ {
+ consistent = TX_FALSE;
+ }
+
+ if ((walk_ptr -> tx_thread_state != TX_READY) && (walk_ptr != transitioning))
+ {
+ consistent = TX_FALSE;
+ }
+
+ if (walk_ptr -> tx_thread_priority != priority)
+ {
+ consistent = TX_FALSE;
+ }
+
+ /* The ring must be doubly linked. */
+ if (walk_ptr -> tx_thread_ready_next -> tx_thread_ready_previous != walk_ptr)
+ {
+ consistent = TX_FALSE;
+ }
+
+ walk_ptr = walk_ptr -> tx_thread_ready_next;
+ steps++;
+
+ if (walk_ptr == head_ptr)
+ {
+ walk_ptr = TX_NULL;
+ }
+ }
+
+ /* A ring that did not close inside the bound is broken. */
+ if (walk_ptr != TX_NULL)
+ {
+ consistent = TX_FALSE;
+ }
+ }
+
+ /* Whatever the scheduler has selected to execute must be a member of the
+ ready list that its own priority field names. */
+#ifdef TX_THREAD_SMP_MAX_CORES
+
+ for (core = 0; core < ((UINT) TX_THREAD_SMP_MAX_CORES); core++)
+ {
+
+ execute_ptr = _tx_thread_execute_ptr[core];
+
+ if (execute_ptr != TX_NULL)
+ {
+ if (ready_ring_contains(execute_ptr -> tx_thread_priority, execute_ptr) == TX_FALSE)
+ {
+ consistent = TX_FALSE;
+ }
+ }
+ }
+#else
+
+ execute_ptr = _tx_thread_execute_ptr;
+
+ if (execute_ptr != TX_NULL)
+ {
+ if (ready_ring_contains(execute_ptr -> tx_thread_priority, execute_ptr) == TX_FALSE)
+ {
+ consistent = TX_FALSE;
+ }
+ }
+#endif
+
+ TX_RESTORE
+
+ return(consistent);
+}
+
+
+static UINT scheduler_consistent(void)
+{
+ return(scheduler_consistent_except(TX_NULL));
+}
+
+
+/* Report whether thread_ptr is absent from every ready list and from every
+ execute slot, which is what a completed or terminated thread must be once
+ its transition has finished. */
+
+static UINT fully_detached(TX_THREAD *thread_ptr)
+{
+
+TX_INTERRUPT_SAVE_AREA
+
+UINT priority;
+UINT detached;
+#ifdef TX_THREAD_SMP_MAX_CORES
+UINT core;
+#endif
+
+
+ detached = TX_TRUE;
+
+ TX_DISABLE
+
+ for (priority = 0; priority < ((UINT) TX_MAX_PRIORITIES); priority++)
+ {
+ if (ready_ring_contains(priority, thread_ptr) == TX_TRUE)
+ {
+ detached = TX_FALSE;
+ }
+ }
+
+#ifdef TX_THREAD_SMP_MAX_CORES
+
+ for (core = 0; core < ((UINT) TX_THREAD_SMP_MAX_CORES); core++)
+ {
+ if (_tx_thread_execute_ptr[core] == thread_ptr)
+ {
+ detached = TX_FALSE;
+ }
+ }
+#else
+
+ if (_tx_thread_execute_ptr == thread_ptr)
+ {
+ detached = TX_FALSE;
+ }
+#endif
+
+ TX_RESTORE
+
+ return(detached);
+}
+
+
+static void arm_probe(UINT probe_delete, UINT probe_reset, UINT probe_misc, UINT probe_reuse)
+{
+
+ probe_state = PROBE_NOT_ATTEMPTED;
+ probe_suspending = PROBE_NOT_ATTEMPTED;
+ probe_delete_status = PROBE_NOT_ATTEMPTED;
+ probe_core_delete_status = PROBE_NOT_ATTEMPTED;
+ probe_reset_status = PROBE_NOT_ATTEMPTED;
+ probe_core_reset_status = PROBE_NOT_ATTEMPTED;
+ probe_resume_status = PROBE_NOT_ATTEMPTED;
+ probe_suspend_status = PROBE_NOT_ATTEMPTED;
+ probe_terminate_status = PROBE_NOT_ATTEMPTED;
+ probe_create_status = PROBE_NOT_ATTEMPTED;
+ probe_consistent = PROBE_NOT_ATTEMPTED;
+ probe_ran = TX_FALSE;
+ delete_succeeded = TX_FALSE;
+
+ probe_delete_enabled = probe_delete;
+ probe_reset_enabled = probe_reset;
+ probe_misc_enabled = probe_misc;
+ probe_reuse_enabled = probe_reuse;
+}
+
+
+/* The exit notification callback. Everything it learns is recorded for the
+ controller to check; it prints nothing and it never suspends, so it obeys
+ the one restriction the manual places on a notification callback. */
+
+static void entry_exit_notify(TX_THREAD *thread_ptr, UINT type)
+{
+
+ /* Only the victim's notifications are of interest. Its control block is
+ reused across scenarios, so the pointer is the identity. */
+ if (thread_ptr != &victim)
+ {
+ return;
+ }
+
+ if (type == TX_THREAD_ENTRY)
+ {
+ callback_entry_counter++;
+ last_notify_type = TX_THREAD_ENTRY;
+ return;
+ }
+
+ if (type != TX_THREAD_EXIT)
+ {
+ return;
+ }
+
+ callback_exit_counter++;
+
+ if (last_notify_type == TX_THREAD_ENTRY)
+ {
+ exit_saw_entry_first = TX_TRUE;
+ }
+
+ last_notify_type = TX_THREAD_EXIT;
+
+ if ((probe_delete_enabled == TX_FALSE) && (probe_reset_enabled == TX_FALSE) &&
+ (probe_misc_enabled == TX_FALSE))
+ {
+ return;
+ }
+
+ probe_ran = TX_TRUE;
+
+ /* What the kernel has published about the thread at this point. */
+ probe_state = thread_ptr -> tx_thread_state;
+ probe_suspending = thread_ptr -> tx_thread_suspending;
+
+ /* None of the services below has a suspension option, which is the only
+ thing the manual forbids a callback from using, so every one of them is
+ reachable from here.
+
+ Where a service is expected to refuse, the core service is called after
+ the _txe_ wrapper as well. The wrapper is what an ordinary build
+ reaches; the core service is what a TX_DISABLE_ERROR_CHECKING build
+ reaches directly, and the refusal has to live there so that disabling
+ error checking cannot disable it. The core call is made only when the
+ wrapper already refused, so that a kernel which allows the operation is
+ not asked to perform it twice. */
+
+ if (probe_delete_enabled == TX_TRUE)
+ {
+
+ probe_delete_status = tx_thread_delete(thread_ptr);
+
+ if (probe_delete_status == TX_SUCCESS)
+ {
+ delete_succeeded = TX_TRUE;
+ }
+ else
+ {
+
+ probe_core_delete_status = _tx_thread_delete(thread_ptr);
+
+ if (probe_core_delete_status == TX_SUCCESS)
+ {
+ delete_succeeded = TX_TRUE;
+ }
+ }
+
+ /* Recreate the control block only if the delete unexpectedly
+ succeeded. On a fixed kernel this never runs. On an unfixed one it
+ is the lawful next step after a successful delete, and it is what
+ turns the window into an observable inconsistency: tx_thread_create
+ zeroes the whole control block, and the auto-start then links it
+ onto RECREATE_PRIORITY's ready list while the old incarnation is
+ still the head of VICTIM_PRIORITY's.
+
+ RECREATE_PRIORITY is the lower of the two so that the stale head is
+ the one the scheduler selects from, and so that the ring at the new
+ priority has a single member. Recreating at the same priority would
+ instead have the auto-start write through the control block's
+ freshly zeroed ready_previous pointer, which is a fault rather than
+ an assertion. */
+ if ((delete_succeeded == TX_TRUE) && (probe_reuse_enabled == TX_TRUE))
+ {
+ probe_create_status = tx_thread_create(thread_ptr, "replacement",
+ replacement_entry, 0,
+ replacement_stack,
+ TEST_STACK_SIZE_PRINTF,
+ RECREATE_PRIORITY, RECREATE_PRIORITY,
+ TX_NO_TIME_SLICE, TX_AUTO_START);
+ }
+ }
+
+ if ((probe_reset_enabled == TX_TRUE) && (delete_succeeded == TX_FALSE))
+ {
+
+ probe_reset_status = tx_thread_reset(thread_ptr);
+
+ if (probe_reset_status != TX_SUCCESS)
+ {
+ probe_core_reset_status = _tx_thread_reset(thread_ptr);
+ }
+
+ /* Follow the reset with the resume that the reset would have unlocked.
+ On a fixed kernel the state is still terminal and the resume is
+ refused. On an unfixed one the reset has moved the state to
+ TX_SUSPENDED, the resume finds a suspending thread whose state is no
+ longer terminal, clears the flag and puts the thread back on the
+ ready list -- and the outer service then skips the removal and
+ reports success for a termination that did not happen. */
+ probe_resume_status = tx_thread_resume(thread_ptr);
+ }
+
+ if ((probe_misc_enabled == TX_TRUE) && (delete_succeeded == TX_FALSE))
+ {
+ probe_suspend_status = tx_thread_suspend(thread_ptr);
+ probe_terminate_status = tx_thread_terminate(thread_ptr);
+ }
+
+ probe_consistent = scheduler_consistent_except(thread_ptr);
+}
+
+
+/* Define what the initial system looks like. */
+
+#ifdef CTEST
+void test_application_define(void *first_unused_memory)
+#else
+void threadx_thread_exit_callback_transition_application_define(void *first_unused_memory)
+#endif
+{
+
+UINT status;
+CHAR *pointer;
+
+
+ /* Put first available memory address into a character pointer. */
+ pointer = (CHAR *) first_unused_memory;
+
+ status = tx_thread_create(&controller, "controller", controller_entry, 0,
+ pointer, TEST_STACK_SIZE_PRINTF,
+ CONTROLLER_PRIORITY, CONTROLLER_PRIORITY,
+ TX_NO_TIME_SLICE, TX_AUTO_START);
+ pointer = pointer + TEST_STACK_SIZE_PRINTF;
+
+ status += tx_thread_create(&peer_a, "peer a", peer_entry, 1,
+ pointer, TEST_STACK_SIZE_PRINTF,
+ VICTIM_PRIORITY, VICTIM_PRIORITY,
+ TX_NO_TIME_SLICE, TX_DONT_START);
+ pointer = pointer + TEST_STACK_SIZE_PRINTF;
+
+ status += tx_thread_create(&peer_b, "peer b", peer_entry, 2,
+ pointer, TEST_STACK_SIZE_PRINTF,
+ VICTIM_PRIORITY, VICTIM_PRIORITY,
+ TX_NO_TIME_SLICE, TX_DONT_START);
+ pointer = pointer + TEST_STACK_SIZE_PRINTF;
+
+ /* The victim's control block is created and destroyed repeatedly, so its
+ two stacks are set aside here rather than per scenario. */
+ victim_stack = pointer;
+ pointer = pointer + TEST_STACK_SIZE_PRINTF;
+ replacement_stack = pointer;
+ pointer = pointer + TEST_STACK_SIZE_PRINTF;
+
+ status += tx_semaphore_create(&victim_semaphore, "victim semaphore", 0);
+ status += tx_mutex_create(&victim_mutex, "victim mutex", TX_INHERIT);
+
+ if (status != TX_SUCCESS)
+ {
+
+ printf("Running Thread Exit Callback Transition Test............... ERROR #1\n");
+ test_control_return(1);
+ }
+
+#ifdef TX_THREAD_SMP_MAX_CORES
+
+ /* Confine every thread this test drives to core 0. The measurements below
+ are of global scheduler structures at chosen moments, and letting the
+ peers and the victim run on other cores would make them a race against
+ the test rather than a property of it. A victim executing on a
+ different core when termination begins is deliberately covered by a
+ separate SMP test, so that the deterministic cases here cannot be made
+ flaky by it. */
+ status = tx_thread_smp_core_exclude(&controller, 0xE);
+ status += tx_thread_smp_core_exclude(&peer_a, 0xE);
+ status += tx_thread_smp_core_exclude(&peer_b, 0xE);
+
+ if (status != TX_SUCCESS)
+ {
+
+ printf("Running Thread Exit Callback Transition Test............... ERROR #2\n");
+ test_control_return(1);
+ }
+#endif
+}
+
+
+/* Create the victim over its own control block, with the exit callback
+ registered. The notify status is folded into the result according to what
+ the configuration supports. */
+
+static UINT create_victim(UINT priority, UINT auto_start)
+{
+
+UINT status;
+UINT notify_status;
+
+
+ status = tx_thread_create(&victim, "victim", victim_entry, 0,
+ victim_stack, TEST_STACK_SIZE_PRINTF,
+ priority, priority, TX_NO_TIME_SLICE, auto_start);
+
+ notify_status = tx_thread_entry_exit_notify(&victim, entry_exit_notify);
+
+#ifndef TX_DISABLE_NOTIFY_CALLBACKS
+
+ if (notify_status != TX_SUCCESS)
+ {
+ status = notify_status;
+ }
+#else
+
+ /* Notification is compiled out, so the registration is expected to be
+ refused and the callback never runs. */
+ if (notify_status != TX_FEATURE_NOT_ENABLED)
+ {
+ status = notify_status;
+ }
+#endif
+
+#ifdef TX_THREAD_SMP_MAX_CORES
+
+ if (status == TX_SUCCESS)
+ {
+ status = tx_thread_smp_core_exclude(&victim, 0xE);
+ }
+#endif
+
+ return(status);
+}
+
+
+/* The gate between scenarios. A kernel that has already failed an expectation
+ has, in these scenarios, already left the scheduler inconsistent, and
+ driving it further would report nothing more while risking a wedge instead
+ of a verdict. */
+
+static UINT ok_so_far(void)
+{
+
+UINT result;
+
+
+ result = TX_FALSE;
+
+ if (expectations_failed == 0)
+ {
+ result = TX_TRUE;
+ }
+
+ return(result);
+}
+
+
+static void controller_entry(ULONG thread_input)
+{
+
+ULONG expected_exits;
+ULONG expected_entries;
+UINT index;
+
+
+ (void) thread_input;
+
+ controller_counter++;
+ expected_exits = 0;
+ expected_entries = 0;
+
+ /* Inform user. */
+ printf("Running Thread Exit Callback Transition Test............... ");
+
+ /* The scheduler must be self-consistent before anything is measured, so
+ that a later failure is attributable to this test. */
+ expect(scheduler_consistent(), 10);
+
+ /* ------------------------------------------------------------------- */
+ /* Scenario A: terminate a ready target that is not the current thread, */
+ /* with two peers ready at the same priority. The callback attempts */
+ /* the delete, and recreates the control block if it succeeded. */
+ /* ------------------------------------------------------------------- */
+
+ if (ok_so_far() == TX_TRUE)
+ {
+
+ peers_run = TX_TRUE;
+ victim_hold = TX_TRUE;
+
+ expect_status(tx_thread_resume(&peer_a), TX_SUCCESS, 11);
+ expect_status(tx_thread_resume(&peer_b), TX_SUCCESS, 12);
+
+ arm_probe(TX_TRUE, TX_FALSE, TX_FALSE, TX_TRUE);
+
+ expect_status(create_victim(VICTIM_PRIORITY, TX_AUTO_START), TX_SUCCESS, 13);
+ }
+
+ if (ok_so_far() == TX_TRUE)
+ {
+
+ /* Let the victim and both peers become ready and start spinning, so
+ that the victim's unlink is from a multi-member ring. */
+ tx_thread_sleep(3);
+
+ expect_status(victim.tx_thread_state, TX_READY, 14);
+ expect(ready_ring_contains(VICTIM_PRIORITY, &victim), 15);
+ expect(ready_ring_contains(VICTIM_PRIORITY, &peer_a), 16);
+ expect(ready_ring_contains(VICTIM_PRIORITY, &peer_b), 17);
+ }
+
+ if (ok_so_far() == TX_TRUE)
+ {
+
+ expect_status(tx_thread_terminate(&victim), TX_SUCCESS, 18);
+
+#ifndef TX_DISABLE_NOTIFY_CALLBACKS
+
+ expect(probe_ran, 19);
+
+ /* The callback runs after the terminal state has been published, and
+ inside the transition, which is what the flag records. */
+ expect_status(probe_state, TX_TERMINATED, 20);
+ expect_status(probe_suspending, TX_TRUE, 21);
+
+ /* Both the wrapper and the core service must refuse. */
+ expect_status(probe_delete_status, TX_DELETE_ERROR, 22);
+ expect_status(probe_core_delete_status, TX_DELETE_ERROR, 23);
+
+ /* So the recreate never happened and nothing is inconsistent. */
+ expect_status(delete_succeeded, TX_FALSE, 24);
+ expect_status(probe_create_status, PROBE_NOT_ATTEMPTED, 25);
+ expect_status(probe_consistent, TX_TRUE, 26);
+ expect_count(replacement_counter, 0, 27);
+#endif
+
+ /* After the service returns the termination is complete: the state is
+ terminal, the marker is clear, the target is on no ready list and in
+ no execute slot, and the peers are undisturbed. */
+ expect_status(victim.tx_thread_state, TX_TERMINATED, 28);
+ expect_status(victim.tx_thread_suspending, TX_FALSE, 29);
+ expect(fully_detached(&victim), 30);
+ expect(ready_ring_contains(VICTIM_PRIORITY, &peer_a), 31);
+ expect(ready_ring_contains(VICTIM_PRIORITY, &peer_b), 32);
+ expect(scheduler_consistent(), 33);
+ }
+
+ if (ok_so_far() == TX_TRUE)
+ {
+
+ arm_probe(TX_FALSE, TX_FALSE, TX_FALSE, TX_FALSE);
+
+ /* The documented behaviour is available again. */
+ expect_status(tx_thread_delete(&victim), TX_SUCCESS, 34);
+ }
+
+ /* ------------------------------------------------------------------- */
+ /* Scenario B: the same termination, but the callback attempts a reset */
+ /* and then the resume that a successful reset would have unlocked. */
+ /* */
+ /* This is the leg where the transition check is the only thing */
+ /* standing between the callback and a reset of a thread that is still */
+ /* on the ready list. Without it the reset succeeds, the resume */
+ /* clears the suspending flag and restores TX_READY, the outer service */
+ /* then finds the flag clear and skips the removal, and */
+ /* tx_thread_terminate returns TX_SUCCESS for a runnable thread. */
+ /* ------------------------------------------------------------------- */
+
+ if (ok_so_far() == TX_TRUE)
+ {
+
+ arm_probe(TX_FALSE, TX_TRUE, TX_FALSE, TX_FALSE);
+
+ expect_status(create_victim(VICTIM_PRIORITY, TX_AUTO_START), TX_SUCCESS, 40);
+ }
+
+ if (ok_so_far() == TX_TRUE)
+ {
+
+ tx_thread_sleep(3);
+
+ expect_status(victim.tx_thread_state, TX_READY, 41);
+ expect_status(tx_thread_terminate(&victim), TX_SUCCESS, 42);
+
+#ifndef TX_DISABLE_NOTIFY_CALLBACKS
+
+ expect(probe_ran, 43);
+ expect_status(probe_state, TX_TERMINATED, 44);
+ expect_status(probe_suspending, TX_TRUE, 45);
+
+ /* The discriminating leg. The target is not the current thread and
+ its published state is terminal, so both of the conditions the
+ service tested before this fix are satisfied. */
+ expect_status(probe_reset_status, TX_NOT_DONE, 46);
+ expect_status(probe_core_reset_status, TX_NOT_DONE, 47);
+
+ /* With the reset refused, the state is still terminal, so the resume
+ cannot reach the branch that would void the termination. */
+ expect_status(probe_resume_status, TX_RESUME_ERROR, 48);
+ expect_status(probe_consistent, TX_TRUE, 49);
+#endif
+
+ /* And the termination stands. A kernel that let the callback reset
+ and resume the target arrives here with the victim ready to run. */
+ expect_status(victim.tx_thread_state, TX_TERMINATED, 50);
+ expect_status(victim.tx_thread_suspending, TX_FALSE, 51);
+ expect(fully_detached(&victim), 52);
+ expect(scheduler_consistent(), 53);
+ }
+
+ if (ok_so_far() == TX_TRUE)
+ {
+
+ arm_probe(TX_FALSE, TX_FALSE, TX_FALSE, TX_FALSE);
+
+ expect_status(tx_thread_reset(&victim), TX_SUCCESS, 54);
+ expect_status(tx_thread_terminate(&victim), TX_SUCCESS, 55);
+ expect_status(tx_thread_delete(&victim), TX_SUCCESS, 56);
+ }
+
+ /* Retire the peers, so that the ring drops to a single member and then to
+ none, and check consistency across both unlinks. */
+
+ if (ok_so_far() == TX_TRUE)
+ {
+
+ peers_run = TX_FALSE;
+ tx_thread_sleep(3);
+
+ expect_status(peer_a.tx_thread_state, TX_COMPLETED, 57);
+ expect_status(peer_b.tx_thread_state, TX_COMPLETED, 58);
+ expect(scheduler_consistent(), 59);
+ expect_status(tx_thread_delete(&peer_a), TX_SUCCESS, 60);
+ expect_status(tx_thread_delete(&peer_b), TX_SUCCESS, 61);
+ }
+
+ /* ------------------------------------------------------------------- */
+ /* Scenario C: terminate a target suspended on an object, so that the */
+ /* non-ready branch of _tx_thread_terminate runs -- suspension cleanup, */
+ /* the terminated extension, the callback, and mutex release, in that */
+ /* order. That branch used to clear the marker before the callback */
+ /* rather than after the last dereference of the target. */
+ /* ------------------------------------------------------------------- */
+
+ if (ok_so_far() == TX_TRUE)
+ {
+
+ arm_probe(TX_TRUE, TX_TRUE, TX_FALSE, TX_FALSE);
+ victim_hold = TX_FALSE;
+ victim_take_mutex = TX_TRUE;
+ victim_wait_semaphore = TX_TRUE;
+
+ expect_status(create_victim(VICTIM_PRIORITY, TX_AUTO_START), TX_SUCCESS, 70);
+ }
+
+ if (ok_so_far() == TX_TRUE)
+ {
+
+ /* Let the victim take the mutex and suspend on the semaphore. */
+ tx_thread_sleep(3);
+
+ expect_status(victim.tx_thread_state, TX_SEMAPHORE_SUSP, 71);
+ expect_pointer(victim_mutex.tx_mutex_owner, &victim, 72);
+ }
+
+ if (ok_so_far() == TX_TRUE)
+ {
+
+ expect_status(tx_thread_terminate(&victim), TX_SUCCESS, 73);
+
+#ifndef TX_DISABLE_NOTIFY_CALLBACKS
+
+ expect(probe_ran, 74);
+ expect_status(probe_state, TX_TERMINATED, 75);
+
+ /* The discriminating leg for this branch. The marker used to be
+ cleared before this point, which left the callback free to delete or
+ reset a control block that the service still had mutex-release
+ processing to do against. */
+ expect_status(probe_suspending, TX_TRUE, 76);
+ expect_status(probe_delete_status, TX_DELETE_ERROR, 77);
+ expect_status(probe_core_delete_status, TX_DELETE_ERROR, 78);
+ expect_status(probe_reset_status, TX_NOT_DONE, 79);
+ expect_status(probe_core_reset_status, TX_NOT_DONE, 80);
+ expect_status(probe_resume_status, TX_RESUME_ERROR, 81);
+ expect_status(delete_succeeded, TX_FALSE, 82);
+ expect_status(probe_consistent, TX_TRUE, 83);
+#endif
+
+ /* The marker is cleared at the safe point, after the mutex release,
+ and the mutex the terminated thread owned has been given up. The
+ value of the flag during the release itself is not observable from
+ application code; what is observable is that there is exactly one
+ clear site and that it sits downstream of the release. */
+ expect_status(victim.tx_thread_suspending, TX_FALSE, 84);
+ expect_pointer(victim_mutex.tx_mutex_owner, TX_NULL, 85);
+ expect_count((unsigned long) victim_semaphore.tx_semaphore_suspended_count, 0, 86);
+ expect(fully_detached(&victim), 87);
+ expect(scheduler_consistent(), 88);
+ }
+
+ if (ok_so_far() == TX_TRUE)
+ {
+
+ arm_probe(TX_FALSE, TX_FALSE, TX_FALSE, TX_FALSE);
+
+ expect_status(tx_thread_delete(&victim), TX_SUCCESS, 89);
+ }
+
+ /* ------------------------------------------------------------------- */
+ /* Scenario D: natural completion, victim alone at its priority. */
+ /* */
+ /* The callback here runs on the completing thread itself, so a */
+ /* callback-side delete that succeeds destroys the host thread on the */
+ /* simulation ports. This scenario is therefore placed after the ones */
+ /* that reach an assertion, and the gate above stops the run before it */
+ /* if any of them has already failed. */
+ /* ------------------------------------------------------------------- */
+
+ if (ok_so_far() == TX_TRUE)
+ {
+
+ arm_probe(TX_TRUE, TX_TRUE, TX_TRUE, TX_FALSE);
+ victim_take_mutex = TX_FALSE;
+ victim_wait_semaphore = TX_FALSE;
+
+ expect_status(create_victim(VICTIM_PRIORITY, TX_AUTO_START), TX_SUCCESS, 100);
+ }
+
+ if (ok_so_far() == TX_TRUE)
+ {
+
+ /* Let the victim run to completion, so that its callback runs inside
+ the completion transition. */
+ tx_thread_sleep(3);
+
+#ifndef TX_DISABLE_NOTIFY_CALLBACKS
+
+ expect(probe_ran, 101);
+ expect_status(probe_state, TX_COMPLETED, 102);
+ expect_status(probe_suspending, TX_TRUE, 103);
+ expect_status(probe_delete_status, TX_DELETE_ERROR, 104);
+ expect_status(probe_core_delete_status, TX_DELETE_ERROR, 105);
+
+ /* Reset of the running thread is refused for two independent reasons
+ here -- it is the current thread, and its transition is in progress
+ -- so this leg is a guard rather than a discriminator. Scenario B
+ is where the transition check is the only thing refusing a reset. */
+ expect_status(probe_reset_status, TX_NOT_DONE, 106);
+ expect_status(probe_core_reset_status, TX_NOT_DONE, 107);
+
+ /* A completed thread is neither resumable nor suspendable, and
+ terminating one is a documented no-op. */
+ expect_status(probe_resume_status, TX_RESUME_ERROR, 108);
+ expect_status(probe_suspend_status, TX_SUSPEND_ERROR, 109);
+ expect_status(probe_terminate_status, TX_SUCCESS, 110);
+ expect_status(delete_succeeded, TX_FALSE, 111);
+ expect_status(probe_consistent, TX_TRUE, 112);
+#endif
+
+ /* The transition has finished, so the calls the callback was refused
+ must succeed. The refusal is transient and nothing has become
+ permanently undeletable. */
+ expect_status(victim.tx_thread_state, TX_COMPLETED, 113);
+ expect_status(victim.tx_thread_suspending, TX_FALSE, 114);
+ expect(fully_detached(&victim), 115);
+ expect(scheduler_consistent(), 116);
+ expect(((victim_counter > 0) ? TX_TRUE : TX_FALSE), 117);
+ }
+
+ if (ok_so_far() == TX_TRUE)
+ {
+
+ arm_probe(TX_FALSE, TX_FALSE, TX_FALSE, TX_FALSE);
+
+ /* Reset, so that the documented reset of a completed thread is covered
+ on a target whose transition has finished, and so that the
+ reset/terminate/delete order is exercised on this branch too. */
+ expect_status(tx_thread_reset(&victim), TX_SUCCESS, 118);
+ expect_status(victim.tx_thread_state, TX_SUSPENDED, 119);
+ expect_status(tx_thread_delete(&victim), TX_DELETE_ERROR, 120);
+ expect_status(tx_thread_terminate(&victim), TX_SUCCESS, 121);
+ expect_status(victim.tx_thread_suspending, TX_FALSE, 122);
+ expect_status(tx_thread_delete(&victim), TX_SUCCESS, 123);
+ }
+
+ /* A safe post-completion recreate at a different priority, which is the
+ lifecycle the callback was refused, performed at the point it is
+ legitimate. It exercises the priority map and the list heads for a
+ recreate that moves priority as well as for one that does not. */
+
+ if (ok_so_far() == TX_TRUE)
+ {
+
+ expect_status(create_victim(RECREATE_PRIORITY, TX_AUTO_START), TX_SUCCESS, 124);
+
+ tx_thread_sleep(3);
+
+ expect_status(victim.tx_thread_state, TX_COMPLETED, 125);
+ expect(scheduler_consistent(), 126);
+ expect_status(tx_thread_delete(&victim), TX_SUCCESS, 127);
+ }
+
+ /* ------------------------------------------------------------------- */
+ /* Scenario E: self termination. The callback fires once, in the */
+ /* victim's own context, the lifecycle mutation is refused there too, */
+ /* and another thread can complete the lifecycle afterwards. */
+ /* ------------------------------------------------------------------- */
+
+ if (ok_so_far() == TX_TRUE)
+ {
+
+ victim_self_terminate = TX_TRUE;
+ expected_exits = callback_exit_counter;
+
+ arm_probe(TX_TRUE, TX_TRUE, TX_FALSE, TX_FALSE);
+
+ expect_status(create_victim(VICTIM_PRIORITY, TX_AUTO_START), TX_SUCCESS, 130);
+ }
+
+ if (ok_so_far() == TX_TRUE)
+ {
+
+ tx_thread_sleep(3);
+
+#ifndef TX_DISABLE_NOTIFY_CALLBACKS
+
+ /* Exactly one exit notification, even though the thread terminated
+ itself from inside its own entry function. */
+ expect_count(callback_exit_counter, expected_exits + 1, 131);
+ expect_status(probe_state, TX_TERMINATED, 132);
+ expect_status(probe_suspending, TX_TRUE, 133);
+ expect_status(probe_delete_status, TX_DELETE_ERROR, 134);
+ expect_status(probe_core_delete_status, TX_DELETE_ERROR, 135);
+ expect_status(probe_reset_status, TX_NOT_DONE, 136);
+ expect_status(delete_succeeded, TX_FALSE, 137);
+ expect_status(probe_consistent, TX_TRUE, 138);
+#else
+
+ expect_count(callback_exit_counter, expected_exits, 139);
+#endif
+
+ expect_status(victim.tx_thread_state, TX_TERMINATED, 140);
+ expect_status(victim.tx_thread_suspending, TX_FALSE, 141);
+ expect(fully_detached(&victim), 142);
+ }
+
+ if (ok_so_far() == TX_TRUE)
+ {
+
+ arm_probe(TX_FALSE, TX_FALSE, TX_FALSE, TX_FALSE);
+
+ expect_status(tx_thread_reset(&victim), TX_SUCCESS, 143);
+ expect_status(tx_thread_terminate(&victim), TX_SUCCESS, 144);
+ expect_status(tx_thread_delete(&victim), TX_SUCCESS, 145);
+ expect(scheduler_consistent(), 146);
+ }
+
+ /* ------------------------------------------------------------------- */
+ /* Scenario F: a benign callback is unaffected. The notification */
+ /* ordering and counts are what they were. */
+ /* ------------------------------------------------------------------- */
+
+ if (ok_so_far() == TX_TRUE)
+ {
+
+ victim_self_terminate = TX_FALSE;
+ expected_exits = callback_exit_counter;
+ expected_entries = callback_entry_counter;
+ exit_saw_entry_first = TX_FALSE;
+ last_notify_type = PROBE_NOT_ATTEMPTED;
+
+ arm_probe(TX_FALSE, TX_FALSE, TX_FALSE, TX_FALSE);
+
+ expect_status(create_victim(VICTIM_PRIORITY, TX_AUTO_START), TX_SUCCESS, 150);
+ }
+
+ if (ok_so_far() == TX_TRUE)
+ {
+
+ tx_thread_sleep(3);
+
+#ifndef TX_DISABLE_NOTIFY_CALLBACKS
+
+ /* This incarnation ran and completed, so it reported exactly one entry
+ and one exit, and the entry came first. The transition marker has
+ changed neither how often notifications are delivered nor in what
+ order. */
+ expect_count(callback_exit_counter, expected_exits + 1, 151);
+ expect_count(callback_entry_counter, expected_entries + 1, 152);
+ expect_status(exit_saw_entry_first, TX_TRUE, 153);
+#else
+
+ expect_count(callback_exit_counter, expected_exits, 154);
+ expect_count(callback_entry_counter, expected_entries, 155);
+#endif
+
+ expect_status(tx_thread_delete(&victim), TX_SUCCESS, 156);
+ expect(scheduler_consistent(), 157);
+ }
+
+ /* ------------------------------------------------------------------- */
+ /* Scenario G: state and boundary cases. A thread that never entered a */
+ /* terminal state, and one that is already fully detached, behave as */
+ /* documented -- the new refusal applies only while the marker is set. */
+ /* ------------------------------------------------------------------- */
+
+ if (ok_so_far() == TX_TRUE)
+ {
+
+ expect_status(create_victim(VICTIM_PRIORITY, TX_DONT_START), TX_SUCCESS, 170);
+
+ /* Suspended, never run: neither state is terminal, so both services
+ are refused for the reason they always were, and the marker is not
+ set on a thread that is not transitioning. */
+ expect_status(tx_thread_delete(&victim), TX_DELETE_ERROR, 171);
+ expect_status(tx_thread_reset(&victim), TX_NOT_DONE, 172);
+ expect_status(victim.tx_thread_suspending, TX_FALSE, 173);
+
+ /* Terminated from a suspended state: deletable immediately, and
+ repeatedly resettable and re-terminable in between. */
+ expect_status(tx_thread_terminate(&victim), TX_SUCCESS, 174);
+ expect_status(victim.tx_thread_suspending, TX_FALSE, 175);
+ expect_status(tx_thread_reset(&victim), TX_SUCCESS, 176);
+ expect_status(tx_thread_terminate(&victim), TX_SUCCESS, 177);
+ expect_status(tx_thread_terminate(&victim), TX_SUCCESS, 178);
+ expect_status(tx_thread_delete(&victim), TX_SUCCESS, 179);
+ expect(scheduler_consistent(), 180);
+ }
+
+ if (expectations_failed != 0)
+ {
+
+ printf("ERROR #%u (%lu of %lu expectations failed:",
+ first_failure, expectations_failed, expectations_checked);
+
+ for (index = 0; index < failed_tag_count; index++)
+ {
+ printf(" %u", failed_tags[index]);
+ }
+
+ if (expectations_failed > ((unsigned long) failed_tag_count))
+ {
+ printf(" ...");
+ }
+
+ printf(")\n");
+ fflush(stdout);
+ test_control_return(1);
+ }
+ else
+ {
+ printf("SUCCESS!\n");
+ test_control_return(0);
+ }
+}
+
+
+static void victim_entry(ULONG thread_input)
+{
+
+ (void) thread_input;
+
+ victim_counter++;
+
+ if (victim_take_mutex == TX_TRUE)
+ {
+ tx_mutex_get(&victim_mutex, TX_NO_WAIT);
+ }
+
+ if (victim_wait_semaphore == TX_TRUE)
+ {
+
+ /* Suspend on an object, so that a termination takes the non-ready
+ branch and runs the suspension cleanup routine. */
+ tx_semaphore_get(&victim_semaphore, TX_WAIT_FOREVER);
+ }
+
+ if (victim_self_terminate == TX_TRUE)
+ {
+
+ /* This does not return: the termination detaches this thread and the
+ preemption check at the end of the service switches away from it. */
+ tx_thread_terminate(tx_thread_identify());
+ }
+
+ while (victim_hold == TX_TRUE)
+ {
+
+ /* Stay ready, so that a terminate issued by another thread finds this
+ thread on the ready list. */
+ tx_thread_relinquish();
+ }
+
+ /* Returning enters the completion transition. */
+}
+
+
+static void peer_entry(ULONG thread_input)
+{
+
+ if (thread_input == 1)
+ {
+ peer_a_counter++;
+ }
+ else
+ {
+ peer_b_counter++;
+ }
+
+ while (peers_run == TX_TRUE)
+ {
+
+ /* Stay ready at the victim's priority, so that the victim's ready ring
+ has more than one member. */
+ tx_thread_relinquish();
+ }
+}
+
+
+static void replacement_entry(ULONG thread_input)
+{
+
+ (void) thread_input;
+
+ replacement_counter++;
+
+ /* Only reached on a kernel that let the callback recreate the control
+ block. Suspend rather than return, so that the run does not depend on a
+ second completion transition over a control block already known to be
+ inconsistent. */
+ tx_thread_suspend(tx_thread_identify());
+}
diff --git a/test/tx/regression/threadx_thread_information_test.c b/test/tx/regression/threadx_thread_information_test.c
index b5d06e71..fef3473f 100644
--- a/test/tx/regression/threadx_thread_information_test.c
+++ b/test/tx/regression/threadx_thread_information_test.c
@@ -9,6 +9,8 @@
/* SPDX-License-Identifier: MIT */
/***************************************************************************/
+/* Portions of this file were generated with AI assistance. */
+
/* This test is for the thread information services. */
#include <stdio.h>
@@ -63,7 +65,7 @@ static void thread_0_entry(ULONG thread_input)
{
UINT status;
-CHAR *name;
+TX_NAME_CONST CHAR *name;
UINT state;
ULONG run_count;
UINT priority;
@@ -96,7 +98,7 @@ ULONG idle_returns;
status += tx_thread_info_get(&thread_0, &name, &state, &run_count, &priority, &preemption_threshold, &time_slice, &next_thread, &suspended_thread);
/* Check for error status. */
- if ((status != TX_SUCCESS) || (state != TX_READY) || (run_count != thread_0.tx_thread_run_count) || (priority != 16) || (preemption_threshold != 16) ||
+ if ((status != TX_SUCCESS) || (name != thread_0.tx_thread_name) || (state != TX_READY) || (run_count != thread_0.tx_thread_run_count) || (priority != 16) || (preemption_threshold != 16) ||
(time_slice != 0) || (next_thread != thread_0.tx_thread_created_next) || (suspended_thread != thread_0.tx_thread_suspended_next))
{
diff --git a/test/tx/regression/threadx_thread_misaligned_stack_test.c b/test/tx/regression/threadx_thread_misaligned_stack_test.c
new file mode 100644
index 00000000..63d64775
--- /dev/null
+++ b/test/tx/regression/threadx_thread_misaligned_stack_test.c
@@ -0,0 +1,150 @@
+/***************************************************************************
+ * 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
+ **************************************************************************/
+
+/* This test creates a thread whose stack does not begin on a ULONG boundary.
+
+ tx_thread_create rounds the starting address up to the next ULONG and then
+ takes a ULONG off the size, so that the rounding cannot push the end of the
+ stack past the memory the caller supplied:
+
+ if (new_stack_start != updated_stack_start)
+ {
+ stack_size = stack_size - (sizeof(ULONG));
+ }
+
+ Every other test in the suite hands tx_thread_create an aligned stack, so
+ that subtraction was never executed -- the only uncovered line in
+ tx_thread_create.c across all five build configurations. A misaligned stack
+ is legitimate: the API takes a VOID * and the alignment fix-up exists
+ precisely to accept one.
+
+ The thread is then run to completion rather than merely created, because the
+ point of the adjustment is that the resulting stack is still usable and still
+ inside the caller's buffer. */
+
+#include <stdio.h>
+#include "tx_api.h"
+
+
+void test_control_return(UINT status);
+
+
+static TX_THREAD thread_0;
+static TX_THREAD thread_1;
+
+/* Thread 1's stack is carved out of this by hand so that its start can be put
+ deliberately off a ULONG boundary. The extra ULONG is what the misaligned
+ start is offset into, so that the stack still ends inside the array after
+ tx_thread_create has rounded the start up. */
+
+static UCHAR misaligned_area[TEST_STACK_SIZE_PRINTF + sizeof(ULONG)];
+
+static ULONG thread_1_counter = 0;
+
+
+static void thread_0_entry(ULONG thread_input);
+static void thread_1_entry(ULONG thread_input);
+
+
+/* Define what the initial system looks like. */
+
+#ifdef CTEST
+void test_application_define(void *first_unused_memory)
+#else
+void threadx_thread_misaligned_stack_application_define(void *first_unused_memory)
+#endif
+{
+
+UINT status;
+CHAR *pointer;
+CHAR *misaligned_stack;
+
+
+ /* Setup a pointer. */
+ pointer = (CHAR *) first_unused_memory;
+
+ status = tx_thread_create(&thread_0, "thread 0", thread_0_entry, 0,
+ pointer, TEST_STACK_SIZE_PRINTF,
+ 16, 16, TX_NO_TIME_SLICE, TX_AUTO_START);
+ pointer = pointer + TEST_STACK_SIZE_PRINTF;
+
+ if (status != TX_SUCCESS)
+ {
+
+ printf("Running Thread Misaligned Stack Test................................ ERROR #1\n");
+ test_control_return(1);
+ }
+
+ /* Push the start one byte past a ULONG boundary. The array itself is
+ aligned, so adding one is enough to guarantee the fix-up runs whatever
+ the alignment of the array turned out to be. */
+ misaligned_stack = ((CHAR *) misaligned_area) + 1;
+
+ status = tx_thread_create(&thread_1, "thread 1", thread_1_entry, 0,
+ misaligned_stack, TEST_STACK_SIZE_PRINTF,
+ 15, 15, TX_NO_TIME_SLICE, TX_AUTO_START);
+
+ if (status != TX_SUCCESS)
+ {
+
+ printf("Running Thread Misaligned Stack Test................................ ERROR #2\n");
+ test_control_return(1);
+ }
+}
+
+
+static void thread_0_entry(ULONG thread_input)
+{
+
+ /* Thread 1 is the higher priority of the two, so this thread first gets the
+ processor while thread 1 is sleeping. Sleep past that, so the check below
+ is made against a thread that has actually finished. */
+ tx_thread_sleep(5);
+
+ /* Both increments must have happened: one before thread 1 slept and one
+ after it woke, which is the half that needed the stack to survive a
+ context switch. */
+ if (thread_1_counter != ((ULONG) 2))
+ {
+
+ printf("Running Thread Misaligned Stack Test................................ ERROR #3\n");
+ test_control_return(1);
+ }
+
+ /* The thread that ran on the misaligned stack must have run to completion,
+ which is what says the adjusted stack was usable. */
+ if (thread_1.tx_thread_state != TX_COMPLETED)
+ {
+
+ printf("Running Thread Misaligned Stack Test................................ ERROR #4\n");
+ test_control_return(1);
+ }
+
+ printf("Running Thread Misaligned Stack Test................................ SUCCESS!\n");
+ test_control_return(0);
+}
+
+
+static void thread_1_entry(ULONG thread_input)
+{
+
+ /* Use the stack either side of a context switch rather than returning
+ straight away. */
+ thread_1_counter++;
+
+ tx_thread_sleep(1);
+
+ thread_1_counter++;
+}
diff --git a/test/tx/regression/threadx_thread_multi_level_preemption_threshold_test.c b/test/tx/regression/threadx_thread_multi_level_preemption_threshold_test.c
index 59ed4f4d..f7159384 100644
--- a/test/tx/regression/threadx_thread_multi_level_preemption_threshold_test.c
+++ b/test/tx/regression/threadx_thread_multi_level_preemption_threshold_test.c
@@ -9,6 +9,8 @@
/* SPDX-License-Identifier: MIT */
/***************************************************************************/
+/* Portions of this file were generated with AI assistance. */
+
/* This test is designed to test multi-level preemption threshold. The protection placed
by a thread must be preserved after higher-priority thread preemption that is above the threshold. */
@@ -74,7 +76,7 @@ static TX_TIMER timer_0;
/* Define counters. */
-static unsigned long timer_0_counter = 0;
+static volatile unsigned long timer_0_counter = 0;
static unsigned long thread_1_0_counter = 0;
static unsigned long thread_2_1_counter = 0;
static unsigned long thread_3_2_counter = 0;
diff --git a/test/tx/regression/threadx_thread_priority_change.c b/test/tx/regression/threadx_thread_priority_change.c
index 1cb52ff9..ac750a5f 100644
--- a/test/tx/regression/threadx_thread_priority_change.c
+++ b/test/tx/regression/threadx_thread_priority_change.c
@@ -354,6 +354,15 @@ static void thread_3_entry(ULONG thread_input)
UINT old_priority;
UINT loop;
+/* The interrupt below only fires on a narrow window: thread 3 at priority 6,
+ ready, and not yet at the head of its priority list. Waiting for it without a
+ bound made this test hang indefinitely when the window was never hit, which
+ showed up in CI as a timeout carrying no information about what went wrong.
+ The TX_NOT_INTERRUPTABLE path already stopped after a fixed amount of work;
+ this gives the interruptable path the same protection. */
+UINT attempts = 0;
+#define MAX_PRIORITY_CHANGE_ATTEMPTS 100000
+
/* Resume threads 4. */
tx_thread_resume(&thread_4);
@@ -378,7 +387,23 @@ UINT loop;
if (thread_4_counter)
thread_1_counter++;
- } while (test_isr_dispatch);
+ attempts++;
+
+ } while (test_isr_dispatch && (attempts < MAX_PRIORITY_CHANGE_ATTEMPTS));
+
+ /* Stop the interrupt regardless, so a window that was never hit does not
+ leave the handler installed for the rest of the test. */
+ test_isr_dispatch = TX_NULL;
+
+ if (attempts >= MAX_PRIORITY_CHANGE_ATTEMPTS)
+ {
+
+ /* The window was not reached. That is a gap in what this test covered on
+ this run rather than a fault in the code under test, so say so instead
+ of failing, and leave the counters alone so the checks that follow
+ still mean what they did before. */
+ printf("(priority change window not reached in %u attempts) ", attempts);
+ }
}
diff --git a/test/tx/regression/threadx_thread_stack_checking_test.c b/test/tx/regression/threadx_thread_stack_checking_test.c
index 9db10d0a..2ef9a2df 100644
--- a/test/tx/regression/threadx_thread_stack_checking_test.c
+++ b/test/tx/regression/threadx_thread_stack_checking_test.c
@@ -9,6 +9,8 @@
/* SPDX-License-Identifier: MIT */
/***************************************************************************/
+// Portions of this file were generated with AI assistance.
+
/* This test is for the thread stack checking services. */
#include <stdio.h>
@@ -200,6 +202,19 @@ TX_THREAD fake_thread;
fake_thread.tx_thread_stack_highest_ptr = TX_NULL;
_tx_thread_stack_analyze(&fake_thread);
+ /* Call the stack analyze with a highest stack pointer below the start of the stack, which is
+ what a stack overflow or a corrupted control block looks like. This used to hang. */
+ fake_thread.tx_thread_id = ((ULONG) 0x54485244);
+ fake_thread.tx_thread_stack_start = (void *) 0x2000;
+ fake_thread.tx_thread_stack_highest_ptr = (void *) 0x1000;
+ _tx_thread_stack_analyze(&fake_thread);
+
+ /* Call the stack analyze with a highest stack pointer equal to the start of the stack. */
+ fake_thread.tx_thread_id = ((ULONG) 0x54485244);
+ fake_thread.tx_thread_stack_start = (void *) 0x2000;
+ fake_thread.tx_thread_stack_highest_ptr = (void *) 0x2000;
+ _tx_thread_stack_analyze(&fake_thread);
+
/* Clear the pattern in thread 2's stack. */
TX_MEMSET(thread_2_stack_start, (CHAR) 0x11, TEST_STACK_SIZE_PRINTF);
diff --git a/test/tx/regression/threadx_thread_stack_fill_value_test.c b/test/tx/regression/threadx_thread_stack_fill_value_test.c
new file mode 100644
index 00000000..fa6eda22
--- /dev/null
+++ b/test/tx/regression/threadx_thread_stack_fill_value_test.c
@@ -0,0 +1,190 @@
+/***************************************************************************/
+/* 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 */
+/***************************************************************************/
+
+// Portions of this file were generated with AI assistance.
+
+/* This test checks that the stack fill value recorded in a thread control block is the
+ value that was actually written into that thread's stack. The two used to disagree
+ when TX_ENABLE_RANDOM_NUMBER_STACK_FILLING was enabled, because the control block was
+ cleared after the random value had been chosen and the stack had been filled with it,
+ which left the stack checking and stack analyze routines looking for a pattern that is
+ not the one present in the stack. */
+
+#include <stdio.h>
+#include "tx_api.h"
+
+
+/* Number of probe threads created. A random fill value can legitimately come out as zero,
+ which hides the defect for that one thread, so several threads are created and at least
+ one of them is required to have produced a non-zero pattern. */
+
+#define TEST_PROBE_THREADS 16
+
+
+static unsigned long thread_0_counter = 0;
+static TX_THREAD thread_0;
+
+static TX_THREAD probe_thread;
+static ULONG probe_stack[TX_MINIMUM_STACK / sizeof(ULONG)];
+
+
+/* Define task prototypes. */
+
+static void thread_0_entry(ULONG task_input);
+
+
+/* Prototype for test control return. */
+
+void test_control_return(UINT status);
+
+
+/* Define what the initial system looks like. */
+
+#ifdef CTEST
+void test_application_define(void *first_unused_memory)
+#else
+void threadx_thread_stack_fill_value_application_define(void *first_unused_memory)
+#endif
+{
+
+UINT status;
+CHAR *pointer;
+
+ pointer = (CHAR *) first_unused_memory;
+
+ /* Put system definition stuff in here, e.g. thread creates and other assorted
+ create information. */
+
+ status = tx_thread_create(&thread_0, "thread 0", thread_0_entry, 0,
+ pointer, TEST_STACK_SIZE_PRINTF,
+ 16, 16, TX_NO_TIME_SLICE, TX_AUTO_START);
+
+ /* Check for status. */
+ if (status != TX_SUCCESS)
+ {
+
+ printf("Running Thread Stack Fill Value Test................................ ERROR #1\n");
+ test_control_return(1);
+ }
+}
+
+
+/* Define the test threads. */
+
+static void thread_0_entry(ULONG thread_input)
+{
+
+UINT status;
+UINT i;
+ULONG stack_pattern;
+UINT non_zero_seen = TX_FALSE;
+
+
+ /* Increment thread 0 counter. */
+ thread_0_counter++;
+
+ /* Inform user of success getting to this test. */
+ printf("Running Thread Stack Fill Value Test................................ ");
+
+ for (i = 0; i < TEST_PROBE_THREADS; i++)
+ {
+
+ /* Create a thread that is never started, so that its stack still holds nothing but
+ the fill pattern and the initial stack frame built at the top of the stack. */
+ status = tx_thread_create(&probe_thread, "probe thread", thread_0_entry, 0,
+ probe_stack, sizeof(probe_stack),
+ 17, 17, TX_NO_TIME_SLICE, TX_DONT_START);
+
+ /* Check for status. */
+ if (status != TX_SUCCESS)
+ {
+
+ printf("ERROR #2\n");
+ test_control_return(1);
+ }
+
+ /* Pick up the pattern the create service wrote into the unused part of the stack. */
+ stack_pattern = *(((ULONG *) probe_thread.tx_thread_stack_start) + 4);
+
+#ifndef TX_DISABLE_STACK_FILLING
+#if defined(TX_ENABLE_RANDOM_NUMBER_STACK_FILLING) && defined(TX_ENABLE_STACK_CHECKING)
+
+ /* The control block must hold the value that was written into the stack. */
+ if (probe_thread.tx_thread_stack_fill_value != stack_pattern)
+ {
+
+ printf("ERROR #3\n");
+ test_control_return(1);
+ }
+
+ /* The random value is duplicated into all four bytes of the fill value. */
+ if ((stack_pattern & 0xFFUL) !=
+ ((stack_pattern >> 24) & 0xFFUL))
+ {
+
+ printf("ERROR #4\n");
+ test_control_return(1);
+ }
+
+ if (stack_pattern != ((ULONG) 0))
+ {
+
+ non_zero_seen = TX_TRUE;
+ }
+#else
+
+ /* Without random filling, the pattern is the fixed one. */
+ if (stack_pattern != ((ULONG) 0xEFEFEFEFUL))
+ {
+
+ printf("ERROR #5\n");
+ test_control_return(1);
+ }
+
+ non_zero_seen = TX_TRUE;
+#endif
+#else
+
+ /* Stack filling is disabled, so there is nothing to compare. */
+ non_zero_seen = TX_TRUE;
+#endif
+
+ /* Terminate and delete the probe thread so that the next pass starts from a clean
+ slate. A thread created with TX_DONT_START is suspended rather than completed,
+ so it must be terminated before it can be deleted. */
+ status = tx_thread_terminate(&probe_thread);
+
+ if (status == TX_SUCCESS)
+ {
+
+ status = tx_thread_delete(&probe_thread);
+ }
+
+ /* Check for status. */
+ if (status != TX_SUCCESS)
+ {
+
+ printf("ERROR #6\n");
+ test_control_return(1);
+ }
+ }
+
+ /* A run in which every fill value came out as zero would prove nothing. */
+ if (non_zero_seen != TX_TRUE)
+ {
+
+ printf("ERROR #7\n");
+ test_control_return(1);
+ }
+
+ /* Success! */
+ printf("SUCCESS!\n");
+ test_control_return(0);
+}
diff --git a/test/tx/regression/threadx_thread_wait_abort_and_isr_test.c b/test/tx/regression/threadx_thread_wait_abort_and_isr_test.c
index e15b410f..6745ad0d 100644
--- a/test/tx/regression/threadx_thread_wait_abort_and_isr_test.c
+++ b/test/tx/regression/threadx_thread_wait_abort_and_isr_test.c
@@ -12,6 +12,7 @@
/* This test is designed to test for simultaneous thread suspension lifting AND thread wait abort calls. */
#include <stdio.h>
+#include <time.h>
#include "tx_api.h"
static unsigned long thread_0_counter = 0;
@@ -31,6 +32,90 @@ extern UINT _tx_timer_system_clock;
static TX_SEMAPHORE semaphore_0;
+#ifdef __linux__
+
+/* The window this test waits for is a few instructions wide, and the interrupt
+ that samples it is the same one that wakes the thread being sampled, so the
+ two run in lockstep: the tick wakes thread 0, thread 0 does a fixed amount of
+ work and suspends, and the next tick arrives a fixed interval later with
+ thread 0 in the same place every time. That is the resonance the handler below
+ describes, and perturbing the handler's duration only shifts the phase rather
+ than decorrelating it.
+
+ The simulator's timer thread waits on _tx_linux_timer_semaphore with a
+ one-tick deadline and delivers an interrupt early if the semaphore is posted,
+ which the port itself relies on elsewhere. A plain POSIX thread posting it
+ therefore injects interrupts at moments unrelated to the tick grid, which
+ samples thread 0 at arbitrary points in its cycle instead of the same one.
+
+ It posts only when nothing is outstanding, so interrupts can never be queued
+ faster than they are serviced and the injector cannot starve the system. */
+
+#include <pthread.h>
+#include <semaphore.h>
+#include <unistd.h>
+
+extern sem_t _tx_linux_timer_semaphore;
+
+static volatile int injector_stop = 0;
+static pthread_t injector_thread;
+
+#define INJECTOR_INTERVAL_USEC 100
+
+
+static void *interrupt_injector(void *p)
+{
+
+int pending;
+
+
+ (void) p;
+
+ while (injector_stop == 0)
+ {
+
+ if ((sem_getvalue(&_tx_linux_timer_semaphore, &pending) == 0) && (pending == 0))
+ {
+ sem_post(&_tx_linux_timer_semaphore);
+ }
+
+ usleep(INJECTOR_INTERVAL_USEC);
+ }
+
+ return(NULL);
+}
+
+
+static void interrupt_injector_start(void)
+{
+
+ injector_stop = 0;
+ if (pthread_create(&injector_thread, NULL, interrupt_injector, NULL) != 0)
+ {
+
+ /* Without the injector the loop still runs, on its own budget. */
+ injector_stop = 1;
+ }
+}
+
+
+static void interrupt_injector_stop(void)
+{
+
+ if (injector_stop == 0)
+ {
+
+ injector_stop = 1;
+ pthread_join(injector_thread, NULL);
+ }
+}
+
+#else
+#define interrupt_injector_start()
+#define interrupt_injector_stop()
+#endif
+
+
/* Define thread prototypes. */
static void thread_0_entry(ULONG thread_input);
@@ -177,6 +262,56 @@ static void thread_0_entry(ULONG thread_input)
UINT status;
+/* The window this test waits for is probabilistic, and the handler above says
+ as much: it can settle into a resonance in which the condition is never met.
+ Waiting for it without a bound means such a run never ends, and this test has
+ been the most expensive thing in the suite: run one at a time in CI it took
+ between 148 and 726 seconds per configuration, 88 percent of the whole
+ ThreadX suite, against 273 seconds for the other ninety five tests together.
+
+ The budget is in wall clock seconds rather than ticks on purpose. The
+ simulated tick clock is not a proxy for elapsed time here: a tick arrives only
+ when the port's timer thread gets to run, so under load, or under coverage
+ instrumentation, ticks fall behind and never catch up. A budget of 20000
+ ticks, nominally 200 seconds, failed to stop a run that took 726 seconds,
+ because fewer than 20000 ticks had passed. time() does not drift that way.
+
+ How many windows to ask for is set by what a run can actually reach. Before the
+ injector above, four CI runs of the same tree measured this loop in two modes:
+ one where a window arrives in milliseconds, and one where a single window
+ costs around forty seconds. Seven of twenty configuration-runs landed in the
+ slow mode and ran out of budget, reaching 0, 3, 3, 3, 4, 7 and 7 of the ten
+ then asked for, every one of them still reporting a pass, and the count had to
+ come down to three to stay reachable. With the interrupt source decorrelated
+ from the thread it samples the slow mode does not occur: all six
+ configurations of a run reach ten of ten in under a second. The count is
+ therefore back to ten, on the same reasoning that lowered it.
+
+ The SMP copy deliberately does not get the injector. It has never been in the
+ slow mode, reaching its twenty windows in about a second, and injecting
+ interrupts there measurably hurts: 8.9 seconds against 0 to 1 for the same
+ twenty, which is the extra interrupt traffic contending across the simulated
+ cores. Its budget and ceiling still match this one; only the injector differs,
+ because only this copy has the problem it solves.
+
+ Reaching the window no times at all is different in kind, which is what the
+ ceiling below is for. The check after the loop compares semaphore bookkeeping
+ that a window has to have touched to mean anything, so a pass with a count of
+ zero claims coverage the run did not have. One of those twenty runs did
+ exactly that, and said so only in an artifact nobody reads. A run that has not
+ reached the window once keeps trying up to the ceiling, and fails if it still
+ has not.
+
+ The budget and the ceiling stay as they were, as a net rather than a working
+ limit. They were sized for the slow mode: 180 seconds covers three windows at
+ the worst rate measured, 17 to 40 seconds each. Nothing should approach them
+ now, and if something does, the behaviour is what it was before. */
+#define WAIT_ABORT_WINDOWS_WANTED ((ULONG) 10)
+#define WAIT_ABORT_SECOND_BUDGET ((ULONG) 180)
+#define WAIT_ABORT_ZERO_WINDOW_CEILING ((ULONG) 300)
+
+time_t start_wall;
+
/* Setup ISR for this test. */
test_isr_dispatch = isr_entry;
@@ -185,7 +320,9 @@ UINT status;
printf("Running Thread Wait Abort and ISR Resume Test....................... ");
/* Loop to exploit the probability window inside tx_thread_wait_abort. */
- while (condition_count < 10)
+ start_wall = time(TX_NULL);
+ interrupt_injector_start();
+ while (condition_count < WAIT_ABORT_WINDOWS_WANTED)
{
/* Suspend on the semaphore that is going to be set via the ISR. */
@@ -226,11 +363,50 @@ UINT status;
#endif
}
+
+ /* Out of budget? A run that has not reached the window even once has
+ verified nothing yet, so it gets the higher ceiling before giving up. */
+#ifdef TX_NOT_INTERRUPTABLE
+ if (((ULONG) (time(TX_NULL) - start_wall)) > WAIT_ABORT_SECOND_BUDGET)
+ break;
+#else
+ if (condition_count == 0)
+ {
+ if (((ULONG) (time(TX_NULL) - start_wall)) > WAIT_ABORT_ZERO_WINDOW_CEILING)
+ break;
+ }
+ else if (((ULONG) (time(TX_NULL) - start_wall)) > WAIT_ABORT_SECOND_BUDGET)
+ break;
+#endif
}
+ interrupt_injector_stop();
+
/* Clear ISR dispatch. */
test_isr_dispatch = TX_NULL;
+ /* Say what this run reached, on every run and not only a short one. A count
+ printed only on shortfall cannot be told apart from a count nobody
+ recorded, and this line is what the CI artifacts carry. Falling short is a
+ gap in what was exercised rather than a fault in the code under test, so
+ the check below still runs and still means what it did. */
+ printf("(reached %lu of %lu windows in %lu seconds) ",
+ (ULONG) condition_count, WAIT_ABORT_WINDOWS_WANTED,
+ (ULONG) (time(TX_NULL) - start_wall));
+
+#ifndef TX_NOT_INTERRUPTABLE
+
+ /* Reached it no times? Then the check below compares bookkeeping no window
+ ever touched, and a pass would report coverage this run did not have. */
+ if (condition_count == 0)
+ {
+
+ /* Test error! */
+ printf("ERROR #8\n");
+ test_control_return(4);
+ }
+#endif
+
#ifdef TX_NOT_INTERRUPTABLE
/* At this point, check to see if we got all the semaphores! */
if ((thread_0_counter != (semaphore_put_counter - semaphore_0.tx_semaphore_count)) ||
diff --git a/test/tx/regression/threadx_timer_information_test.c b/test/tx/regression/threadx_timer_information_test.c
index ae3bb414..cbb88239 100644
--- a/test/tx/regression/threadx_timer_information_test.c
+++ b/test/tx/regression/threadx_timer_information_test.c
@@ -9,6 +9,8 @@
/* SPDX-License-Identifier: MIT */
/***************************************************************************/
+/* Portions of this file were generated with AI assistance. */
+
/* This test is designed to test timer information services. */
#include <stdio.h>
@@ -104,7 +106,7 @@ static void thread_0_entry(ULONG thread_input)
UINT status;
UINT interrupt_status;
-CHAR *name;
+TX_NAME_CONST CHAR *name;
UINT active;
ULONG remaining_ticks;
ULONG reschedule_ticks;
@@ -231,7 +233,7 @@ TX_TIMER_INTERNAL **list_head;
status += tx_timer_info_get(&timer_0, &name, &active, &remaining_ticks, &reschedule_ticks, &next_timer);
/* Check for successful completion. */
- if ((status != TX_SUCCESS) || (active != TX_TRUE) || (remaining_ticks != 100) || (reschedule_ticks != 200) || (next_timer != &timer_1))
+ if ((status != TX_SUCCESS) || (name != timer_0.tx_timer_name) || (active != TX_TRUE) || (remaining_ticks != 100) || (reschedule_ticks != 200) || (next_timer != &timer_1))
{
/* Application timer error. */
diff --git a/test/tx/regression/threadx_timer_simple_test.c b/test/tx/regression/threadx_timer_simple_test.c
index cad67086..f9c93ee4 100644
--- a/test/tx/regression/threadx_timer_simple_test.c
+++ b/test/tx/regression/threadx_timer_simple_test.c
@@ -9,6 +9,8 @@
/* SPDX-License-Identifier: MIT */
/***************************************************************************/
+/* Portions of this file were generated with AI assistance. */
+
/* This test is designed to test a simple application timer services, including create,
activate, deactivate, change, and delete. */
@@ -63,7 +65,7 @@ static void thread_1_entry(ULONG thread_input);
static void timer_0_expiration(ULONG timer_input);
static void timer_1_expiration(ULONG timer_input);
-UINT _txe_timer_create(TX_TIMER *timer_ptr, CHAR *name_ptr,
+UINT _txe_timer_create(TX_TIMER *timer_ptr, TX_NAME_CONST CHAR *name_ptr,
VOID (*expiration_function)(ULONG), ULONG expiration_input,
ULONG initial_ticks, ULONG reschedule_ticks, UINT auto_activate, UINT timer_control_block_size);
diff --git a/test/tx/regression/threadx_trace_basic_test.c b/test/tx/regression/threadx_trace_basic_test.c
index c40be8f1..cacf72b3 100644
--- a/test/tx/regression/threadx_trace_basic_test.c
+++ b/test/tx/regression/threadx_trace_basic_test.c
@@ -9,6 +9,8 @@
/* SPDX-License-Identifier: MIT */
/***************************************************************************/
+/* Portions of this file were generated with AI assistance. */
+
/* This test is designed to test trace functionality in ThreadX. */
@@ -23,7 +25,7 @@
extern VOID (*test_isr_dispatch)(void);
extern UINT _tx_trace_interrupt_control(UINT new_posture);
-extern VOID _tx_trace_object_register(UCHAR object_type, VOID *object_ptr, CHAR *object_name, ULONG parameter_1, ULONG parameter_2);
+extern VOID _tx_trace_object_register(UCHAR object_type, VOID *object_ptr, TX_NAME_CONST CHAR *object_name, ULONG parameter_1, ULONG parameter_2);
extern VOID _tx_trace_object_unregister(VOID *object_ptr);
static unsigned long thread_0_counter = 0;
@@ -66,6 +68,16 @@ static unsigned long error = 0;
static unsigned long full_buffer = 0;
static void *save_pointer;
+#define TRACE_BLOCK_POOL_SIZE ((ULONG) (((100U + sizeof(ALIGN_TYPE) - 1U) / sizeof(ALIGN_TYPE)) * sizeof(ALIGN_TYPE)))
+#define TRACE_BYTE_POOL_SIZE ((ULONG) (((1000U + sizeof(ALIGN_TYPE) - 1U) / sizeof(ALIGN_TYPE)) * sizeof(ALIGN_TYPE)))
+
+/* Keep pool storage aligned for ports with pointer-wide metadata. */
+static struct
+{
+ ALIGN_TYPE alignment;
+ CHAR bytes[TRACE_BLOCK_POOL_SIZE + TRACE_BYTE_POOL_SIZE + 400U];
+} object_memory;
+
#if defined(TX_WIN32_MEMORY_SIZE) || defined(TX_LINUX_MEMORY_SIZE)
static FILE *trace_dump_file;
@@ -185,25 +197,21 @@ INT status;
CHAR *pointer;
- /* Setup a pointer. */
- pointer = (CHAR *) first_unused_memory;
-
- /* Adjust it forward just to make sure there is some space for the test below. */
- pointer = pointer + 200;
+ /* Setup aligned object storage. */
+ pointer = object_memory.bytes;
/* Create a bunch of objects before being enabled. */
/* Create a timer for the test. */
save_pointer = (void *) pointer;
tx_timer_create(&timer_0, "timer 0", timer_entry, 0, 2, 2, TX_AUTO_ACTIVATE);
- tx_block_pool_create(&block_pool_0, "block pool 0", 20, pointer, 100);
- pointer = pointer + 100;
- tx_byte_pool_create(&byte_pool_0, "byte pool 0", pointer, 1000);
- pointer = pointer + 1000;
+ tx_block_pool_create(&block_pool_0, "block pool 0", 20, pointer, TRACE_BLOCK_POOL_SIZE);
+ pointer = pointer + TRACE_BLOCK_POOL_SIZE;
+ tx_byte_pool_create(&byte_pool_0, "byte pool 0", pointer, TRACE_BYTE_POOL_SIZE);
+ pointer = pointer + TRACE_BYTE_POOL_SIZE;
tx_event_flags_create(&group_0, "event flags group 0");
tx_mutex_create(&mutex_0, "mutex 0", TX_NO_INHERIT);
tx_queue_create(&queue_0, "queue 0", 16, pointer, 400);
- pointer = pointer + 400;
tx_semaphore_create(&semaphore_0, "semaphore 0", 1);
/* Enable event tracing. */
@@ -230,6 +238,9 @@ CHAR *pointer;
}
#endif
+ pointer = (CHAR *) first_unused_memory;
+ pointer = pointer + 200;
+
/* Put system definition stuff in here, e.g. thread creates and other assorted
create information. */
@@ -281,12 +292,11 @@ static void thread_0_entry(ULONG thread_input)
UINT status;
UINT old_interrupt;
CHAR *pointer;
-TX_INTERRUPT_SAVE_AREA
ULONG object;
/* Coverage for build without TraceX enabled. */
- tx_saved_posture = _tx_trace_interrupt_control(TX_INT_DISABLE);
- _tx_trace_interrupt_control(tx_saved_posture);
+ old_interrupt = _tx_trace_interrupt_control(TX_INT_DISABLE);
+ _tx_trace_interrupt_control(old_interrupt);
#ifndef TX_ENABLE_EVENT_TRACE
_tx_trace_object_register(0, TX_NULL, TX_NULL, 0, 0);
_tx_trace_object_register(1, TX_NULL, TX_NULL, 0, 0);
@@ -354,14 +364,13 @@ ULONG object;
/* Now, create them all again. */
pointer = (CHAR *) save_pointer;
- tx_block_pool_create(&block_pool_0, "block pool 0", 20, pointer, 100);
- pointer = pointer + 100;
- tx_byte_pool_create(&byte_pool_0, "byte pool 0", pointer, 1000);
- pointer = pointer + 1000;
+ tx_block_pool_create(&block_pool_0, "block pool 0", 20, pointer, TRACE_BLOCK_POOL_SIZE);
+ pointer = pointer + TRACE_BLOCK_POOL_SIZE;
+ tx_byte_pool_create(&byte_pool_0, "byte pool 0", pointer, TRACE_BYTE_POOL_SIZE);
+ pointer = pointer + TRACE_BYTE_POOL_SIZE;
tx_event_flags_create(&group_0, "event flags group 0");
tx_mutex_create(&mutex_0, "mutex 0", TX_NO_INHERIT);
tx_queue_create(&queue_0, "queue 0", 16, pointer, 400);
- pointer = pointer + 400;
tx_semaphore_create(&semaphore_0, "semaphore 0", 1);
/* Attempt to enable event tracing again. */
diff --git a/test/tx/regression/threadx_trace_entry_update_test.c b/test/tx/regression/threadx_trace_entry_update_test.c
new file mode 100644
index 00000000..f18c2d9b
--- /dev/null
+++ b/test/tx/regression/threadx_trace_entry_update_test.c
@@ -0,0 +1,383 @@
+/***************************************************************************
+ * 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
+ **************************************************************************/
+// Portions of this file were generated with AI assistance.
+
+/* This test drives the trace entry update paths -- the blocks guarded by
+ TX_ENABLE_EVENT_TRACE that go back and patch a trace entry after the call
+ they belong to has finished its work.
+
+ Those blocks all have the same shape:
+
+ if (entry_ptr != TX_NULL)
+ {
+ if (time_stamp == entry_ptr -> tx_trace_buffer_entry_time_stamp)
+ {
+ entry_ptr -> tx_trace_buffer_entry_information_field_N = ...;
+ }
+ }
+
+ entry_ptr comes from _tx_trace_buffer_current_ptr, which stays TX_NULL until
+ tx_trace_enable() is called at run time. Compiling with TX_ENABLE_EVENT_TRACE
+ is therefore not enough to reach them, and the trace_build configuration on
+ its own never does: the only other test that enables tracing --
+ threadx_trace_basic_test -- exercises the enable API itself and never calls
+ tx_block_allocate or tx_byte_allocate at all. So the update blocks in
+ tx_block_allocate.c, tx_byte_allocate.c, tx_thread_system_suspend.c and
+ tx_thread_system_resume.c were absent from the coverage report's covered set
+ while appearing in its denominator.
+
+ Both allocators are driven twice, because they carry two of these blocks
+ each and only one is on the path that succeeds immediately: the second sits
+ after a suspension, where the allocation completes in the releasing thread's
+ context. Hence the second thread, which exists only to release what
+ thread 0 is waiting on.
+
+ The test is built in all five configurations, and four of them do not define
+ TX_ENABLE_EVENT_TRACE. There tx_trace_enable reports TX_FEATURE_NOT_ENABLED,
+ the update blocks do not exist, and the allocation sequence below is still a
+ valid exercise of both allocators' suspension paths -- so the test is
+ meaningful in every configuration rather than skipped in four. */
+
+#include <stdio.h>
+#include "tx_api.h"
+#define TX_SOURCE_CODE
+#include "tx_trace.h"
+
+
+void test_control_return(UINT status);
+
+
+static TX_THREAD thread_0;
+static TX_THREAD thread_1;
+
+static TX_BLOCK_POOL block_pool_0;
+static TX_BYTE_POOL byte_pool_0;
+static TX_SEMAPHORE semaphore_0;
+
+/* The trace buffer is sized generously on purpose. The inner test of each
+ update block compares the saved time stamp against the one in the entry, and
+ a buffer small enough to wrap between the call and its completion would
+ overwrite the entry and take the false branch every time -- the block would
+ be reached and its body still never run. */
+
+static UCHAR trace_buffer[16384];
+
+/* Enough storage for several 20-byte blocks on both 32-bit and 64-bit builds. */
+
+static UCHAR block_pool_area[100];
+static UCHAR byte_pool_area[512];
+
+/* Allocated by thread 0 and released by thread 1, which is what lets thread 0's
+ suspended allocations complete. */
+
+static void *held_block;
+static void *held_bytes;
+
+static UINT error = 0;
+
+
+static void thread_0_entry(ULONG thread_input);
+static void thread_1_entry(ULONG thread_input);
+
+
+/* Define what the initial system looks like. */
+
+#ifdef CTEST
+void test_application_define(void *first_unused_memory)
+#else
+void threadx_trace_entry_update_application_define(void *first_unused_memory)
+#endif
+{
+
+UINT status;
+CHAR *pointer;
+
+
+ /* Setup a pointer. */
+ pointer = (CHAR *) first_unused_memory;
+
+ /* Create the pools before tracing is enabled, so that the events this test
+ cares about are the allocations rather than the creates. */
+ status = tx_block_pool_create(&block_pool_0, "trace entry update block pool",
+ 20, block_pool_area, sizeof(block_pool_area));
+
+ if (status != TX_SUCCESS)
+ {
+
+ printf("Running Trace Entry Update Test..................................... ERROR #1\n");
+ test_control_return(1);
+ }
+
+ status = tx_byte_pool_create(&byte_pool_0, "trace entry update byte pool",
+ byte_pool_area, sizeof(byte_pool_area));
+
+ if (status != TX_SUCCESS)
+ {
+
+ printf("Running Trace Entry Update Test..................................... ERROR #2\n");
+ test_control_return(1);
+ }
+
+ /* Turn tracing on. This is the whole point of the test: without it
+ _tx_trace_buffer_current_ptr stays TX_NULL and every update block below
+ takes its guarding false branch. */
+ _tx_trace_initialize();
+ status = tx_trace_enable(trace_buffer, sizeof(trace_buffer), 16);
+
+#ifdef TX_ENABLE_EVENT_TRACE
+
+ if (status != TX_SUCCESS)
+ {
+
+ printf("Running Trace Entry Update Test..................................... ERROR #3\n");
+ test_control_return(1);
+ }
+#else
+
+ /* Four of the five build configurations reach this branch. The rest of the
+ test still runs; it just is not exercising any update block. */
+ if (status != TX_FEATURE_NOT_ENABLED)
+ {
+
+ printf("Running Trace Entry Update Test..................................... ERROR #4\n");
+ test_control_return(1);
+ }
+#endif
+
+ /* Register an object with no name at all, now that tracing is on. The name
+ copy in _tx_trace_object_register guards against a TX_NULL name and
+ breaks out of the copy loop, and nothing else in the suite takes that
+ branch with tracing enabled -- threadx_trace_basic_test makes the
+ equivalent call only under #ifndef TX_ENABLE_EVENT_TRACE, against the
+ no-op stub.
+
+ A semaphore rather than a thread, deliberately: for a thread object the
+ register function dereferences the pointer it is handed to read the
+ thread's priority, so that type needs a real TX_THREAD behind it. */
+ status = tx_semaphore_create(&semaphore_0, TX_NULL, 1);
+
+ if (status != TX_SUCCESS)
+ {
+
+ printf("Running Trace Entry Update Test..................................... ERROR #5\n");
+ test_control_return(1);
+ }
+
+ /* Thread 0 drives the test and must run first, so it takes the higher
+ priority -- the lower number. */
+ status = tx_thread_create(&thread_0, "thread 0", thread_0_entry, 0,
+ pointer, TEST_STACK_SIZE_PRINTF,
+ 15, 15, TX_NO_TIME_SLICE, TX_AUTO_START);
+ pointer = pointer + TEST_STACK_SIZE_PRINTF;
+
+ if (status != TX_SUCCESS)
+ {
+
+ printf("Running Trace Entry Update Test..................................... ERROR #6\n");
+ test_control_return(1);
+ }
+
+ /* Thread 1 releases what thread 0 waits on. It runs only while thread 0 is
+ suspended, which is exactly when it is needed. */
+ status = tx_thread_create(&thread_1, "thread 1", thread_1_entry, 0,
+ pointer, TEST_STACK_SIZE_PRINTF,
+ 16, 16, TX_NO_TIME_SLICE, TX_AUTO_START);
+ pointer = pointer + TEST_STACK_SIZE_PRINTF;
+
+ if (status != TX_SUCCESS)
+ {
+
+ printf("Running Trace Entry Update Test..................................... ERROR #7\n");
+ test_control_return(1);
+ }
+}
+
+
+static void thread_0_entry(ULONG thread_input)
+{
+
+UINT status;
+void *block_ptr;
+void *byte_ptr;
+UINT i;
+
+
+ /* Empty the block pool. The first allocation takes the immediate-success
+ path through tx_block_allocate, which carries the first update block.
+ The exact capacity is port-dependent because each block has a pointer-
+ sized header, so allocate until the pool reports that it is empty. */
+ held_block = TX_NULL;
+ for (i = 0; i < 6u; i++)
+ {
+
+ status = tx_block_allocate(&block_pool_0, &block_ptr, TX_NO_WAIT);
+
+ if (status == TX_NO_MEMORY)
+ {
+
+ break;
+ }
+
+ if (status != TX_SUCCESS)
+ {
+
+ error++;
+ break;
+ }
+
+ /* Keep the first one for thread 1 to give back. */
+ if (held_block == TX_NULL)
+ {
+
+ held_block = block_ptr;
+ }
+ }
+
+ if ((status != TX_NO_MEMORY) || (held_block == TX_NULL))
+ {
+
+ error++;
+ }
+
+ /* The pool is empty now, so this suspends. It completes in thread 1's
+ context when the block comes back, which is the second update block in
+ tx_block_allocate -- and the suspend and resume it goes through carry the
+ update blocks in tx_thread_system_suspend and tx_thread_system_resume. */
+ status = tx_block_allocate(&block_pool_0, &block_ptr, TX_WAIT_FOREVER);
+
+ if (status != TX_SUCCESS)
+ {
+
+ printf("Running Trace Entry Update Test..................................... ERROR #8\n");
+ test_control_return(1);
+ }
+
+ /* Now the same shape for the byte pool. Take most of it first, on the path
+ that succeeds immediately. */
+ status = tx_byte_allocate(&byte_pool_0, &held_bytes, 300, TX_NO_WAIT);
+
+ if (status != TX_SUCCESS)
+ {
+
+ printf("Running Trace Entry Update Test..................................... ERROR #9\n");
+ test_control_return(1);
+ }
+
+ /* Not enough left, so this one suspends and is satisfied by thread 1's
+ release. */
+ status = tx_byte_allocate(&byte_pool_0, &byte_ptr, 300, TX_WAIT_FOREVER);
+
+ if (status != TX_SUCCESS)
+ {
+
+ printf("Running Trace Entry Update Test..................................... ERROR #10\n");
+ test_control_return(1);
+ }
+
+ /* Sleep, so that thread 1 gets to run out its entry function and suspend
+ itself. That leaves nothing ready to execute, which is a third update
+ block in tx_thread_system_suspend -- the one on the path that sets
+ _tx_thread_execute_ptr to TX_NULL. The two suspensions above never reach
+ it, because the other thread was always ready to take over. */
+ status = tx_thread_sleep(5);
+
+ if (status != TX_SUCCESS)
+ {
+
+ printf("Running Trace Entry Update Test..................................... ERROR #11\n");
+ test_control_return(1);
+ }
+
+#ifdef TX_ENABLE_EVENT_TRACE
+
+ /* Every update block above compares a saved time stamp against the one in
+ the entry, so a port whose time source never changes makes all of them
+ take their true branch for the wrong reason: not because the entry
+ survived, but because every entry reads the same. Check the buffer holds
+ at least one entry with a time stamp the port actually supplied. The
+ events written by this point number in the dozens, so a single zero
+ reading from a real clock cannot fail this. */
+ {
+
+ TX_TRACE_BUFFER_ENTRY *entry_ptr;
+ UINT stamped;
+
+
+ stamped = TX_FALSE;
+
+ for (entry_ptr = _tx_trace_buffer_start_ptr; entry_ptr < _tx_trace_buffer_end_ptr; entry_ptr++)
+ {
+
+ if (entry_ptr -> tx_trace_buffer_entry_time_stamp != ((ULONG) 0))
+ {
+
+ stamped = TX_TRUE;
+ break;
+ }
+ }
+
+ if (stamped != TX_TRUE)
+ {
+
+ printf("Running Trace Entry Update Test..................................... ERROR #13\n");
+ test_control_return(1);
+ }
+ }
+#endif
+
+ if (error)
+ {
+
+ printf("Running Trace Entry Update Test..................................... ERROR #12\n");
+ test_control_return(1);
+ }
+ else
+ {
+
+ printf("Running Trace Entry Update Test..................................... SUCCESS!\n");
+ test_control_return(0);
+ }
+}
+
+
+static void thread_1_entry(ULONG thread_input)
+{
+
+UINT status;
+
+
+ /* Reached only when thread 0 has suspended on the empty block pool. */
+ status = tx_block_release(held_block);
+
+ if (status != TX_SUCCESS)
+ {
+
+ error++;
+ }
+
+ /* And again when thread 0 has suspended on the byte pool. Thread 0 preempts
+ on the release above, runs until it suspends a second time, and control
+ comes back here. */
+ status = tx_byte_release(held_bytes);
+
+ if (status != TX_SUCCESS)
+ {
+
+ error++;
+ }
+
+ /* Thread 0 reports the result; this thread has nothing left to do. */
+ tx_thread_suspend(&thread_1);
+}
diff --git a/test/tx/thread_stack_alignment/threadx_thread_stack_alignment_schedule_test.c b/test/tx/thread_stack_alignment/threadx_thread_stack_alignment_schedule_test.c
new file mode 100644
index 00000000..67642449
--- /dev/null
+++ b/test/tx/thread_stack_alignment/threadx_thread_stack_alignment_schedule_test.c
@@ -0,0 +1,154 @@
+/***************************************************************************
+ * 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 Codex (gpt-6.1-sol).
+ * 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
+ **************************************************************************/
+
+/* Exercise actual Linux thread creation and scheduling across sleep/wake.
+ Check metadata before the real port builder can touch an invalid address. */
+#include "tx_api.h"
+#include <stdio.h>
+
+#define STACK_BYTES 8192U
+#define PADDING_BYTES 32U
+#define SENTINEL 0xA5U
+
+static TX_THREAD worker;
+static TX_THREAD observer;
+static ALIGN_TYPE worker_storage[(STACK_BYTES + 2U * PADDING_BYTES) / sizeof(ALIGN_TYPE)];
+static ALIGN_TYPE observer_storage[(STACK_BYTES + 2U * PADDING_BYTES) / sizeof(ALIGN_TYPE)];
+static UCHAR *worker_input;
+static UCHAR *observer_input;
+static UINT offset;
+static volatile UINT visits;
+
+VOID __real__tx_thread_stack_build(TX_THREAD *thread_ptr, VOID (*entry)(VOID));
+
+/* An assertion exits cleanly on the original tree, before stack dereference or
+ creation of either application pthread. The CTest timeout catches hangs. */
+static VOID require(UINT condition, const CHAR *message)
+{
+ if (condition == 0U)
+ {
+ printf("FAIL: %s\n", message);
+ exit(1);
+ }
+}
+
+/* Validate full-width metadata before delegating to the genuine Linux builder. */
+VOID __wrap__tx_thread_stack_build(TX_THREAD *thread_ptr, VOID (*entry)(VOID))
+{
+UCHAR *input = (thread_ptr == &worker) ? worker_input : observer_input;
+ULONG remainder = (ULONG) ((ALIGN_TYPE) input % sizeof(ULONG));
+ULONG adjustment = (remainder == 0U) ? 0U : (ULONG) sizeof(ULONG) - remainder;
+UCHAR *expected_start = input + adjustment;
+ULONG expected_size = STACK_BYTES - (ULONG) sizeof(ULONG);
+
+ require((thread_ptr == &worker) || (thread_ptr == &observer), "unexpected thread build");
+ if (adjustment != 0U)
+ {
+ expected_size -= (ULONG) sizeof(ULONG);
+ }
+ printf("port build: input=%p start=%p end=%p size=%u\n", (VOID *) input,
+ thread_ptr -> tx_thread_stack_start, thread_ptr -> tx_thread_stack_end,
+ (UINT) thread_ptr -> tx_thread_stack_size);
+ require(thread_ptr -> tx_thread_stack_start == expected_start, "stack start retains high bits");
+ require(thread_ptr -> tx_thread_stack_size == expected_size, "stack guard reservations");
+ require(thread_ptr -> tx_thread_stack_end == expected_start + expected_size - 1U,
+ "stack end remains in supplied storage");
+ __real__tx_thread_stack_build(thread_ptr, entry);
+ require((ALIGN_TYPE) thread_ptr -> tx_thread_stack_ptr >= (ALIGN_TYPE) expected_start,
+ "initial stack pointer lower bound");
+ require((ALIGN_TYPE) thread_ptr -> tx_thread_stack_ptr <= (ALIGN_TYPE) (expected_start + expected_size - 1U),
+ "initial stack pointer upper bound");
+}
+
+/* Check padding after the real builder and after the worker has run. */
+static VOID check_padding(UCHAR *bytes, UINT start_offset)
+{
+UINT i;
+
+ for (i = 0U; i < sizeof(worker_storage); i++)
+ {
+ if ((i < PADDING_BYTES + start_offset) || (i >= PADDING_BYTES + start_offset + STACK_BYTES))
+ {
+ require(bytes[i] == SENTINEL, "caller padding survives creation/scheduling");
+ }
+ }
+}
+
+/* The worker must run twice, separated by an actual timer suspension. */
+static VOID worker_entry(ULONG input)
+{
+ require(input == 17U, "scalar worker input");
+ require(tx_thread_identify() == &worker, "worker was scheduled");
+ visits++;
+ require(tx_thread_sleep(1U) == TX_SUCCESS, "worker sleep/wake");
+ visits++;
+}
+
+/* A lower-priority observer gives the worker time to complete after waking. */
+static VOID observer_entry(ULONG input)
+{
+ require(input == 23U, "scalar observer input");
+ require(tx_thread_sleep(3U) == TX_SUCCESS, "observer sleep/wake");
+ require(visits == 2U, "worker ran across sleep/wake");
+ require(worker.tx_thread_state == TX_COMPLETED, "worker completed");
+ check_padding((UCHAR *) worker_storage, offset);
+ check_padding((UCHAR *) observer_storage, 0U);
+ puts("PASS: high-address stack survived real creation and scheduling");
+ exit(0);
+}
+
+/* Timer processing in ISR mode avoids an unrelated timer-thread stack. */
+VOID tx_application_define(VOID *unused)
+{
+UINT status;
+
+ (void) unused;
+ status = tx_thread_create(&worker, "alignment worker", worker_entry, 17U,
+ worker_input, STACK_BYTES, 15U, 15U, TX_NO_TIME_SLICE, TX_DONT_START);
+ require(status == TX_SUCCESS, "worker create");
+ require(worker.tx_thread_stack_highest_ptr == worker.tx_thread_stack_ptr, "worker highest pointer");
+ status = tx_thread_create(&observer, "alignment observer", observer_entry, 23U,
+ observer_input, STACK_BYTES, 16U, 16U, TX_NO_TIME_SLICE, TX_DONT_START);
+ require(status == TX_SUCCESS, "observer create");
+ require(observer.tx_thread_stack_highest_ptr == observer.tx_thread_stack_ptr, "observer highest pointer");
+ require((worker.tx_thread_state == TX_SUSPENDED) && (observer.tx_thread_state == TX_SUSPENDED),
+ "created threads suspended before resume");
+ check_padding((UCHAR *) worker_storage, offset);
+ check_padding((UCHAR *) observer_storage, 0U);
+ require(tx_thread_resume(&worker) == TX_SUCCESS, "worker resume");
+ require(tx_thread_resume(&observer) == TX_SUCCESS, "observer resume");
+}
+
+/* Require the genuine 4/8/8 Linux mismatch and actual high-address storage. */
+INT main(INT argc, CHAR **argv)
+{
+ setvbuf(stdout, TX_NULL, _IONBF, 0);
+ require(argc == 2, "one alignment offset required");
+ require((argv[1][0] >= '0') && (argv[1][0] <= '3') && (argv[1][1] == '\0'), "valid offset");
+ offset = (UINT) (argv[1][0] - '0');
+ worker_input = (UCHAR *) worker_storage + PADDING_BYTES + offset;
+ observer_input = (UCHAR *) observer_storage + PADDING_BYTES;
+ printf("sizeof(ULONG)=%zu sizeof(ALIGN_TYPE)=%zu sizeof(void*)=%zu offset=%u\n",
+ sizeof(ULONG), sizeof(ALIGN_TYPE), sizeof(VOID *), offset);
+ require((sizeof(ULONG) == 4U) && (sizeof(ALIGN_TYPE) == 8U) && (sizeof(VOID *) == 8U),
+ "native64 Linux port widths");
+ require(((ALIGN_TYPE) worker_input > (ALIGN_TYPE) ((ULONG) ~0U)) &&
+ ((ALIGN_TYPE) observer_input > (ALIGN_TYPE) ((ULONG) ~0U)), "high-address stacks");
+ memset(worker_storage, SENTINEL, sizeof(worker_storage));
+ memset(observer_storage, SENTINEL, sizeof(observer_storage));
+ tx_kernel_enter();
+ require(0U, "kernel unexpectedly returned");
+ return(1);
+}
diff --git a/test/tx/thread_stack_alignment/threadx_thread_stack_alignment_test.c b/test/tx/thread_stack_alignment/threadx_thread_stack_alignment_test.c
new file mode 100644
index 00000000..f3ecbac7
--- /dev/null
+++ b/test/tx/thread_stack_alignment/threadx_thread_stack_alignment_test.c
@@ -0,0 +1,228 @@
+/***************************************************************************
+ * 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 Codex (gpt-6.1-sol).
+ * 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
+ **************************************************************************/
+
+/* Execute the real create service and MISRA shim with Linux port types.
+ Only interrupt, stack-builder and scheduler boundaries are recorders. */
+#define TX_SOURCE_CODE
+#include "tx_api.h"
+#include "tx_thread.h"
+#include <stdio.h>
+
+#define STACK_BYTES 8192U
+#define PADDING_BYTES 32U
+#define SENTINEL 0xA5U
+#define CASE_COUNT (sizeof(ULONG) * sizeof(ULONG))
+
+/* Kernel state owned by this harness; the real shim declares it extern. */
+TX_THREAD *_tx_thread_created_ptr;
+TX_THREAD *_tx_thread_execute_ptr;
+ULONG _tx_thread_created_count;
+volatile ULONG _tx_thread_system_state;
+volatile UINT _tx_thread_preempt_disable;
+
+static TX_THREAD threads[CASE_COUNT];
+static ALIGN_TYPE storage[(STACK_BYTES + 2U * PADDING_BYTES + 16U) / sizeof(ALIGN_TYPE)];
+static VOID *recorded_start;
+static VOID *recorded_end;
+static ULONG recorded_size;
+static VOID *safe_stack_ptr;
+static UINT interrupt_depth;
+static UINT interrupt_errors;
+static UINT build_calls;
+static UINT preempt_calls;
+static UINT unexpected_calls;
+static UINT failures;
+
+/* Report all cases, including on the old tree, without using corrupt metadata
+ as an address to read or write. */
+static VOID check(UINT condition, const CHAR *message)
+{
+ if (condition == 0U)
+ {
+ printf("FAIL: %s\n", message);
+ failures++;
+ }
+}
+
+/* Record the stack handed to the port. The supplied pointer belongs to the
+ original allocation even when the create service truncates its metadata. */
+VOID _tx_thread_stack_build(TX_THREAD *thread_ptr, VOID (*entry)(VOID))
+{
+ recorded_start = thread_ptr -> tx_thread_stack_start;
+ recorded_end = thread_ptr -> tx_thread_stack_end;
+ recorded_size = thread_ptr -> tx_thread_stack_size;
+ thread_ptr -> tx_thread_stack_ptr = safe_stack_ptr;
+ build_calls++;
+ check(entry == _tx_thread_shell_entry, "stack builder shell entry");
+}
+
+/* Preserve and restore the port's interrupt posture contract. */
+UINT _tx_thread_interrupt_disable(VOID)
+{
+UINT previous = interrupt_depth;
+
+ interrupt_depth++;
+ return(previous);
+}
+
+VOID _tx_thread_interrupt_restore(UINT previous)
+{
+ if ((interrupt_depth == 0U) || (previous + 1U != interrupt_depth))
+ {
+ interrupt_errors++;
+ }
+ interrupt_depth = previous;
+}
+
+/* DONT_START reaches preemption checking, but must never resume or execute. */
+VOID _tx_thread_system_preempt_check(VOID)
+{
+ preempt_calls++;
+ check(_tx_thread_preempt_disable == 0U, "balanced preemption posture");
+}
+
+VOID _tx_thread_system_resume(TX_THREAD *thread_ptr)
+{
+ (void) thread_ptr;
+ unexpected_calls++;
+}
+
+VOID _tx_thread_shell_entry(VOID)
+{
+ unexpected_calls++;
+}
+
+VOID _tx_thread_timeout(ULONG input)
+{
+ (void) input;
+ unexpected_calls++;
+}
+
+static VOID thread_entry(ULONG input)
+{
+ (void) input;
+ unexpected_calls++;
+}
+
+/* Check the complete fill region, caller padding and both reserved stack
+ boundaries. Expectations use remainder arithmetic, independently of the
+ create service's alignment mask. */
+static VOID run_case(UINT offset, ULONG size, UINT index)
+{
+TX_THREAD *thread_ptr = &threads[index];
+UCHAR *bytes = (UCHAR *) storage;
+UCHAR *input = bytes + PADDING_BYTES + offset;
+ULONG remainder = (ULONG) ((ALIGN_TYPE) input % sizeof(ULONG));
+ULONG adjustment = (remainder == 0U) ? 0U : (ULONG) sizeof(ULONG) - remainder;
+UCHAR *expected_start = input + adjustment;
+ULONG expected_size = size - size % (ULONG) sizeof(ULONG) - (ULONG) sizeof(ULONG);
+UCHAR *expected_end;
+UINT status;
+UINT i;
+UINT calls_before = build_calls;
+TX_THREAD *next;
+TX_THREAD *previous;
+
+ if (adjustment != 0U)
+ {
+ expected_size -= (ULONG) sizeof(ULONG);
+ }
+ expected_end = expected_start + expected_size - 1U;
+ safe_stack_ptr = expected_start + expected_size - 2U * sizeof(ULONG);
+ memset(storage, SENTINEL, sizeof(storage));
+ status = _tx_thread_create(thread_ptr, "alignment", thread_entry, 17U,
+ input, size, 15U, 15U, TX_NO_TIME_SLICE, TX_DONT_START);
+ printf("offset=%u size=%u input=%p start=%p end=%p usable=%u\n",
+ offset, (UINT) size, (VOID *) input, recorded_start, recorded_end, (UINT) recorded_size);
+ check(status == TX_SUCCESS, "create status");
+ check(build_calls == calls_before + 1U, "one stack build per create");
+ check(recorded_start == expected_start, "port start retains high bits and alignment");
+ check(thread_ptr -> tx_thread_stack_start == expected_start, "stored stack start");
+ check(recorded_size == expected_size, "port stack size and guards");
+ check(thread_ptr -> tx_thread_stack_size == expected_size, "stored stack size");
+ check(recorded_end == expected_end, "port stack end within caller storage");
+ check(thread_ptr -> tx_thread_stack_end == expected_end, "stored stack end");
+ check(thread_ptr -> tx_thread_stack_highest_ptr == safe_stack_ptr, "highest stack pointer");
+ check(thread_ptr -> tx_thread_stack_ptr == safe_stack_ptr, "initial stack pointer");
+ check(thread_ptr -> tx_thread_state == TX_SUSPENDED, "DONT_START suspended state");
+ check(thread_ptr -> tx_thread_id == TX_THREAD_ID, "created thread ID");
+ check(thread_ptr -> tx_thread_entry == thread_entry, "entry function");
+ check(thread_ptr -> tx_thread_entry_parameter == 17U, "scalar entry parameter");
+ check(thread_ptr -> tx_thread_priority == 15U, "priority");
+ check(thread_ptr -> tx_thread_preempt_threshold == 15U, "preemption threshold");
+ check(thread_ptr -> tx_thread_time_slice == TX_NO_TIME_SLICE, "time slice");
+ check(thread_ptr -> tx_thread_timer.tx_timer_internal_timeout_function == _tx_thread_timeout,
+ "timeout callback");
+ check(thread_ptr -> tx_thread_timer.tx_timer_internal_extension_ptr == thread_ptr,
+ "timeout thread pointer retains high bits");
+
+ for (i = 0U; i < sizeof(storage); i++)
+ {
+ UCHAR expected = ((i >= PADDING_BYTES + offset) &&
+ (i < PADDING_BYTES + offset + size)) ? (UCHAR) TX_STACK_FILL : SENTINEL;
+ if (bytes[i] != expected)
+ {
+ check(0U, "fill region or caller sentinel corrupted");
+ break;
+ }
+ }
+ for (i = 0U; i < sizeof(ULONG); i++)
+ {
+ check(expected_start[i] == (UCHAR) TX_STACK_FILL, "bottom stack guard fill");
+ check(expected_end[1U + i] == (UCHAR) TX_STACK_FILL, "top reserved guard fill");
+ }
+ check(_tx_thread_created_count == index + 1U, "created list count");
+ check(_tx_thread_created_ptr == &threads[0], "created list head");
+ next = _tx_thread_created_ptr;
+ previous = thread_ptr;
+ for (i = 0U; i <= index; i++)
+ {
+ check(next == &threads[i], "created list order");
+ check(next -> tx_thread_created_previous == previous, "created list back link");
+ previous = next;
+ next = next -> tx_thread_created_next;
+ }
+ check(next == _tx_thread_created_ptr, "created list closes");
+ check((interrupt_depth == 0U) && (interrupt_errors == 0U), "balanced interrupt posture");
+ check(preempt_calls == index + 1U, "preemption check count");
+ check(unexpected_calls == 0U, "no scheduling at recorder boundary");
+}
+
+/* Native64 is a required precondition, never a successful no-op test. */
+INT main(VOID)
+{
+UINT offset;
+UINT tail;
+UINT index = 0U;
+
+ printf("sizeof(ULONG)=%zu sizeof(ALIGN_TYPE)=%zu sizeof(void*)=%zu\n",
+ sizeof(ULONG), sizeof(ALIGN_TYPE), sizeof(VOID *));
+ if ((sizeof(ULONG) != 4U) || (sizeof(ALIGN_TYPE) != 8U) || (sizeof(VOID *) != 8U) ||
+ ((ALIGN_TYPE) storage <= (ALIGN_TYPE) ((ULONG) ~0U)))
+ {
+ puts("FAIL: native64 high-address preconditions absent");
+ return(1);
+ }
+ for (offset = 0U; offset < sizeof(ULONG); offset++)
+ {
+ for (tail = 0U; tail < sizeof(ULONG); tail++)
+ {
+ run_case(offset, STACK_BYTES + tail, index++);
+ }
+ }
+ printf("%s: %u alignment/size cases, %u failures\n", (failures == 0U) ? "PASS" : "FAIL",
+ index, failures);
+ return((failures == 0U) ? 0 : 1);
+}
diff --git a/test/tx/thread_transition/threadx_thread_transition_configuration_test.c b/test/tx/thread_transition/threadx_thread_transition_configuration_test.c
new file mode 100644
index 00000000..fe6acb7e
--- /dev/null
+++ b/test/tx/thread_transition/threadx_thread_transition_configuration_test.c
@@ -0,0 +1,607 @@
+/***************************************************************************
+ * 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
+ **************************************************************************/
+
+
+/**************************************************************************/
+/**************************************************************************/
+/** */
+/** ThreadX Test */
+/** */
+/** Thread lifecycle transition configuration coverage */
+/** */
+/**************************************************************************/
+/**************************************************************************/
+
+/* The two configurations that matter to this change and that neither
+ regression tree builds are TX_NOT_INTERRUPTABLE and
+ TX_DISABLE_ERROR_CHECKING. This executable is built once for each
+ combination of them, against the four sources that change, and drives the
+ completion and termination transitions directly.
+
+ Why each one needs its own build rather than a case in the in-tree test:
+
+ - TX_NOT_INTERRUPTABLE selects a different transition. There the exit
+ notification callback runs with interrupts still disabled and the
+ interruptable suspension path -- which is what sets and clears the
+ transition marker in the configurations the trees do build -- is not
+ used at all, so the marker is set and cleared by explicit stores that
+ exist only in this configuration. A marker left set there would make a
+ normally completed thread permanently undeletable, which is the failure
+ mode worth more than a compile.
+
+ - TX_DISABLE_ERROR_CHECKING removes the _txe_ wrappers, so the public
+ names bind straight to the core services. The refusal has to live in
+ the core services for exactly that reason, and the way to demonstrate it
+ is to call the public names in a build that has no wrappers.
+
+ The scheduler is not linked. _tx_thread_system_ni_suspend and
+ _tx_thread_system_suspend are recorders, which is what makes the marker's
+ value at the moment of detachment directly observable -- and, in the
+ TX_NOT_INTERRUPTABLE completion path, the reason the marker has to be
+ cleared before the call rather than after it: on a real port that call
+ returns to the scheduler and never comes back.
+
+ Everything here is straight-line: there are no threads, no timers and no
+ waits, so there is nothing that can hang. */
+
+#include <stdio.h>
+#include <string.h>
+
+/* tx_api.h is included without TX_SOURCE_CODE, so that the public service names
+ are mapped the way an application sees them: to the _txe_ wrappers with error
+ checking on, and straight to the core services with it off. Letting the header
+ make that choice is the point -- the refusal has to survive the choice. Only
+ then is TX_SOURCE_CODE defined, for the internal declarations the recorders
+ below implement. */
+
+#include "tx_api.h"
+
+/* tx_thread_entry_exit_notify is both a service name and a TX_THREAD field name,
+ and it is the only name in this file that is both, so the mapping above would
+ rewrite the field assignment further down into a member that does not exist.
+ Dropping just that one mapping keeps the mapping that matters -- the one on the
+ three services this test calls -- while leaving the field reachable. The test
+ never calls the notify service; it plants the callback in the control block
+ directly, because the create path that would otherwise do it is not linked. */
+
+#undef tx_thread_entry_exit_notify
+
+#define TX_SOURCE_CODE
+#include "tx_thread.h"
+
+
+/* Counters the force-included port shim's macros drive. */
+
+unsigned int test_interrupt_disable_depth = 0;
+unsigned int test_interrupt_disable_max_depth = 0;
+unsigned int test_interrupt_restore_underflows = 0;
+unsigned int test_delete_port_completion_count = 0;
+unsigned int test_reset_port_completion_count = 0;
+
+
+#define TEST_STACK_BYTES 512
+
+static TX_THREAD victim;
+static TX_THREAD other;
+static UCHAR victim_stack[TEST_STACK_BYTES];
+
+/* What the recorders saw. */
+
+static unsigned int ni_suspend_calls = 0;
+static UINT ni_suspend_suspending = 0xFFFFFFFFU;
+static UINT ni_suspend_state = 0xFFFFFFFFU;
+static unsigned int ni_suspend_depth = 0xFFFFFFFFU;
+
+static unsigned int suspend_calls = 0;
+static UINT suspend_suspending = 0xFFFFFFFFU;
+
+static unsigned int stack_build_calls = 0;
+static unsigned int preempt_check_calls = 0;
+static unsigned int timer_deactivate_calls = 0;
+
+/* What the exit callback saw. */
+
+static unsigned int callback_calls = 0;
+static UINT callback_state = 0xFFFFFFFFU;
+static UINT callback_suspending = 0xFFFFFFFFU;
+static unsigned int callback_depth = 0xFFFFFFFFU;
+static UINT callback_delete_status = 0xFFFFFFFFU;
+static UINT callback_reset_status = 0xFFFFFFFFU;
+static UINT callback_delete_completions = 0xFFFFFFFFU;
+static UINT callback_reset_completions = 0xFFFFFFFFU;
+
+static unsigned int entry_calls = 0;
+
+static unsigned long expectations_checked = 0;
+static unsigned long expectations_failed = 0;
+
+
+static void expect(int condition, const char *what)
+{
+
+ expectations_checked++;
+
+ if (condition == 0)
+ {
+ expectations_failed++;
+ printf(" FAILED: %s\n", what);
+ }
+}
+
+
+static void expect_uint(UINT actual, UINT wanted, const char *what)
+{
+
+ expectations_checked++;
+
+ if (actual != wanted)
+ {
+ expectations_failed++;
+ printf(" FAILED: %s (got %lu, wanted %lu)\n", what,
+ (unsigned long) actual, (unsigned long) wanted);
+ }
+}
+
+
+/* ------------------------------------------------------------------------ */
+/* Recorders standing in for the scheduler. */
+/* ------------------------------------------------------------------------ */
+
+VOID _tx_thread_system_ni_suspend(TX_THREAD *thread_ptr, ULONG wait_option)
+{
+
+ (void) wait_option;
+
+ ni_suspend_calls++;
+ ni_suspend_suspending = thread_ptr -> tx_thread_suspending;
+ ni_suspend_state = thread_ptr -> tx_thread_state;
+ ni_suspend_depth = test_interrupt_disable_depth;
+}
+
+
+VOID _tx_thread_system_suspend(TX_THREAD *thread_ptr)
+{
+
+ suspend_calls++;
+ suspend_suspending = thread_ptr -> tx_thread_suspending;
+
+ /* The interruptable suspension path is where the marker is cleared in the
+ configurations the regression trees build, so the recorder clears it as
+ the real one does, on the same condition. */
+ if (thread_ptr -> tx_thread_suspending == TX_TRUE)
+ {
+ thread_ptr -> tx_thread_suspending = TX_FALSE;
+ }
+
+ /* It also decrements the preemption disable count, which its callers have
+ raised on its behalf. The recorder has to do the same, or the balance
+ this test asserts across the whole transition would be off by one for a
+ reason that belongs to the recorder rather than to the code under
+ test. */
+ _tx_thread_preempt_disable--;
+}
+
+
+VOID _tx_thread_stack_build(TX_THREAD *thread_ptr, VOID (*function_ptr)(VOID))
+{
+
+ (void) thread_ptr;
+ (void) function_ptr;
+
+ stack_build_calls++;
+}
+
+
+VOID _tx_thread_system_preempt_check(VOID)
+{
+ preempt_check_calls++;
+}
+
+
+/* _tx_thread_terminate deactivates the target's timeout timer, and
+ _txe_thread_reset refuses a call made from the timer thread. Neither the
+ timer component nor the timer thread is linked, so both are supplied here:
+ the deactivation as a recorder, and the timer thread as a control block that
+ is never the current thread, so the wrapper's check is exercised and takes
+ the not-the-timer-thread branch. */
+
+VOID _tx_timer_system_deactivate(TX_TIMER_INTERNAL *timer_ptr)
+{
+
+ (void) timer_ptr;
+
+ timer_deactivate_calls++;
+}
+
+
+TX_THREAD _tx_timer_thread;
+
+
+/* ------------------------------------------------------------------------ */
+/* The thread under test. */
+/* ------------------------------------------------------------------------ */
+
+static void reset_records(void)
+{
+
+ ni_suspend_calls = 0;
+ ni_suspend_suspending = 0xFFFFFFFFU;
+ ni_suspend_state = 0xFFFFFFFFU;
+ ni_suspend_depth = 0xFFFFFFFFU;
+ suspend_calls = 0;
+ suspend_suspending = 0xFFFFFFFFU;
+ stack_build_calls = 0;
+ preempt_check_calls = 0;
+ timer_deactivate_calls = 0;
+ callback_calls = 0;
+ callback_state = 0xFFFFFFFFU;
+ callback_suspending = 0xFFFFFFFFU;
+ callback_depth = 0xFFFFFFFFU;
+ callback_delete_status = 0xFFFFFFFFU;
+ callback_reset_status = 0xFFFFFFFFU;
+ callback_delete_completions = 0xFFFFFFFFU;
+ callback_reset_completions = 0xFFFFFFFFU;
+ entry_calls = 0;
+
+ test_interrupt_disable_depth = 0;
+ test_interrupt_disable_max_depth = 0;
+ test_interrupt_restore_underflows = 0;
+ test_delete_port_completion_count = 0;
+ test_reset_port_completion_count = 0;
+}
+
+
+static VOID victim_entry(ULONG input)
+{
+ (void) input;
+ entry_calls++;
+}
+
+
+static VOID exit_notify(TX_THREAD *thread_ptr, UINT type)
+{
+
+ if (type != TX_THREAD_EXIT)
+ {
+ return;
+ }
+
+ callback_calls++;
+ callback_state = thread_ptr -> tx_thread_state;
+ callback_suspending = thread_ptr -> tx_thread_suspending;
+ callback_depth = test_interrupt_disable_depth;
+
+ /* The public names. With error checking on these are the _txe_ wrappers;
+ with it off they are the core services. Either way the refusal has to
+ come back. */
+ callback_delete_status = tx_thread_delete(thread_ptr);
+ callback_reset_status = tx_thread_reset(thread_ptr);
+
+ /* A refused delete or reset must not have run the port's completion
+ processing, which on a real port tears the thread's port resources
+ down. */
+ callback_delete_completions = test_delete_port_completion_count;
+ callback_reset_completions = test_reset_port_completion_count;
+}
+
+
+/* Put the victim's control block into the shape the kernel would have left it
+ in after tx_thread_create, for the fields the four sources under test read.
+ tx_thread_create itself is not linked, because the scheduler it resumes into
+ is not linked either. */
+
+static void make_victim(UINT state)
+{
+
+ memset(&victim, 0, sizeof(victim));
+ memset(victim_stack, 0xEF, sizeof(victim_stack));
+
+ victim.tx_thread_id = TX_THREAD_ID;
+ victim.tx_thread_state = state;
+ victim.tx_thread_suspending = TX_FALSE;
+ victim.tx_thread_entry = victim_entry;
+ victim.tx_thread_entry_parameter = 0;
+#ifndef TX_DISABLE_NOTIFY_CALLBACKS
+
+ /* The field itself does not exist when notification is compiled out. */
+ victim.tx_thread_entry_exit_notify = exit_notify;
+#endif
+ victim.tx_thread_stack_start = victim_stack;
+ victim.tx_thread_stack_size = sizeof(victim_stack);
+ victim.tx_thread_priority = 16;
+ victim.tx_thread_preempt_threshold = 16;
+ victim.tx_thread_suspend_cleanup = TX_NULL;
+
+ /* A created list with the victim and one other thread on it, so that
+ delete takes the link-up-the-neighbours branch rather than the
+ only-thread branch. */
+ memset(&other, 0, sizeof(other));
+ other.tx_thread_id = TX_THREAD_ID;
+ other.tx_thread_state = TX_SUSPENDED;
+ other.tx_thread_created_next = &victim;
+ other.tx_thread_created_previous = &victim;
+
+ victim.tx_thread_created_next = &other;
+ victim.tx_thread_created_previous = &other;
+
+ _tx_thread_created_ptr = &victim;
+ _tx_thread_created_count = 2;
+
+ _tx_thread_current_ptr = &victim;
+ _tx_thread_execute_ptr = &victim;
+ _tx_thread_preempt_disable = 0;
+ _tx_thread_system_state = 0;
+}
+
+
+static void scenario_completion(void)
+{
+
+ printf("Completion transition:\n");
+
+ reset_records();
+ make_victim(TX_SUSPENDED);
+
+ /* _tx_thread_shell_entry picks the thread up from the current pointer,
+ calls its entry function, and then runs the completion transition. */
+ _tx_thread_shell_entry();
+
+ expect_uint((UINT) entry_calls, 1, "the entry function ran exactly once");
+
+#ifndef TX_DISABLE_NOTIFY_CALLBACKS
+
+ expect_uint((UINT) callback_calls, 1, "the exit callback ran exactly once");
+
+ /* The terminal state is published before the callback. */
+ expect_uint(callback_state, TX_COMPLETED, "the callback saw TX_COMPLETED");
+
+ /* And the transition is marked while the callback runs, in this
+ configuration as in the interruptable one. */
+ expect_uint(callback_suspending, TX_TRUE, "the callback saw the transition marked");
+
+#ifdef TX_NOT_INTERRUPTABLE
+
+ /* In this configuration the callback runs inside the interrupt lock. */
+ expect(callback_depth > 0, "the callback ran with interrupts disabled");
+#else
+ expect_uint((UINT) callback_depth, 0, "the callback ran with interrupts enabled");
+#endif
+
+ /* The two refusals, from whichever of the wrapper and the core service
+ this build binds the public name to. */
+ expect_uint(callback_delete_status, TX_DELETE_ERROR, "the callback's delete was refused");
+ expect_uint(callback_reset_status, TX_NOT_DONE, "the callback's reset was refused");
+
+ /* A refused delete or reset ran no port completion processing. */
+ expect_uint(callback_delete_completions, 0, "the refused delete ran no port completion");
+ expect_uint(callback_reset_completions, 0, "the refused reset ran no port completion");
+#endif
+
+ /* The detachment saw the marker in the state that keeps the thread
+ deletable afterwards. This is the failure mode the configuration exists
+ to catch: on a real port the suspension call below returns to the
+ scheduler and does not come back, so a marker cleared after it rather
+ than before it would stay set for ever and the completed thread could
+ never be deleted. */
+#ifdef TX_NOT_INTERRUPTABLE
+
+ expect_uint((UINT) ni_suspend_calls, 1, "the non-interruptable suspension ran once");
+ expect_uint(ni_suspend_state, TX_COMPLETED, "it was entered with the terminal state");
+ expect_uint(ni_suspend_suspending, TX_FALSE,
+ "the marker was already cleared when the suspension was entered");
+ expect(ni_suspend_depth > 0, "the suspension was entered with interrupts disabled");
+ expect_uint((UINT) suspend_calls, 0, "the interruptable suspension was not used");
+#else
+
+ expect_uint((UINT) suspend_calls, 1, "the interruptable suspension ran once");
+ expect_uint(suspend_suspending, TX_TRUE,
+ "the marker was still set when the suspension was entered, for it to clear");
+ expect_uint((UINT) ni_suspend_calls, 0, "the non-interruptable suspension was not used");
+#endif
+
+ /* Either way the thread is deletable once the transition has finished. */
+ expect_uint(victim.tx_thread_suspending, TX_FALSE, "the marker is clear afterwards");
+ expect_uint(tx_thread_delete(&victim), TX_SUCCESS, "the completed thread deletes afterwards");
+ expect_uint((UINT) test_delete_port_completion_count, 1,
+ "the successful delete ran the port completion once");
+
+ /* And the interrupt lock is balanced across the whole transition. */
+ expect_uint((UINT) test_interrupt_disable_depth, 0, "the interrupt lock is balanced");
+ expect_uint((UINT) test_interrupt_restore_underflows, 0, "no unmatched interrupt restore");
+}
+
+
+static void scenario_terminate_ready(void)
+{
+
+ printf("Termination of a ready thread:\n");
+
+ reset_records();
+ make_victim(TX_READY);
+
+ /* Terminate a target that is not the current thread, so that the reset the
+ callback attempts is refused by the transition check alone rather than
+ by the current-thread rule. */
+ _tx_thread_current_ptr = &other;
+
+ expect_uint(tx_thread_terminate(&victim), TX_SUCCESS, "the termination succeeded");
+
+#ifndef TX_DISABLE_NOTIFY_CALLBACKS
+
+ expect_uint((UINT) callback_calls, 1, "the exit callback ran exactly once");
+ expect_uint(callback_state, TX_TERMINATED, "the callback saw TX_TERMINATED");
+ expect_uint(callback_suspending, TX_TRUE, "the callback saw the transition marked");
+ expect_uint(callback_delete_status, TX_DELETE_ERROR, "the callback's delete was refused");
+ expect_uint(callback_reset_status, TX_NOT_DONE, "the callback's reset was refused");
+ expect_uint(callback_reset_completions, 0, "the refused reset ran no port completion");
+ expect_uint((UINT) stack_build_calls, 0, "the refused reset rebuilt no stack");
+#endif
+
+#ifdef TX_NOT_INTERRUPTABLE
+
+ expect_uint((UINT) ni_suspend_calls, 1, "the non-interruptable suspension ran once");
+ expect(callback_depth > 0, "the callback ran with interrupts disabled");
+#else
+
+ expect_uint((UINT) suspend_calls, 1, "the interruptable suspension ran once");
+#endif
+
+ /* The marker is cleared in the common tail, after the callback and after
+ the mutex-release processing. */
+ expect_uint(victim.tx_thread_suspending, TX_FALSE, "the marker is clear afterwards");
+ expect_uint(victim.tx_thread_state, TX_TERMINATED, "the state is terminal afterwards");
+ expect_uint((UINT) _tx_thread_preempt_disable, 0, "the preempt disable count is balanced");
+ expect_uint((UINT) test_interrupt_disable_depth, 0, "the interrupt lock is balanced");
+ expect_uint((UINT) test_interrupt_restore_underflows, 0, "no unmatched interrupt restore");
+
+ expect_uint(tx_thread_delete(&victim), TX_SUCCESS, "the terminated thread deletes afterwards");
+}
+
+
+static void scenario_terminate_suspended(void)
+{
+
+UINT reset_status;
+
+
+ printf("Termination of a suspended thread:\n");
+
+ reset_records();
+ make_victim(TX_SUSPENDED);
+ _tx_thread_current_ptr = &other;
+
+ expect_uint(tx_thread_terminate(&victim), TX_SUCCESS, "the termination succeeded");
+
+#ifndef TX_DISABLE_NOTIFY_CALLBACKS
+
+ expect_uint((UINT) callback_calls, 1, "the exit callback ran exactly once");
+ expect_uint(callback_state, TX_TERMINATED, "the callback saw TX_TERMINATED");
+
+ /* The non-ready branch used to clear the marker before this point. */
+ expect_uint(callback_suspending, TX_TRUE, "the callback saw the transition marked");
+ expect_uint(callback_delete_status, TX_DELETE_ERROR, "the callback's delete was refused");
+ expect_uint(callback_reset_status, TX_NOT_DONE, "the callback's reset was refused");
+ expect_uint((UINT) stack_build_calls, 0, "the refused reset rebuilt no stack");
+#endif
+
+ /* This branch never calls a suspension routine: the thread was already off
+ the ready list. */
+ expect_uint((UINT) ni_suspend_calls, 0, "no non-interruptable suspension on this branch");
+ expect_uint((UINT) suspend_calls, 0, "no interruptable suspension on this branch");
+
+ expect_uint(victim.tx_thread_suspending, TX_FALSE, "the marker is clear afterwards");
+ expect_uint((UINT) _tx_thread_preempt_disable, 0, "the preempt disable count is balanced");
+ expect_uint((UINT) test_interrupt_disable_depth, 0, "the interrupt lock is balanced");
+ expect_uint((UINT) test_interrupt_restore_underflows, 0, "no unmatched interrupt restore");
+
+ /* The documented lifecycle is available afterwards, on this branch too. */
+ reset_status = tx_thread_reset(&victim);
+ expect_uint(reset_status, TX_SUCCESS, "the terminated thread resets afterwards");
+ expect_uint(victim.tx_thread_state, TX_SUSPENDED, "the reset left it suspended");
+ expect_uint((UINT) stack_build_calls, 1, "the successful reset rebuilt the stack once");
+ expect_uint((UINT) test_reset_port_completion_count, 1,
+ "the successful reset ran the port completion once");
+
+ expect_uint(tx_thread_delete(&victim), TX_DELETE_ERROR, "a reset thread is not deletable");
+ expect_uint(tx_thread_terminate(&victim), TX_SUCCESS, "it terminates again");
+ expect_uint(tx_thread_delete(&victim), TX_SUCCESS, "and then deletes");
+}
+
+
+static void scenario_marker_not_set_outside_transition(void)
+{
+
+ printf("Outside a transition:\n");
+
+ reset_records();
+ make_victim(TX_SUSPENDED);
+ _tx_thread_current_ptr = &other;
+
+ /* Neither state is terminal, so both services refuse for the reason they
+ always did, and the marker plays no part. */
+ expect_uint(victim.tx_thread_suspending, TX_FALSE, "the marker is clear on a live thread");
+ expect_uint(tx_thread_delete(&victim), TX_DELETE_ERROR, "delete refuses a suspended thread");
+ expect_uint(tx_thread_reset(&victim), TX_NOT_DONE, "reset refuses a suspended thread");
+
+ /* A terminal state with the marker clear is the ordinary deletable case,
+ which is what the refusal must not have broken. */
+ victim.tx_thread_state = TX_COMPLETED;
+ victim.tx_thread_suspending = TX_FALSE;
+ expect_uint(tx_thread_reset(&victim), TX_SUCCESS, "reset accepts a settled completed thread");
+
+ victim.tx_thread_state = TX_TERMINATED;
+ expect_uint(tx_thread_delete(&victim), TX_SUCCESS, "delete accepts a settled terminated thread");
+
+ /* And a terminal state with the marker set is the new refusal, tested
+ directly rather than through a transition, so that both services are
+ covered on both terminal states. */
+ make_victim(TX_COMPLETED);
+ victim.tx_thread_suspending = TX_TRUE;
+ _tx_thread_current_ptr = &other;
+ expect_uint(tx_thread_delete(&victim), TX_DELETE_ERROR, "delete refuses a marked completed thread");
+ expect_uint(tx_thread_reset(&victim), TX_NOT_DONE, "reset refuses a marked completed thread");
+
+ victim.tx_thread_state = TX_TERMINATED;
+ expect_uint(tx_thread_delete(&victim), TX_DELETE_ERROR, "delete refuses a marked terminated thread");
+ expect_uint(tx_thread_reset(&victim), TX_NOT_DONE, "reset refuses a marked terminated thread");
+
+ /* Clearing the marker makes both available again, which is the whole
+ claim about the refusal being transient. */
+ victim.tx_thread_suspending = TX_FALSE;
+ expect_uint(tx_thread_reset(&victim), TX_SUCCESS, "clearing the marker restores reset");
+ victim.tx_thread_state = TX_TERMINATED;
+ expect_uint(tx_thread_delete(&victim), TX_SUCCESS, "clearing the marker restores delete");
+}
+
+
+int main(void)
+{
+
+ printf("Thread lifecycle transition configuration test: ");
+
+#ifdef TX_NOT_INTERRUPTABLE
+ printf("TX_NOT_INTERRUPTABLE ");
+#else
+ printf("interruptable ");
+#endif
+#ifdef TX_DISABLE_ERROR_CHECKING
+ printf("TX_DISABLE_ERROR_CHECKING ");
+#else
+ printf("error checking ");
+#endif
+#ifdef TX_DISABLE_NOTIFY_CALLBACKS
+ printf("TX_DISABLE_NOTIFY_CALLBACKS");
+#else
+ printf("notify callbacks");
+#endif
+ printf("\n");
+
+ scenario_completion();
+ scenario_terminate_ready();
+ scenario_terminate_suspended();
+ scenario_marker_not_set_outside_transition();
+
+ printf("%lu of %lu expectations passed.\n",
+ expectations_checked - expectations_failed, expectations_checked);
+
+ if (expectations_failed != 0)
+ {
+ printf("FAILED: %lu expectations.\n", expectations_failed);
+ return(1);
+ }
+
+ printf("SUCCESS!\n");
+ return(0);
+}
diff --git a/test/tx/thread_transition/threadx_thread_transition_host_test_port.h b/test/tx/thread_transition/threadx_thread_transition_host_test_port.h
new file mode 100644
index 00000000..204933e3
--- /dev/null
+++ b/test/tx/thread_transition/threadx_thread_transition_host_test_port.h
@@ -0,0 +1,120 @@
+/***************************************************************************
+ * 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
+ **************************************************************************/
+
+
+/**************************************************************************/
+/**************************************************************************/
+/** */
+/** ThreadX Test */
+/** */
+/** Thread lifecycle transition host test port shim */
+/** */
+/**************************************************************************/
+/**************************************************************************/
+
+/* TX_NOT_INTERRUPTABLE and TX_DISABLE_ERROR_CHECKING are not among the five
+ build configurations either regression tree compiles, and both trees build
+ the whole ThreadX library once per configuration, so there is no way to
+ reach those two from inside them. The completion and termination
+ transitions have code that only exists under TX_NOT_INTERRUPTABLE -- the
+ callback there runs with interrupts disabled and the transition marker has
+ to be set and cleared explicitly rather than by the interruptable
+ suspension path -- and that code is exactly where a marker cleared at the
+ wrong point would make a normally completed thread permanently undeletable.
+ It therefore needs a test rather than a compile.
+
+ This harness compiles the four sources that change, plus the two error
+ wrappers whose absence a TX_DISABLE_ERROR_CHECKING build is defined by,
+ directly into a test executable, once per configuration. It is deliberately
+ not part of the coverage report: the same file compiled under different
+ feature macros has different line sets, and merging them would confuse the
+ union rather than add to it. The configurations the trees do build are what
+ the coverage figure is taken from.
+
+ This header is force-included ahead of each source under test. It brings
+ the base port's headers in first, so every declaration a source needs is the
+ port's own, and then replaces the primitives that would otherwise reach the
+ host's pthread emulation:
+
+ - the interrupt lock becomes a counter, which is also how the test asserts
+ that the transition really did run with interrupts disabled and left the
+ nesting balanced,
+ - the delete and reset port completions become recorders, because on this
+ port they cancel and join the pthread backing the thread, which is not
+ something a host test that owns no such thread can be asked to do. */
+
+#ifndef THREADX_THREAD_TRANSITION_HOST_TEST_PORT_H
+#define THREADX_THREAD_TRANSITION_HOST_TEST_PORT_H
+
+#define TX_SOURCE_CODE
+
+#include "tx_api.h"
+
+#undef TX_INTERRUPT_SAVE_AREA
+#undef TX_DISABLE
+#undef TX_RESTORE
+#undef TX_THREAD_DELETE_PORT_COMPLETION
+#undef TX_THREAD_RESET_PORT_COMPLETION
+
+/* Nesting depth of the stand-in interrupt lock, the deepest it has reached,
+ and the number of times a restore was issued without a matching disable.
+ Defined by the test, which is the only translation unit that reads them. */
+
+extern unsigned int test_interrupt_disable_depth;
+extern unsigned int test_interrupt_disable_max_depth;
+extern unsigned int test_interrupt_restore_underflows;
+
+/* Counts of the two port completions, and the interrupt depth each was
+ entered at. */
+
+extern unsigned int test_delete_port_completion_count;
+extern unsigned int test_reset_port_completion_count;
+
+#define TX_INTERRUPT_SAVE_AREA
+
+#define TX_DISABLE \
+ { \
+ test_interrupt_disable_depth++; \
+ if (test_interrupt_disable_depth > test_interrupt_disable_max_depth) \
+ { \
+ test_interrupt_disable_max_depth = test_interrupt_disable_depth; \
+ } \
+ }
+
+#define TX_RESTORE \
+ { \
+ if (test_interrupt_disable_depth == 0U) \
+ { \
+ test_interrupt_restore_underflows++; \
+ } \
+ else \
+ { \
+ test_interrupt_disable_depth--; \
+ } \
+ }
+
+#define TX_THREAD_DELETE_PORT_COMPLETION(thread_ptr) \
+ { \
+ (void) (thread_ptr); \
+ test_delete_port_completion_count++; \
+ }
+
+#define TX_THREAD_RESET_PORT_COMPLETION(thread_ptr) \
+ { \
+ (void) (thread_ptr); \
+ test_reset_port_completion_count++; \
+ }
+
+#endif