summaryrefslogtreecommitdiff
path: root/test/tx/module_manager/threadx_module_manager_object_authentication_test.c
diff options
context:
space:
mode:
Diffstat (limited to 'test/tx/module_manager/threadx_module_manager_object_authentication_test.c')
-rw-r--r--test/tx/module_manager/threadx_module_manager_object_authentication_test.c1217
1 files changed, 1217 insertions, 0 deletions
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);
+}