diff options
Diffstat (limited to 'test')
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, ¶m); + 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, ¶m); + 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, ¶m); + 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, ¶m); + 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, ¶m); + 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, ¶m); + 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, ¤t_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, ¤t_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, ¤t_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, ¤t_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 |
