diff options
Diffstat (limited to 'test/tx/module_manager')
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); +} |
