From 7e4856d9e38814489c75012b7bf4ee95d173ee0d Mon Sep 17 00:00:00 2001 From: hanzhijian Date: Mon, 21 Sep 2026 10:28:17 +0800 Subject: [PATCH 1/3] [kernel] support heap overrides and validate allocation sizes Make heap hook setters, slab page APIs and test heap locks weak so alternate allocators can consistently own their state. Reject calloc multiplication overflow and invalid or overflowing aligned allocations; include aligned allocation regression tests. --- examples/test/mem_align_test.c | 90 ++++++++++++++++++++++++++++++++++ src/kservice.c | 82 +++++++++++++++++++------------ src/utest/Kconfig | 5 ++ src/utest/SConscript | 3 ++ 4 files changed, 148 insertions(+), 32 deletions(-) create mode 100644 examples/test/mem_align_test.c 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/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..7ca25b8e439b 100644 --- a/src/utest/Kconfig +++ b/src/utest/Kconfig @@ -84,6 +84,11 @@ 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 + rsource "perf/Kconfig" rsource "../klibc/utest/Kconfig" diff --git a/src/utest/SConscript b/src/utest/SConscript index 52aca3dcf698..49ec168c4f9c 100644 --- a/src/utest/SConscript +++ b/src/utest/SConscript @@ -58,6 +58,9 @@ 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')] + # Stressful testcase for scheduler (MP/UP) if GetDepend(['RT_UTEST_SCHEDULER']): src += ['sched_timeout_race_tc.c'] From 0d771179ab279e0e0c0902bff6faac1f0140b896 Mon Sep 17 00:00:00 2001 From: hanzhijian Date: Mon, 21 Sep 2026 10:28:17 +0800 Subject: [PATCH 2/3] [components][asan] add heap AddressSanitizer runtime Add GCC kernel-address instrumentation and a component-owned heap adapter using weak API overrides. Track redzones, allocation sizes and shadow memory under allocator synchronization; require cross-CPU heap locking for SMP. Keep allocator/runtime code uninstrumented and document deferred bulk-memory checks and allocator-specific UAF coverage. --- components/utilities/Kconfig | 1 + components/utilities/asan/Kconfig | 77 ++++ components/utilities/asan/SConscript | 23 + components/utilities/asan/asan.c | 630 ++++++++++++++++++++++++++ components/utilities/asan/asan.h | 63 +++ components/utilities/asan/asan_heap.c | 359 +++++++++++++++ src/SConscript | 18 + src/klibc/kstring.c | 15 + 8 files changed, 1186 insertions(+) create mode 100644 components/utilities/asan/Kconfig create mode 100644 components/utilities/asan/SConscript create mode 100644 components/utilities/asan/asan.c create mode 100644 components/utilities/asan/asan.h create mode 100644 components/utilities/asan/asan_heap.c 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..20d1e6930b00 --- /dev/null +++ b/components/utilities/asan/Kconfig @@ -0,0 +1,77 @@ +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. + + Bulk memory helpers rt_memcpy/rt_memset/rt_memmove are excluded from + instrumentation and do not check source or destination ranges yet. + Overflows and use-after-free through these helpers can go undetected, + even when the caller is instrumented. ASan-aware replacements and + range checks are deferred to a later phase. + + 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 storage for internal metadata. + Integration with poisoned freed blocks is not supported yet, + so use-after-free detection is 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..9767e052ca71 --- /dev/null +++ b/components/utilities/asan/asan.h @@ -0,0 +1,63 @@ +/* + * 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 is currently enabled only for the small-mem heap. + * memheap/slab reuse freed storage for allocator metadata; integration with + * poisoned freed blocks is not supported, so their freed blocks stay accessible. + */ +#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/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..dea3341abdc0 100644 --- a/src/klibc/kstring.c +++ b/src/klibc/kstring.c @@ -10,6 +10,18 @@ #include +/* + * AddressSanitizer: bulk memory helpers are not instrumented in this version. + * ASan-aware replacements with explicit source/destination range checks are + * deferred to a later phase. Calls to these helpers can therefore miss heap + * buffer overflows and use-after-free accesses, even from instrumented code. + */ +#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 From 524e4316cdcfdadd8f62402706f98d4cecba829f Mon Sep 17 00:00:00 2001 From: hanzhijian Date: Mon, 21 Sep 2026 10:28:17 +0800 Subject: [PATCH 3/3] [asan] add component tests and QEMU CI coverage Add instrumented C/C++ regression tests for heap boundaries, realloc, alignment, hooks, heap statistics and concurrency. Cover SMP lock dependencies with Kconfig tests and run the ASan suite in QEMU A9 CI. Keep test build flags separate from the runtime exclusions. --- .github/utest/configs/components/asan.cfg | 8 + .github/workflows/utest_auto_run.yml | 2 + components/utilities/asan/SConscript | 3 + components/utilities/asan/utest/Kconfig | 8 + components/utilities/asan/utest/SConscript | 13 + .../utilities/asan/utest/asan_cpp_test.cpp | 25 + components/utilities/asan/utest/asan_tc.c | 624 ++++++++++++++++++ components/utilities/utest/Kconfig | 1 + tools/testcases/test_asan_config.py | 49 ++ 9 files changed, 733 insertions(+) create mode 100644 .github/utest/configs/components/asan.cfg create mode 100644 components/utilities/asan/utest/Kconfig create mode 100644 components/utilities/asan/utest/SConscript create mode 100644 components/utilities/asan/utest/asan_cpp_test.cpp create mode 100644 components/utilities/asan/utest/asan_tc.c create mode 100644 tools/testcases/test_asan_config.py 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/asan/SConscript b/components/utilities/asan/SConscript index 34a69a03c32c..ce4db00278d9 100644 --- a/components/utilities/asan/SConscript +++ b/components/utilities/asan/SConscript @@ -20,4 +20,7 @@ group = DefineGroup('asan', src, depend=['RT_USING_ASAN'], CPPPATH=CPPPATH, CFLAGS=CFLAGS, CXXFLAGS=CFLAGS, LINKFLAGS=LINKFLAGS, LOCAL_CFLAGS=' -fno-sanitize=kernel-address') +# Do not extend in place: DefineGroup retains the runtime source list. +group = group + SConscript('utest/SConscript') + Return('group') diff --git a/components/utilities/asan/utest/Kconfig b/components/utilities/asan/utest/Kconfig new file mode 100644 index 000000000000..6b25c99925b8 --- /dev/null +++ b/components/utilities/asan/utest/Kconfig @@ -0,0 +1,8 @@ +menu "AddressSanitizer" + +config RT_UTEST_ASAN + bool "AddressSanitizer Test" + default n + depends on RT_USING_ASAN + +endmenu diff --git a/components/utilities/asan/utest/SConscript b/components/utilities/asan/utest/SConscript new file mode 100644 index 000000000000..b6a24a3811f5 --- /dev/null +++ b/components/utilities/asan/utest/SConscript @@ -0,0 +1,13 @@ +from building import * + +cwd = GetCurrentDir() +src = ['asan_tc.c'] +if GetDepend('RT_USING_CPLUSPLUS'): + src += ['asan_cpp_test.cpp'] + +# Tests need global instrumentation; do not inherit the runtime's LOCAL_CFLAGS. +group = DefineGroup('asan_utest', src, + depend=['RT_USING_UTESTCASES', 'RT_UTEST_ASAN', 'RT_USING_ASAN'], + CPPPATH=[cwd]) + +Return('group') diff --git a/components/utilities/asan/utest/asan_cpp_test.cpp b/components/utilities/asan/utest/asan_cpp_test.cpp new file mode 100644 index 000000000000..e4925eb58505 --- /dev/null +++ b/components/utilities/asan/utest/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/components/utilities/asan/utest/asan_tc.c b/components/utilities/asan/utest/asan_tc.c new file mode 100644 index 000000000000..43bb6738a73e --- /dev/null +++ b/components/utilities/asan/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/components/utilities/utest/Kconfig b/components/utilities/utest/Kconfig index f2bb5f6f8879..a75a8833115b 100644 --- a/components/utilities/utest/Kconfig +++ b/components/utilities/utest/Kconfig @@ -29,6 +29,7 @@ menu "RT-Thread Utestcases" rsource "../../../components/lwp/utest/Kconfig" rsource "../../../components/mm/utest/Kconfig" rsource "../../../components/net/utest/Kconfig" + rsource "../asan/utest/Kconfig" rsource "../../../components/utilities/utest/utest/Kconfig" endmenu 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()