diff options
Diffstat (limited to 'test/tx')
60 files changed, 13856 insertions, 103 deletions
diff --git a/test/tx/cmake/CMakeLists.txt b/test/tx/cmake/CMakeLists.txt index 97b3615b..2f9c9b9f 100644 --- a/test/tx/cmake/CMakeLists.txt +++ b/test/tx/cmake/CMakeLists.txt @@ -14,7 +14,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 +37,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 +77,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) 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_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_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 |
