summaryrefslogtreecommitdiff
path: root/test/tx/module_manager
diff options
context:
space:
mode:
Diffstat (limited to 'test/tx/module_manager')
-rw-r--r--test/tx/module_manager/threadx_module_manager_block_pool_parameters_test.c1452
-rw-r--r--test/tx/module_manager/threadx_module_manager_cortex_a7_mmu_test.c205
-rw-r--r--test/tx/module_manager/threadx_module_manager_cortex_a7_range_check_test.c443
-rw-r--r--test/tx/module_manager/threadx_module_manager_delete_ownership_test.c1031
-rw-r--r--test/tx/module_manager/threadx_module_manager_host_test_port.h80
-rw-r--r--test/tx/module_manager/threadx_module_manager_live_object_deallocation_test.c1262
-rw-r--r--test/tx/module_manager/threadx_module_manager_object_allocate_overflow_test.c830
-rw-r--r--test/tx/module_manager/threadx_module_manager_object_authentication_test.c1217
-rw-r--r--test/tx/module_manager/threadx_module_manager_object_name_range_test.c1608
-rw-r--r--test/tx/module_manager/threadx_module_manager_queue_message_range_test.c836
-rw-r--r--test/tx/module_manager/threadx_module_manager_thread_kernel_stack_test.c589
11 files changed, 9553 insertions, 0 deletions
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);
+}