From a967d005f6330380f6b36ec85ab296967bf0aa0a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20Desbiens?= Date: Mon, 28 Sep 2026 10:52:12 -0400 Subject: [PATCH] Merge commit from fork _txm_module_manager_tx_block_pool_create_dispatch proved that two ALIGN_TYPE words of the extra-parameter array a module supplies lay inside the module's data, and then used indices 0 through 3. The two words it had not proved were read twice each in privileged context: once by the dispatcher's own buffer check, which takes index 1 as the pool start and index 2 as the length to range-check it over, and once when the argument list for _txe_block_pool_create was built. So the out-of-bounds read was performed by the validation code itself, and the size the caller's pool start was then validated against was a word the manager had not proved the module owned. A module reaches this by calling its kernel dispatcher directly with an array that ends two words before the boundary of its data or of a shared region, which the module library's own four-word array never does. The read window is eight bytes on every module port and there is nothing in it the module can lengthen. The extent becomes sizeof(ALIGN_TYPE[4]), which is what the two siblings of the same shape have always had: queue create validates four words for four, and byte-pool create three for three. Both checks on this path are data-only, with no fallback to the portable code-region check, so the four Cortex-A35 and Cortex-A35 SMP module port combinations whose data check is the constant (TX_SUCCESS) refuse these create requests from a memory-protected module before and after this change alike. The regression test drives all three create dispatchers and measures the extent each one validates rather than sampling either side of it: it anchors the array at the end of each region a module owns and walks it backwards until the dispatcher accepts, and the smallest room accepted is the extent. It measures the highest index each dispatcher uses through what the stubbed service records, so the invariant the three are checked against is measured on both sides. It also measures what a module can learn from the answers, since the buffer check is a comparison against a threshold the module chooses and the dispatcher's refusal is distinguishable from every status these services return. Compiled against the previous dispatch header the test reports the extent as two words and the service reached carrying a word planted past the end of the region; against this one it reports four. Assisted-by: Claude Code (Opus 5) --- .../inc/txm_module_manager_dispatch.h | 4 +- test/tx/cmake/module_manager/CMakeLists.txt | 21 + ...odule_manager_block_pool_parameters_test.c | 1452 +++++++++++++++++ 3 files changed, 1476 insertions(+), 1 deletion(-) create mode 100644 test/tx/module_manager/threadx_module_manager_block_pool_parameters_test.c diff --git a/common_modules/module_manager/inc/txm_module_manager_dispatch.h b/common_modules/module_manager/inc/txm_module_manager_dispatch.h index 0160a1d1..35e493d4 100644 --- a/common_modules/module_manager/inc/txm_module_manager_dispatch.h +++ b/common_modules/module_manager/inc/txm_module_manager_dispatch.h @@ -17,6 +17,8 @@ // Some portions generated by Claude Code (Opus 5). +// Some portions generated by Claude Code (Opus 5). + /**************************************************************************/ /**************************************************************************/ @@ -79,7 +81,7 @@ ALIGN_TYPE return_value; if (!TXM_MODULE_MANAGER_PARAM_CHECK_DEREFERENCE_STRING(module_instance, param_1)) return(TXM_MODULE_INVALID_MEMORY); - if (!TXM_MODULE_MANAGER_ENSURE_INSIDE_MODULE_DATA(module_instance, (ALIGN_TYPE)extra_parameters, sizeof(ALIGN_TYPE[2]))) + if (!TXM_MODULE_MANAGER_ENSURE_INSIDE_MODULE_DATA(module_instance, (ALIGN_TYPE)extra_parameters, sizeof(ALIGN_TYPE[4]))) return(TXM_MODULE_INVALID_MEMORY); if (!TXM_MODULE_MANAGER_PARAM_CHECK_BUFFER_WRITE(module_instance, extra_parameters[1], extra_parameters[2])) diff --git a/test/tx/cmake/module_manager/CMakeLists.txt b/test/tx/cmake/module_manager/CMakeLists.txt index f87970ad..109ee72a 100644 --- a/test/tx/cmake/module_manager/CMakeLists.txt +++ b/test/tx/cmake/module_manager/CMakeLists.txt @@ -21,6 +21,7 @@ set(SOURCE_DIR ${REPO_ROOT}/test/tx/module_manager) set(module_manager_dir ${REPO_ROOT}/common_modules/module_manager) set(cortex_a7_module_dir ${REPO_ROOT}/ports_module/cortex_a7/gnu) +set(cortex_m4_module_dir ${REPO_ROOT}/ports_module/cortex_m4/gnu) add_executable( threadx_module_manager_thread_kernel_stack_test @@ -173,3 +174,23 @@ target_compile_options( add_test(${CMAKE_BUILD_TYPE}::threadx_module_manager_object_allocate_overflow_test threadx_module_manager_object_allocate_overflow_test) + +add_executable( + threadx_module_manager_block_pool_parameters_test + ${SOURCE_DIR}/threadx_module_manager_block_pool_parameters_test.c + ${module_manager_dir}/src/txm_module_manager_util.c) + +target_include_directories( + threadx_module_manager_block_pool_parameters_test + PRIVATE ${SOURCE_DIR} + ${REPO_ROOT}/common/inc + ${REPO_ROOT}/common_modules/inc + ${module_manager_dir}/inc + ${cortex_m4_module_dir}/inc) + +target_compile_options( + threadx_module_manager_block_pool_parameters_test + PRIVATE -include ${SOURCE_DIR}/threadx_module_manager_host_test_port.h) + +add_test(${CMAKE_BUILD_TYPE}::threadx_module_manager_block_pool_parameters_test + threadx_module_manager_block_pool_parameters_test) 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..fb3f6ff9 --- /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__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 +#include + +#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, 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, 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, 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); +}