diff options
Diffstat (limited to 'test/tx/module_manager/threadx_module_manager_object_authentication_test.c')
| -rw-r--r-- | test/tx/module_manager/threadx_module_manager_object_authentication_test.c | 1217 |
1 files changed, 1217 insertions, 0 deletions
diff --git a/test/tx/module_manager/threadx_module_manager_object_authentication_test.c b/test/tx/module_manager/threadx_module_manager_object_authentication_test.c new file mode 100644 index 00000000..396f57f2 --- /dev/null +++ b/test/tx/module_manager/threadx_module_manager_object_authentication_test.c @@ -0,0 +1,1217 @@ +/*************************************************************************** + * Copyright (c) 2026 Eclipse ThreadX contributors + * + * This program and the accompanying materials are made available under the + * terms of the MIT License which is available at + * https://opensource.org/licenses/MIT. + * + * AI Disclosure: This file was largely AI-generated by Claude Code (Opus 5). + * The AI-generated portions may be considered public domain (CC0-1.0) + * and not subject to the project's licence. The human contributor has + * reviewed and verified that the code is correct. + * + * SPDX-License-Identifier: MIT and CC0-1.0 + **************************************************************************/ + + +/**************************************************************************/ +/**************************************************************************/ +/** */ +/** ThreadX Test */ +/** */ +/** Module Manager kernel object authentication */ +/** */ +/**************************************************************************/ +/**************************************************************************/ + +/* A memory-protected module names the kernel objects it wants operated on by + address, and the Module Manager decides whether a privileged service may + dereference that address. Deciding it by asking only whether the address lies + outside the module is not enough, and the reason is the object pool: the pool + is outside every module, so an address shifted into the interior of one of the + module's own privileged allocations passes that test while denoting no object. + The bytes the shifted address then presents as a control block are bytes the + module put there through ordinary create and set services, so the control block + ID at the front of them can be made to read as any type the module likes. + + This test drives the manager's authentication of such addresses on the host. + The object pool behind it is modelled rather than run -- a bump allocator that + reuses the block it most recently released, which is how the address of a freed + object comes back for the cases about stale addresses -- but the two records + authentication actually consults are built the way the system builds them: the + module allocation list is built by running the real + _txm_module_manager_object_allocate, and the kernel created lists are built the + way the create and delete services maintain them, by writing the control block + ID and linking the object in, and by clearing the ID and unlinking it. + + What the test asserts is that an address is accepted when, and only when, it is + the exact address of a live object of the type the service expects. Every + aligned interior offset of a legitimate object is rejected as a foreign type, + including the offsets where this test has planted that type's own ID, which is + the case the reported attack is built on. */ + +#include <stdio.h> + +#include "threadx_module_manager_host_test_port.h" + +#include "tx_thread.h" +#include "tx_trace.h" +#include "tx_timer.h" +#include "tx_queue.h" +#include "tx_event_flags.h" +#include "tx_semaphore.h" +#include "tx_mutex.h" +#include "tx_block_pool.h" +#include "tx_byte_pool.h" +#include "txm_module.h" +#include "txm_module_manager_util.h" + + +/* Define the stand-in interrupt lock the port shim counts through. */ + +unsigned int test_interrupt_disable_depth; +unsigned int test_interrupt_disable_max_depth; +unsigned int test_interrupt_restore_underflows; + + +/* Define the modelled object pool. */ + +#define TEST_POOL_BYTES ((ULONG) 8192) +#define TEST_MODULE_MEMORY_BYTES 512U +#define TEST_MAX_BLOCKS 32U + +static union +{ + ALIGN_TYPE test_pool_alignment; + UCHAR test_pool_bytes[TEST_POOL_BYTES]; +} test_pool_arena; + +static ULONG test_pool_offset; + + +/* Define the memory that stands for the calling module's own code and data. A + kernel object here is one the module could write behind the kernel's back, so + authentication has to refuse it however genuine it looks. */ + +static union +{ + ALIGN_TYPE test_data_alignment; + UCHAR test_data_bytes[TEST_MODULE_MEMORY_BYTES]; +} test_module_data; + +static union +{ + ALIGN_TYPE test_code_alignment; + UCHAR test_code_bytes[TEST_MODULE_MEMORY_BYTES]; +} test_module_code; + + +/* Define the objects that stand for application-owned objects shared with a + module. These live outside the pool, so the manager has no allocation record of + them and the kernel created list is the only record of them there is. */ + +static TX_QUEUE test_host_queue; +static TX_THREAD test_host_thread; +static TX_MUTEX test_host_stranger; + + +/* Define the block the modelled pool most recently released, so that a later + allocation of the same size lands on the address a freed object had. */ + +static UCHAR *test_released_start; +static ULONG test_released_bytes; + + +/* Define the manager state the code under test reaches for. */ + +TX_BYTE_POOL _txm_module_manager_object_pool; +UINT _txm_module_manager_object_pool_created; +TX_MUTEX _txm_module_manager_mutex; +TX_THREAD *_tx_thread_current_ptr; + + +/* Define the kernel created lists authentication walks. */ + +TX_BLOCK_POOL *_tx_block_pool_created_ptr; +ULONG _tx_block_pool_created_count; +TX_BYTE_POOL *_tx_byte_pool_created_ptr; +ULONG _tx_byte_pool_created_count; +TX_EVENT_FLAGS_GROUP *_tx_event_flags_created_ptr; +ULONG _tx_event_flags_created_count; +TX_MUTEX *_tx_mutex_created_ptr; +ULONG _tx_mutex_created_count; +TX_QUEUE *_tx_queue_created_ptr; +ULONG _tx_queue_created_count; +TX_SEMAPHORE *_tx_semaphore_created_ptr; +ULONG _tx_semaphore_created_count; +TX_THREAD *_tx_thread_created_ptr; +ULONG _tx_thread_created_count; +TX_TIMER *_tx_timer_created_ptr; +ULONG _tx_timer_created_count; + + +#ifdef TX_ENABLE_EVENT_TRACE + +/* Define the trace state the reset path's trace insert refers to when the tree is + configured with event tracing. The insert does nothing while the buffer pointer + is null, which is the state a system that has not enabled tracing is in, so the + cases below run identically in every configuration this tree builds. */ + +TX_TRACE_HEADER *_tx_trace_header_ptr; +TX_TRACE_BUFFER_ENTRY *_tx_trace_buffer_start_ptr; +TX_TRACE_BUFFER_ENTRY *_tx_trace_buffer_end_ptr; +TX_TRACE_BUFFER_ENTRY *_tx_trace_buffer_current_ptr; +ULONG _tx_trace_event_enable_bits; +ULONG _tx_trace_simulated_time; +VOID (*_tx_trace_full_notify_function)(VOID *buffer); +volatile ULONG _tx_thread_system_state; + +#endif + + +static UINT test_failures; +static ULONG test_checks; +static ULONG test_stack_builds; + + +/* Record the outcome of one expectation. */ +static VOID test_expect(const char *description, ULONG actual, ULONG expected) +{ + test_checks++; + + if (actual != expected) + { + printf("FAIL: %s (expected %lu, got %lu)\n", description, (unsigned long) expected, (unsigned long) actual); + test_failures++; + } +} + + +/* Report every module object type this manager authenticates, and the name and + control block size of each, so that the cases below can be written once and run + against all of them. */ + +typedef struct TEST_OBJECT_KIND_STRUCT +{ + UINT test_kind_type; + const char *test_kind_name; + ULONG test_kind_size; + ULONG test_kind_id; +} TEST_OBJECT_KIND; + +static const TEST_OBJECT_KIND test_object_kinds[] = +{ + { TXM_BLOCK_POOL_OBJECT, "block pool", (ULONG) sizeof(TX_BLOCK_POOL), TX_BLOCK_POOL_ID }, + { TXM_BYTE_POOL_OBJECT, "byte pool", (ULONG) sizeof(TX_BYTE_POOL), TX_BYTE_POOL_ID }, + { TXM_EVENT_FLAGS_OBJECT, "event flags", (ULONG) sizeof(TX_EVENT_FLAGS_GROUP), TX_EVENT_FLAGS_ID }, + { TXM_MUTEX_OBJECT, "mutex", (ULONG) sizeof(TX_MUTEX), TX_MUTEX_ID }, + { TXM_QUEUE_OBJECT, "queue", (ULONG) sizeof(TX_QUEUE), TX_QUEUE_ID }, + { TXM_SEMAPHORE_OBJECT, "semaphore", (ULONG) sizeof(TX_SEMAPHORE), TX_SEMAPHORE_ID }, + { TXM_THREAD_OBJECT, "thread", (ULONG) sizeof(TX_THREAD), TX_THREAD_ID }, + { TXM_TIMER_OBJECT, "timer", (ULONG) sizeof(TX_TIMER), TX_TIMER_ID } +}; + +#define TEST_OBJECT_KIND_COUNT (sizeof(test_object_kinds) / sizeof(test_object_kinds[0])) + + +/* Round a request up the way _tx_byte_allocate does. */ +static ULONG test_round_up(ULONG memory_size) +{ + return((((memory_size + ((ULONG) sizeof(ALIGN_TYPE))) - ((ULONG) 1)) / ((ULONG) sizeof(ALIGN_TYPE))) * ((ULONG) sizeof(ALIGN_TYPE))); +} + + +/* Stand in for the byte pool the manager allocates object memory from. + + The two size bounds are the real ones. The placement is a bump allocator with + one refinement: a request that exactly fits the block most recently released is + given that block back. A first-fit byte pool reuses freed memory, and the cases + about an address that has been freed and allocated again need the address to + actually come back. */ +UINT _txe_byte_allocate(TX_BYTE_POOL *pool_ptr, VOID **memory_ptr, ULONG memory_size, ULONG wait_option) +{ + +ULONG rounded_size; +UCHAR *block_start; + + + (VOID) wait_option; + + if ((pool_ptr != &_txm_module_manager_object_pool) || (memory_ptr == TX_NULL)) + { + return(TX_POOL_ERROR); + } + + if (memory_size == ((ULONG) 0)) + { + return(TX_SIZE_ERROR); + } + + if (memory_size > pool_ptr -> tx_byte_pool_size) + { + return(TX_SIZE_ERROR); + } + + rounded_size = test_round_up(memory_size); + + if ((test_released_start != TX_NULL) && (test_released_bytes == rounded_size)) + { + block_start = test_released_start; + test_released_start = TX_NULL; + test_released_bytes = ((ULONG) 0); + } + else + { + if ((test_pool_offset + rounded_size) > TEST_POOL_BYTES) + { + return(TX_NO_MEMORY); + } + + block_start = &test_pool_arena.test_pool_bytes[test_pool_offset]; + test_pool_offset = test_pool_offset + rounded_size; + } + + pool_ptr -> tx_byte_pool_available = pool_ptr -> tx_byte_pool_available - rounded_size; + + *memory_ptr = (VOID *) block_start; + + return(TX_SUCCESS); +} + + +/* Stand in for the release side, remembering the block so it can come back. */ +UINT _txe_byte_release(VOID *memory_ptr) +{ + +UCHAR *block_start; + + + block_start = (UCHAR *) memory_ptr; + + if ((block_start < test_pool_arena.test_pool_bytes) || + (block_start >= &test_pool_arena.test_pool_bytes[TEST_POOL_BYTES])) + { + return(TX_PTR_ERROR); + } + + /* The model does not track block lengths, and the only released block a case + needs to come back is the one it just released, whose length it knows. The + length is recorded by the caller through test_release_size. */ + test_released_start = block_start; + + return(TX_SUCCESS); +} + + +/* Tell the model how long the block just released was. */ +static VOID test_release_size(ULONG object_size) +{ + test_released_bytes = test_round_up(object_size + ((ULONG) sizeof(TXM_MODULE_ALLOCATED_OBJECT))); +} + + +/* Stand in for the protection mutex. */ +UINT _txe_mutex_get(TX_MUTEX *mutex_ptr, ULONG wait_option) +{ + (VOID) mutex_ptr; + (VOID) wait_option; + + return(TX_SUCCESS); +} + + +UINT _txe_mutex_put(TX_MUTEX *mutex_ptr) +{ + (VOID) mutex_ptr; + + return(TX_SUCCESS); +} + + +/* Stand in for the module port's data range check, which the portable + outside-the-module test is built on. */ +UINT _txm_module_manager_inside_data_check(TXM_MODULE_INSTANCE *module_instance, ALIGN_TYPE obj_ptr, + ULONG obj_size, UINT write_request) +{ + +ULONG data_start; +ULONG data_end; + + + (VOID) module_instance; + (VOID) write_request; + + data_start = (ULONG) (ALIGN_TYPE) test_module_data.test_data_bytes; + data_end = data_start + ((ULONG) TEST_MODULE_MEMORY_BYTES); + + /* The whole range has to lie inside the module's data, not just its first + address, and the subtraction is done on the end rather than the start so that + a size which would carry the range past the top of the address space cannot + wrap into an accepting answer. */ + if (((ULONG) obj_ptr >= data_start) && (obj_size <= (data_end - (ULONG) obj_ptr))) + { + return(TX_TRUE); + } + + return(TX_FALSE); +} + + +/* Outside is not the negation of inside: a range that only partly reaches into the + module's data is neither. It has to end before that data begins or begin after it + ends, and a size that would carry the end past the top of the address space is + refused rather than allowed to compare as though it had not. */ +UINT _txm_module_manager_outside_data_check(TXM_MODULE_INSTANCE *module_instance, ALIGN_TYPE obj_ptr, + ULONG obj_size) +{ + +ULONG data_start; +ULONG data_end; + + + (VOID) module_instance; + + data_start = (ULONG) (ALIGN_TYPE) test_module_data.test_data_bytes; + data_end = data_start + ((ULONG) TEST_MODULE_MEMORY_BYTES); + + if (obj_size > (~((ULONG) 0) - (ULONG) obj_ptr)) + { + return(TX_FALSE); + } + + if ((((ULONG) obj_ptr + obj_size) <= data_start) || ((ULONG) obj_ptr >= data_end)) + { + return(TX_TRUE); + } + + return(TX_FALSE); +} + + +/* Stand in for the port primitives the manager sources under test refer to but + that no case here exercises. */ +VOID _txm_module_manager_alignment_adjust(TXM_MODULE_PREAMBLE *module_preamble, ULONG *code_size, + ULONG *code_alignment, ULONG *data_size, ULONG *data_alignment) +{ + (VOID) module_preamble; + (VOID) code_size; + (VOID) code_alignment; + (VOID) data_size; + (VOID) data_alignment; +} + + +VOID _txm_module_manager_thread_stack_build(TX_THREAD *thread_ptr, VOID (*shell_function)(TX_THREAD *, TXM_MODULE_INSTANCE *)) +{ + (VOID) thread_ptr; + (VOID) shell_function; + + test_stack_builds++; +} + + +/* Do to an object what the kernel's create service does to it, as far as + authentication can see: write the control block ID, and link the object onto + the created list for its type. + + The chain each list is built as ends in a self-link on its oldest member rather + than closing back on the head, so that a walk bounded by the created count + visits every member and a walk that ignored the count would not run off the + end. What is being tested is the bound, not the model. */ +static VOID test_object_create(VOID *object_ptr, UINT object_type) +{ + switch (object_type) + { + + case TXM_BLOCK_POOL_OBJECT: + { + TX_BLOCK_POOL *block_pool_ptr = (TX_BLOCK_POOL *) object_ptr; + + block_pool_ptr -> tx_block_pool_id = TX_BLOCK_POOL_ID; + block_pool_ptr -> tx_block_pool_created_next = (_tx_block_pool_created_ptr == TX_NULL) ? block_pool_ptr : _tx_block_pool_created_ptr; + _tx_block_pool_created_ptr = block_pool_ptr; + _tx_block_pool_created_count++; + break; + } + + case TXM_BYTE_POOL_OBJECT: + { + TX_BYTE_POOL *byte_pool_ptr = (TX_BYTE_POOL *) object_ptr; + + byte_pool_ptr -> tx_byte_pool_id = TX_BYTE_POOL_ID; + byte_pool_ptr -> tx_byte_pool_created_next = (_tx_byte_pool_created_ptr == TX_NULL) ? byte_pool_ptr : _tx_byte_pool_created_ptr; + _tx_byte_pool_created_ptr = byte_pool_ptr; + _tx_byte_pool_created_count++; + break; + } + + case TXM_EVENT_FLAGS_OBJECT: + { + TX_EVENT_FLAGS_GROUP *event_flags_ptr = (TX_EVENT_FLAGS_GROUP *) object_ptr; + + event_flags_ptr -> tx_event_flags_group_id = TX_EVENT_FLAGS_ID; + event_flags_ptr -> tx_event_flags_group_created_next = (_tx_event_flags_created_ptr == TX_NULL) ? event_flags_ptr : _tx_event_flags_created_ptr; + _tx_event_flags_created_ptr = event_flags_ptr; + _tx_event_flags_created_count++; + break; + } + + case TXM_MUTEX_OBJECT: + { + TX_MUTEX *mutex_ptr = (TX_MUTEX *) object_ptr; + + mutex_ptr -> tx_mutex_id = TX_MUTEX_ID; + mutex_ptr -> tx_mutex_created_next = (_tx_mutex_created_ptr == TX_NULL) ? mutex_ptr : _tx_mutex_created_ptr; + _tx_mutex_created_ptr = mutex_ptr; + _tx_mutex_created_count++; + break; + } + + case TXM_QUEUE_OBJECT: + { + TX_QUEUE *queue_ptr = (TX_QUEUE *) object_ptr; + + queue_ptr -> tx_queue_id = TX_QUEUE_ID; + queue_ptr -> tx_queue_created_next = (_tx_queue_created_ptr == TX_NULL) ? queue_ptr : _tx_queue_created_ptr; + _tx_queue_created_ptr = queue_ptr; + _tx_queue_created_count++; + break; + } + + case TXM_SEMAPHORE_OBJECT: + { + TX_SEMAPHORE *semaphore_ptr = (TX_SEMAPHORE *) object_ptr; + + semaphore_ptr -> tx_semaphore_id = TX_SEMAPHORE_ID; + semaphore_ptr -> tx_semaphore_created_next = (_tx_semaphore_created_ptr == TX_NULL) ? semaphore_ptr : _tx_semaphore_created_ptr; + _tx_semaphore_created_ptr = semaphore_ptr; + _tx_semaphore_created_count++; + break; + } + + case TXM_THREAD_OBJECT: + { + TX_THREAD *thread_ptr = (TX_THREAD *) object_ptr; + + thread_ptr -> tx_thread_id = TX_THREAD_ID; + thread_ptr -> tx_thread_created_next = (_tx_thread_created_ptr == TX_NULL) ? thread_ptr : _tx_thread_created_ptr; + _tx_thread_created_ptr = thread_ptr; + _tx_thread_created_count++; + break; + } + + case TXM_TIMER_OBJECT: + default: + { + TX_TIMER *timer_ptr = (TX_TIMER *) object_ptr; + + timer_ptr -> tx_timer_id = TX_TIMER_ID; + timer_ptr -> tx_timer_created_next = (_tx_timer_created_ptr == TX_NULL) ? timer_ptr : _tx_timer_created_ptr; + _tx_timer_created_ptr = timer_ptr; + _tx_timer_created_count++; + break; + } + } +} + + +/* Do to an object what the kernel's delete service does: clear the control block + ID, and take the object off the created list for its type. + + Every case that deletes has one object of that type on the list, which is the + shape that matters here, so the model empties the list rather than unlinking a + member of a longer one. */ +static VOID test_object_delete(VOID *object_ptr, UINT object_type) +{ + switch (object_type) + { + + case TXM_BLOCK_POOL_OBJECT: + + ((TX_BLOCK_POOL *) object_ptr) -> tx_block_pool_id = TX_CLEAR_ID; + _tx_block_pool_created_ptr = TX_NULL; + _tx_block_pool_created_count = ((ULONG) 0); + break; + + case TXM_BYTE_POOL_OBJECT: + + ((TX_BYTE_POOL *) object_ptr) -> tx_byte_pool_id = TX_CLEAR_ID; + _tx_byte_pool_created_ptr = TX_NULL; + _tx_byte_pool_created_count = ((ULONG) 0); + break; + + case TXM_EVENT_FLAGS_OBJECT: + + ((TX_EVENT_FLAGS_GROUP *) object_ptr) -> tx_event_flags_group_id = TX_CLEAR_ID; + _tx_event_flags_created_ptr = TX_NULL; + _tx_event_flags_created_count = ((ULONG) 0); + break; + + case TXM_MUTEX_OBJECT: + + ((TX_MUTEX *) object_ptr) -> tx_mutex_id = TX_CLEAR_ID; + _tx_mutex_created_ptr = TX_NULL; + _tx_mutex_created_count = ((ULONG) 0); + break; + + case TXM_QUEUE_OBJECT: + + ((TX_QUEUE *) object_ptr) -> tx_queue_id = TX_CLEAR_ID; + _tx_queue_created_ptr = TX_NULL; + _tx_queue_created_count = ((ULONG) 0); + break; + + case TXM_SEMAPHORE_OBJECT: + + ((TX_SEMAPHORE *) object_ptr) -> tx_semaphore_id = TX_CLEAR_ID; + _tx_semaphore_created_ptr = TX_NULL; + _tx_semaphore_created_count = ((ULONG) 0); + break; + + case TXM_THREAD_OBJECT: + + ((TX_THREAD *) object_ptr) -> tx_thread_id = TX_CLEAR_ID; + _tx_thread_created_ptr = TX_NULL; + _tx_thread_created_count = ((ULONG) 0); + break; + + case TXM_TIMER_OBJECT: + default: + + ((TX_TIMER *) object_ptr) -> tx_timer_id = TX_CLEAR_ID; + _tx_timer_created_ptr = TX_NULL; + _tx_timer_created_count = ((ULONG) 0); + break; + } +} + + +/* Bring the pool, the created lists and the modules back to a known state. + + Everything is reset together deliberately. The allocation lists point into the + pool and the created lists point at objects in it, so a case that left either + behind would have the next case reading the wreckage of an earlier one rather + than what it was written to check. */ +static VOID test_reset(TXM_MODULE_INSTANCE *module_a, TXM_MODULE_INSTANCE *module_b) +{ + +UINT index; + + + for (index = 0U; index < (UINT) TEST_POOL_BYTES; index++) + { + test_pool_arena.test_pool_bytes[index] = (UCHAR) 0; + } + + for (index = 0U; index < TEST_MODULE_MEMORY_BYTES; index++) + { + test_module_data.test_data_bytes[index] = (UCHAR) 0; + test_module_code.test_code_bytes[index] = (UCHAR) 0; + } + + test_pool_offset = ((ULONG) 0); + test_released_start = TX_NULL; + test_released_bytes = ((ULONG) 0); + + _txm_module_manager_object_pool.tx_byte_pool_id = TX_BYTE_POOL_ID; + _txm_module_manager_object_pool.tx_byte_pool_start = test_pool_arena.test_pool_bytes; + _txm_module_manager_object_pool.tx_byte_pool_size = TEST_POOL_BYTES; + _txm_module_manager_object_pool.tx_byte_pool_available = TEST_POOL_BYTES; + _txm_module_manager_object_pool_created = TX_TRUE; + + _tx_block_pool_created_ptr = TX_NULL; + _tx_block_pool_created_count = ((ULONG) 0); + _tx_byte_pool_created_ptr = TX_NULL; + _tx_byte_pool_created_count = ((ULONG) 0); + _tx_event_flags_created_ptr = TX_NULL; + _tx_event_flags_created_count = ((ULONG) 0); + _tx_mutex_created_ptr = TX_NULL; + _tx_mutex_created_count = ((ULONG) 0); + _tx_queue_created_ptr = TX_NULL; + _tx_queue_created_count = ((ULONG) 0); + _tx_semaphore_created_ptr = TX_NULL; + _tx_semaphore_created_count = ((ULONG) 0); + _tx_thread_created_ptr = TX_NULL; + _tx_thread_created_count = ((ULONG) 0); + _tx_timer_created_ptr = TX_NULL; + _tx_timer_created_count = ((ULONG) 0); + + module_a -> txm_module_instance_object_list_count = ((ULONG) 0); + module_a -> txm_module_instance_object_list_head = TX_NULL; + module_b -> txm_module_instance_object_list_count = ((ULONG) 0); + module_b -> txm_module_instance_object_list_head = TX_NULL; +} + + +/* Allocate object memory for a module through the real manager path. */ +static VOID *test_allocate(TXM_MODULE_INSTANCE *module_instance, ULONG object_size) +{ + +VOID *object_ptr; +UINT status; + + + object_ptr = TX_NULL; + status = _txm_module_manager_object_allocate(&object_ptr, object_size, module_instance); + + if (status != TX_SUCCESS) + { + printf("FAIL: the modelled pool refused an allocation of %lu bytes (status %u)\n", + (unsigned long) object_size, status); + test_failures++; + + return(TX_NULL); + } + + return(object_ptr); +} + + +/* Allocate and create one object of a type, the way a module does. */ +static VOID *test_allocate_and_create(TXM_MODULE_INSTANCE *module_instance, const TEST_OBJECT_KIND *kind) +{ + +VOID *object_ptr; + + + object_ptr = test_allocate(module_instance, kind -> test_kind_size); + + if (object_ptr != TX_NULL) + { + test_object_create(object_ptr, kind -> test_kind_type); + } + + return(object_ptr); +} + + +/* Ask the manager to authenticate an address, and report what it said. */ +static ULONG test_authenticate(TXM_MODULE_INSTANCE *module_instance, VOID *object_ptr, UINT object_type) +{ + +ULONG accepted; + + + test_interrupt_disable_depth = 0U; + test_interrupt_disable_max_depth = 0U; + test_interrupt_restore_underflows = 0U; + + accepted = (ULONG) _txm_module_manager_param_check_typed_object_for_use(module_instance, + (ALIGN_TYPE) object_ptr, + object_type); + + /* Whatever it decided, it must have left the interrupt lock as it found it. */ + if ((test_interrupt_disable_depth != 0U) || (test_interrupt_restore_underflows != 0U)) + { + printf("FAIL: authentication left the interrupt lock unbalanced (depth %u, underflows %u)\n", + test_interrupt_disable_depth, test_interrupt_restore_underflows); + test_failures++; + } + + return(accepted); +} + + +/* Plant a value at an offset into an object, the way a module reaches interior + words of its own privileged allocations through ordinary create and set + services, and report what was there before. */ +static ULONG test_plant(VOID *object_ptr, ULONG offset, ULONG value) +{ + +ULONG *word_ptr; +ULONG previous; + + + word_ptr = (ULONG *) (VOID *) (((UCHAR *) object_ptr) + offset); + previous = *word_ptr; + *word_ptr = value; + + return(previous); +} + + +int main(void) +{ + +TXM_MODULE_INSTANCE module_a; +TXM_MODULE_INSTANCE module_b; +TXM_MODULE_INSTANCE *owner; +const TEST_OBJECT_KIND *kind; +const TEST_OBJECT_KIND *other_kind; +VOID *object; +VOID *other_object; +VOID *raw_object; +VOID *shifted; +UCHAR *shifted_bytes; +ULONG offset; +ULONG interior_offsets; +ULONG accepted_interiors; +ULONG index; +ULONG other_index; +ULONG saved; +UINT status; +char description[128]; + + + /* Report expectations before anything can crash, so that a case that brings + the process down is still preceded by everything that passed. */ + setvbuf(stdout, TX_NULL, _IONBF, 0); + + test_failures = 0U; + test_checks = ((ULONG) 0); + + /* Set up two memory-protected modules. Module A does the asking throughout; + module B is there to own objects A did not allocate. */ + module_a.txm_module_instance_property_flags = TXM_MODULE_MEMORY_PROTECTION; + module_a.txm_module_instance_code_start = (VOID *) test_module_code.test_code_bytes; + module_a.txm_module_instance_code_end = (VOID *) &test_module_code.test_code_bytes[TEST_MODULE_MEMORY_BYTES - 1U]; + module_a.txm_module_instance_data_start = (VOID *) test_module_data.test_data_bytes; + module_a.txm_module_instance_data_end = (VOID *) &test_module_data.test_data_bytes[TEST_MODULE_MEMORY_BYTES - 1U]; + + module_b = module_a; + + _tx_thread_current_ptr = TX_NULL; + + /**********************************************************************/ + /* The reported attack: an address shifted into a legitimate object. */ + /**********************************************************************/ + + /* A module allocates and creates a timer of its own, entirely legitimately, + and then presents addresses inside it as though they were a thread. The + word at each of those addresses is set to TX_THREAD_ID first, which is what + the reported chain arranges through ordinary create and set services, so + that every address offered is one that an ID test on its own would accept. */ + test_reset(&module_a, &module_b); + + kind = &test_object_kinds[6]; + object = test_allocate(&module_a, (ULONG) sizeof(TX_THREAD)); + test_object_create(object, TXM_TIMER_OBJECT); + + test_expect("a timer's own address is not accepted as a thread", + test_authenticate(&module_a, object, TXM_THREAD_OBJECT), (ULONG) TX_FALSE); + + interior_offsets = ((ULONG) 0); + accepted_interiors = ((ULONG) 0); + + for (offset = (ULONG) sizeof(ULONG); offset < (ULONG) sizeof(TX_THREAD); offset = offset + ((ULONG) sizeof(ULONG))) + { + shifted_bytes = ((UCHAR *) object) + offset; + shifted = (VOID *) shifted_bytes; + + saved = test_plant(object, offset, TX_THREAD_ID); + + interior_offsets++; + + if (test_authenticate(&module_a, shifted, TXM_THREAD_OBJECT) == (ULONG) TX_TRUE) + { + accepted_interiors++; + } + + (VOID) test_plant(object, offset, saved); + } + + test_expect("every aligned interior offset of a legitimate object was offered", + (ULONG) (interior_offsets > ((ULONG) 0)), (ULONG) TX_TRUE); + test_expect("no aligned interior address carrying a planted thread ID is accepted as a thread", + accepted_interiors, ((ULONG) 0)); + + /* The unaligned offsets the report names, and the address one past the end. */ + (VOID) test_plant(object, ((ULONG) 0), TX_THREAD_ID); + + shifted = (VOID *) (((UCHAR *) object) + 1); + test_expect("an address one byte into a legitimate object is not accepted", + test_authenticate(&module_a, shifted, TXM_THREAD_OBJECT), (ULONG) TX_FALSE); + + shifted = (VOID *) (((UCHAR *) object) + 4); + test_expect("an address four bytes into a legitimate object is not accepted", + test_authenticate(&module_a, shifted, TXM_THREAD_OBJECT), (ULONG) TX_FALSE); + + shifted = (VOID *) (((UCHAR *) object) + (ULONG) sizeof(TX_THREAD)); + test_expect("the address one past a legitimate object is not accepted", + test_authenticate(&module_a, shifted, TXM_THREAD_OBJECT), (ULONG) TX_FALSE); + + /**********************************************************************/ + /* Exact addresses of live objects, for every type the manager knows. */ + /**********************************************************************/ + + for (index = ((ULONG) 0); index < (ULONG) TEST_OBJECT_KIND_COUNT; index++) + { + kind = &test_object_kinds[index]; + + test_reset(&module_a, &module_b); + + object = test_allocate_and_create(&module_a, kind); + + (void) snprintf(description, sizeof(description), + "the exact address of a live %s is accepted as one", kind -> test_kind_name); + test_expect(description, test_authenticate(&module_a, object, kind -> test_kind_type), (ULONG) TX_TRUE); + + /* Every other type must refuse it, whatever its ID has been made to say. */ + for (other_index = ((ULONG) 0); other_index < (ULONG) TEST_OBJECT_KIND_COUNT; other_index++) + { + if (other_index == index) + { + continue; + } + + other_kind = &test_object_kinds[other_index]; + + saved = test_plant(object, ((ULONG) 0), other_kind -> test_kind_id); + + (void) snprintf(description, sizeof(description), + "a %s carrying a %s ID is not accepted as a %s", + kind -> test_kind_name, other_kind -> test_kind_name, other_kind -> test_kind_name); + test_expect(description, test_authenticate(&module_a, object, other_kind -> test_kind_type), (ULONG) TX_FALSE); + + (VOID) test_plant(object, ((ULONG) 0), saved); + } + + /* A raw allocation of exactly the right size, never created. */ + raw_object = test_allocate(&module_a, kind -> test_kind_size); + + (void) snprintf(description, sizeof(description), + "an uncreated allocation the size of a %s is not accepted as one", kind -> test_kind_name); + test_expect(description, test_authenticate(&module_a, raw_object, kind -> test_kind_type), (ULONG) TX_FALSE); + + /* Deleting the object takes it off the created list and clears its ID. */ + test_object_delete(object, kind -> test_kind_type); + + (void) snprintf(description, sizeof(description), + "a deleted %s is not accepted as one", kind -> test_kind_name); + test_expect(description, test_authenticate(&module_a, object, kind -> test_kind_type), (ULONG) TX_FALSE); + } + + /**********************************************************************/ + /* Addresses that have been freed, and addresses that have come back. */ + /**********************************************************************/ + + test_reset(&module_a, &module_b); + + kind = &test_object_kinds[4]; + object = test_allocate_and_create(&module_a, kind); + + test_expect("a live queue is accepted before anything is given back", + test_authenticate(&module_a, object, TXM_QUEUE_OBJECT), (ULONG) TX_TRUE); + + /* Memory holding a live object is not given back at all. The object stays + created, the allocation stays owned, and the address stays authentic, so + the state this once had to defend against -- an object on the created list + whose memory the manager no longer owns -- cannot be reached. */ + _tx_thread_current_ptr = (TX_THREAD *) test_allocate(&module_a, (ULONG) sizeof(TX_THREAD)); + _tx_thread_current_ptr -> tx_thread_module_instance_ptr = &module_a; + + status = _txm_module_manager_object_deallocate(object); + test_expect("memory holding a live object is not given back", (ULONG) status, (ULONG) TX_DELETE_ERROR); + + test_expect("and the live object is still accepted afterwards", + test_authenticate(&module_a, object, TXM_QUEUE_OBJECT), (ULONG) TX_TRUE); + + /* Deleted first, the same memory is given back, and the address comes round + again as a fresh, raw allocation. It is once again an exact allocation start + of the right size, and the ID of the object that used to be there can be + written back into it by the module that now owns it, so the created list is + the only thing that refuses it. */ + test_object_delete(object, kind -> test_kind_type); + test_release_size(kind -> test_kind_size); + status = _txm_module_manager_object_deallocate(object); + test_expect("a deleted object's memory is given back", (ULONG) status, (ULONG) TX_SUCCESS); + + raw_object = test_allocate(&module_a, kind -> test_kind_size); + test_expect("the freed address is handed out again", + (ULONG) (raw_object == object), (ULONG) TX_TRUE); + (VOID) test_plant(raw_object, ((ULONG) 0), kind -> test_kind_id); + test_expect("and is not accepted as the object that used to be there", + test_authenticate(&module_a, raw_object, TXM_QUEUE_OBJECT), (ULONG) TX_FALSE); + + _tx_thread_current_ptr = TX_NULL; + + /**********************************************************************/ + /* The control block ID in memory the manager gives back. */ + /**********************************************************************/ + + /* Authentication reads the kernel's created list, so it refuses a recycled + address whatever the words in it say. The size-based check is the one that + reads the control block ID, and it is kept for dispatchers outside this + repository, so clearing the ID on the way out is what stops memory a module + wrote an ID into presenting that ID to whoever holds the address next. */ + + test_reset(&module_a, &module_b); + + kind = &test_object_kinds[4]; + raw_object = test_allocate(&module_a, kind -> test_kind_size); + + _tx_thread_current_ptr = (TX_THREAD *) test_allocate(&module_a, (ULONG) sizeof(TX_THREAD)); + _tx_thread_current_ptr -> tx_thread_module_instance_ptr = &module_a; + + (VOID) test_plant(raw_object, ((ULONG) 0), kind -> test_kind_id); + + test_expect("memory carrying a planted ID reads as an object of that type", + (ULONG) _txm_module_manager_object_id_check((ALIGN_TYPE) raw_object, TXM_QUEUE_OBJECT), + (ULONG) TX_TRUE); + + test_release_size(kind -> test_kind_size); + status = _txm_module_manager_object_deallocate(raw_object); + test_expect("the allocation is given back", (ULONG) status, (ULONG) TX_SUCCESS); + + object = test_allocate(&module_a, kind -> test_kind_size); + test_expect("the address comes round again", + (ULONG) (object == raw_object), (ULONG) TX_TRUE); + + test_expect("and no longer reads as an object of that type", + (ULONG) _txm_module_manager_object_id_check((ALIGN_TYPE) object, TXM_QUEUE_OBJECT), + (ULONG) TX_FALSE); + + _tx_thread_current_ptr = TX_NULL; + + /**********************************************************************/ + /* Objects the application owns, and objects nobody owns. */ + /**********************************************************************/ + + test_reset(&module_a, &module_b); + + /* An application-owned object lives outside the pool, so the manager has no + allocation record of it. The kernel created list is the record that stands + in its place, and it is what makes sharing such an object with a module + possible at all. */ + test_object_create(&test_host_queue, TXM_QUEUE_OBJECT); + test_object_create(&test_host_thread, TXM_THREAD_OBJECT); + + test_expect("an application-owned queue on the created list is accepted", + test_authenticate(&module_a, &test_host_queue, TXM_QUEUE_OBJECT), (ULONG) TX_TRUE); + test_expect("an application-owned thread on the created list is accepted", + test_authenticate(&module_a, &test_host_thread, TXM_THREAD_OBJECT), (ULONG) TX_TRUE); + test_expect("but not as the wrong type", + test_authenticate(&module_a, &test_host_queue, TXM_THREAD_OBJECT), (ULONG) TX_FALSE); + + /* Memory outside the pool that carries the right ID but is on no created + list. Without the created list this would be indistinguishable from a real + object, and every word of privileged memory that happened to hold an ID + would be usable as one. */ + test_host_stranger.tx_mutex_id = TX_MUTEX_ID; + test_expect("memory carrying a mutex ID but on no created list is refused", + test_authenticate(&module_a, &test_host_stranger, TXM_MUTEX_OBJECT), (ULONG) TX_FALSE); + + test_object_delete(&test_host_queue, TXM_QUEUE_OBJECT); + test_expect("a deleted application-owned queue is refused", + test_authenticate(&module_a, &test_host_queue, TXM_QUEUE_OBJECT), (ULONG) TX_FALSE); + + test_object_delete(&test_host_thread, TXM_THREAD_OBJECT); + + /**********************************************************************/ + /* Objects another module allocated. */ + /**********************************************************************/ + + test_reset(&module_a, &module_b); + + kind = &test_object_kinds[5]; + object = test_allocate_and_create(&module_b, kind); + + /* A created object belonging to another module is authentic, and is accepted + as such: it is the exact address of a live semaphore. Whether module A is + *authorised* to operate on module B's semaphore is a separate question + about ownership, which this check does not answer and does not claim to. */ + test_expect("another module's live semaphore is authentic", + test_authenticate(&module_a, object, TXM_SEMAPHORE_OBJECT), (ULONG) TX_TRUE); + + /* An allocation another module has not created is not an object at all, and + neither module can use it as one. */ + raw_object = test_allocate(&module_b, kind -> test_kind_size); + test_expect("another module's uncreated allocation is not an object", + test_authenticate(&module_a, raw_object, TXM_SEMAPHORE_OBJECT), (ULONG) TX_FALSE); + test_expect("nor is it one to the module that allocated it", + test_authenticate(&module_b, raw_object, TXM_SEMAPHORE_OBJECT), (ULONG) TX_FALSE); + + /* An interior address of another module's object is refused the same way an + interior address of the caller's own object is. */ + shifted = (VOID *) (((UCHAR *) object) + (ULONG) sizeof(ULONG)); + (VOID) test_plant(object, (ULONG) sizeof(ULONG), TX_SEMAPHORE_ID); + test_expect("an interior address of another module's object is refused", + test_authenticate(&module_a, shifted, TXM_SEMAPHORE_OBJECT), (ULONG) TX_FALSE); + + /**********************************************************************/ + /* Addresses and types that are not objects at all. */ + /**********************************************************************/ + + test_reset(&module_a, &module_b); + + object = test_allocate_and_create(&module_a, &test_object_kinds[6]); + + test_expect("a null address is refused", + test_authenticate(&module_a, TX_NULL, TXM_THREAD_OBJECT), (ULONG) TX_FALSE); + test_expect("an object type the manager does not know is refused", + test_authenticate(&module_a, object, 0U), (ULONG) TX_FALSE); + test_expect("and so is one past the types it does know", + test_authenticate(&module_a, object, TXM_TIMER_OBJECT + 1U), (ULONG) TX_FALSE); + + /* A control block inside the calling module's own memory is one the module can + write behind the kernel's back, so it is refused however genuine it looks. */ + owner = &module_a; + test_object_create(test_module_data.test_data_bytes, TXM_MUTEX_OBJECT); + test_expect("a mutex in the module's own data is refused", + test_authenticate(owner, test_module_data.test_data_bytes, TXM_MUTEX_OBJECT), (ULONG) TX_FALSE); + + test_object_create(test_module_code.test_code_bytes, TXM_SEMAPHORE_OBJECT); + test_expect("a semaphore in the module's own code is refused", + test_authenticate(owner, test_module_code.test_code_bytes, TXM_SEMAPHORE_OBJECT), (ULONG) TX_FALSE); + + /**********************************************************************/ + /* The bound on every scan. */ + /**********************************************************************/ + + test_reset(&module_a, &module_b); + + object = test_allocate_and_create(&module_a, &test_object_kinds[6]); + + /* A count that claims more objects than the list holds must not make either + scan walk past the end of it. The chain the model builds ends in a self + link, so a scan that ignored the count would spin rather than crash; the + count is what stops it. */ + module_a.txm_module_instance_object_list_count = ((ULONG) 64); + _tx_thread_created_count = ((ULONG) 64); + + test_expect("a live thread is still accepted when the counts overstate the lists", + test_authenticate(&module_a, object, TXM_THREAD_OBJECT), (ULONG) TX_TRUE); + + other_object = (VOID *) &test_host_stranger; + test_expect("and an address on neither list is still refused", + test_authenticate(&module_a, other_object, TXM_THREAD_OBJECT), (ULONG) TX_FALSE); + + /* An empty allocation list with a head that has not been cleared must not be + followed either. */ + module_a.txm_module_instance_object_list_count = ((ULONG) 0); + _tx_thread_created_count = ((ULONG) 1); + + test_expect("a live thread is accepted with an empty allocation list", + test_authenticate(&module_a, object, TXM_THREAD_OBJECT), (ULONG) TX_TRUE); + + _tx_thread_created_count = ((ULONG) 0); + test_expect("and refused once the created list is empty too", + test_authenticate(&module_a, object, TXM_THREAD_OBJECT), (ULONG) TX_FALSE); + + /**********************************************************************/ + /* The size-based check kept for dispatchers outside this repository. */ + /**********************************************************************/ + + test_reset(&module_a, &module_b); + + kind = &test_object_kinds[3]; + object = test_allocate_and_create(&module_a, kind); + + test_expect("the size-based check accepts an exact allocation of that size", + (ULONG) _txm_module_manager_param_check_object_for_use(&module_a, (ALIGN_TYPE) object, + kind -> test_kind_size), + (ULONG) TX_TRUE); + + test_expect("and refuses an allocation of a different size", + (ULONG) _txm_module_manager_param_check_object_for_use(&module_a, (ALIGN_TYPE) object, + kind -> test_kind_size + ((ULONG) 4)), + (ULONG) TX_FALSE); + + /* The interior address the reported attack is built on is what this check has + to refuse, and it does, which is what makes hardening it worthwhile even + though it cannot be given a type. */ + interior_offsets = ((ULONG) 0); + accepted_interiors = ((ULONG) 0); + + for (offset = (ULONG) sizeof(ULONG); offset < kind -> test_kind_size; offset = offset + ((ULONG) sizeof(ULONG))) + { + shifted = (VOID *) (((UCHAR *) object) + offset); + + saved = test_plant(object, offset, kind -> test_kind_id); + + interior_offsets++; + + if (_txm_module_manager_param_check_object_for_use(&module_a, (ALIGN_TYPE) shifted, + kind -> test_kind_size) == TX_TRUE) + { + accepted_interiors++; + } + + (VOID) test_plant(object, offset, saved); + } + + test_expect("the size-based check was offered every aligned interior offset", + (ULONG) (interior_offsets > ((ULONG) 0)), (ULONG) TX_TRUE); + test_expect("and accepted none of them", + accepted_interiors, ((ULONG) 0)); + + /**********************************************************************/ + /* The disclosed sink: resetting a thread that is not a module thread. */ + /**********************************************************************/ + + test_reset(&module_a, &module_b); + + /* A thread the application created carries no module instance. Reset reads + the shell entry function out of that instance to rebuild the thread's + stack, so a null one would be followed in privileged mode. Such a thread is + authentic and can be found by name, and one that has run to completion + passes reset's own state test, so refusing it has to be reset's own job. */ + _tx_thread_current_ptr = &test_host_thread; + + object = test_allocate(&module_a, (ULONG) sizeof(TX_THREAD)); + test_object_create(object, TXM_THREAD_OBJECT); + + ((TX_THREAD *) object) -> tx_thread_state = TX_COMPLETED; + ((TX_THREAD *) object) -> tx_thread_module_instance_ptr = TX_NULL; + + test_stack_builds = ((ULONG) 0); + status = _txm_module_manager_thread_reset((TX_THREAD *) object); + + test_expect("resetting a completed thread with no module instance is refused", + (ULONG) status, (ULONG) TX_NOT_DONE); + test_expect("...without building a stack frame", test_stack_builds, ((ULONG) 0)); + test_expect("...and without moving it out of the completed state", + (ULONG) ((TX_THREAD *) object) -> tx_thread_state, (ULONG) TX_COMPLETED); + + /* A module thread in the same state is reset, so the guard has not broken the + operation it protects. */ + ((TX_THREAD *) object) -> tx_thread_module_instance_ptr = &module_a; + module_a.txm_module_instance_shell_entry_function = TX_NULL; + + test_stack_builds = ((ULONG) 0); + status = _txm_module_manager_thread_reset((TX_THREAD *) object); + + test_expect("a completed module thread is reset", (ULONG) status, (ULONG) TX_SUCCESS); + test_expect("...building its stack frame once", test_stack_builds, ((ULONG) 1)); + test_expect("...and leaving it suspended", + (ULONG) ((TX_THREAD *) object) -> tx_thread_state, (ULONG) TX_SUSPENDED); + + _tx_thread_current_ptr = TX_NULL; + + /**********************************************************************/ + /* What the whole run has to have held. */ + /**********************************************************************/ + + /**********************************************************************/ + /* Why a size cannot stand in for a type. */ + /**********************************************************************/ + + /* Authentication establishes an object's type from the kernel's created list + for that type, rather than from the size of its control block, because a size + does not identify one. Distinct control blocks share a size: on a 32-bit + target a byte pool, a mutex and a timer are all the same size as each other, + and a block pool is the same size as an event flags group. + + This is asserted rather than left as a remark so that the reasoning is checked + against the structures rather than remembered. It holds while any two distinct + types collide, so adding a field to one of them does not make it fail; it + fails only if every type becomes distinguishable by size, which is when the + remark above would need revisiting. */ + test_expect("distinct object types share a control block size", + (ULONG) ((sizeof(TX_BYTE_POOL) == sizeof(TX_MUTEX)) || + (sizeof(TX_BYTE_POOL) == sizeof(TX_TIMER)) || + (sizeof(TX_MUTEX) == sizeof(TX_TIMER)) || + (sizeof(TX_BLOCK_POOL) == sizeof(TX_EVENT_FLAGS_GROUP))), + (ULONG) TX_TRUE); + + test_expect("every case ran", (ULONG) (test_checks > ((ULONG) 100)), (ULONG) TX_TRUE); + + if (test_failures == 0U) + { + printf("SUCCESS! %lu expectations\n", (unsigned long) test_checks); + return(0); + } + + printf("ERROR: %u expectation(s) failed of %lu\n", test_failures, (unsigned long) test_checks); + return(1); +} |
