diff --git a/.github/utest/configs/components/asan.cfg b/.github/utest/configs/components/asan.cfg new file mode 100644 index 000000000000..751ef66bc9b4 --- /dev/null +++ b/.github/utest/configs/components/asan.cfg @@ -0,0 +1,8 @@ +# dependencies +CONFIG_RT_CONSOLEBUF_SIZE=1024 +CONFIG_RT_USING_CI_ACTION=y + +CONFIG_RT_USING_ASAN=y +CONFIG_RT_ASAN_SHADOW_SIZE=65536 +CONFIG_RT_ASAN_TRACK_MAX=512 +CONFIG_RT_UTEST_ASAN=y diff --git a/.github/workflows/utest_auto_run.yml b/.github/workflows/utest_auto_run.yml index 9a4915737b6a..087b747af820 100644 --- a/.github/workflows/utest_auto_run.yml +++ b/.github/workflows/utest_auto_run.yml @@ -152,6 +152,8 @@ jobs: config_file: "components/dfs.cfg" - platform: { UTEST: "A9", RTT_BSP: "bsp/qemu-vexpress-a9", QEMU_ARCH: "arm", QEMU_MACHINE: "vexpress-a9", SD_FILE: "sd.bin", KERNEL: "standard", "SMP_RUN":"" } config_file: "components/libc.cfg" + - platform: { UTEST: "A9", RTT_BSP: "bsp/qemu-vexpress-a9", QEMU_ARCH: "arm", QEMU_MACHINE: "vexpress-a9", SD_FILE: "sd.bin", KERNEL: "standard", "SMP_RUN":"" } + config_file: "components/asan.cfg" env: TEST_QEMU_ARCH: ${{ matrix.platform.QEMU_ARCH }} diff --git a/components/utilities/Kconfig b/components/utilities/Kconfig index c32cd692bde6..d2dc05c0bbc1 100644 --- a/components/utilities/Kconfig +++ b/components/utilities/Kconfig @@ -244,5 +244,6 @@ config RT_USING_RESOURCE_ID rsource "libadt/Kconfig" rsource "rt-link/Kconfig" +rsource "asan/Kconfig" endmenu diff --git a/components/utilities/asan/Kconfig b/components/utilities/asan/Kconfig new file mode 100644 index 000000000000..5a59c2142417 --- /dev/null +++ b/components/utilities/asan/Kconfig @@ -0,0 +1,72 @@ +menuconfig RT_USING_ASAN + bool "Enable AddressSanitizer (heap overflow & use-after-free check)" + default n + depends on RT_USING_HEAP && !RT_USING_USERHEAP + depends on RT_USING_SMALL_MEM_AS_HEAP || RT_USING_MEMHEAP_AS_HEAP || RT_USING_SLAB_AS_HEAP + depends on !RT_USING_SMP || RT_USING_MUTEX || RT_USING_HEAP_ISR + help + Enable runtime AddressSanitizer (kernel-address) support. It + instruments memory accesses to detect heap buffer overflow and + use-after-free at runtime. + + It requires the toolchain to support '-fsanitize=kernel-address' + (GCC 8+, verified on ARM and RISC-V). + + The shadow memory is a static array of RT_ASAN_SHADOW_SIZE bytes + and covers the first RT_ASAN_SHADOW_SIZE * 8 bytes of the heap. + Accesses beyond that range are not checked. + + Each allocation reserves a size header, alignment padding and at least + 8 bytes of right redzone. Realloc grows within its reserved capacity or + allocates and copies the requested user bytes; shrinking keeps capacity. + Aligned allocations also use the requested size for their redzones. + Only the system heap APIs are wrapped; direct allocator/page APIs and + accesses in prebuilt, uninstrumented libraries are not checked. + + The component overrides the weak system heap interfaces and owns + the underlying allocator, heap lock and allocation hooks. Do not + combine it with another implementation overriding the same APIs. + + SMP requires a mutex or the interrupt-safe heap spinlock to + serialize allocator access across CPUs. + + Heap algorithm support: + - small mem (RT_USING_SMALL_MEM_AS_HEAP): full support, detects + both heap-buffer-overflow and use-after-free. + - slab (RT_USING_SLAB_AS_HEAP) and memheap + (RT_USING_MEMHEAP_AS_HEAP): detects heap-buffer-overflow only. + Their allocators reuse freed blocks for internal metadata written + through instrumented rt_memset/rt_memcpy, so poisoning a whole + freed block would raise false positives; use-after-free is + therefore disabled for these two. + - userheap (RT_USING_USERHEAP): not supported (mutually exclusive). + + if RT_USING_ASAN + config RT_ASAN_SHADOW_SIZE + int "ASan shadow memory size (bytes)" + default 65536 + help + Size of the static shadow memory array. Each byte maps 8 + bytes of the heap, so the checked heap range is + RT_ASAN_SHADOW_SIZE * 8 bytes. + + config RT_ASAN_TRACK_MAX + int "Max number of tracked active allocations" + default 512 + help + Size of the allocation tracking table. Each entry records + one live block (ptr, size, owner thread). Reduce this on + memory-constrained MCUs (e.g. 128 or 64). When the table + is full, further allocations still get redzones and free/realloc + handling, but reports may lack allocation/owner details. + + config RT_ASAN_BACKTRACE + bool "Print full backtrace on report" + default y + help + When a violation is reported, also dump the full call stack + of the faulting thread via rt_backtrace(). This requires the + target architecture to implement a backtrace backend (unwind + table or frame pointer chain). Architectures without one + print nothing extra. + endif diff --git a/components/utilities/asan/SConscript b/components/utilities/asan/SConscript new file mode 100644 index 000000000000..34a69a03c32c --- /dev/null +++ b/components/utilities/asan/SConscript @@ -0,0 +1,23 @@ +from building import * +Import('rtconfig') + +cwd = GetCurrentDir() +src = Glob('*.c') +CPPPATH = [cwd] +CFLAGS = '' +LINKFLAGS = '' + +# DefineGroup adds CFLAGS/CXXFLAGS/LINKFLAGS to the shared build environment. +# The runtime is provided here and does not need libasan. +if rtconfig.PLATFORM == 'gcc': + CFLAGS = ' -fsanitize=kernel-address -fno-omit-frame-pointer' + LINKFLAGS = ' -fsanitize=kernel-address' + +# The ASan runtime itself must not be instrumented, otherwise it would +# recurse infinitely. '-fno-sanitize=kernel-address' is appended after the +# global '-fsanitize=kernel-address' and therefore overrides it. +group = DefineGroup('asan', src, depend=['RT_USING_ASAN'], CPPPATH=CPPPATH, + CFLAGS=CFLAGS, CXXFLAGS=CFLAGS, LINKFLAGS=LINKFLAGS, + LOCAL_CFLAGS=' -fno-sanitize=kernel-address') + +Return('group') diff --git a/components/utilities/asan/asan.c b/components/utilities/asan/asan.c new file mode 100644 index 000000000000..f13559a256b4 --- /dev/null +++ b/components/utilities/asan/asan.c @@ -0,0 +1,630 @@ +/* + * Copyright (c) 2006-2024, RT-Thread Development Team + * + * SPDX-License-Identifier: Apache-2.0 + * + * Change Logs: + * Date Author Notes + * 2026-08-30 RT-Thread first version (heap-only AddressSanitizer) + */ + +#include +#include + +#ifdef RT_USING_ASAN + +#include "asan.h" + +#define DBG_TAG "asan" +#define DBG_LVL DBG_INFO +#include + +/* + * Runtime AddressSanitizer (kernel-address) for RT-Thread. + * + * The compiler instruments every memory load/store and calls + * __asan_loadN_noabort / __asan_storeN_noabort. Those helpers check a + * shadow byte (8 bytes of application memory -> 1 shadow byte) and report + * when the access touches a poisoned granule. + * + * The system heap calls this runtime while holding its allocator lock. Each + * allocation reserves a header and redzones independently of the tracking table. + */ + +/* ---- shadow memory ---- */ +static rt_uintptr_t asan_heap_base; /* first checked address */ +static rt_uintptr_t asan_heap_limit; /* base + coverage */ +static rt_uint8_t asan_shadow[RT_ASAN_SHADOW_SIZE]; /* 8 bytes -> 1 byte */ + +/* Use hardware locks: scheduler-aware spinlocks can access heap-allocated + * thread objects and recursively enter the sanitizer. Never print or call + * instrumented memory helpers while this lock is held. + */ +#ifdef RT_USING_SMP +static RT_DEFINE_HW_SPINLOCK(asan_spinlock); +#endif + +static rt_base_t asan_lock(void) +{ + rt_base_t level = rt_hw_local_irq_disable(); +#ifdef RT_USING_SMP + rt_hw_spin_lock(&asan_spinlock); +#endif + return level; +} + +static void asan_unlock(rt_base_t level) +{ +#ifdef RT_USING_SMP + rt_hw_spin_unlock(&asan_spinlock); +#endif + rt_hw_local_irq_enable(level); +} + +/* total number of violations reported, exposed for utest/CI verification */ +static volatile rt_uint32_t asan_report_count; + +rt_uint32_t rt_asan_report_count_get(void) +{ + rt_base_t level = asan_lock(); + rt_uint32_t count = asan_report_count; + asan_unlock(level); + return count; +} + +#define ASAN_SHADOW_SCALE 8 +#define ASAN_POISON 0xF8 /* whole granule poisoned */ +#define ASAN_MIN(a, b) ((a) < (b) ? (a) : (b)) + +/* ---- allocation tracking table ---- */ +#ifndef RT_ASAN_TRACK_MAX +#define RT_ASAN_TRACK_MAX 512 +#endif + +struct asan_track +{ + rt_uintptr_t ptr; + rt_size_t size; + rt_uint8_t used; + char owner[RT_NAME_MAX]; +}; + +static struct asan_track asan_tracks[RT_ASAN_TRACK_MAX]; + +/* most recently freed block, for use-after-free diagnosis */ +static struct asan_track asan_last_freed; + +/* ---- helpers ---- */ +rt_inline rt_bool_t asan_addr_in_range(rt_uintptr_t addr) +{ + return addr >= asan_heap_base && addr < asan_heap_limit; +} + +/* check whether [addr, addr+size) touches any poisoned byte */ +static rt_bool_t asan_range_is_poisoned(rt_uintptr_t addr, rt_size_t size) +{ + rt_uintptr_t off; + rt_size_t n; + rt_uint8_t shadow; + rt_bool_t poisoned = RT_FALSE; + rt_base_t level; + + if (!size || addr >= asan_heap_limit) + { + return RT_FALSE; + } + if (addr < asan_heap_base) + { + n = asan_heap_base - addr; + if (size <= n) + { + return RT_FALSE; + } + addr = asan_heap_base; + size -= n; + } + size = ASAN_MIN(size, asan_heap_limit - addr); + level = asan_lock(); + while (size) + { + off = addr - asan_heap_base; + n = ASAN_MIN(ASAN_SHADOW_SCALE - (off & (ASAN_SHADOW_SCALE - 1)), size); + shadow = asan_shadow[off >> 3]; + if (shadow && (shadow >= ASAN_SHADOW_SCALE || + (off & (ASAN_SHADOW_SCALE - 1)) + n > shadow)) + { + poisoned = RT_TRUE; + break; + } + addr += n; + size -= n; + } + asan_unlock(level); + return poisoned; +} + +static void asan_locate_block(rt_uintptr_t addr) +{ + struct asan_track block = { 0 }; + const char *kind = "overflow candidate"; + rt_uintptr_t best_end = 0; + rt_bool_t freed = RT_FALSE; + rt_uint32_t i; + rt_base_t level = asan_lock(); + + if (asan_last_freed.used && addr >= asan_last_freed.ptr && + addr - asan_last_freed.ptr < asan_last_freed.size) + { + block = asan_last_freed; + kind = "USE-AFTER-FREE"; + freed = RT_TRUE; + } + for (i = 0; i < RT_ASAN_TRACK_MAX; i++) + { + if (!asan_tracks[i].used) + { + continue; + } + if (addr >= asan_tracks[i].ptr && addr - asan_tracks[i].ptr < asan_tracks[i].size) + { + block = asan_tracks[i]; + kind = "inside block"; + break; + } + if (!freed) + { + rt_uintptr_t end = asan_tracks[i].ptr + asan_tracks[i].size; + if (end <= addr && end >= best_end) + { + best_end = end; + block = asan_tracks[i]; + } + } + } + asan_unlock(level); + if (block.used) + { + rt_kprintf("== block : %p size %lu owner %.*s (%s, offset +%lu)\n", + (void *)block.ptr, (unsigned long)block.size, + RT_NAME_MAX, block.owner, kind, (unsigned long)(addr - block.ptr)); + } + else + { + rt_kprintf("== block : (no nearby active allocation)\n"); + } +} + +static void asan_report(rt_uintptr_t addr, rt_size_t size, rt_bool_t is_write, rt_uintptr_t pc) +{ + rt_thread_t self = rt_thread_self(); + + rt_base_t level = asan_lock(); + asan_report_count++; + asan_unlock(level); + + rt_kprintf("\n"); + rt_kprintf("=================================================================\n"); + rt_kprintf("== ADDRESS SANITIZER: %s\n", + is_write ? "invalid heap access on WRITE" : "invalid heap access on READ"); + rt_kprintf("== address: %p size: %lu\n", (void *)addr, (unsigned long)size); + rt_kprintf("== pc : %p\n", (void *)pc); + if (self) + { + rt_kprintf("== thread : %.*s\n", RT_NAME_MAX, self->parent.name); + } + asan_locate_block(addr); +#ifdef RT_ASAN_BACKTRACE + rt_backtrace(); +#endif + rt_kprintf("=================================================================\n"); +} + +/* ---- instrumented access checks ---- */ +#define ASAN_DEFINE_CHECK(_size, _suffix) \ + void __asan_load##_suffix##_noabort(rt_uintptr_t addr) \ + { \ + if (asan_range_is_poisoned(addr, _size)) \ + asan_report(addr, _size, RT_FALSE, \ + (rt_uintptr_t)__builtin_return_address(0)); \ + } \ + void __asan_store##_suffix##_noabort(rt_uintptr_t addr) \ + { \ + if (asan_range_is_poisoned(addr, _size)) \ + asan_report(addr, _size, RT_TRUE, \ + (rt_uintptr_t)__builtin_return_address(0)); \ + } + +ASAN_DEFINE_CHECK(1, 1) +ASAN_DEFINE_CHECK(2, 2) +ASAN_DEFINE_CHECK(4, 4) +ASAN_DEFINE_CHECK(8, 8) +ASAN_DEFINE_CHECK(16, 16) + +/* variable-length variants */ +void __asan_loadN_noabort(rt_uintptr_t addr, rt_size_t size) +{ + if (asan_range_is_poisoned(addr, size)) + { + asan_report(addr, size, RT_FALSE, (rt_uintptr_t)__builtin_return_address(0)); + } +} + +void __asan_storeN_noabort(rt_uintptr_t addr, rt_size_t size) +{ + if (asan_range_is_poisoned(addr, size)) + { + asan_report(addr, size, RT_TRUE, (rt_uintptr_t)__builtin_return_address(0)); + } +} + +/* misc symbols referenced by some GCC versions */ +void __asan_init(void) {} +void __asan_handle_no_return(void) {} + +/* ---- poison / unpoison (allocator integration) ---- */ +static void asan_unpoison_range(rt_uintptr_t addr, rt_size_t size) +{ + rt_uintptr_t a = addr; + rt_uintptr_t end = addr + size; + + if (size == 0) + { + return; + } + + while (a < end) + { + rt_uintptr_t off; + rt_uint8_t *sh; + rt_size_t n; + rt_uint8_t k; + + if (!asan_addr_in_range(a)) + { + return; + } + + off = a - asan_heap_base; + sh = &asan_shadow[off >> 3]; + k = off & (ASAN_SHADOW_SCALE - 1); + n = ASAN_MIN((rt_size_t)(ASAN_SHADOW_SCALE - k), end - a); + + if (k + n == ASAN_SHADOW_SCALE) + { + *sh = 0; /* whole granule addressable */ + } + else + { + *sh = (rt_uint8_t)(k + n); /* addressable prefix through this range */ + } + + a += n; + } +} + +static void asan_poison_range(rt_uintptr_t addr, rt_size_t size) +{ + rt_uintptr_t a = addr; + rt_uintptr_t end = addr + size; + + if (size == 0) + { + return; + } + + while (a < end) + { + rt_uintptr_t off; + rt_uint8_t *sh; + rt_size_t n; + rt_uint8_t k; + + if (!asan_addr_in_range(a)) + { + return; + } + + off = a - asan_heap_base; + sh = &asan_shadow[off >> 3]; + k = off & (ASAN_SHADOW_SCALE - 1); + n = ASAN_MIN((rt_size_t)(ASAN_SHADOW_SCALE - k), end - a); + + if (n == ASAN_SHADOW_SCALE) + { + *sh = ASAN_POISON; /* whole granule poisoned */ + } + else + { + *sh = k; /* only first k bytes stay addressable */ + } + + a += n; + } +} + +/* ---- allocation tracking ---- */ +static void asan_track_add(rt_uintptr_t ptr, rt_size_t size, rt_thread_t self) +{ + rt_uint32_t i; + + /* update an existing record (e.g. realloc growing in place keeps the same + * user pointer but a larger size), otherwise append a new one */ + for (i = 0; i < RT_ASAN_TRACK_MAX; i++) + { + if (asan_tracks[i].used && asan_tracks[i].ptr == ptr) + { + asan_tracks[i].size = size; + return; + } + } + + for (i = 0; i < RT_ASAN_TRACK_MAX; i++) + { + if (!asan_tracks[i].used) + { + asan_tracks[i].ptr = ptr; + asan_tracks[i].size = size; + asan_tracks[i].used = 1; + { + rt_size_t n = 0; + if (self) + { + while (n < RT_NAME_MAX - 1 && self->parent.name[n]) + { + asan_tracks[i].owner[n] = self->parent.name[n]; + n++; + } + } + asan_tracks[i].owner[n] = '\0'; + } + return; + } + } +} + +static rt_uint32_t asan_track_find(rt_uintptr_t ptr) +{ + rt_uint32_t i; + + for (i = 0; i < RT_ASAN_TRACK_MAX; i++) + { + if (asan_tracks[i].used && asan_tracks[i].ptr == ptr) + { + return i; + } + } + + return RT_ASAN_TRACK_MAX; /* not found */ +} + +/* The user pointer is aligned independently of the underlying heap's alignment. + * Keep allocation metadata in-band so tracking-table exhaustion is harmless. + */ +#define ASAN_ALIGNMENT ((RT_ALIGN_SIZE > ASAN_SHADOW_SCALE) ? RT_ALIGN_SIZE : ASAN_SHADOW_SCALE) +#define ASAN_REDZONE ASAN_SHADOW_SCALE +#ifdef RT_USING_SLAB_AS_HEAP +#define ASAN_ALLOC_ALIGNMENT RT_MM_PAGE_SIZE +#else +#define ASAN_ALLOC_ALIGNMENT ASAN_ALIGNMENT +#endif +struct asan_header +{ + void *raw; + rt_size_t size; + rt_size_t capacity; +}; + +void *rt_asan_malloc(rt_size_t size, void *(*alloc)(rt_size_t)) +{ + return rt_asan_malloc_align(size, ASAN_ALIGNMENT, alloc); +} + +void *rt_asan_malloc_align(rt_size_t size, rt_size_t align, void *(*alloc)(rt_size_t)) +{ + rt_size_t overhead; + struct asan_header *header; + rt_uintptr_t p; + void *raw; + rt_size_t capacity; + rt_base_t level; + rt_thread_t self; + + if (!size || !align || (align & (align - 1))) + { + return RT_NULL; + } + if (align < ASAN_ALIGNMENT) + { + align = ASAN_ALIGNMENT; + } + /* A power-of-two alignment is at most half the address space, so this + * addition cannot wrap. Reserve room for both allocator rounding steps. + */ + overhead = sizeof(struct asan_header) + align - 1 + ASAN_REDZONE; + if (size > (rt_size_t)-1 - overhead - (ASAN_ALIGNMENT - 1) - (ASAN_ALLOC_ALIGNMENT - 1)) + { + return RT_NULL; + } + capacity = RT_ALIGN(size, ASAN_ALIGNMENT); + raw = alloc(capacity + overhead); + if (!raw) + { + return RT_NULL; + } + p = RT_ALIGN((rt_uintptr_t)raw + sizeof(*header), align); + header = (struct asan_header *)p - 1; + header->raw = raw; + header->size = size; + header->capacity = capacity; + self = rt_thread_self(); + + level = asan_lock(); + asan_unpoison_range((rt_uintptr_t)raw, p + capacity + ASAN_REDZONE - (rt_uintptr_t)raw); + asan_poison_range(p - ASAN_REDZONE, ASAN_REDZONE); + asan_unpoison_range(p, size); + asan_poison_range(p + size, capacity + ASAN_REDZONE - size); + if (asan_last_freed.used && asan_last_freed.ptr == p) + { + asan_last_freed.used = 0; + } + asan_track_add(p, size, self); + asan_unlock(level); + return (void *)p; +} + +void rt_asan_free(void *ptr, void (*release)(void *)) +{ + struct asan_header *header; + rt_uintptr_t p = (rt_uintptr_t)ptr; + rt_size_t capacity; + void *raw; + rt_uint32_t idx; + rt_base_t level; + + if (!ptr) + { + return; + } + header = (struct asan_header *)ptr - 1; + raw = header->raw; + capacity = header->capacity; + level = asan_lock(); + idx = asan_track_find(p); + if (idx != RT_ASAN_TRACK_MAX) + { + asan_last_freed = asan_tracks[idx]; + asan_tracks[idx].used = 0; + } + else + { + asan_last_freed.ptr = p; + asan_last_freed.size = header->size; + asan_last_freed.used = 1; + asan_last_freed.owner[0] = '\0'; + } + /* Allocators may reuse any part of the raw block for their metadata. */ + asan_unpoison_range((rt_uintptr_t)raw, p + capacity + ASAN_REDZONE - (rt_uintptr_t)raw); + asan_unlock(level); + release(raw); +#if RT_ASAN_HAS_UAF_DETECTION + /* The heap lock still prevents reuse while the shadow is updated. */ + level = asan_lock(); + asan_poison_range(p, capacity + ASAN_REDZONE); + asan_unlock(level); +#endif +} + +void *rt_asan_realloc(void *ptr, rt_size_t size, + void *(*alloc)(rt_size_t), void (*release)(void *)) +{ + struct asan_header *header; + void *result; + rt_base_t level; + rt_thread_t self; + + if (!ptr) + { + return rt_asan_malloc(size, alloc); + } + if (!size) + { + rt_asan_free(ptr, release); + return RT_NULL; + } + header = (struct asan_header *)ptr - 1; + if (size <= header->capacity) + { + self = rt_thread_self(); + level = asan_lock(); + header->size = size; + asan_unpoison_range((rt_uintptr_t)ptr, size); + asan_poison_range((rt_uintptr_t)ptr + size, header->capacity + ASAN_REDZONE - size); + asan_track_add((rt_uintptr_t)ptr, size, self); + asan_unlock(level); + return ptr; + } + result = rt_asan_malloc(size, alloc); + if (result) + { + /* Copy only user bytes, never allocator padding or a redzone. */ + rt_memcpy(result, ptr, header->size); + rt_asan_free(ptr, release); + } + return result; +} + +/* + * Override the weak rt_system_heap_init to capture the heap range and + * initialize the shadow before the component heap init runs. + */ +void rt_system_heap_init(void *begin_addr, void *end_addr) +{ + rt_uintptr_t begin = (rt_uintptr_t)begin_addr; + rt_uintptr_t end = (rt_uintptr_t)end_addr; + + /* User pointers are explicitly aligned to shadow granules. */ + RT_ASSERT(end > begin && end - begin >= ASAN_SHADOW_SCALE); + asan_heap_base = RT_ALIGN(begin, ASAN_SHADOW_SCALE); + asan_heap_limit = asan_heap_base + ASAN_MIN(end - asan_heap_base, + (rt_uintptr_t)sizeof(asan_shadow) * ASAN_SHADOW_SCALE); + + /* + * Start with everything addressable: the heap allocators store their own + * metadata (headers, free lists, the heap object itself) inside the heap + * region, so an initially-poisoned shadow would report their internal + * accesses as false positives. Detection is provided by poisoning the + * block tail on allocation and the whole block on free instead. + */ + rt_memset(asan_shadow, 0, sizeof(asan_shadow)); + + /* Initialize the allocator owned by the ASan adapter. */ + rt_asan_heap_init(begin_addr, end_addr); +} + +#ifdef RT_USING_FINSH +#include + +static int asan_info(int argc, char **argv) +{ + rt_uint32_t i; + rt_uint32_t active = 0; + struct asan_track block; + rt_base_t level; + + RT_UNUSED(argc); + RT_UNUSED(argv); + rt_kprintf("\n-- AddressSanitizer status --\n"); + rt_kprintf("shadow : %p, %lu bytes\n", asan_shadow, (unsigned long)sizeof(asan_shadow)); + rt_kprintf("coverage : %p - %p\n", (void *)asan_heap_base, (void *)asan_heap_limit); + level = asan_lock(); + block = asan_last_freed; + asan_unlock(level); + if (block.used) + { + rt_kprintf("last free: %p size %lu owner %.*s\n", (void *)block.ptr, + (unsigned long)block.size, RT_NAME_MAX, block.owner); + } + else + { + rt_kprintf("last free: (none)\n"); + } + rt_kprintf("\n-- active allocations (snapshot per entry) --\n"); + for (i = 0; i < RT_ASAN_TRACK_MAX; i++) + { + level = asan_lock(); + block = asan_tracks[i]; + asan_unlock(level); + if (block.used) + { + active++; + rt_kprintf(" %p %6lu %.*s\n", (void *)block.ptr, + (unsigned long)block.size, RT_NAME_MAX, block.owner); + } + } + rt_kprintf("total: %u active blocks\n", active); + return 0; +} +MSH_CMD_EXPORT(asan_info, dump AddressSanitizer status); +#endif /* RT_USING_FINSH */ + +#endif /* RT_USING_ASAN */ diff --git a/components/utilities/asan/asan.h b/components/utilities/asan/asan.h new file mode 100644 index 000000000000..44a5b49e0a55 --- /dev/null +++ b/components/utilities/asan/asan.h @@ -0,0 +1,64 @@ +/* + * Copyright (c) 2006-2024, RT-Thread Development Team + * + * SPDX-License-Identifier: Apache-2.0 + * + * Change Logs: + * Date Author Notes + * 2026-08-30 RT-Thread the first version + */ + +#ifndef __ASAN_H__ +#define __ASAN_H__ + +#include + +/* + * Use-after-free detection requires poisoning a whole freed block. This is only + * safe for allocators whose internal metadata is written by non-instrumented + * code (small mem). memheap/slab reuse freed blocks for metadata written via + * instrumented rt_memset/rt_memcpy, so their freed blocks must not be poisoned. + */ +#if defined(RT_USING_SMALL_MEM_AS_HEAP) +#define RT_ASAN_HAS_UAF_DETECTION 1 +#else +#define RT_ASAN_HAS_UAF_DETECTION 0 +#endif + +#ifdef __cplusplus +extern "C" { +#endif + +#if defined(RT_UTEST_ASAN) && defined(RT_HOOK_USING_FUNC_PTR) +/* Test-only query: never replace hooks already installed by the application. + * Hook registration must remain quiescent while the hook unit test runs. + */ +rt_bool_t rt_asan_test_hooks_in_use(void); +#endif + +void rt_asan_heap_init(void *begin_addr, void *end_addr); + +/* Internal system-heap integration. The caller must hold the heap lock. + * Callbacks operate on the underlying allocator, never rt_malloc/rt_free. + */ +void *rt_asan_malloc(rt_size_t size, void *(*alloc)(rt_size_t)); +void *rt_asan_malloc_align(rt_size_t size, rt_size_t align, void *(*alloc)(rt_size_t)); +void rt_asan_free(void *ptr, void (*release)(void *)); +void *rt_asan_realloc(void *ptr, rt_size_t size, + void *(*alloc)(rt_size_t), void (*release)(void *)); + +/** + * @brief Get the total number of AddressSanitizer violations reported. + * + * This is used by the utest/CI harness to verify that a deliberate + * heap-buffer-overflow or use-after-free is actually detected at runtime. + * + * @return The accumulated report count. + */ +rt_uint32_t rt_asan_report_count_get(void); + +#ifdef __cplusplus +} +#endif + +#endif /* __ASAN_H__ */ diff --git a/components/utilities/asan/asan_heap.c b/components/utilities/asan/asan_heap.c new file mode 100644 index 000000000000..223f9e9e2431 --- /dev/null +++ b/components/utilities/asan/asan_heap.c @@ -0,0 +1,359 @@ +/* + * Copyright (c) 2006-2026, RT-Thread Development Team + * + * SPDX-License-Identifier: Apache-2.0 + * + * ASan system heap adapter. Override the default weak heap interfaces and + * keep the allocator, lock and hooks together. Raw allocations and shadow + * updates are serialized by the same lock, including realloc and page APIs. + */ + +#include +#include +#include "asan.h" + +#ifdef RT_USING_ASAN +#ifdef RT_USING_HOOK +static void (*rt_malloc_hook)(void **ptr, rt_size_t size); +static void (*rt_realloc_entry_hook)(void **ptr, rt_size_t size); +static void (*rt_realloc_exit_hook)(void **ptr, rt_size_t size); +static void (*rt_free_hook)(void **ptr); + +#if defined(RT_UTEST_ASAN) && defined(RT_HOOK_USING_FUNC_PTR) +rt_bool_t rt_asan_test_hooks_in_use(void) +{ + return rt_malloc_hook || rt_realloc_entry_hook || + rt_realloc_exit_hook || rt_free_hook; +} +#endif + +void rt_malloc_sethook(void (*hook)(void **ptr, rt_size_t size)) +{ + rt_malloc_hook = hook; +} + +void rt_realloc_set_entry_hook(void (*hook)(void **ptr, rt_size_t size)) +{ + rt_realloc_entry_hook = hook; +} + +void rt_realloc_set_exit_hook(void (*hook)(void **ptr, rt_size_t size)) +{ + rt_realloc_exit_hook = hook; +} + +void rt_free_sethook(void (*hook)(void **ptr)) +{ + rt_free_hook = hook; +} + +#endif /* RT_USING_HOOK */ + +#if defined(RT_USING_HEAP_ISR) +static struct rt_spinlock _heap_spinlock; +#elif defined(RT_USING_MUTEX) +static struct rt_mutex _lock; +#endif + +rt_inline void _heap_lock_init(void) +{ +#if defined(RT_USING_HEAP_ISR) + rt_spin_lock_init(&_heap_spinlock); +#elif defined(RT_USING_MUTEX) + rt_mutex_init(&_lock, "heap", RT_IPC_FLAG_PRIO); +#endif +} + +rt_inline rt_base_t _heap_lock(void) +{ +#if defined(RT_USING_HEAP_ISR) + return rt_spin_lock_irqsave(&_heap_spinlock); +#elif defined(RT_USING_MUTEX) + if (rt_thread_self()) + { + return rt_mutex_take(&_lock, RT_WAITING_FOREVER); + } + else + { + return RT_EOK; + } +#else + rt_enter_critical(); + return RT_EOK; +#endif +} + +rt_inline void _heap_unlock(rt_base_t level) +{ +#if defined(RT_USING_HEAP_ISR) + rt_spin_unlock_irqrestore(&_heap_spinlock, level); +#elif defined(RT_USING_MUTEX) + RT_ASSERT(level == RT_EOK); + if (rt_thread_self()) + { + rt_mutex_release(&_lock); + } +#else + rt_exit_critical(); +#endif +} + +#ifdef RT_USING_UTESTCASES +/* Keep heap-observation tests synchronized with the active allocator. */ +rt_base_t rt_heap_lock(void); +void rt_heap_unlock(rt_base_t level); + +rt_base_t rt_heap_lock(void) +{ + return _heap_lock(); +} + +void rt_heap_unlock(rt_base_t level) +{ + _heap_unlock(level); +} +#endif + +#if defined(RT_USING_SMALL_MEM_AS_HEAP) +static rt_smem_t system_heap; +rt_inline void _smem_info(rt_size_t *total, + rt_size_t *used, rt_size_t *max_used) +{ + if (total) + { + *total = system_heap->total; + } + if (used) + { + *used = system_heap->used; + } + if (max_used) + { + *max_used = system_heap->max; + } +} +#define _MEM_INIT(_name, _start, _size) \ + system_heap = rt_smem_init(_name, _start, _size) +#define _MEM_MALLOC(_size) \ + rt_smem_alloc(system_heap, _size) +#define _MEM_FREE(_ptr) \ + rt_smem_free(_ptr) +#define _MEM_INFO(_total, _used, _max) \ + _smem_info(_total, _used, _max) +#elif defined(RT_USING_MEMHEAP_AS_HEAP) +static struct rt_memheap system_heap; +void *_memheap_alloc(struct rt_memheap *heap, rt_size_t size); +void _memheap_free(void *rmem); +#define _MEM_INIT(_name, _start, _size) \ + do \ + { \ + rt_memheap_init(&system_heap, _name, _start, _size); \ + system_heap.locked = RT_TRUE; \ + } while (0) +#define _MEM_MALLOC(_size) \ + _memheap_alloc(&system_heap, _size) +#define _MEM_FREE(_ptr) \ + _memheap_free(_ptr) +#define _MEM_INFO(_total, _used, _max) \ + rt_memheap_info(&system_heap, _total, _used, _max) +#elif defined(RT_USING_SLAB_AS_HEAP) +static rt_slab_t system_heap; +rt_inline void _slab_info(rt_size_t *total, + rt_size_t *used, rt_size_t *max_used) +{ + if (total) + { + *total = system_heap->total; + } + if (used) + { + *used = system_heap->used; + } + if (max_used) + { + *max_used = system_heap->max; + } +} +#define _MEM_INIT(_name, _start, _size) \ + system_heap = rt_slab_init(_name, _start, _size) +#define _MEM_MALLOC(_size) \ + rt_slab_alloc(system_heap, _size) +#define _MEM_FREE(_ptr) \ + rt_slab_free(system_heap, _ptr) +#define _MEM_INFO _slab_info +#else +#define _MEM_INIT(...) +#define _MEM_MALLOC(...) RT_NULL +#define _MEM_FREE(...) +#define _MEM_INFO(...) +#endif + +static void *_asan_heap_alloc(rt_size_t size) +{ + return _MEM_MALLOC(size); +} + +static void _asan_heap_free(void *ptr) +{ + _MEM_FREE(ptr); +} + +void rt_asan_heap_init(void *begin_addr, void *end_addr) +{ + rt_uintptr_t begin_align = RT_ALIGN((rt_uintptr_t)begin_addr, RT_ALIGN_SIZE); + rt_uintptr_t end_align = RT_ALIGN_DOWN((rt_uintptr_t)end_addr, RT_ALIGN_SIZE); + + RT_ASSERT(end_align > begin_align); + + /* Initialize system memory heap */ + _MEM_INIT("heap", (void *)begin_align, end_align - begin_align); + /* Initialize multi thread contention lock */ + _heap_lock_init(); +} + +void *rt_malloc(rt_size_t size) +{ + rt_base_t level; + void *ptr; + + /* Enter critical zone */ + level = _heap_lock(); + /* allocate memory block from system heap */ + ptr = rt_asan_malloc(size, _asan_heap_alloc); + /* Exit critical zone */ + _heap_unlock(level); + /* call 'rt_malloc' hook */ + RT_OBJECT_HOOK_CALL(rt_malloc_hook, (&ptr, size)); + return ptr; +} + +void *rt_realloc(void *ptr, rt_size_t newsize) +{ + rt_base_t level; + void *nptr; + + /* Entry hook */ + RT_OBJECT_HOOK_CALL(rt_realloc_entry_hook, (&ptr, newsize)); + /* Enter critical zone */ + level = _heap_lock(); + /* Change the size of previously allocated memory block */ + nptr = rt_asan_realloc(ptr, newsize, _asan_heap_alloc, _asan_heap_free); + /* Exit critical zone */ + _heap_unlock(level); + /* Exit hook */ + RT_OBJECT_HOOK_CALL(rt_realloc_exit_hook, (&nptr, newsize)); + return nptr; +} + +void *rt_calloc(rt_size_t count, rt_size_t size) +{ + void *p; + + if (size && count > (rt_size_t)-1 / size) + { + return RT_NULL; + } + + /* allocate 'count' objects of size 'size' */ + p = rt_malloc(count * size); + /* zero the memory */ + if (p) + { + rt_memset(p, 0, count * size); + } + return p; +} + +void rt_free(void *ptr) +{ + rt_base_t level; + + /* call 'rt_free' hook */ + RT_OBJECT_HOOK_CALL(rt_free_hook, (&ptr)); + /* NULL check */ + if (ptr == RT_NULL) + { + return; + } + /* Enter critical zone */ + level = _heap_lock(); + rt_asan_free(ptr, _asan_heap_free); + /* Exit critical zone */ + _heap_unlock(level); +} + +void rt_memory_info(rt_size_t *total, + rt_size_t *used, + rt_size_t *max_used) +{ + rt_base_t level; + + /* Enter critical zone */ + level = _heap_lock(); + _MEM_INFO(total, used, max_used); + /* Exit critical zone */ + _heap_unlock(level); +} + +#if defined(RT_USING_SLAB) && defined(RT_USING_SLAB_AS_HEAP) +void *rt_page_alloc(rt_size_t npages) +{ + rt_base_t level; + void *ptr; + + /* Enter critical zone */ + level = _heap_lock(); + /* alloc page */ + ptr = rt_slab_page_alloc(system_heap, npages); + /* Exit critical zone */ + _heap_unlock(level); + return ptr; +} + +void rt_page_free(void *addr, rt_size_t npages) +{ + rt_base_t level; + + /* Enter critical zone */ + level = _heap_lock(); + /* free page */ + rt_slab_page_free(system_heap, addr, npages); + /* Exit critical zone */ + _heap_unlock(level); +} +#endif + +void *rt_malloc_align(rt_size_t size, rt_size_t align) +{ + void *ptr; + rt_base_t level; + + if (!size || !align || (align & (align - 1))) + { + return RT_NULL; + } + if (align < sizeof(void *)) + { + align = sizeof(void *); + } + + /* Keep the requested size rather than tracking an oversized backing block. */ + level = _heap_lock(); + ptr = rt_asan_malloc_align(size, align, _asan_heap_alloc); + _heap_unlock(level); + RT_OBJECT_HOOK_CALL(rt_malloc_hook, (&ptr, size)); + return ptr; +} + +void rt_free_align(void *ptr) +{ + if (ptr == RT_NULL) + { + return; + } + /* The ASan header is read by the non-instrumented runtime under the heap + * lock. The old pointer-before-buffer layout is not used in this mode. + */ + rt_free(ptr); +} +#endif /* RT_USING_ASAN */ diff --git a/examples/test/asan_cpp_test.cpp b/examples/test/asan_cpp_test.cpp new file mode 100644 index 000000000000..e4925eb58505 --- /dev/null +++ b/examples/test/asan_cpp_test.cpp @@ -0,0 +1,25 @@ +/* + * Copyright (c) 2026, RT-Thread Development Team + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include "asan.h" +#include "utest.h" + +extern "C" void test_asan_cpp(void) +{ + volatile char *p = static_cast(rt_malloc(16)); + uassert_not_null(p); + if (!p) + { + return; + } + rt_uint32_t before = rt_asan_report_count_get(); + p[15] = 1; + uassert_int_equal(rt_asan_report_count_get(), before); + p[16] = 2; + uassert_true(rt_asan_report_count_get() > before); + rt_free(const_cast(p)); +} diff --git a/examples/test/mem_align_test.c b/examples/test/mem_align_test.c new file mode 100644 index 000000000000..9dd84aebb65d --- /dev/null +++ b/examples/test/mem_align_test.c @@ -0,0 +1,90 @@ +/* + * Copyright (c) 2026, RT-Thread Development Team + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include "utest.h" + +/* These tests also run without ASan: size arithmetic must be safe in both modes. */ +static void test_align_valid(void) +{ + static const rt_size_t sizes[] = { 1, 13, 16, 31, 32 }; + static const rt_size_t aligns[] = { 1, 2, sizeof(void *), 16, 64, 256 }; + rt_size_t i; + rt_size_t j; + + for (i = 0; i < sizeof(sizes) / sizeof(sizes[0]); i++) + { + for (j = 0; j < sizeof(aligns) / sizeof(aligns[0]); j++) + { + char *p = (char *)rt_malloc_align(sizes[i], aligns[j]); + uassert_not_null(p); + if (!p) + { + return; + } + uassert_true(((rt_uintptr_t)p & (aligns[j] - 1)) == 0); + rt_memset(p, 0x5a, sizes[i]); + uassert_int_equal(p[0], 0x5a); + uassert_int_equal(p[sizes[i] - 1], 0x5a); + rt_free_align(p); + } + } + rt_free_align(RT_NULL); +} + +static void test_align_invalid(void) +{ + static const rt_size_t aligns[] = { 0, 3, 6, 12, (rt_size_t)-1 }; + rt_size_t i; + void *p; + + for (i = 0; i < sizeof(aligns) / sizeof(aligns[0]); i++) + { + p = rt_malloc_align(16, aligns[i]); + uassert_null(p); + rt_free_align(p); + } + p = rt_malloc_align(0, 64); + uassert_null(p); + rt_free_align(p); +} + +static void test_align_overflow(void) +{ + static const rt_size_t requests[][2] = { + { (rt_size_t)-1, 64 }, + { (rt_size_t)-3, 64 }, + { (rt_size_t)-64, 64 }, + { (rt_size_t)-256, 256 }, + { (rt_size_t)-1 - sizeof(void *), 1 }, + { (rt_size_t)-1 - RT_ALIGN_SIZE, 1 }, + { (rt_size_t)-1 / 2 + 1, (rt_size_t)-1 / 2 + 1 }, + }; + rt_size_t i; + + for (i = 0; i < sizeof(requests) / sizeof(requests[0]); i++) + { + void *p = rt_malloc_align(requests[i][0], requests[i][1]); + uassert_null(p); + rt_free_align(p); + } +} + +static void testcase(void) +{ + rt_size_t failures = 0; + + /* Each unit resets the framework counters, so retain earlier failures. */ + UTEST_UNIT_RUN(test_align_valid); + failures += utest_handle_get()->failed_num; + UTEST_UNIT_RUN(test_align_invalid); + failures += utest_handle_get()->failed_num; + UTEST_UNIT_RUN(test_align_overflow); + failures += utest_handle_get()->failed_num; + uassert_int_equal(failures, 0); +} + +UTEST_TC_EXPORT(testcase, "core.mem_align", RT_NULL, RT_NULL, 1000); diff --git a/src/SConscript b/src/SConscript index 7b2dec5e4ce1..4f6dc3fd078a 100644 --- a/src/SConscript +++ b/src/SConscript @@ -28,6 +28,17 @@ if GetDepend('RT_USING_SMP') == False: else: SrcRemove(src, ['cpu_up.c', 'scheduler_up.c']) +# AddressSanitizer: heap allocators keep their metadata (headers, free lists) +# inside the heap region, so instrumenting them would report their own header +# accesses as false positives. Move them to a separate non-instrumented group. +asan_alloc_src = [] +if GetDepend('RT_USING_ASAN'): + for alloc_name in ['mem.c', 'memheap.c', 'slab.c']: + matched = [x for x in src if os.path.basename(x.rstr()) == alloc_name] + if matched: + asan_alloc_src += matched + SrcRemove(src, [alloc_name]) + LOCAL_CFLAGS = '' LINKFLAGS = '' @@ -59,6 +70,13 @@ else: LINKFLAGS=LINKFLAGS, LOCAL_CFLAGS=LOCAL_CFLAGS, CPPDEFINES=['__RTTHREAD__'], LOCAL_CPPDEFINES=['__RT_KERNEL_SOURCE__']) +# AddressSanitizer: build heap allocators without instrumentation. +if GetDepend('RT_USING_ASAN') and asan_alloc_src: + group = group + DefineGroup('KernelAlloc', asan_alloc_src, depend=['RT_USING_ASAN'], + CPPPATH=inc, CPPDEFINES=['__RTTHREAD__'], + LOCAL_CPPDEFINES=['__RT_KERNEL_SOURCE__'], + LOCAL_CFLAGS=' -fno-sanitize=kernel-address') + list = os.listdir(cwd) for item in list: if os.path.isfile(os.path.join(cwd, item, 'SConscript')): diff --git a/src/klibc/kstring.c b/src/klibc/kstring.c index b6d553ffa34d..aac9894a33b1 100644 --- a/src/klibc/kstring.c +++ b/src/klibc/kstring.c @@ -10,6 +10,18 @@ #include +/* + * AddressSanitizer: rt_memcpy/rt_memset/rt_memmove copy word-at-a-time and may + * legally read/write a few bytes past the requested byte count (word-aligned + * bulk loops). When instrumented these accesses fall into poisoned redzones and + * raise false positives, so disable instrumentation for them. + */ +#ifdef RT_USING_ASAN +#define RT_KLIB_NO_ASAN __attribute__((no_sanitize_address)) +#else +#define RT_KLIB_NO_ASAN +#endif + #if defined(RT_KLIBC_USING_LIBC_MEMSET) || \ defined(RT_KLIBC_USING_LIBC_MEMCPY) || \ defined(RT_KLIBC_USING_LIBC_MEMMOVE) || \ @@ -36,6 +48,7 @@ * @return The address of source memory. */ #ifndef RT_KLIBC_USING_USER_MEMSET +RT_KLIB_NO_ASAN void *rt_memset(void *s, int c, size_t count) { #if defined(RT_KLIBC_USING_LIBC_MEMSET) @@ -121,6 +134,7 @@ RTM_EXPORT(rt_memset); * @return The address of destination memory */ #ifndef RT_KLIBC_USING_USER_MEMCPY +RT_KLIB_NO_ASAN void *rt_memcpy(void *dst, const void *src, size_t count) { #if defined(RT_KLIBC_USING_LIBC_MEMCPY) @@ -211,6 +225,7 @@ RTM_EXPORT(rt_memcpy); * @return The address of destination memory. */ #ifndef RT_KLIBC_USING_USER_MEMMOVE +RT_KLIB_NO_ASAN void *rt_memmove(void *dest, const void *src, size_t n) { #ifdef RT_KLIBC_USING_LIBC_MEMMOVE diff --git a/src/kservice.c b/src/kservice.c index db321552616f..2739b4231915 100644 --- a/src/kservice.c +++ b/src/kservice.c @@ -944,7 +944,7 @@ static void (*rt_free_hook)(void **ptr); * * @param hook the hook function. */ -void rt_malloc_sethook(void (*hook)(void **ptr, rt_size_t size)) +rt_weak void rt_malloc_sethook(void (*hook)(void **ptr, rt_size_t size)) { rt_malloc_hook = hook; } @@ -955,7 +955,7 @@ void rt_malloc_sethook(void (*hook)(void **ptr, rt_size_t size)) * * @param hook the hook function. */ -void rt_realloc_set_entry_hook(void (*hook)(void **ptr, rt_size_t size)) +rt_weak void rt_realloc_set_entry_hook(void (*hook)(void **ptr, rt_size_t size)) { rt_realloc_entry_hook = hook; } @@ -966,7 +966,7 @@ void rt_realloc_set_entry_hook(void (*hook)(void **ptr, rt_size_t size)) * * @param hook the hook function. */ -void rt_realloc_set_exit_hook(void (*hook)(void **ptr, rt_size_t size)) +rt_weak void rt_realloc_set_exit_hook(void (*hook)(void **ptr, rt_size_t size)) { rt_realloc_exit_hook = hook; } @@ -977,7 +977,7 @@ void rt_realloc_set_exit_hook(void (*hook)(void **ptr, rt_size_t size)) * * @param hook the hook function */ -void rt_free_sethook(void (*hook)(void **ptr)) +rt_weak void rt_free_sethook(void (*hook)(void **ptr)) { rt_free_hook = hook; } @@ -1035,8 +1035,8 @@ rt_inline void _heap_unlock(rt_base_t level) #define rt_heap_lock() _heap_lock() #define rt_heap_unlock() _heap_unlock() #else -rt_base_t rt_heap_lock(void) __attribute__((alias("_heap_lock"))); -void rt_heap_unlock(rt_base_t level) __attribute__((alias("_heap_unlock"))); +rt_weak rt_base_t rt_heap_lock(void) __attribute__((alias("_heap_lock"))); +rt_weak void rt_heap_unlock(rt_base_t level) __attribute__((alias("_heap_unlock"))); #endif /* _MSC_VER */ #endif @@ -1211,6 +1211,11 @@ rt_weak void *rt_calloc(rt_size_t count, rt_size_t size) { void *p; + if (size && count > (rt_size_t)-1 / size) + { + return RT_NULL; + } + /* allocate 'count' objects of size 'size' */ p = rt_malloc(count * size); /* zero the memory */ @@ -1269,7 +1274,7 @@ rt_weak void rt_memory_info(rt_size_t *total, RTM_EXPORT(rt_memory_info); #if defined(RT_USING_SLAB) && defined(RT_USING_SLAB_AS_HEAP) -void *rt_page_alloc(rt_size_t npages) +rt_weak void *rt_page_alloc(rt_size_t npages) { rt_base_t level; void *ptr; @@ -1283,7 +1288,7 @@ void *rt_page_alloc(rt_size_t npages) return ptr; } -void rt_page_free(void *addr, rt_size_t npages) +rt_weak void rt_page_free(void *addr, rt_size_t npages) { rt_base_t level; @@ -1302,47 +1307,61 @@ void rt_page_free(void *addr, rt_size_t npages) * * @param size is the allocated memory block size. * - * @param align is the alignment size. + * @param align is a nonzero power-of-two alignment size. + * + * @note Zero-sized requests, invalid alignments and size overflows return RT_NULL. * * @return The memory block address was returned successfully, otherwise it was * returned empty RT_NULL. */ rt_weak void *rt_malloc_align(rt_size_t size, rt_size_t align) { - void *ptr = RT_NULL; - void *align_ptr = RT_NULL; - int uintptr_size = 0; - rt_size_t align_size = 0; - - /* sizeof pointer */ - uintptr_size = sizeof(void*); - uintptr_size -= 1; + void *ptr; + void *align_ptr; + const rt_size_t uintptr_mask = sizeof(void *) - 1; + rt_size_t align_size; - /* align the alignment size to uintptr size byte */ - align = ((align + uintptr_size) & ~uintptr_size); + if (!size || !align || (align & (align - 1))) + { + return RT_NULL; + } + if (align < sizeof(void *)) + { + align = sizeof(void *); + } - /* get total aligned size */ - align_size = ((size + uintptr_size) & ~uintptr_size) + align; - /* allocate memory block from heap */ + if (size > (rt_size_t)-1 - uintptr_mask) + { + return RT_NULL; + } + align_size = RT_ALIGN(size, sizeof(void *)); + if (align_size > (rt_size_t)-1 - align) + { + return RT_NULL; + } + align_size += align; +#ifdef RT_USING_SLAB_AS_HEAP + if (align_size > (rt_size_t)-1 - (RT_MM_PAGE_SIZE - 1)) +#else + if (align_size > (rt_size_t)-1 - (RT_ALIGN_SIZE - 1)) +#endif + { + return RT_NULL; + } ptr = rt_malloc(align_size); if (ptr != RT_NULL) { - /* the allocated memory block is aligned */ if (((rt_uintptr_t)ptr & (align - 1)) == 0) { align_ptr = (void *)((rt_uintptr_t)ptr + align); } else { - align_ptr = (void *)(((rt_uintptr_t)ptr + (align - 1)) & ~(align - 1)); + align_ptr = (void *)RT_ALIGN((rt_uintptr_t)ptr, align); } - - /* set the pointer before alignment pointer to the real pointer */ - *((rt_uintptr_t *)((rt_uintptr_t)align_ptr - sizeof(void *))) = (rt_uintptr_t)ptr; - + *((rt_uintptr_t *)align_ptr - 1) = (rt_uintptr_t)ptr; ptr = align_ptr; } - return ptr; } RTM_EXPORT(rt_malloc_align); @@ -1355,11 +1374,10 @@ RTM_EXPORT(rt_malloc_align); */ rt_weak void rt_free_align(void *ptr) { - void *real_ptr = RT_NULL; + void *real_ptr; - /* NULL check */ if (ptr == RT_NULL) return; - real_ptr = (void *) * (rt_uintptr_t *)((rt_uintptr_t)ptr - sizeof(void *)); + real_ptr = (void *)*((rt_uintptr_t *)ptr - 1); rt_free(real_ptr); } RTM_EXPORT(rt_free_align); diff --git a/src/utest/Kconfig b/src/utest/Kconfig index 7f478c591ffa..dba4d70a872a 100644 --- a/src/utest/Kconfig +++ b/src/utest/Kconfig @@ -84,6 +84,16 @@ menu "Kernel Core" default n depends on RT_USING_MEMPOOL + config RT_UTEST_MEM_ALIGN + bool "Aligned Memory Allocation Test" + default n + depends on RT_USING_HEAP + + config RT_UTEST_ASAN + bool "AddressSanitizer Test" + default n + depends on RT_USING_ASAN + rsource "perf/Kconfig" rsource "../klibc/utest/Kconfig" diff --git a/src/utest/SConscript b/src/utest/SConscript index 52aca3dcf698..03d32cd6b9d4 100644 --- a/src/utest/SConscript +++ b/src/utest/SConscript @@ -58,6 +58,14 @@ if GetDepend(['RT_UTEST_MTSAFE_KPRINT']): if GetDepend(['RT_UTEST_MEMPOOL']): src += ['mempool_tc.c'] +if GetDepend(['RT_UTEST_MEM_ALIGN']): + src += [os.path.join(cwd, '../../examples/test/mem_align_test.c')] + +if GetDepend(['RT_UTEST_ASAN']): + src += ['asan_tc.c'] + if GetDepend(['RT_USING_CPLUSPLUS']): + src += [os.path.join(cwd, '../../examples/test/asan_cpp_test.cpp')] + # Stressful testcase for scheduler (MP/UP) if GetDepend(['RT_UTEST_SCHEDULER']): src += ['sched_timeout_race_tc.c'] diff --git a/src/utest/asan_tc.c b/src/utest/asan_tc.c new file mode 100644 index 000000000000..43bb6738a73e --- /dev/null +++ b/src/utest/asan_tc.c @@ -0,0 +1,624 @@ +/* + * Copyright (c) 2006-2024, RT-Thread Development Team + * + * SPDX-License-Identifier: Apache-2.0 + * + * Change Logs: + * Date Author Notes + * 2026-08-30 RT-Thread the first version + */ + +/** + * Test Case Name: AddressSanitizer Heap Detection Test + * + * Test Objectives: + * - Verify the runtime AddressSanitizer (kernel-address) detects heap memory + * violations on real targets + * - Verify heap-buffer-overflow (read/write), use-after-free (read/write) and + * realloc overflow are reported + * - Verify normal in-bounds accesses do not raise false positives + * + * Test Scenarios: + * - **Scenario 1 (Heap Overflow Write / test_asan_overflow_write):** + * 1. Allocate a 10-byte block (redzone includes [10, 16)) + * 2. Write at offset 12 which falls into the poisoned redzone + * 3. Assert the ASan report counter increased + * - **Scenario 2 (Heap Overflow Read / test_asan_overflow_read):** + * 1. Allocate a 10-byte block + * 2. Read at offset 12 inside the poisoned redzone + * 3. Assert the ASan report counter increased + * - **Scenario 3 (No False Positive / test_asan_no_false_positive):** + * 1. Allocate a 10-byte block + * 2. Write to in-bounds offsets 0 and 9 + * 3. Assert the ASan report counter did not change + * - **Scenario 4 (Realloc Overflow / test_asan_realloc_overflow):** + * 1. Allocate 10 bytes and realloc to 20 bytes (redzone includes [20, 24)) + * 2. Write at offset 22 inside the new poisoned redzone + * 3. Assert the ASan report counter increased + * - **Scenario 5 (Use-After-Free Read / test_asan_uaf_read):** + * (only when RT_ASAN_HAS_UAF_DETECTION is enabled) + * 1. Allocate and free a block + * 2. Read from the freed block + * 3. Assert the ASan report counter increased + * - **Scenario 6 (Use-After-Free Write / test_asan_uaf_write):** + * (only when RT_ASAN_HAS_UAF_DETECTION is enabled) + * 1. Allocate and free a block + * 2. Write to the freed block + * 3. Assert the ASan report counter increased + * + * Verification Metrics: + * - Overflow/UAF accesses increase rt_asan_report_count_get() + * - In-bounds accesses leave the counter unchanged + * + * Dependencies: + * - RT_USING_ASAN enabled + * - Heap-based dynamic memory (rt_malloc/rt_free/rt_realloc) + * + * Expected Results: + * - All enabled scenarios pass without assertion failures + */ + +#include +#include "utest.h" +#include "asan.h" + +static rt_err_t utest_tc_init(void) +{ + return RT_EOK; +} + +static rt_err_t utest_tc_cleanup(void) +{ + return RT_EOK; +} + +static void test_asan_overflow_write(void) +{ + rt_uint32_t before; + rt_uint32_t after; + volatile char *p; + + p = (char *)rt_malloc(10); + uassert_not_null(p); + if (!p) + { + return; + } + + before = rt_asan_report_count_get(); + p[12] = 0x41; /* heap-buffer-overflow write (redzone [10, 16)) */ + after = rt_asan_report_count_get(); + + rt_free((void *)p); + + uassert_true(after > before); +} + +static void test_asan_overflow_read(void) +{ + rt_uint32_t before; + rt_uint32_t after; + volatile char v; + volatile char *p; + + p = (char *)rt_malloc(10); + uassert_not_null(p); + if (!p) + { + return; + } + + before = rt_asan_report_count_get(); + v = p[12]; /* heap-buffer-overflow read (redzone [10, 16)) */ + after = rt_asan_report_count_get(); + + (void)v; + rt_free((void *)p); + + uassert_true(after > before); +} + +static void test_asan_no_false_positive(void) +{ + rt_uint32_t before; + rt_uint32_t after; + volatile char *p; + + p = (char *)rt_malloc(10); + uassert_not_null(p); + if (!p) + { + return; + } + + before = rt_asan_report_count_get(); + p[0] = 0x01; /* first in-bounds byte */ + p[9] = 0x02; /* last in-bounds byte */ + after = rt_asan_report_count_get(); + + rt_free((void *)p); + + uassert_int_equal(after, before); +} + +static void test_asan_realloc_overflow(void) +{ + rt_uint32_t before; + rt_uint32_t after; + volatile char *p; + volatile char *q; + + p = (char *)rt_malloc(10); + uassert_not_null(p); + if (!p) + { + return; + } + + q = (char *)rt_realloc((void *)p, 20); + uassert_not_null(q); + if (!q) + { + rt_free((void *)p); + return; + } + + before = rt_asan_report_count_get(); + q[22] = 0x41; /* heap-buffer-overflow write (redzone [20, 24)) */ + after = rt_asan_report_count_get(); + + rt_free((void *)q); + + uassert_true(after > before); +} + +#if RT_ASAN_HAS_UAF_DETECTION +static void test_asan_uaf_read(void) +{ + rt_uint32_t before; + rt_uint32_t after; + volatile char v; + volatile char *p; + + p = (char *)rt_malloc(10); + uassert_not_null(p); + if (!p) + { + return; + } + + rt_free((void *)p); + + before = rt_asan_report_count_get(); + v = p[0]; /* use-after-free read */ + after = rt_asan_report_count_get(); + + (void)v; + + uassert_true(after > before); +} + +static void test_asan_uaf_write(void) +{ + rt_uint32_t before; + rt_uint32_t after; + volatile char *p; + + p = (char *)rt_malloc(10); + uassert_not_null(p); + if (!p) + { + return; + } + + rt_free((void *)p); + + before = rt_asan_report_count_get(); + p[0] = 0x41; /* use-after-free write */ + after = rt_asan_report_count_get(); + + uassert_true(after > before); +} +#endif /* RT_ASAN_HAS_UAF_DETECTION */ + +/* Aligned requests must have a redzone too; exercise every partial granule. */ +static void test_asan_boundaries(void) +{ + rt_size_t size; + for (size = 1; size <= 32; size++) + { + volatile char *p = (char *)rt_malloc(size); + rt_uint32_t before; + volatile char value; + uassert_not_null(p); + if (!p) + { + return; + } + uassert_true(((rt_uintptr_t)p & (RT_ALIGN_SIZE - 1)) == 0); + before = rt_asan_report_count_get(); + p[0] = 1; + p[size - 1] = 2; + uassert_int_equal(rt_asan_report_count_get(), before); + value = p[size]; + RT_UNUSED(value); + uassert_true(rt_asan_report_count_get() > before); + before = rt_asan_report_count_get(); + p[size] = 3; + uassert_true(rt_asan_report_count_get() > before); + rt_free((void *)p); + } +} + +static void test_asan_aligned_boundaries(void) +{ + static const rt_size_t sizes[] = { 1, 8, 13, 16, 31, 32 }; + static const rt_size_t aligns[] = { sizeof(void *), 16, 64, 256 }; + rt_size_t i; + rt_size_t j; + + for (i = 0; i < sizeof(sizes) / sizeof(sizes[0]); i++) + { + for (j = 0; j < sizeof(aligns) / sizeof(aligns[0]); j++) + { + rt_uint32_t before = rt_asan_report_count_get(); + volatile char *p = (char *)rt_malloc_align(sizes[i], aligns[j]); + volatile char value; + uassert_not_null(p); + if (!p) + { + return; + } + uassert_true(((rt_uintptr_t)p & (aligns[j] - 1)) == 0); + p[0] = 1; + p[sizes[i] - 1] = 2; + uassert_int_equal(rt_asan_report_count_get(), before); + value = p[sizes[i]]; + uassert_int_equal(rt_asan_report_count_get(), before + 1); + p[sizes[i]] = 3; + uassert_int_equal(rt_asan_report_count_get(), before + 2); + /* Read the left redzone without corrupting the allocation header. */ + value = p[-1]; + RT_UNUSED(value); + uassert_int_equal(rt_asan_report_count_get(), before + 3); + before = rt_asan_report_count_get(); + rt_free_align((void *)p); + uassert_int_equal(rt_asan_report_count_get(), before); + } + } +} + +#if RT_ASAN_HAS_UAF_DETECTION +static void test_asan_aligned_uaf(void) +{ + volatile char *p = (char *)rt_malloc_align(13, 64); + volatile char value; + rt_uint32_t before; + + uassert_not_null(p); + if (!p) + { + return; + } + rt_free_align((void *)p); + before = rt_asan_report_count_get(); + value = p[0]; + RT_UNUSED(value); + uassert_int_equal(rt_asan_report_count_get(), before + 1); + p[12] = 1; + uassert_int_equal(rt_asan_report_count_get(), before + 2); +} +#endif + +static void test_asan_realloc_lifecycle(void) +{ + volatile char *p = (char *)rt_realloc(RT_NULL, 13); + volatile char *q; + rt_uint32_t before = rt_asan_report_count_get(); + rt_size_t i; + + uassert_not_null(p); + if (!p) + { + return; + } + for (i = 0; i < 13; i++) + { + p[i] = (char)i; + } + q = (char *)rt_realloc((void *)p, 64); + uassert_not_null(q); + if (!q) + { + rt_free((void *)p); + return; + } + for (i = 0; i < 13; i++) + { + uassert_int_equal(q[i], (char)i); + } + uassert_int_equal(rt_asan_report_count_get(), before); + p = (char *)rt_realloc((void *)q, 16); + uassert_true(p == q); + before = rt_asan_report_count_get(); + p[16] = 1; + uassert_true(rt_asan_report_count_get() > before); + before = rt_asan_report_count_get(); + p = (char *)rt_realloc((void *)q, 32); + uassert_true(p == q); + p[31] = 7; + uassert_null(rt_realloc((void *)p, (rt_size_t)-1)); + uassert_int_equal(p[31], 7); + uassert_int_equal(rt_asan_report_count_get(), before); + uassert_null(rt_realloc((void *)p, 0)); +#if RT_ASAN_HAS_UAF_DETECTION + before = rt_asan_report_count_get(); + { + volatile char value = p[0]; + RT_UNUSED(value); + } + uassert_true(rt_asan_report_count_get() > before); +#endif + uassert_null(rt_malloc((rt_size_t)-1)); + uassert_null(rt_malloc((rt_size_t)-16)); + uassert_null(rt_malloc((rt_size_t)-64)); + uassert_null(rt_calloc((rt_size_t)-1 / 2 + 1, 2)); + uassert_null(rt_realloc(RT_NULL, 0)); + rt_free(RT_NULL); +} + +/* Run with RT_ASAN_TRACK_MAX=1 as well: correctness must not need a free slot. */ +static void test_asan_calloc_reuse(void) +{ + rt_size_t i; + rt_uint32_t before = rt_asan_report_count_get(); + for (i = 0; i < 64; i++) + { + char *p = (char *)rt_calloc(3, 5); + uassert_not_null(p); + if (!p) + { + return; + } + uassert_int_equal(p[0], 0); + uassert_int_equal(p[14], 0); + rt_free(p); + } + uassert_int_equal(rt_asan_report_count_get(), before); +} + +#ifdef RT_USING_SEMAPHORE +static struct rt_semaphore asan_done; +static rt_uint32_t asan_worker_errors[2]; + +static void asan_worker(void *parameter) +{ + rt_size_t id = (rt_size_t)parameter; + rt_size_t i; + for (i = 0; i < 200; i++) + { + rt_size_t size = i % 63 + 1; + char *p = (char *)rt_malloc(size); + char *q; + if (!p) + { + asan_worker_errors[id]++; + break; + } + rt_memset(p, (int)(id + 1), size); + /* Let the creator start both workers even if it has lower priority. */ + rt_thread_mdelay(1); + q = (char *)rt_realloc(p, 96); + if (!q) + { + rt_free(p); + asan_worker_errors[id]++; + break; + } + if (q[0] != (char)(id + 1) || q[size - 1] != (char)(id + 1)) + { + asan_worker_errors[id]++; + } + rt_thread_yield(); + p = (char *)rt_realloc(q, 1); + if (p != q || p[0] != (char)(id + 1)) + { + asan_worker_errors[id]++; + } + rt_free(p); + p = (char *)rt_malloc_align(size, 64); + if (!p) + { + asan_worker_errors[id]++; + break; + } + rt_thread_yield(); + p[0] = (char)(id + 1); + p[size - 1] = (char)(id + 1); + rt_free_align(p); + } + rt_sem_release(&asan_done); +} + +static void test_asan_concurrent_realloc(void) +{ + rt_thread_t threads[2]; + rt_size_t i; + rt_size_t started = 0; + rt_uint32_t before = rt_asan_report_count_get(); + + rt_sem_init(&asan_done, "asan_done", 0, RT_IPC_FLAG_PRIO); + for (i = 0; i < 2; i++) + { + asan_worker_errors[i] = 0; + threads[i] = rt_thread_create("asan_work", asan_worker, (void *)i, 2048, + RT_THREAD_PRIORITY_MAX / 2, 1); + uassert_not_null(threads[i]); + if (threads[i]) + { +#ifdef RT_USING_SMP + rt_thread_control(threads[i], RT_THREAD_CTRL_BIND_CPU, (void *)(i % RT_CPUS_NR)); +#endif + rt_thread_startup(threads[i]); + started++; + } + } + for (i = 0; i < started; i++) + { + rt_sem_take(&asan_done, RT_WAITING_FOREVER); + } + rt_sem_detach(&asan_done); + uassert_int_equal(asan_worker_errors[0], 0); + uassert_int_equal(asan_worker_errors[1], 0); + uassert_int_equal(rt_asan_report_count_get(), before); +} +#endif + +#ifdef RT_USING_CPLUSPLUS +extern void test_asan_cpp(void); +#endif + +extern rt_base_t rt_heap_lock(void); +extern void rt_heap_unlock(rt_base_t level); + +/* Statistics and page APIs must use the heap owned by the overriding adapter. */ +static void test_asan_heap_adapter(void) +{ + rt_size_t total = 0, used = 0, maximum = 0; + rt_uint32_t before = rt_asan_report_count_get(); + void *p = rt_malloc(64); + rt_base_t level = rt_heap_lock(); + + rt_heap_unlock(level); + + uassert_not_null(p); + rt_memory_info(&total, &used, &maximum); + uassert_true(total > 0); + uassert_true(used >= 64); + uassert_true(maximum >= used); + uassert_true(total >= used); + rt_free(p); + +#if defined(RT_USING_SLAB_AS_HEAP) + p = rt_page_alloc(1); + uassert_not_null(p); + if (p) + { + uassert_true(((rt_uintptr_t)p & (RT_MM_PAGE_SIZE - 1)) == 0); + rt_memset(p, 0x5a, RT_MM_PAGE_SIZE); + rt_page_free(p, 1); + } +#endif + uassert_int_equal(rt_asan_report_count_get(), before); +} + +#ifdef RT_HOOK_USING_FUNC_PTR +static rt_thread_t probe_thread; +static unsigned probe_malloc, probe_entry, probe_exit, probe_free; +static void *probe_old, *probe_new; +static void probe_alloc_hook(void **ptr, rt_size_t size) +{ + if (rt_thread_self() == probe_thread && *ptr && size == 37) + { + probe_malloc++; + } +} +static void probe_entry_hook(void **ptr, rt_size_t size) +{ + if (rt_thread_self() == probe_thread && size == 73) + { + probe_old = *ptr; + probe_entry++; + } +} +static void probe_exit_hook(void **ptr, rt_size_t size) +{ + if (rt_thread_self() == probe_thread && size == 73) + { + probe_new = *ptr; + probe_exit++; + } +} +static void probe_free_hook(void **ptr) +{ + if (rt_thread_self() == probe_thread && *ptr == probe_new) + { + probe_free++; + } +} +static void test_asan_hook_probe(void) +{ + void *p, *q; + /* The public API has no getters. Leave application hooks untouched. + * Run this unit without concurrent hook registration, just like other + * tests which temporarily change global callbacks. + */ + if (rt_asan_test_hooks_in_use()) + { + rt_kprintf("[asan] hook test skipped: application hooks are installed\n"); + return; + } + probe_thread = rt_thread_self(); + probe_malloc = probe_entry = probe_exit = probe_free = 0; + rt_malloc_sethook(probe_alloc_hook); + rt_realloc_set_entry_hook(probe_entry_hook); + rt_realloc_set_exit_hook(probe_exit_hook); + rt_free_sethook(probe_free_hook); + p = rt_malloc(37); + q = rt_realloc(p, 73); + rt_free(q ? q : p); + rt_malloc_sethook(RT_NULL); + rt_realloc_set_entry_hook(RT_NULL); + rt_realloc_set_exit_hook(RT_NULL); + rt_free_sethook(RT_NULL); + uassert_false(rt_asan_test_hooks_in_use()); + uassert_not_null(p); + uassert_not_null(q); + uassert_true(probe_old == p); + uassert_true(probe_new == q); + uassert_int_equal(probe_malloc, 1); + uassert_int_equal(probe_entry, 1); + uassert_int_equal(probe_exit, 1); + uassert_int_equal(probe_free, 1); +} +#endif + +/* utest_unit_run resets its counters; retain failures from every unit. */ +#define ASAN_UNIT_RUN(unit) \ + do \ + { \ + UTEST_UNIT_RUN(unit); \ + failures += utest_handle_get()->failed_num; \ + } while (0) + +static void testcase(void) +{ + rt_size_t failures = 0; +#ifdef RT_HOOK_USING_FUNC_PTR + ASAN_UNIT_RUN(test_asan_hook_probe); +#endif + ASAN_UNIT_RUN(test_asan_heap_adapter); + ASAN_UNIT_RUN(test_asan_overflow_write); + ASAN_UNIT_RUN(test_asan_overflow_read); + ASAN_UNIT_RUN(test_asan_no_false_positive); + ASAN_UNIT_RUN(test_asan_realloc_overflow); + ASAN_UNIT_RUN(test_asan_boundaries); + ASAN_UNIT_RUN(test_asan_aligned_boundaries); + ASAN_UNIT_RUN(test_asan_realloc_lifecycle); + ASAN_UNIT_RUN(test_asan_calloc_reuse); +#ifdef RT_USING_SEMAPHORE + ASAN_UNIT_RUN(test_asan_concurrent_realloc); +#endif +#ifdef RT_USING_CPLUSPLUS + ASAN_UNIT_RUN(test_asan_cpp); +#endif +#if RT_ASAN_HAS_UAF_DETECTION + ASAN_UNIT_RUN(test_asan_aligned_uaf); + ASAN_UNIT_RUN(test_asan_uaf_read); + ASAN_UNIT_RUN(test_asan_uaf_write); +#endif + uassert_int_equal(failures, 0); +} +#undef ASAN_UNIT_RUN + +UTEST_TC_EXPORT(testcase, "components.asan_tc", utest_tc_init, utest_tc_cleanup, 1000); diff --git a/tools/testcases/test_asan_config.py b/tools/testcases/test_asan_config.py new file mode 100644 index 000000000000..a774a0b8cf35 --- /dev/null +++ b/tools/testcases/test_asan_config.py @@ -0,0 +1,49 @@ +#!/usr/bin/env python3 +# Copyright (c) 2006-2026, RT-Thread Development Team +# SPDX-License-Identifier: Apache-2.0 + +"""Check ASan lock dependencies using the repository's real Kconfig tree. + +Run with: python -m unittest discover -s tools/testcases -p test_asan_config.py +Requires kconfiglib, also used by the configuration tools. +""" + +import itertools +import os +from pathlib import Path +import unittest + +import kconfiglib + + +class AsanConfigTest(unittest.TestCase): + def test_heap_lock_combinations(self): + bsp = Path(__file__).resolve().parents[2] / 'bsp/qemu-vexpress-a9' + previous = Path.cwd() + try: + os.chdir(bsp) + for smp, mutex, isr in itertools.product((0, 2), repeat=3): + with self.subTest(smp=smp, mutex=mutex, isr=isr): + config = kconfiglib.Kconfig('Kconfig', warn=False) + # Start without optional subsystems that select mutexes. + for symbol in config.unique_defined_syms: + if symbol.type in (kconfiglib.BOOL, kconfiglib.TRISTATE): + symbol.set_value(0) + for name in ('RT_USING_SMALL_MEM', + 'RT_USING_SMALL_MEM_AS_HEAP'): + config.syms[name].set_value(2) + for name, value in (('RT_USING_SMP', smp), + ('RT_USING_MUTEX', mutex), + ('RT_USING_HEAP_ISR', isr)): + config.syms[name].set_value(value) + self.assertEqual(config.syms[name].tri_value, value) + config.syms['RT_USING_ASAN'].set_value(2) + expected = 2 if not smp or mutex or isr else 0 + self.assertEqual(config.syms['RT_USING_ASAN'].tri_value, + expected) + finally: + os.chdir(previous) + + +if __name__ == '__main__': + unittest.main()