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