/*************************************************************************** * 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 #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); }