From 7e4856d9e38814489c75012b7bf4ee95d173ee0d Mon Sep 17 00:00:00 2001 From: hanzhijian Date: Mon, 21 Sep 2026 10:28:17 +0800 Subject: [PATCH 1/4] [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/4] [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/4] [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() From e530e17dd2e6a85aafb089fff67cbb6fba218fb0 Mon Sep 17 00:00:00 2001 From: hanzhijian Date: Tue, 13 Oct 2026 15:25:24 +0800 Subject: [PATCH 4/4] [asan] check libc accesses and cover boundary cases Wrap common libc memory, string and formatting calls with explicit heap checks. Preserve native formatting behavior, validate string and writeback arguments, and limit output to known heap bounds. Add boundary and compatibility tests, fix RT string precision handling, and preserve structured SCons compiler and linker arguments. Validation: 27 ARM build/configuration combinations, local CI configuration, 9 Python tests, native LP64 UBSan checks, and negative controls. --- .github/utest/configs/components/asan.cfg | 2 + components/utilities/asan/Kconfig | 47 +- components/utilities/asan/SConscript | 11 + components/utilities/asan/asan.c | 102 +- components/utilities/asan/asan.h | 15 + components/utilities/asan/asan_libc.c | 529 +++++++ components/utilities/asan/utest/Kconfig | 10 + components/utilities/asan/utest/SConscript | 2 + .../utilities/asan/utest/asan_cpp_test.cpp | 25 + .../utilities/asan/utest/asan_libc_tc.c | 1265 +++++++++++++++++ src/klibc/kstdio.c | 4 + src/klibc/kstring.c | 21 +- src/klibc/rt_vsnprintf_std.c | 24 +- src/klibc/rt_vsnprintf_tiny.c | 11 +- src/klibc/utest/TC_rt_sprintf.c | 7 + tools/building.py | 26 +- tools/testcases/test_asan_build.py | 172 +++ tools/testcases/test_asan_config.py | 21 + tools/testcases/test_asan_libc_host.py | 261 ++++ 19 files changed, 2513 insertions(+), 42 deletions(-) create mode 100644 components/utilities/asan/asan_libc.c create mode 100644 components/utilities/asan/utest/asan_libc_tc.c create mode 100644 tools/testcases/test_asan_build.py create mode 100644 tools/testcases/test_asan_libc_host.py diff --git a/.github/utest/configs/components/asan.cfg b/.github/utest/configs/components/asan.cfg index 751ef66bc9b4..a3c7bd0a404d 100644 --- a/.github/utest/configs/components/asan.cfg +++ b/.github/utest/configs/components/asan.cfg @@ -3,6 +3,8 @@ CONFIG_RT_CONSOLEBUF_SIZE=1024 CONFIG_RT_USING_CI_ACTION=y CONFIG_RT_USING_ASAN=y +CONFIG_RT_ASAN_USING_LIBC=y CONFIG_RT_ASAN_SHADOW_SIZE=65536 CONFIG_RT_ASAN_TRACK_MAX=512 CONFIG_RT_UTEST_ASAN=y +CONFIG_RT_UTEST_ASAN_LIBC=y diff --git a/components/utilities/asan/Kconfig b/components/utilities/asan/Kconfig index 20d1e6930b00..21c33d8472cc 100644 --- a/components/utilities/asan/Kconfig +++ b/components/utilities/asan/Kconfig @@ -20,14 +20,15 @@ menuconfig RT_USING_ASAN 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. + Only the system heap APIs are wrapped. Direct allocator/page APIs and + accesses in prebuilt libraries outside the optional libc wrappers + 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. + Built-in rt_memcpy/rt_memset/rt_memmove implementations explicitly + check the requested source and destination ranges, including when + configured to call libc. User-defined replacements must provide + their own checks. Enable the libc wrappers to check additional + standard memory, string and formatting entry points. The component overrides the weak system heap interfaces and owns the underlying allocator, heap lock and allocation hooks. Do not @@ -47,6 +48,38 @@ menuconfig RT_USING_ASAN - userheap (RT_USING_USERHEAP): not supported (mutually exclusive). if RT_USING_ASAN + config RT_ASAN_USING_LIBC + bool "Check common libc memory, string and formatting calls" + default n + help + Requires GCC and GNU ld --wrap. Wrap memcpy, memmove, memset, + strlen, strnlen, strcpy, strncpy, strcat, strncat, sprintf, + vsprintf, snprintf and vsnprintf. Disable the corresponding + compiler builtins in both C and C++ compilation. + + String sources and non-positional printf arguments (%s and + %n, with standard integer/floating-point argument types) are + checked before calling libc. Keep libc's formatting behavior + and execute %n only once. A known poisoned heap boundary + limits output; attempted overflows are reported, even when + the supplied snprintf limit exceeds the allocation. + + Invalid memory/string inputs skip the operation after a + report. Invalid formatting inputs return -1; heap output + overflows are truncated and reported. The program continues. + + Positional arguments, wide strings and unknown format + extensions (including formats disabled in Newlib's headers) + retain libc behavior but their argument pointers + are not fully checked. Format-string and known destination + bounds are still checked. Preflight checks cannot detect + accesses caused by concurrent or %n-induced source mutation. + + --wrap cannot replace calls resolved inside the same object, + private/reentrant/fortified libc entry points, explicit + __builtin calls or dynamic modules linked separately. It + does not add stack/global coverage or extend heap coverage. + config RT_ASAN_SHADOW_SIZE int "ASan shadow memory size (bytes)" default 65536 diff --git a/components/utilities/asan/SConscript b/components/utilities/asan/SConscript index ce4db00278d9..a41d74e06789 100644 --- a/components/utilities/asan/SConscript +++ b/components/utilities/asan/SConscript @@ -3,15 +3,26 @@ Import('rtconfig') cwd = GetCurrentDir() src = Glob('*.c') +if not GetDepend('RT_ASAN_USING_LIBC'): + SrcRemove(src, ['asan_libc.c']) CPPPATH = [cwd] CFLAGS = '' LINKFLAGS = '' +if GetDepend('RT_ASAN_USING_LIBC') and rtconfig.PLATFORM != 'gcc': + raise RuntimeError('ASan libc wrappers require GCC and GNU ld --wrap') + # 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' + if GetDepend('RT_ASAN_USING_LIBC'): + libc_functions = ['memcpy', 'memmove', 'memset', 'strlen', 'strnlen', + 'strcpy', 'strncpy', 'strcat', 'strncat', + 'sprintf', 'vsprintf', 'snprintf', 'vsnprintf'] + CFLAGS += ''.join(' -fno-builtin-' + name for name in libc_functions) + LINKFLAGS += ''.join(' -Wl,--wrap=' + name for name in libc_functions) # The ASan runtime itself must not be instrumented, otherwise it would # recurse infinitely. '-fno-sanitize=kernel-address' is appended after the diff --git a/components/utilities/asan/asan.c b/components/utilities/asan/asan.c index f13559a256b4..d14e08591881 100644 --- a/components/utilities/asan/asan.c +++ b/components/utilities/asan/asan.c @@ -94,6 +94,22 @@ 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; +/* A compiler can lower struct assignments to libc memcpy. Once libc is + * wrapped, copying a snapshot to a heap-allocated thread stack would re-enter + * the shadow lock. Volatile byte copies prevent that lowering under the lock. + */ +static void asan_track_copy(struct asan_track *dst, const struct asan_track *src) +{ + volatile rt_uint8_t *out = (volatile rt_uint8_t *)dst; + const volatile rt_uint8_t *in = (const volatile rt_uint8_t *)src; + rt_size_t i; + + for (i = 0; i < sizeof(*dst); i++) + { + out[i] = in[i]; + } +} + /* ---- helpers ---- */ rt_inline rt_bool_t asan_addr_in_range(rt_uintptr_t addr) { @@ -155,7 +171,7 @@ static void asan_locate_block(rt_uintptr_t addr) if (asan_last_freed.used && addr >= asan_last_freed.ptr && addr - asan_last_freed.ptr < asan_last_freed.size) { - block = asan_last_freed; + asan_track_copy(&block, &asan_last_freed); kind = "USE-AFTER-FREE"; freed = RT_TRUE; } @@ -167,7 +183,7 @@ static void asan_locate_block(rt_uintptr_t addr) } if (addr >= asan_tracks[i].ptr && addr - asan_tracks[i].ptr < asan_tracks[i].size) { - block = asan_tracks[i]; + asan_track_copy(&block, &asan_tracks[i]); kind = "inside block"; break; } @@ -177,7 +193,7 @@ static void asan_locate_block(rt_uintptr_t addr) if (end <= addr && end >= best_end) { best_end = end; - block = asan_tracks[i]; + asan_track_copy(&block, &asan_tracks[i]); } } } @@ -219,6 +235,72 @@ static void asan_report(rt_uintptr_t addr, rt_size_t size, rt_bool_t is_write, r rt_kprintf("=================================================================\n"); } +/* ---- explicit library access checks ---- */ +#ifdef RT_ASAN_USING_LIBC +rt_bool_t rt_asan_accessible_size(const void *addr, rt_size_t *size) +{ + rt_uintptr_t current = (rt_uintptr_t)addr; + rt_uintptr_t off; + rt_size_t count = 0; + rt_uint8_t shadow; + rt_size_t offset; + rt_base_t level; + + if (!asan_addr_in_range(current)) + { + return RT_FALSE; + } + level = asan_lock(); + while (current < asan_heap_limit) + { + off = current - asan_heap_base; + offset = off & (ASAN_SHADOW_SCALE - 1); + shadow = asan_shadow[off >> 3]; + if (shadow) + { + if (shadow < ASAN_SHADOW_SCALE && offset < shadow) + { + if (shadow - offset >= asan_heap_limit - current) + { + break; + } + count += shadow - offset; + } + *size = count; + asan_unlock(level); + return RT_TRUE; + } + offset = ASAN_MIN(ASAN_SHADOW_SCALE - offset, asan_heap_limit - current); + count += offset; + current += offset; + } + asan_unlock(level); + return RT_FALSE; +} +#endif + +rt_bool_t rt_asan_check_read(const void *addr, rt_size_t size) +{ + if (asan_range_is_poisoned((rt_uintptr_t)addr, size)) + { + asan_report((rt_uintptr_t)addr, size, RT_FALSE, + (rt_uintptr_t)__builtin_return_address(0)); + return RT_FALSE; + } + return RT_TRUE; +} + +rt_bool_t rt_asan_check_write(void *addr, rt_size_t size) +{ + if (asan_range_is_poisoned((rt_uintptr_t)addr, size)) + { + asan_report((rt_uintptr_t)addr, size, RT_TRUE, + (rt_uintptr_t)__builtin_return_address(0)); + return RT_FALSE; + } + return RT_TRUE; +} + /* ---- instrumented access checks ---- */ #define ASAN_DEFINE_CHECK(_size, _suffix) \ void __asan_load##_suffix##_noabort(rt_uintptr_t addr) \ @@ -367,15 +449,17 @@ static void asan_track_add(rt_uintptr_t ptr, rt_size_t size, rt_thread_t self) asan_tracks[i].used = 1; { rt_size_t n = 0; + volatile char *owner = asan_tracks[i].owner; if (self) { - while (n < RT_NAME_MAX - 1 && self->parent.name[n]) + const volatile char *name = self->parent.name; + while (n < RT_NAME_MAX - 1 && name[n]) { - asan_tracks[i].owner[n] = self->parent.name[n]; + owner[n] = name[n]; n++; } } - asan_tracks[i].owner[n] = '\0'; + owner[n] = '\0'; } return; } @@ -492,7 +576,7 @@ void rt_asan_free(void *ptr, void (*release)(void *)) idx = asan_track_find(p); if (idx != RT_ASAN_TRACK_MAX) { - asan_last_freed = asan_tracks[idx]; + asan_track_copy(&asan_last_freed, &asan_tracks[idx]); asan_tracks[idx].used = 0; } else @@ -597,7 +681,7 @@ static int asan_info(int argc, char **argv) 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_track_copy(&block, &asan_last_freed); asan_unlock(level); if (block.used) { @@ -612,7 +696,7 @@ static int asan_info(int argc, char **argv) for (i = 0; i < RT_ASAN_TRACK_MAX; i++) { level = asan_lock(); - block = asan_tracks[i]; + asan_track_copy(&block, &asan_tracks[i]); asan_unlock(level); if (block.used) { diff --git a/components/utilities/asan/asan.h b/components/utilities/asan/asan.h index 9767e052ca71..ccd6fdc64246 100644 --- a/components/utilities/asan/asan.h +++ b/components/utilities/asan/asan.h @@ -37,6 +37,21 @@ rt_bool_t rt_asan_test_hooks_in_use(void); void rt_asan_heap_init(void *begin_addr, void *end_addr); +/* Check library accesses against the covered system-heap shadow. Return + * RT_FALSE after reporting poison; RT_TRUE means no covered poison was found, + * not that memory outside the coverage is valid. These checks do not abort. + */ +rt_bool_t rt_asan_check_read(const void *addr, rt_size_t size); +rt_bool_t rt_asan_check_write(void *addr, rt_size_t size); + +#ifdef RT_ASAN_USING_LIBC +/* Return RT_TRUE only when the covered shadow contains a poisoned boundary + * at/after addr. *size is the readable/writable prefix before that boundary. + * RT_FALSE means the boundary is unknown, including coverage ending first. + */ +rt_bool_t rt_asan_accessible_size(const void *addr, rt_size_t *size); +#endif + /* Internal system-heap integration. The caller must hold the heap lock. * Callbacks operate on the underlying allocator, never rt_malloc/rt_free. */ diff --git a/components/utilities/asan/asan_libc.c b/components/utilities/asan/asan_libc.c new file mode 100644 index 000000000000..3d93c59302da --- /dev/null +++ b/components/utilities/asan/asan_libc.c @@ -0,0 +1,529 @@ +/* + * Copyright (c) 2006-2026, RT-Thread Development Team + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include "asan.h" +#include +#include +#include +#include +#ifdef __NEWLIB__ +#include +#endif + +/* GNU ld redirects unresolved libc references here. This file belongs to the + * non-instrumented runtime group: all checks below are explicit. Never call a + * wrapped entry from its own wrapper; __real_* bypasses the redirection. + */ +void *__real_memcpy(void *dst, const void *src, size_t size); +void *__real_memmove(void *dst, const void *src, size_t size); +void *__real_memset(void *dst, int value, size_t size); +char *__real_strcpy(char *dst, const char *src); +char *__real_strncpy(char *dst, const char *src, size_t size); +char *__real_strcat(char *dst, const char *src); +char *__real_strncat(char *dst, const char *src, size_t size); +int __real_vsprintf(char *dst, const char *fmt, va_list args); +int __real_vsnprintf(char *dst, size_t size, const char *fmt, va_list args); + +static rt_bool_t check_string(const char *src, size_t limit, size_t *length) +{ + rt_size_t available = 0; + rt_bool_t bounded = RT_FALSE; + size_t i = 0; + + if (limit) + { + bounded = rt_asan_accessible_size(src, &available); + } + while (i < limit) + { + if (bounded && i >= available) + { + *length = i; + rt_asan_check_read(src + i, 1); + return RT_FALSE; + } + if (!src[i]) + { + break; + } + i++; + } + *length = i; + return RT_TRUE; +} + +void *__wrap_memcpy(void *dst, const void *src, size_t size) +{ + rt_bool_t readable = rt_asan_check_read(src, size); + rt_bool_t writable = rt_asan_check_write(dst, size); + return readable && writable ? __real_memcpy(dst, src, size) : dst; +} + +void *__wrap_memmove(void *dst, const void *src, size_t size) +{ + rt_bool_t readable = rt_asan_check_read(src, size); + rt_bool_t writable = rt_asan_check_write(dst, size); + return readable && writable ? __real_memmove(dst, src, size) : dst; +} + +void *__wrap_memset(void *dst, int value, size_t size) +{ + return rt_asan_check_write(dst, size) ? __real_memset(dst, value, size) : dst; +} + +size_t __wrap_strlen(const char *src) +{ + size_t length; + check_string(src, (size_t)-1, &length); + return length; +} + +size_t __wrap_strnlen(const char *src, size_t size) +{ + size_t length; + check_string(src, size, &length); + return length; +} + +char *__wrap_strcpy(char *dst, const char *src) +{ + size_t length; + if (!check_string(src, (size_t)-1, &length) || !rt_asan_check_write(dst, length + 1)) + { + return dst; + } + return __real_strcpy(dst, src); +} + +char *__wrap_strncpy(char *dst, const char *src, size_t size) +{ + size_t length; + rt_bool_t readable = check_string(src, size, &length); + rt_bool_t writable = rt_asan_check_write(dst, size); + return readable && writable ? __real_strncpy(dst, src, size) : dst; +} + +char *__wrap_strcat(char *dst, const char *src) +{ + size_t prefix, length; + if (!check_string(dst, (size_t)-1, &prefix) || + !check_string(src, (size_t)-1, &length) || + !rt_asan_check_write(dst + prefix, length + 1)) + { + return dst; + } + return __real_strcat(dst, src); +} + +char *__wrap_strncat(char *dst, const char *src, size_t size) +{ + size_t prefix, length; + if (!check_string(dst, (size_t)-1, &prefix) || + !check_string(src, size, &length) || + !rt_asan_check_write(dst + prefix, length + 1)) + { + return dst; + } + return __real_strncat(dst, src, size); +} + +enum format_length +{ + LENGTH_DEFAULT, + LENGTH_HH, + LENGTH_H, + LENGTH_L, + LENGTH_LL, + LENGTH_J, + LENGTH_Z, + LENGTH_T, + LENGTH_CAPITAL_L +}; + +/* The format is checked before parsing, so its characters are readable. + * Unsupported extensions stop argument inspection instead of guessing their + * va_arg types. The original libc still formats them; destination protection + * does not require knowing the conversion syntax. + */ +static rt_bool_t check_format_arguments(const char *fmt, va_list args) +{ + size_t ignored; + const char *scan; + if (!check_string(fmt, (size_t)-1, &ignored)) + { + return RT_FALSE; + } + /* Find positional conversions before consuming any argument, including + * *N$ widths/precisions. Literal dollar signs and %% do not opt out. + */ + for (scan = fmt; *scan;) + { + if (*scan++ == '%') + { + if (*scan == '%') + { + scan++; + continue; + } + while (*scan && strchr("-+ #0'.*0123456789hljztL", *scan)) + { + scan++; + } + if (*scan == '$') + { + return RT_TRUE; + } + } + } + while (*fmt) + { + enum format_length length = LENGTH_DEFAULT; + int precision = -1; + if (*fmt++ != '%') + { + continue; + } + while (*fmt && strchr("-+ #0'", *fmt)) + { + fmt++; + } + if (*fmt == '*') + { + fmt++; + if (*fmt == '$' || (*fmt >= '0' && *fmt <= '9')) + { + return RT_TRUE; + } + (void)va_arg(args, int); + } + else + { + while (*fmt >= '0' && *fmt <= '9') + { + fmt++; + } + } + if (*fmt == '$' || (*fmt >= '0' && *fmt <= '9')) + { + return RT_TRUE; + } + if (*fmt == '.') + { + fmt++; + precision = 0; + if (*fmt == '*') + { + fmt++; + if (*fmt == '$' || (*fmt >= '0' && *fmt <= '9')) + { + return RT_TRUE; + } + precision = va_arg(args, int); + } + else + { + while (*fmt >= '0' && *fmt <= '9') + { + int digit = *fmt++ - '0'; + precision = precision > (INT_MAX - digit) / 10 ? INT_MAX : precision * 10 + digit; + } + } + } + switch (*fmt) + { + case 'h': + length = *++fmt == 'h' ? (fmt++, LENGTH_HH) : LENGTH_H; + break; + case 'l': + length = *++fmt == 'l' ? (fmt++, LENGTH_LL) : LENGTH_L; + break; + case 'j': + length = LENGTH_J; + fmt++; + break; + case 'z': + length = LENGTH_Z; + fmt++; + break; + case 't': + length = LENGTH_T; + fmt++; + break; + case 'L': + length = LENGTH_CAPITAL_L; + fmt++; + break; + default: + break; + } +#if defined(__NEWLIB__) && !defined(_WANT_IO_C99_FORMATS) + if (length == LENGTH_HH || length == LENGTH_J || length == LENGTH_Z || length == LENGTH_T || + *fmt == 'a' || *fmt == 'A') + { + return RT_TRUE; + } +#endif +#if defined(__NEWLIB__) && !defined(_WANT_IO_LONG_LONG) + if (length == LENGTH_LL) + { + return RT_TRUE; + } +#endif +#if defined(__NEWLIB__) && !defined(_WANT_IO_LONG_DOUBLE) + if (length == LENGTH_CAPITAL_L) + { + return RT_TRUE; + } +#endif + switch (*fmt) + { + case 'd': + case 'i': + switch (length) + { + case LENGTH_L: + (void)va_arg(args, long); + break; + case LENGTH_LL: + (void)va_arg(args, long long); + break; + case LENGTH_J: + (void)va_arg(args, intmax_t); + break; + case LENGTH_Z: + case LENGTH_T: + (void)va_arg(args, ptrdiff_t); + break; + case LENGTH_DEFAULT: + case LENGTH_H: + case LENGTH_HH: + (void)va_arg(args, int); + break; + default: + return RT_TRUE; + } + break; + case 'u': + case 'o': + case 'x': + case 'X': + switch (length) + { + case LENGTH_L: + (void)va_arg(args, unsigned long); + break; + case LENGTH_LL: + (void)va_arg(args, unsigned long long); + break; + case LENGTH_J: + (void)va_arg(args, uintmax_t); + break; + case LENGTH_Z: + case LENGTH_T: + (void)va_arg(args, size_t); + break; + case LENGTH_DEFAULT: + case LENGTH_H: + case LENGTH_HH: + (void)va_arg(args, unsigned int); + break; + default: + return RT_TRUE; + } + break; + case 'a': + case 'A': + case 'e': + case 'E': + case 'f': + case 'F': + case 'g': + case 'G': + if (length == LENGTH_CAPITAL_L) + { + (void)va_arg(args, long double); + } + else if (length == LENGTH_DEFAULT || length == LENGTH_L) + { + (void)va_arg(args, double); + } + else + { + return RT_TRUE; + } + break; + case 'c': + if (length != LENGTH_DEFAULT) + { + return RT_TRUE; + } + (void)va_arg(args, int); + break; + case 'p': + if (length != LENGTH_DEFAULT) + { + return RT_TRUE; + } + (void)va_arg(args, void *); + break; + case 's': + { + const char *src; + if (length != LENGTH_DEFAULT) + { + return RT_TRUE; + } + src = va_arg(args, const char *); + /* Preserve libc's handling of a NULL %s argument. */ + if (src && !check_string(src, precision < 0 ? (size_t)-1 : (size_t)precision, &ignored)) + { + return RT_FALSE; + } + break; + } + case 'n': + { + void *dst; + size_t size; + switch (length) + { + case LENGTH_HH: + dst = va_arg(args, signed char *); + size = sizeof(signed char); + break; + case LENGTH_H: + dst = va_arg(args, short *); + size = sizeof(short); + break; + case LENGTH_L: + dst = va_arg(args, long *); + size = sizeof(long); + break; + case LENGTH_LL: + dst = va_arg(args, long long *); + size = sizeof(long long); + break; + case LENGTH_J: + dst = va_arg(args, intmax_t *); + size = sizeof(intmax_t); + break; + case LENGTH_Z: + case LENGTH_T: + dst = va_arg(args, ptrdiff_t *); + size = sizeof(ptrdiff_t); + break; + case LENGTH_DEFAULT: + dst = va_arg(args, int *); + size = sizeof(int); + break; + default: + return RT_TRUE; + } + if (!rt_asan_check_write(dst, size)) + { + return RT_FALSE; + } + break; + } + case '%': + if (length != LENGTH_DEFAULT) + { + return RT_TRUE; + } + break; + default: + return RT_TRUE; + } + fmt++; + } + return RT_TRUE; +} + +static int checked_format(char *dst, size_t size, const char *fmt, va_list args, rt_bool_t unbounded) +{ + va_list probe; + rt_size_t available = 0; + rt_bool_t bounded = RT_FALSE; + rt_bool_t valid; + size_t limit = size; + int result; + +#ifdef __NEWLIB__ + /* Newlib rejects this limit before reading arguments or writing output. + * Clamping it first would turn EOVERFLOW into a successful call. + */ + if (!unbounded && size > INT_MAX) + { + return __real_vsnprintf(dst, size, fmt, args); + } +#endif + va_copy(probe, args); + valid = check_format_arguments(fmt, probe); + va_end(probe); + if (!valid) + { + return -1; + } + if (size) + { + bounded = rt_asan_accessible_size(dst, &available); + if (bounded && !available) + { + rt_asan_check_write(dst, 1); + return -1; + } + if (bounded && limit > available) + { + limit = available; + } + } + /* No sizing pass: it would execute %n twice and could change subsequent + * source data. Clamp output to known heap storage, then report the bytes + * libc would have written under the original limit. Valid calls retain + * libc's return value, truncation rules and format extensions. + */ + result = unbounded && !bounded ? __real_vsprintf(dst, fmt, args) : __real_vsnprintf(dst, limit, fmt, args); + if (result >= 0 && size) + { + size_t written = (size_t)result + 1; + if (written > size) + { + written = size; + } + rt_asan_check_write(dst, written); + } + return result; +} + +int __wrap_vsprintf(char *dst, const char *fmt, va_list args) +{ + return checked_format(dst, (size_t)-1, fmt, args, RT_TRUE); +} + +int __wrap_vsnprintf(char *dst, size_t size, const char *fmt, va_list args) +{ + return checked_format(dst, size, fmt, args, RT_FALSE); +} + +int __wrap_sprintf(char *dst, const char *fmt, ...) +{ + int result; + va_list args; + va_start(args, fmt); + result = checked_format(dst, (size_t)-1, fmt, args, RT_TRUE); + va_end(args); + return result; +} + +int __wrap_snprintf(char *dst, size_t size, const char *fmt, ...) +{ + int result; + va_list args; + va_start(args, fmt); + result = checked_format(dst, size, fmt, args, RT_FALSE); + va_end(args); + return result; +} diff --git a/components/utilities/asan/utest/Kconfig b/components/utilities/asan/utest/Kconfig index 6b25c99925b8..67d84640ff14 100644 --- a/components/utilities/asan/utest/Kconfig +++ b/components/utilities/asan/utest/Kconfig @@ -5,4 +5,14 @@ config RT_UTEST_ASAN default n depends on RT_USING_ASAN +config RT_UTEST_ASAN_LIBC + bool "ASan string and formatting boundary tests" + default n + depends on RT_UTEST_ASAN + help + Verify library accesses to the instrumented system heap. Known + coverage gaps are printed as observations, rather than required + behavior. Valid operations must not report violations. This suite + can expose existing library bugs and is enabled separately. + endmenu diff --git a/components/utilities/asan/utest/SConscript b/components/utilities/asan/utest/SConscript index b6a24a3811f5..b7e9e106475f 100644 --- a/components/utilities/asan/utest/SConscript +++ b/components/utilities/asan/utest/SConscript @@ -2,6 +2,8 @@ from building import * cwd = GetCurrentDir() src = ['asan_tc.c'] +if GetDepend('RT_UTEST_ASAN_LIBC'): + src += ['asan_libc_tc.c'] if GetDepend('RT_USING_CPLUSPLUS'): src += ['asan_cpp_test.cpp'] diff --git a/components/utilities/asan/utest/asan_cpp_test.cpp b/components/utilities/asan/utest/asan_cpp_test.cpp index e4925eb58505..4bc7680b2597 100644 --- a/components/utilities/asan/utest/asan_cpp_test.cpp +++ b/components/utilities/asan/utest/asan_cpp_test.cpp @@ -7,6 +7,10 @@ #include #include "asan.h" #include "utest.h" +#ifdef RT_ASAN_USING_LIBC +#include +#include +#endif extern "C" void test_asan_cpp(void) { @@ -23,3 +27,24 @@ extern "C" void test_asan_cpp(void) uassert_true(rt_asan_report_count_get() > before); rt_free(const_cast(p)); } + +#ifdef RT_ASAN_USING_LIBC +extern "C" void test_asan_libc_cpp(void) +{ + char *p = static_cast(rt_malloc(10)); + uassert_not_null(p); + if (!p) + { + return; + } + rt_uint32_t before = rt_asan_report_count_get(); + std::memcpy(p, "123456789", 10); + uassert_int_equal(rt_asan_report_count_get(), before); + std::strcpy(p, "1234567890"); + uassert_true(rt_asan_report_count_get() > before); + before = rt_asan_report_count_get(); + std::sprintf(p, "%s", "1234567890"); + uassert_true(rt_asan_report_count_get() > before); + rt_free(p); +} +#endif diff --git a/components/utilities/asan/utest/asan_libc_tc.c b/components/utilities/asan/utest/asan_libc_tc.c new file mode 100644 index 000000000000..c858ecb4f9a0 --- /dev/null +++ b/components/utilities/asan/utest/asan_libc_tc.c @@ -0,0 +1,1265 @@ +/* + * Copyright (c) 2006-2026, RT-Thread Development Team + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include +#include +#include +#include +#include +#include +#ifdef __NEWLIB__ +#include +#endif +#include "asan.h" +#include "utest.h" + +/* Volatile function pointers exercise library symbols, including optimized + * builds, rather than constant-folded builtins in the test itself. + */ +static char *(* volatile copy_string)(char *, const char *) = rt_strcpy; +static char *(* volatile copy_nstring)(char *, const char *, size_t) = rt_strncpy; +static size_t (* volatile string_length)(const char *) = rt_strlen; +static size_t (* volatile bounded_length)(const char *, size_t) = rt_strnlen; +static int (* volatile format_string)(char *, const char *, ...) = rt_sprintf; +static int (* volatile format_nstring)(char *, size_t, const char *, ...) = rt_snprintf; +static int (* volatile format_va)(char *, const char *, va_list) = rt_vsprintf; +static int (* volatile format_nva)(char *, size_t, const char *, va_list) = rt_vsnprintf; +static char *(* volatile libc_copy)(char *, const char *) = strcpy; +static int (* volatile libc_format)(char *, const char *, ...) = sprintf; +static void *(* volatile copy_memory)(void *, const void *, size_t) = rt_memcpy; +static void *(* volatile move_memory)(void *, const void *, size_t) = rt_memmove; +static void *(* volatile set_memory)(void *, int, size_t) = rt_memset; +#ifdef RT_ASAN_USING_LIBC +/* strnlen is hidden by some toolchains' POSIX feature profiles. */ +size_t strnlen(const char *src, size_t size); +int __real_vsnprintf(char *dst, size_t size, const char *fmt, va_list args); +#ifdef RT_USING_CPLUSPLUS +void test_asan_libc_cpp(void); +#endif +#endif +static rt_bool_t libc_path; + +enum library_operation +{ + COPY_EXACT, + COPY_TERMINATOR_OVERFLOW, + COPY_SOURCE_OVERREAD, + COPY_UAF_SOURCE, + NCOPY_PAD, + NCOPY_OVERFLOW, + NCOPY_BOUNDED, + NCOPY_ZERO, + LENGTH_SOURCE_OVERREAD, + LENGTH_BOUNDED, + LENGTH_ZERO, + FORMAT_EXACT, + FORMAT_OVERFLOW, + FORMAT_VA_OVERFLOW, + FORMAT_UAF_SOURCE, + FORMAT_UAF_DESTINATION, + FORMAT_SOURCE_OVERREAD, + FORMAT_FORMAT_OVERREAD, + NFORMAT_TRUNCATE, + NFORMAT_LARGE_LIMIT, + NFORMAT_OVERFLOW, + NFORMAT_VA_OVERFLOW, + NFORMAT_ZERO, + NFORMAT_PRECISION, + NFORMAT_PRECISION_EXACT, + NFORMAT_PRECISION_ZERO, + NFORMAT_PRECISION_NEGATIVE, + NFORMAT_PRECISION_BARE, + NFORMAT_WIDTH, + NFORMAT_WIDTH_PRECISION, + NFORMAT_WRITEBACK, + LIBC_COPY_OVERFLOW, + LIBC_FORMAT_EXACT, + LIBC_FORMAT_OVERFLOW, + MEMORY_COPY_WRITE, + MEMORY_COPY_READ, + MEMORY_MOVE_WRITE, + MEMORY_MOVE_READ, + MEMORY_SET_WRITE, + MEMORY_UAF_READ, + MEMORY_UAF_WRITE, + MEMORY_ZERO, + MEMORY_EXACT, + MEMORY_OVERLAP, + RANGE_CHECKS +}; + +enum expectation +{ + OBSERVE, + CLEAN, + DETECT +}; + +/* Only the setup bypasses ASan: put a terminator in the first redzone byte + * so deliberately unterminated sources stop after one invalid byte. The + * allocator reserves this redzone; no allocator metadata is overwritten. + */ +static __attribute__((no_sanitize_address)) void prepare_source(char *src) +{ + size_t i; + for (i = 0; i < 10; i++) + { + src[i] = 'a'; + } + src[10] = '\0'; +} + +#ifdef RT_ASAN_USING_LIBC +/* The allocator reserves at least eight right-redzone bytes. Only these + * fixture helpers access them; normal test accesses remain instrumented. + */ +static __attribute__((no_sanitize_address)) void set_redzone_byte(char *ptr, unsigned char value) +{ + *(volatile unsigned char *)ptr = value; +} + +static __attribute__((no_sanitize_address)) unsigned char get_redzone_byte(const char *ptr) +{ + return *(const volatile unsigned char *)ptr; +} +#endif + +static int call_vsprintf(char *dst, const char *fmt, ...) +{ + int result; + va_list args; + va_start(args, fmt); + result = format_va(dst, fmt, args); + va_end(args); + return result; +} + +static int call_vsnprintf(char *dst, size_t size, const char *fmt, ...) +{ + int result; + va_list args; + va_start(args, fmt); + result = format_nva(dst, size, fmt, args); + va_end(args); + return result; +} + +static void check_operation(enum library_operation op, const char *name, enum expectation expected) +{ + char *src = rt_malloc(10); + /* Keep the destination valid when isolating an unterminated source. */ + char *dst = rt_malloc(op == COPY_SOURCE_OVERREAD ? 32 : 10); + rt_uint32_t before, reports; + rt_bool_t src_freed = RT_FALSE, dst_freed = RT_FALSE; + int result = 0; + + uassert_not_null(src); + uassert_not_null(dst); + if (!src || !dst) + { + rt_free(src); + rt_free(dst); + return; + } + prepare_source(src); + set_memory(dst, 0, op == COPY_SOURCE_OVERREAD ? 32 : 10); + before = rt_asan_report_count_get(); + switch (op) + { + case COPY_EXACT: + src[9] = '\0'; + copy_string(dst, src); + break; + case COPY_TERMINATOR_OVERFLOW: + copy_string(dst, "1234567890"); + break; + case COPY_SOURCE_OVERREAD: + copy_string(dst, src); + break; + case COPY_UAF_SOURCE: + src[9] = '\0'; + rt_free(src); + src_freed = RT_TRUE; + copy_string(dst, src); + break; + case NCOPY_PAD: + copy_nstring(dst, "a", 10); + break; + case NCOPY_OVERFLOW: + copy_nstring(dst, "a", 11); + break; + case NCOPY_BOUNDED: + copy_nstring(dst, src, 10); + break; + case NCOPY_ZERO: + copy_nstring(dst + 10, src + 10, 0); + break; + case LENGTH_SOURCE_OVERREAD: + string_length(src); + break; + case LENGTH_BOUNDED: + uassert_int_equal(bounded_length(src, 10), 10); + break; + case LENGTH_ZERO: + uassert_int_equal(bounded_length(src + 10, 0), 0); + break; + case FORMAT_EXACT: + result = format_string(dst, "%s", "123456789"); + break; + case FORMAT_OVERFLOW: + format_string(dst, "%s", "1234567890"); + break; + case FORMAT_VA_OVERFLOW: + call_vsprintf(dst, "%s", "1234567890"); + break; + case FORMAT_UAF_SOURCE: + src[9] = '\0'; + rt_free(src); + src_freed = RT_TRUE; + format_nstring(dst, 10, "%s", src); + break; + case FORMAT_UAF_DESTINATION: + rt_free(dst); + dst_freed = RT_TRUE; + format_string(dst, "%s", "a"); + break; + case FORMAT_SOURCE_OVERREAD: + format_nstring(dst, 10, "%s", src); + break; + case FORMAT_FORMAT_OVERREAD: + format_nstring(dst, 10, src); + break; + case NFORMAT_TRUNCATE: + format_nstring(dst, 10, "%s", "123456789012"); + break; + case NFORMAT_LARGE_LIMIT: + format_nstring(dst, 100, "%s", "a"); + break; + case NFORMAT_OVERFLOW: + format_nstring(dst, 11, "%s", "1234567890"); + break; + case NFORMAT_VA_OVERFLOW: + call_vsnprintf(dst, 11, "%s", "1234567890"); + break; + case NFORMAT_ZERO: + format_nstring(dst + 10, 0, "%s", "a"); + break; + case NFORMAT_PRECISION: + format_nstring(dst, 10, "%.3s", src); + break; + case NFORMAT_PRECISION_EXACT: + format_nstring(dst, 10, "%.3s", src + 7); + break; + case NFORMAT_PRECISION_ZERO: + format_nstring(dst, 10, "%.0s", src); + break; + case NFORMAT_PRECISION_NEGATIVE: + uassert_int_equal(format_nstring(dst, 10, "%.*s", -1, "abc"), 3); + uassert_str_equal(dst, "abc"); + uassert_int_equal(format_nstring(dst, 10, "%.*s", INT_MIN, "abc"), 3); + uassert_str_equal(dst, "abc"); + uassert_int_equal(call_vsnprintf(dst, 10, "%5.*s", -1, "abc"), 5); + uassert_str_equal(dst, " abc"); + uassert_int_equal(format_nstring(dst, 10, "%.*d", -1, 0), 1); + uassert_str_equal(dst, "0"); + break; + case NFORMAT_PRECISION_BARE: + uassert_int_equal(format_nstring(dst, 10, "%.s", src), 0); + uassert_str_equal(dst, ""); + uassert_int_equal(format_nstring(dst, 10, "%.*s", 0, src), 0); + uassert_str_equal(dst, ""); + break; + case NFORMAT_WIDTH: + format_nstring(dst, 10, "%3s", "abcdefg"); + break; + case NFORMAT_WIDTH_PRECISION: + format_nstring(dst, 10, "%5.3s", src); + break; + case NFORMAT_WRITEBACK: + format_nstring(dst, 10, "a%n", (int *)(src + 8)); + break; + case LIBC_COPY_OVERFLOW: + libc_copy(dst, "1234567890"); + break; + case LIBC_FORMAT_EXACT: + result = libc_format(dst, "%s", "123456789"); + break; + case LIBC_FORMAT_OVERFLOW: + libc_format(dst, "%s", "1234567890"); + break; + case MEMORY_COPY_WRITE: + copy_memory(dst, "123456789012", 13); + break; + case MEMORY_COPY_READ: + copy_memory(dst, src + 8, 3); + break; + case MEMORY_MOVE_WRITE: + move_memory(dst, "123456789012", 13); + break; + case MEMORY_MOVE_READ: + move_memory(dst, src + 8, 3); + break; + case MEMORY_SET_WRITE: + set_memory(dst, 'a', 13); + break; + case MEMORY_UAF_READ: + rt_free(src); + src_freed = RT_TRUE; + copy_memory(dst, src, 3); + break; + case MEMORY_UAF_WRITE: + rt_free(dst); + dst_freed = RT_TRUE; + set_memory(dst, 'a', 3); + break; + case MEMORY_ZERO: + copy_memory(dst + 10, src + 10, 0); + move_memory(dst + 10, src + 10, 0); + set_memory(dst + 10, 0, 0); + break; + case MEMORY_EXACT: + set_memory(dst, 'b', 10); + copy_memory(dst + 1, src + 1, 9); + uassert_int_equal(dst[0], 'b'); + uassert_int_equal(dst[9], 'a'); + break; + case MEMORY_OVERLAP: + set_memory(dst, 'b', 10); + dst[0] = 'a'; + move_memory(dst + 1, dst, 9); + uassert_int_equal(dst[1], 'a'); + uassert_int_equal(dst[9], 'b'); + break; + case RANGE_CHECKS: + uassert_true(rt_asan_check_read(src, 10)); + uassert_true(rt_asan_check_write(dst, 10)); + uassert_true(rt_asan_check_read(src + 10, 0)); + uassert_true(rt_asan_check_write(dst + 10, 0)); + uassert_false(rt_asan_check_read(src + 8, 3)); + uassert_false(rt_asan_check_write(dst + 8, 3)); + break; + } + reports = rt_asan_report_count_get() - before; + switch (op) + { + case COPY_EXACT: + uassert_str_equal(dst, "aaaaaaaaa"); + break; + case FORMAT_EXACT: + case LIBC_FORMAT_EXACT: + rt_kprintf("[asan-libc] format exact result=%d\n", result); + uassert_int_equal(result, 9); + uassert_str_equal(dst, "123456789"); + break; + case NFORMAT_TRUNCATE: + uassert_str_equal(dst, "123456789"); + break; + case NFORMAT_LARGE_LIMIT: + uassert_str_equal(dst, "a"); + break; + case NFORMAT_PRECISION: + case NFORMAT_PRECISION_EXACT: + uassert_str_equal(dst, "aaa"); + break; + case NFORMAT_PRECISION_ZERO: + uassert_str_equal(dst, ""); + break; + case NFORMAT_WIDTH: + uassert_str_equal(dst, "abcdefg"); + break; + case NFORMAT_WIDTH_PRECISION: + uassert_str_equal(dst, " aaa"); + break; + default: + break; + } + if (!src_freed) + { + rt_free(src); + } + if (!dst_freed) + { + rt_free(dst); + } + rt_kprintf("[asan-libc] %s / %s: reports=%lu expected=%s\n", libc_path ? "libc" : "rt", name, + (unsigned long)reports, expected == OBSERVE ? "observe" : expected == CLEAN ? "clean" + : "detect"); + if (expected == CLEAN) + { + uassert_int_equal(reports, 0); + } + else if (expected == DETECT) + { + uassert_true(reports > 0); + } +} + +static void test_library_boundaries(void) +{ + enum expectation copy_expected = DETECT; + enum expectation ncopy_expected = DETECT; + enum expectation length_expected = DETECT; + enum expectation format_expected = DETECT; + enum expectation memcpy_expected = DETECT; + enum expectation memmove_expected = DETECT; + enum expectation memset_expected = DETECT; + enum expectation libc_expected = OBSERVE; + +#if (defined(RT_KLIBC_USING_LIBC_STRCPY) && !defined(RT_ASAN_USING_LIBC)) || defined(RT_KLIBC_USING_USER_STRCPY) + copy_expected = OBSERVE; +#endif +#if (defined(RT_KLIBC_USING_LIBC_STRNCPY) && !defined(RT_ASAN_USING_LIBC)) || defined(RT_KLIBC_USING_USER_STRNCPY) + ncopy_expected = OBSERVE; +#endif +#if defined(RT_KLIBC_USING_LIBC_VSNPRINTF) && !defined(RT_ASAN_USING_LIBC) + format_expected = OBSERVE; +#endif +#if (defined(RT_KLIBC_USING_LIBC_STRLEN) && !defined(RT_ASAN_USING_LIBC)) || defined(RT_KLIBC_USING_USER_STRLEN) + length_expected = OBSERVE; +#endif +#ifdef RT_KLIBC_USING_USER_MEMCPY + memcpy_expected = OBSERVE; +#endif +#ifdef RT_KLIBC_USING_USER_MEMMOVE + memmove_expected = OBSERVE; +#endif +#ifdef RT_KLIBC_USING_USER_MEMSET + memset_expected = OBSERVE; +#endif +#ifdef RT_ASAN_USING_LIBC + libc_expected = DETECT; + if (libc_path) + { + copy_expected = ncopy_expected = length_expected = format_expected = DETECT; + memcpy_expected = memmove_expected = memset_expected = DETECT; + } +#endif + check_operation(COPY_EXACT, "rt_strcpy exact", CLEAN); + check_operation(COPY_TERMINATOR_OVERFLOW, "rt_strcpy terminator overflow", copy_expected); + check_operation(COPY_SOURCE_OVERREAD, "rt_strcpy source overread", copy_expected); + check_operation(NCOPY_PAD, "rt_strncpy padding", CLEAN); + check_operation(NCOPY_OVERFLOW, "rt_strncpy padding overflow", ncopy_expected); + check_operation(NCOPY_BOUNDED, "rt_strncpy bounded unterminated source", CLEAN); + check_operation(NCOPY_ZERO, "rt_strncpy zero", CLEAN); + check_operation(LENGTH_SOURCE_OVERREAD, "rt_strlen source overread", length_expected); + check_operation(LENGTH_BOUNDED, "rt_strnlen bounded source", CLEAN); + check_operation(LENGTH_ZERO, "rt_strnlen zero", CLEAN); + check_operation(FORMAT_EXACT, "rt_sprintf exact", CLEAN); + check_operation(FORMAT_OVERFLOW, "rt_sprintf terminator overflow", format_expected); + check_operation(FORMAT_VA_OVERFLOW, "rt_vsprintf terminator overflow", format_expected); + check_operation(FORMAT_SOURCE_OVERREAD, "rt_snprintf source overread", format_expected); + check_operation(FORMAT_FORMAT_OVERREAD, "rt_snprintf format overread", format_expected); + check_operation(NFORMAT_TRUNCATE, "rt_snprintf truncation", CLEAN); + check_operation(NFORMAT_LARGE_LIMIT, "rt_snprintf large limit short output", CLEAN); + check_operation(NFORMAT_OVERFLOW, "rt_snprintf excessive output", format_expected); + check_operation(NFORMAT_VA_OVERFLOW, "rt_vsnprintf excessive output", format_expected); + check_operation(NFORMAT_ZERO, "rt_snprintf zero", CLEAN); + /* A precision-bounded %s must not read beyond its precision, even when + * the source is not terminated. Keep this a real assertion: it exposes + * regressions in the formatter's precision handling. + */ + check_operation(NFORMAT_PRECISION, "rt_snprintf precision bounded source", CLEAN); + check_operation(NFORMAT_PRECISION_EXACT, "rt_snprintf precision at allocation end", CLEAN); + check_operation(NFORMAT_PRECISION_ZERO, "rt_snprintf zero precision", CLEAN); + check_operation(NFORMAT_PRECISION_NEGATIVE, "rt_snprintf negative dynamic precision", CLEAN); + check_operation(NFORMAT_PRECISION_BARE, "rt_snprintf bare and dynamic zero precision", CLEAN); + check_operation(NFORMAT_WIDTH, "rt_snprintf width does not truncate", CLEAN); + check_operation(NFORMAT_WIDTH_PRECISION, "rt_snprintf width with bounded precision", CLEAN); +#if defined(RT_KLIBC_USING_VSNPRINTF_WRITEBACK_SPECIFIER) || defined(RT_KLIBC_USING_LIBC_VSNPRINTF) + check_operation(NFORMAT_WRITEBACK, "rt_snprintf writeback overflow", format_expected); +#elif defined(RT_ASAN_USING_LIBC) + if (libc_path) + { + check_operation(NFORMAT_WRITEBACK, "snprintf writeback overflow", DETECT); + } +#endif + check_operation(LIBC_COPY_OVERFLOW, "libc strcpy overflow", libc_expected); + check_operation(LIBC_FORMAT_EXACT, "libc sprintf exact", CLEAN); + check_operation(LIBC_FORMAT_OVERFLOW, "libc sprintf overflow", libc_expected); + check_operation(MEMORY_COPY_WRITE, "rt_memcpy destination overflow", memcpy_expected); + check_operation(MEMORY_COPY_READ, "rt_memcpy source overread", memcpy_expected); + check_operation(MEMORY_MOVE_WRITE, "rt_memmove destination overflow", memmove_expected); + check_operation(MEMORY_MOVE_READ, "rt_memmove source overread", memmove_expected); + check_operation(MEMORY_SET_WRITE, "rt_memset destination overflow", memset_expected); + check_operation(MEMORY_ZERO, "memory functions zero", CLEAN); + check_operation(MEMORY_EXACT, "memory functions exact unaligned range", CLEAN); + check_operation(MEMORY_OVERLAP, "rt_memmove valid overlap", CLEAN); + check_operation(RANGE_CHECKS, "explicit range checks", DETECT); +#if RT_ASAN_HAS_UAF_DETECTION + check_operation(COPY_UAF_SOURCE, "rt_strcpy freed source", copy_expected); + check_operation(FORMAT_UAF_SOURCE, "rt_snprintf freed source", format_expected); + check_operation(FORMAT_UAF_DESTINATION, "rt_sprintf freed destination", format_expected); + check_operation(MEMORY_UAF_READ, "rt_memcpy freed source", memcpy_expected); + check_operation(MEMORY_UAF_WRITE, "rt_memset freed destination", memset_expected); +#endif +} + +#ifdef RT_ASAN_USING_LIBC +/* Exercise partial granules and exact 8-byte multiples, with both redzones. + * Each invalid wrapper call must report before changing the destination. + */ +static void test_libc_memory_sizes(void) +{ + static const size_t sizes[] = { 1, 2, 7, 8, 9, 15, 16, 17, 31, 32, 33 }; + void *(*functions[])(void *, const void *, size_t) = { memcpy, memmove }; + size_t i, j, offset; + rt_size_t available; + rt_uint32_t before; + for (i = 0; i < sizeof(sizes) / sizeof(sizes[0]); i++) + { + size_t size = sizes[i]; + char *src = rt_malloc(size); + char *dst = rt_malloc(size); + uassert_not_null(src); + uassert_not_null(dst); + if (!src || !dst) + { + rt_free(src); + rt_free(dst); + return; + } + before = rt_asan_report_count_get(); + memset(src, 'a', size); + set_redzone_byte(dst + size, 0x5a); + for (offset = 0; offset <= size; offset++) + { + uassert_true(rt_asan_accessible_size(src + offset, &available)); + uassert_int_equal(available, size - offset); + } + for (j = 0; j < sizeof(functions) / sizeof(functions[0]); j++) + { + memset(dst, 'b', size); + uassert_true(functions[j](dst, src, size) == dst); + uassert_int_equal(dst[0], 'a'); + uassert_int_equal(dst[size - 1], 'a'); + uassert_true(functions[j](dst + 1, src + 1, size - 1) == dst + 1); + uassert_true(functions[j](dst + size, src + size, 0) == dst + size); + uassert_int_equal(rt_asan_report_count_get(), before); + memset(dst, 'b', size); + uassert_true(functions[j](dst, src, size + 1) == dst); + uassert_true(rt_asan_report_count_get() > before); + uassert_int_equal(get_redzone_byte(dst + size), 0x5a); + uassert_int_equal(dst[0], 'b'); + uassert_int_equal(dst[size - 1], 'b'); + before = rt_asan_report_count_get(); + functions[j](dst, src - 1, 1); + uassert_true(rt_asan_report_count_get() > before); + uassert_int_equal(get_redzone_byte(dst + size), 0x5a); + uassert_int_equal(dst[0], 'b'); + before = rt_asan_report_count_get(); + functions[j](dst - 1, src, 1); + uassert_true(rt_asan_report_count_get() > before); + uassert_int_equal(get_redzone_byte(dst + size), 0x5a); + uassert_int_equal(dst[0], 'b'); + before = rt_asan_report_count_get(); + /* Isolate a destination overflow from a valid source. */ + functions[j](dst + size, src, 1); + uassert_true(rt_asan_report_count_get() > before); + uassert_int_equal(get_redzone_byte(dst + size), 0x5a); + before = rt_asan_report_count_get(); + } + uassert_true(memset(dst, 'c', size) == dst); + uassert_true(memset(dst + size, 'c', 0) == dst + size); + uassert_int_equal(rt_asan_report_count_get(), before); + uassert_true(memset(dst, 'd', size + 1) == dst); + uassert_true(rt_asan_report_count_get() > before); + uassert_int_equal(get_redzone_byte(dst + size), 0x5a); + uassert_int_equal(dst[size - 1], 'c'); + before = rt_asan_report_count_get(); + memset(dst - 1, 'd', 1); + uassert_true(rt_asan_report_count_get() > before); + uassert_int_equal(get_redzone_byte(dst + size), 0x5a); + rt_free(src); + rt_free(dst); + } + /* Overlap must work in both directions; memcpy overlap is not valid. */ + { + char *p = rt_malloc(8); + uassert_not_null(p); + if (!p) + { + return; + } + before = rt_asan_report_count_get(); + strcpy(p, "abcdefg"); + memmove(p + 1, p, 7); + uassert_int_equal(p[7], 'g'); + memmove(p, p + 1, 7); + p[7] = 0; + uassert_str_equal(p, "abcdefg"); + uassert_int_equal(rt_asan_report_count_get(), before); + rt_free(p); + } +} + +static void test_libc_extreme_counts(void) +{ + char *src = rt_malloc(16); + char *dst = rt_malloc(16); + rt_uint32_t before; + uassert_not_null(src); + uassert_not_null(dst); + if (!src || !dst) + { + rt_free(src); + rt_free(dst); + return; + } + strcpy(src, "a"); + strcpy(dst, "keep"); + before = rt_asan_report_count_get(); + uassert_true(memmove(dst, dst, 16) == dst); + uassert_int_equal(strnlen(src, (size_t)-1), 1); + strncat(dst, src, (size_t)-1); + uassert_str_equal(dst, "keepa"); + uassert_int_equal(rt_asan_report_count_get(), before); + /* These counts must not wrap the range end into an apparently valid span. + * The libc operation must be skipped, not called with the enormous count. + */ + memcpy(dst, src, (size_t)-1); + uassert_true(rt_asan_report_count_get() > before); + uassert_str_equal(dst, "keepa"); + before = rt_asan_report_count_get(); + memmove(dst, src, (size_t)-1); + uassert_true(rt_asan_report_count_get() > before); + uassert_str_equal(dst, "keepa"); + before = rt_asan_report_count_get(); + memset(dst, 0, (size_t)-1); + uassert_true(rt_asan_report_count_get() > before); + uassert_str_equal(dst, "keepa"); + before = rt_asan_report_count_get(); + strncpy(dst, src, (size_t)-1); /* Huge padding after the early NUL. */ + uassert_true(rt_asan_report_count_get() > before); + uassert_str_equal(dst, "keepa"); + before = rt_asan_report_count_get(); + uassert_false(rt_asan_check_read(src, (rt_size_t)-1)); + uassert_false(rt_asan_check_write(dst, (rt_size_t)-1)); + uassert_true(rt_asan_report_count_get() > before); + rt_free(src); + rt_free(dst); +} + +static void test_libc_string_sizes(void) +{ + static const size_t sizes[] = { 1, 7, 8, 9, 15, 16, 17 }; + size_t i, offset; + rt_uint32_t before; + for (i = 0; i < sizeof(sizes) / sizeof(sizes[0]); i++) + { + size_t size = sizes[i]; + char *src = rt_malloc(size); + char *dst = rt_malloc(size); + uassert_not_null(src); + uassert_not_null(dst); + if (!src || !dst) + { + rt_free(src); + rt_free(dst); + return; + } + before = rt_asan_report_count_get(); + memset(src, 'a', size); + set_redzone_byte(src + size, 0); + src[size - 1] = 0; + for (offset = 0; offset < size; offset++) + { + uassert_int_equal(strlen(src + offset), size - offset - 1); + uassert_int_equal(strnlen(src + offset, size + 1), size - offset - 1); + } + uassert_true(strcpy(dst, src) == dst); + uassert_str_equal(dst, src); + uassert_true(strncpy(dst, "", size) == dst); + for (offset = 0; offset < size; offset++) + { + uassert_int_equal(dst[offset], 0); + } + uassert_true(strncpy(dst + size, src + size, 0) == dst + size); + uassert_int_equal(strnlen(src + size, 0), 0); + uassert_int_equal(rt_asan_report_count_get(), before); + strcpy(dst + 1, src); + uassert_true(rt_asan_report_count_get() > before); + uassert_int_equal(dst[0], 0); + before = rt_asan_report_count_get(); + strncpy(dst, "", size + 1); + uassert_true(rt_asan_report_count_get() > before); + before = rt_asan_report_count_get(); + strcpy(dst, src - 1); + uassert_true(rt_asan_report_count_get() > before); + before = rt_asan_report_count_get(); + memset(src, 'a', size); + set_redzone_byte(src + size, 0); /* Unterminated, with a poisoned byte next. */ + uassert_int_equal(strnlen(src, size), size); + uassert_true(strncpy(dst, src, size) == dst); + uassert_int_equal(dst[size - 1], 'a'); + uassert_int_equal(rt_asan_report_count_get(), before); + uassert_int_equal(strlen(src), size); + uassert_true(rt_asan_report_count_get() > before); + before = rt_asan_report_count_get(); + uassert_int_equal(strnlen(src, size + 1), size); + uassert_true(rt_asan_report_count_get() > before); + before = rt_asan_report_count_get(); + memset(dst, 'b', size); + strcpy(dst, src); + uassert_true(rt_asan_report_count_get() > before); + uassert_int_equal(dst[0], 'b'); + before = rt_asan_report_count_get(); + /* A large bound is legal if the source terminates before it. */ + src[0] = 0; + uassert_int_equal(strnlen(src, (size_t)-1), 0); + strcat(dst, src); /* The destination is still unterminated. */ + uassert_true(rt_asan_report_count_get() > before); + uassert_int_equal(dst[0], 'b'); + rt_free(src); + rt_free(dst); + } +} + +static void test_libc_append(void) +{ + char *dst = rt_malloc(10); + char *src = rt_malloc(10); + rt_uint32_t before; + uassert_not_null(dst); + uassert_not_null(src); + if (!dst || !src) + { + rt_free(dst); + rt_free(src); + return; + } + prepare_source(src); + before = rt_asan_report_count_get(); + strcpy(dst, "abc"); + strcat(dst, "def"); + uassert_str_equal(dst, "abcdef"); + uassert_int_equal(rt_asan_report_count_get(), before); + strcat(dst, "ghij"); + uassert_true(rt_asan_report_count_get() > before); + uassert_str_equal(dst, "abcdef"); + before = rt_asan_report_count_get(); + strcat(dst, src); + uassert_true(rt_asan_report_count_get() > before); + before = rt_asan_report_count_get(); + strcpy(dst, "a"); + strncat(dst, src, 3); + uassert_str_equal(dst, "aaaa"); + strncat(dst, "b", 100); + uassert_str_equal(dst, "aaaab"); + strncat(dst, src + 10, 0); + uassert_int_equal(rt_asan_report_count_get(), before); + strncat(dst, src, 10); + uassert_true(rt_asan_report_count_get() > before); + rt_free(src); + rt_free(dst); +} + +static void test_libc_append_boundaries(void) +{ + char *src = rt_malloc(10); + char *dst = rt_malloc(10); + rt_uint32_t before; + uassert_not_null(src); + uassert_not_null(dst); + if (!src || !dst) + { + rt_free(src); + rt_free(dst); + return; + } + prepare_source(src); + before = rt_asan_report_count_get(); + strcpy(dst, "123456789"); + uassert_true(strcat(dst, "") == dst); + uassert_true(strncat(dst, src + 10, 0) == dst); + uassert_int_equal(rt_asan_report_count_get(), before); + strncat(dst, "a", 1); + uassert_true(rt_asan_report_count_get() > before); + uassert_str_equal(dst, "123456789"); + before = rt_asan_report_count_get(); + strcpy(dst, ""); + strncat(dst, src, 9); /* Read exactly nine bytes, then add a terminator. */ + uassert_str_equal(dst, "aaaaaaaaa"); + uassert_int_equal(rt_asan_report_count_get(), before); + strcpy(dst, ""); + strncat(dst, src, 11); + uassert_true(rt_asan_report_count_get() > before); + uassert_str_equal(dst, ""); + before = rt_asan_report_count_get(); + memset(dst, 'b', 10); + strncat(dst, "a", 0); /* Even zero-count append must scan its destination. */ + uassert_true(rt_asan_report_count_get() > before); + uassert_int_equal(dst[0], 'b'); + before = rt_asan_report_count_get(); + strcat(dst + 10, ""); + uassert_true(rt_asan_report_count_get() > before); + before = rt_asan_report_count_get(); + strcpy(dst, "ok"); + strcat(dst, src - 1); + uassert_true(rt_asan_report_count_get() > before); + uassert_str_equal(dst, "ok"); + rt_free(src); + rt_free(dst); +} + +static void test_libc_format_boundaries(void) +{ + char *src = rt_malloc(10); + char *dst = rt_malloc(10); + rt_uint32_t before; + uassert_not_null(src); + uassert_not_null(dst); + if (!src || !dst) + { + rt_free(src); + rt_free(dst); + return; + } + prepare_source(src); + set_redzone_byte(dst + 10, 0x5a); + before = rt_asan_report_count_get(); + uassert_int_equal(format_nstring(NULL, 0, "%s", "abc"), 3); + uassert_int_equal(call_vsnprintf(NULL, 0, "%s", "abc"), 3); + uassert_int_equal(format_nstring(dst, 1, "%s", "abc"), 3); + uassert_str_equal(dst, ""); + uassert_int_equal(format_nstring(dst, 10, "%.*s", 10, src), 10); + uassert_str_equal(dst, "aaaaaaaaa"); + uassert_int_equal(format_nstring(dst, 10, "%.*s", 0, src + 10), 0); + uassert_int_equal(format_nstring(dst, 10, "%.*s", 0, src - 1), 0); + uassert_int_equal(format_nstring(dst + 10, 0, ""), 0); + uassert_int_equal(format_nstring(dst + 9, 1, ""), 0); + uassert_int_equal(format_string(dst + 9, ""), 0); + uassert_int_equal(rt_asan_report_count_get(), before); + + uassert_int_equal(format_string(dst, "%s", "1234567890"), 10); + uassert_true(rt_asan_report_count_get() > before); + uassert_str_equal(dst, "123456789"); + uassert_int_equal(get_redzone_byte(dst + 10), 0x5a); + before = rt_asan_report_count_get(); + uassert_int_equal(format_nstring(dst, 11, "%s", "1234567890"), 10); + uassert_true(rt_asan_report_count_get() > before); + uassert_str_equal(dst, "123456789"); + uassert_int_equal(get_redzone_byte(dst + 10), 0x5a); + before = rt_asan_report_count_get(); + strcpy(dst, "keep"); + uassert_int_equal(format_nstring(dst, 10, "%.*s", 11, src), -1); + uassert_true(rt_asan_report_count_get() > before); + uassert_str_equal(dst, "keep"); + before = rt_asan_report_count_get(); + uassert_int_equal(format_nstring(dst, 10, "%.*s", -1, src), -1); + uassert_true(rt_asan_report_count_get() > before); + uassert_str_equal(dst, "keep"); + before = rt_asan_report_count_get(); + uassert_int_equal(format_nstring(NULL, 0, "%s", src), -1); + uassert_true(rt_asan_report_count_get() > before); + before = rt_asan_report_count_get(); + uassert_int_equal(call_vsnprintf(dst, 10, src), -1); + uassert_true(rt_asan_report_count_get() > before); + uassert_str_equal(dst, "keep"); + before = rt_asan_report_count_get(); + uassert_int_equal(call_vsprintf(dst, "%s", src - 1), -1); + uassert_true(rt_asan_report_count_get() > before); + uassert_str_equal(dst, "keep"); + before = rt_asan_report_count_get(); + uassert_int_equal(format_string(dst, src - 1), -1); + uassert_true(rt_asan_report_count_get() > before); + before = rt_asan_report_count_get(); + uassert_int_equal(format_nstring(dst + 10, 1, ""), -1); + uassert_true(rt_asan_report_count_get() > before); + before = rt_asan_report_count_get(); + uassert_int_equal(format_string(dst - 1, ""), -1); + uassert_true(rt_asan_report_count_get() > before); + before = rt_asan_report_count_get(); + /* Literal dollar signs and escaped percent signs must not disable checks. */ + uassert_int_equal(format_nstring(dst, 10, "cost $%s", src), -1); + uassert_true(rt_asan_report_count_get() > before); + before = rt_asan_report_count_get(); + uassert_int_equal(format_nstring(dst, 10, "%%2$s %s", src), -1); + uassert_true(rt_asan_report_count_get() > before); + before = rt_asan_report_count_get(); + /* Precision decimal overflow must saturate rather than wrap to zero. */ + uassert_int_equal(format_nstring(dst, 10, "%.999999999999999999999s", src), -1); + uassert_true(rt_asan_report_count_get() > before); + uassert_int_equal(get_redzone_byte(dst + 10), 0x5a); + rt_free(src); + rt_free(dst); +} + +/* Allocate an aligned target with exactly the required size, then one byte + * too few. For the one-byte case use its right redzone as the invalid target. + */ +#define CHECK_WRITEBACK(type, conversion) \ + do \ + { \ + type *value = rt_malloc(sizeof(type)); \ + char *small = rt_malloc(sizeof(type) > 1 ? sizeof(type) - 1 : 1); \ + rt_uint32_t before; \ + uassert_not_null(value); \ + uassert_not_null(small); \ + if (value && small) \ + { \ + *value = 0; \ + before = rt_asan_report_count_get(); \ + uassert_int_equal(format_nstring(NULL, 0, "a" conversion, value), 1); \ + uassert_int_equal(*value, 1); \ + uassert_int_equal(rt_asan_report_count_get(), before); \ + uassert_int_equal(format_nstring(NULL, 0, conversion, \ + (type *)(small + (sizeof(type) == 1))), \ + -1); \ + uassert_true(rt_asan_report_count_get() > before); \ + } \ + rt_free(value); \ + rt_free(small); \ + } while (0) + +static void test_libc_writeback_sizes(void) +{ + CHECK_WRITEBACK(int, "%n"); + CHECK_WRITEBACK(short, "%hn"); + CHECK_WRITEBACK(long, "%ln"); +#if !defined(__NEWLIB__) || defined(_WANT_IO_LONG_LONG) + CHECK_WRITEBACK(long long, "%lln"); +#endif +#if !defined(__NEWLIB__) || defined(_WANT_IO_C99_FORMATS) + CHECK_WRITEBACK(signed char, "%hhn"); + CHECK_WRITEBACK(intmax_t, "%jn"); + CHECK_WRITEBACK(ptrdiff_t, "%zn"); + CHECK_WRITEBACK(ptrdiff_t, "%tn"); +#else + rt_kprintf("[asan-libc] hh/j/z/t writeback unavailable in this Newlib configuration\n"); +#endif +} +#undef CHECK_WRITEBACK + +static void test_libc_all_uaf_entries(void) +{ +#if RT_ASAN_HAS_UAF_DETECTION + unsigned int operation; + for (operation = 0; operation < 31; operation++) + { + char *dead = rt_malloc(16); + char *live = rt_malloc(16); + rt_uint32_t before; + uassert_not_null(dead); + uassert_not_null(live); + if (!dead || !live) + { + rt_free(dead); + rt_free(live); + return; + } + strcpy(dead, "abc"); + strcpy(live, "keep"); + rt_free(dead); + before = rt_asan_report_count_get(); + /* No allocation here: the dead pointer must not have been reused. */ + switch (operation) + { + case 0: + memcpy(live, dead, 1); + break; + case 1: + memmove(live, dead, 1); + break; + case 2: + memset(dead, 0, 1); + break; + case 3: + uassert_int_equal(strlen(dead), 0); + break; + case 4: + uassert_int_equal(strnlen(dead, 1), 0); + break; + case 5: + strcpy(live, dead); + break; + case 6: + strncpy(live, dead, 1); + break; + case 7: + strcat(live, dead); + break; + case 8: + strncat(live, dead, 1); + break; + case 9: + uassert_int_equal(format_string(dead, "a"), -1); + break; + case 10: + uassert_int_equal(call_vsprintf(dead, "a"), -1); + break; + case 11: + uassert_int_equal(format_nstring(dead, 16, "a"), -1); + break; + case 12: + uassert_int_equal(call_vsnprintf(dead, 16, "a"), -1); + break; + case 13: + memcpy(dead, live, 1); + break; + case 14: + memmove(dead, live, 1); + break; + case 15: + strcpy(dead, live); + break; + case 16: + strncpy(dead, live, 1); + break; + case 17: + strcat(dead, live); + break; + case 18: + strncat(dead, live, 1); + break; + case 19: + uassert_int_equal(format_string(live, "%s", dead), -1); + break; + case 20: + uassert_int_equal(call_vsprintf(live, "%s", dead), -1); + break; + case 21: + uassert_int_equal(format_nstring(live, 16, "%s", dead), -1); + break; + case 22: + uassert_int_equal(call_vsnprintf(live, 16, "%s", dead), -1); + break; + case 23: + uassert_int_equal(format_string(live, dead), -1); + break; + case 24: + uassert_int_equal(call_vsprintf(live, dead), -1); + break; + case 25: + uassert_int_equal(format_nstring(live, 16, dead), -1); + break; + case 26: + uassert_int_equal(call_vsnprintf(live, 16, dead), -1); + break; + case 27: + uassert_int_equal(format_string(live, "%n", (int *)dead), -1); + break; + case 28: + uassert_int_equal(call_vsprintf(live, "%n", (int *)dead), -1); + break; + case 29: + uassert_int_equal(format_nstring(live, 16, "%n", (int *)dead), -1); + break; + case 30: + uassert_int_equal(call_vsnprintf(live, 16, "%n", (int *)dead), -1); + break; + } + uassert_true(rt_asan_report_count_get() > before); + uassert_str_equal(live, "keep"); + before = rt_asan_report_count_get(); + /* All zero-count operations that do not scan a destination stay clean. */ + memcpy(dead, live + 16, 0); + memcpy(live + 16, dead, 0); + memmove(dead, dead, 0); + memset(dead, 0, 0); + strncpy(dead, live + 16, 0); + strncpy(live + 16, dead, 0); + uassert_int_equal(strnlen(dead, 0), 0); + uassert_int_equal(format_nstring(dead, 0, ""), 0); + strncat(live, dead, 0); + uassert_str_equal(live, "keep"); + uassert_int_equal(format_nstring(live, 16, "%.0s", dead), 0); + uassert_str_equal(live, ""); + uassert_int_equal(rt_asan_report_count_get(), before); + rt_free(live); + } +#else + rt_kprintf("[asan-libc] all-entry UAF checks unavailable for this heap algorithm\n"); +#endif +} + +static void compare_libc_format(char *dst, size_t size, const char *fmt, ...) +{ + char expected[64] = { 0 }; + int real_result, wrapped_result; + va_list args, copy; + va_start(args, fmt); + va_copy(copy, args); + real_result = __real_vsnprintf(expected, sizeof(expected), fmt, copy); + va_end(copy); + wrapped_result = vsnprintf(dst, size, fmt, args); + va_end(args); + uassert_int_equal(wrapped_result, real_result); + if (real_result >= 0) + { + uassert_str_equal(dst, expected); + } +} + +static int real_format_limit(char *dst, size_t size, const char *fmt, ...) +{ + int result; + va_list args; + va_start(args, fmt); + result = __real_vsnprintf(dst, size, fmt, args); + va_end(args); + return result; +} + +static void test_libc_format_arguments(void) +{ + char *src = rt_malloc(10); + char *dst = rt_malloc(64); + int count = 0; + int real_result, real_errno, wrapped_result; + char expected[64] = { 0 }; + rt_uint32_t before; + + uassert_not_null(src); + uassert_not_null(dst); + if (!src || !dst) + { + rt_free(src); + rt_free(dst); + return; + } + prepare_source(src); + before = rt_asan_report_count_get(); + snprintf(dst, 64, "%*.*s", 5, 3, src + 7); + uassert_str_equal(dst, " aaa"); + src[9] = '\0'; + snprintf(dst, 64, "%.*s", -1, src); + uassert_str_equal(dst, "aaaaaaaaa"); + compare_libc_format(dst, 64, "%ld/%lu/%lld/%s", 12L, 34UL, 56LL, src); + compare_libc_format(dst, 64, "%.2f/%s", 1.25, src); + compare_libc_format(dst, 64, "%+ld/%#lx/%.0d/%-8s", LONG_MIN, ULONG_MAX, 0, src); + compare_libc_format(dst, 64, "%p", (void *)(src + 10)); + /* Unsupported formats retain this SDK's actual behavior. */ + compare_libc_format(dst, 64, "%zu", (size_t)123); + compare_libc_format(dst, 64, "%ls", L"abc"); + compare_libc_format(dst, 64, "%2$s/%1$d", 7, src); + + uassert_int_equal(sprintf(dst, "abcd%n", &count), 4); + uassert_str_equal(dst, "abcd"); + uassert_int_equal(count, 4); + /* Preserve the SDK's parameter rejection, including errno. */ + errno = 0; + real_result = real_format_limit(expected, (size_t)INT_MAX + 1, "abc"); + real_errno = errno; + errno = 0; + wrapped_result = snprintf(dst, (size_t)INT_MAX + 1, "abc"); + uassert_int_equal(wrapped_result, real_result); + uassert_int_equal(errno, real_errno); + uassert_int_equal(rt_asan_report_count_get(), before); + + prepare_source(src); + snprintf(dst, 64, "%ld/%lu/%.2f/%*.*s", 12L, 34UL, 1.25, 5, 11, src); + uassert_true(rt_asan_report_count_get() > before); + before = rt_asan_report_count_get(); + snprintf(dst, 64, "a%ln", (long *)(src + 8)); + uassert_true(rt_asan_report_count_get() > before); + before = rt_asan_report_count_get(); + uassert_int_equal(sprintf(src, "%s", "1234567890"), 10); + uassert_true(rt_asan_report_count_get() > before); + uassert_str_equal(src, "123456789"); + rt_free(src); + rt_free(dst); +} + +static void test_libc_shadow_bounds(void) +{ + char *p = rt_malloc(16); + static char outside[16]; + rt_size_t available; + rt_uint32_t before = rt_asan_report_count_get(); + uassert_not_null(p); + if (!p) + { + return; + } + uassert_true(rt_asan_accessible_size(p, &available)); + uassert_int_equal(available, 16); + uassert_true(rt_asan_accessible_size(p + 4, &available)); + uassert_int_equal(available, 12); + uassert_true(rt_asan_accessible_size(p + 16, &available)); + uassert_int_equal(available, 0); + uassert_false(rt_asan_accessible_size(outside, &available)); + rt_free(p); + + /* Ending coverage is not evidence of an allocation's end. */ + p = rt_malloc((rt_size_t)RT_ASAN_SHADOW_SIZE * 8 + 64); + if (p) + { + uassert_false(rt_asan_accessible_size(p, &available)); + uassert_int_equal(sprintf(p, "%s", "abc"), 3); + uassert_str_equal(p, "abc"); + rt_free(p); + } + else + { + rt_kprintf("[asan-libc] coverage-end allocation skipped: insufficient heap\n"); + } + uassert_int_equal(rt_asan_report_count_get(), before); +} +#endif + +static void testcase(void) +{ + rt_size_t failures; + libc_path = RT_FALSE; + UTEST_UNIT_RUN(test_library_boundaries); + failures = utest_handle_get()->failed_num; +#ifdef RT_ASAN_USING_LIBC + libc_path = RT_TRUE; + copy_string = strcpy; + copy_nstring = strncpy; + string_length = strlen; + bounded_length = strnlen; + format_string = sprintf; + format_nstring = snprintf; + format_va = vsprintf; + format_nva = vsnprintf; + copy_memory = memcpy; + move_memory = memmove; + set_memory = memset; + UTEST_UNIT_RUN(test_library_boundaries); + failures += utest_handle_get()->failed_num; + UTEST_UNIT_RUN(test_libc_memory_sizes); + failures += utest_handle_get()->failed_num; + UTEST_UNIT_RUN(test_libc_extreme_counts); + failures += utest_handle_get()->failed_num; + UTEST_UNIT_RUN(test_libc_string_sizes); + failures += utest_handle_get()->failed_num; + UTEST_UNIT_RUN(test_libc_append); + failures += utest_handle_get()->failed_num; + UTEST_UNIT_RUN(test_libc_append_boundaries); + failures += utest_handle_get()->failed_num; + UTEST_UNIT_RUN(test_libc_format_boundaries); + failures += utest_handle_get()->failed_num; + UTEST_UNIT_RUN(test_libc_writeback_sizes); + failures += utest_handle_get()->failed_num; + UTEST_UNIT_RUN(test_libc_all_uaf_entries); + failures += utest_handle_get()->failed_num; + UTEST_UNIT_RUN(test_libc_format_arguments); + failures += utest_handle_get()->failed_num; + UTEST_UNIT_RUN(test_libc_shadow_bounds); + failures += utest_handle_get()->failed_num; +#ifdef RT_USING_CPLUSPLUS + UTEST_UNIT_RUN(test_asan_libc_cpp); + failures += utest_handle_get()->failed_num; +#endif + copy_string = rt_strcpy; + copy_nstring = rt_strncpy; + string_length = rt_strlen; + bounded_length = rt_strnlen; + format_string = rt_sprintf; + format_nstring = rt_snprintf; + format_va = rt_vsprintf; + format_nva = rt_vsnprintf; + copy_memory = rt_memcpy; + move_memory = rt_memmove; + set_memory = rt_memset; + libc_path = RT_FALSE; +#endif + uassert_int_equal(failures, 0); +} + +UTEST_TC_EXPORT(testcase, "components.asan_libc", RT_NULL, RT_NULL, 1000); diff --git a/src/klibc/kstdio.c b/src/klibc/kstdio.c index 257b1ff1a819..ed4e9994cb16 100644 --- a/src/klibc/kstdio.c +++ b/src/klibc/kstdio.c @@ -51,7 +51,11 @@ RTM_EXPORT(rt_snprintf); */ int rt_vsprintf(char *buf, const char *format, va_list arg_ptr) { +#ifdef RT_KLIBC_USING_LIBC_VSNPRINTF + return vsprintf(buf, format, arg_ptr); +#else return rt_vsnprintf(buf, (size_t) - 1, format, arg_ptr); +#endif } RTM_EXPORT(rt_vsprintf); diff --git a/src/klibc/kstring.c b/src/klibc/kstring.c index dea3341abdc0..082cec84408b 100644 --- a/src/klibc/kstring.c +++ b/src/klibc/kstring.c @@ -11,12 +11,12 @@ #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. + * AddressSanitizer: check the requested ranges once, rather than instrumenting + * each access in the bulk memory helpers. Reports follow the runtime's + * no-abort policy; the operation still executes after reporting a violation. */ #ifdef RT_USING_ASAN +#include "asan.h" #define RT_KLIB_NO_ASAN __attribute__((no_sanitize_address)) #else #define RT_KLIB_NO_ASAN @@ -51,6 +51,9 @@ RT_KLIB_NO_ASAN void *rt_memset(void *s, int c, size_t count) { +#if defined(RT_USING_ASAN) && (!defined(RT_ASAN_USING_LIBC) || !defined(RT_KLIBC_USING_LIBC_MEMSET)) + rt_asan_check_write(s, count); +#endif #if defined(RT_KLIBC_USING_LIBC_MEMSET) return memset(s, c, count); #elif defined(RT_KLIBC_USING_TINY_MEMSET) @@ -137,6 +140,10 @@ RTM_EXPORT(rt_memset); RT_KLIB_NO_ASAN void *rt_memcpy(void *dst, const void *src, size_t count) { +#if defined(RT_USING_ASAN) && (!defined(RT_ASAN_USING_LIBC) || !defined(RT_KLIBC_USING_LIBC_MEMCPY)) + rt_asan_check_read(src, count); + rt_asan_check_write(dst, count); +#endif #if defined(RT_KLIBC_USING_LIBC_MEMCPY) return memcpy(dst, src, count); #elif defined(RT_KLIBC_USING_TINY_MEMCPY) @@ -228,6 +235,10 @@ RTM_EXPORT(rt_memcpy); RT_KLIB_NO_ASAN void *rt_memmove(void *dest, const void *src, size_t n) { +#if defined(RT_USING_ASAN) && (!defined(RT_ASAN_USING_LIBC) || !defined(RT_KLIBC_USING_LIBC_MEMMOVE)) + rt_asan_check_read(src, n); + rt_asan_check_write(dest, n); +#endif #ifdef RT_KLIBC_USING_LIBC_MEMMOVE return memmove(dest, src, n); #else @@ -542,7 +553,7 @@ RTM_EXPORT(rt_strlen); size_t rt_strnlen(const char *s, size_t maxlen) { const char *sc; - for (sc = s; *sc != '\0' && (size_t)(sc - s) < maxlen; ++sc); + for (sc = s; (size_t)(sc - s) < maxlen && *sc != '\0'; ++sc); return sc - s; } #endif /* RT_KLIBC_USING_USER_STRNLEN */ diff --git a/src/klibc/rt_vsnprintf_std.c b/src/klibc/rt_vsnprintf_std.c index 481c99013b00..67355ea6c40d 100644 --- a/src/klibc/rt_vsnprintf_std.c +++ b/src/klibc/rt_vsnprintf_std.c @@ -371,7 +371,7 @@ static inline output_gadget_t buffer_gadget(char* buffer, size_t buffer_size) static inline printf_size_t strnlen_s_(const char* str, printf_size_t maxsize) { const char* s; - for (s = str; *s && maxsize--; ++s); + for (s = str; maxsize && *s; ++s, --maxsize); return (printf_size_t)(s - str); } @@ -1054,6 +1054,10 @@ static inline void format_string_loop(output_gadget_t* output, const char* forma else if (*format == '*') { const int precision_ = va_arg(args, int); precision = precision_ > 0 ? (printf_size_t) precision_ : 0U; + if (precision_ < 0) + { + flags &= ~FLAGS_PRECISION; + } ADVANCE_IN_FORMAT_STRING(format); } } @@ -1249,20 +1253,22 @@ static inline void format_string_loop(output_gadget_t* output, const char* forma out_rev_(output, ")llun(", 6, width, flags); } else { - printf_size_t l = strnlen_s_(p, precision ? precision : PRINTF_MAX_POSSIBLE_BUFFER_SIZE); - // pre padding - if (flags & FLAGS_PRECISION) { - l = (l < precision ? l : precision); - } + printf_size_t l = strnlen_s_(p, (flags & FLAGS_PRECISION) ? precision : PRINTF_MAX_POSSIBLE_BUFFER_SIZE); + // pre padding + if (flags & FLAGS_PRECISION) + { + l = (l < precision ? l : precision); + } if (!(flags & FLAGS_LEFT)) { while (l++ < width) { putchar_via_gadget(output, ' '); } } // string output - while ((*p != 0) && (!(flags & FLAGS_PRECISION) || precision)) { - putchar_via_gadget(output, *(p++)); - --precision; + while ((!(flags & FLAGS_PRECISION) || precision) && (*p != 0)) + { + putchar_via_gadget(output, *(p++)); + --precision; } // post padding if (flags & FLAGS_LEFT) { diff --git a/src/klibc/rt_vsnprintf_tiny.c b/src/klibc/rt_vsnprintf_tiny.c index 36811048a993..7c4e6293ffee 100644 --- a/src/klibc/rt_vsnprintf_tiny.c +++ b/src/klibc/rt_vsnprintf_tiny.c @@ -373,6 +373,7 @@ int rt_vsnprintf(char *buf, size_t size, const char *fmt, va_list args) if (*fmt == '.') { ++fmt; + precision = 0; if (_ISDIGIT(*fmt)) { precision = skip_atoi(&fmt); @@ -385,7 +386,8 @@ int rt_vsnprintf(char *buf, size_t size, const char *fmt, va_list args) } if (precision < 0) { - precision = 0; + /* A negative dynamic precision means no precision. */ + precision = -1; } } @@ -451,12 +453,7 @@ int rt_vsnprintf(char *buf, size_t size, const char *fmt, va_list args) s = "(null)"; } - for (len = 0; (len != field_width) && (s[len] != '\0'); len++); - - if (precision > 0 && len > precision) - { - len = precision; - } + for (len = 0; (precision < 0 || len < precision) && s[len] != '\0'; len++); if (!(flags & LEFT)) { diff --git a/src/klibc/utest/TC_rt_sprintf.c b/src/klibc/utest/TC_rt_sprintf.c index d8bba42c08c8..dbf11e3c5a80 100644 --- a/src/klibc/utest/TC_rt_sprintf.c +++ b/src/klibc/utest/TC_rt_sprintf.c @@ -978,6 +978,12 @@ SPRINTF_TEST_CASE(string_length) SPRINTF_CHECK("", buffer, "%.7s", ""); SPRINTF_CHECK("1234ab", buffer, "%.4s%.2s", "123456", "abcdef"); SPRINTF_CHECK("123", buffer, "%.*s", 3, "123456"); + SPRINTF_CHECK("123456", buffer, "%.*s", -1, "123456"); + SPRINTF_CHECK(" abc", buffer, "%5.*s", -1, "abc"); + SPRINTF_CHECK("", buffer, "%.*s", 0, "123456"); + SPRINTF_CHECK("", buffer, "%.s", "123456"); + uassert_int_equal(rt_snprintf(buffer, sizeof(buffer), "%.*s", -1, "abc"), 3); + uassert_str_equal(buffer, "abc"); #ifdef RT_KLIBC_USING_VSNPRINTF_STANDARD SPRINTF_CHECK("(null)", buffer, "%.*s", 3, (const char *) RT_NULL); #else @@ -988,6 +994,7 @@ SPRINTF_TEST_CASE(string_length) SPRINTF_TEST_CASE(misc) { char buffer[base_buffer_size]; + SPRINTF_CHECK("0", buffer, "%.*d", -1, 0); SPRINTF_CHECK("53000atest-20 bit", buffer, "%u%u%ctest%d %s", 5, 3000, 'a', -20, "bit"); #ifdef RT_KLIBC_USING_VSNPRINTF_DECIMAL_SPECIFIERS SPRINTF_CHECK("0.33", buffer, "%.*f", 2, 0.33333333); diff --git a/tools/building.py b/tools/building.py index 125da8521e76..c3844bd62792 100644 --- a/tools/building.py +++ b/tools/building.py @@ -37,6 +37,7 @@ import platform import logging from SCons.Script import * +from SCons.Util import CLVar from utils import _make_path_relative from mkdist import do_copy_file from options import AddOptions @@ -682,11 +683,26 @@ def _PretreatListParameters(target_list): return True # permit to add this list to the parameter +def _AppendGroupFlags(name, flags): + # BSPs may use argument lists, including arguments containing spaces. + # Tokenize group strings before appending to those lists; CLVar also + # handles the string LOCAL_*FLAGS additions in local_group(). + if not isinstance(Env.get(name, ''), str): + Env[name] = CLVar(Env.get(name, [])) + flags = CLVar(flags) + Env.AppendUnique(**{name: flags}) + def DefineGroup(name, src, depend, **parameters): global Env if not GetDepend(depend): return [] + # Local groups concatenate strings even when no global flags are added. + for flag in ['CFLAGS', 'CCFLAGS', 'CXXFLAGS', 'LINKFLAGS', 'ASFLAGS']: + value = Env.get(flag, '') + if isinstance(value, (list, tuple)): + Env[flag] = CLVar(value) + # find exist group and get path of group group_path = '' for g in Projects: @@ -708,15 +724,15 @@ def DefineGroup(name, src, depend, **parameters): if 'CFLAGS' in group: target = group['CFLAGS'] if len(target) > 0: - Env.AppendUnique(CFLAGS = target) + _AppendGroupFlags('CFLAGS', target) if 'CCFLAGS' in group: target = group['CCFLAGS'] if len(target) > 0: - Env.AppendUnique(CCFLAGS = target) + _AppendGroupFlags('CCFLAGS', target) if 'CXXFLAGS' in group: target = group['CXXFLAGS'] if len(target) > 0: - Env.AppendUnique(CXXFLAGS = target) + _AppendGroupFlags('CXXFLAGS', target) if 'CPPPATH' in group: target = group['CPPPATH'] if _PretreatListParameters(target) == True: @@ -732,11 +748,11 @@ def DefineGroup(name, src, depend, **parameters): if 'LINKFLAGS' in group: target = group['LINKFLAGS'] if len(target) > 0: - Env.AppendUnique(LINKFLAGS = target) + _AppendGroupFlags('LINKFLAGS', target) if 'ASFLAGS' in group: target = group['ASFLAGS'] if len(target) > 0: - Env.AppendUnique(ASFLAGS = target) + _AppendGroupFlags('ASFLAGS', target) if 'LOCAL_CPPPATH' in group: paths = [] for item in group['LOCAL_CPPPATH']: diff --git a/tools/testcases/test_asan_build.py b/tools/testcases/test_asan_build.py new file mode 100644 index 000000000000..3fe8199506f6 --- /dev/null +++ b/tools/testcases/test_asan_build.py @@ -0,0 +1,172 @@ +#!/usr/bin/env python3 +# Copyright (c) 2006-2026, RT-Thread Development Team +# SPDX-License-Identifier: Apache-2.0 + +"""Exercise ASan's SConscript with the flag representations used by BSPs. + +Run with: python -m unittest discover -s tools/testcases -p test_asan_build.py +Requires SCons, also used to build the BSPs. +""" + +import ast +import operator +import os +from pathlib import Path +from types import ModuleType, SimpleNamespace +import unittest +from unittest.mock import patch + +from SCons.Script import Environment +from SCons.Util import CLVar + + +class AsanBuildTest(unittest.TestCase): + def prepare(self, cflags, cxxflags, enabled=True, wrappers=True, + platform='gcc', linkflags='-Wl,--gc-sections', earlier_flags=False): + script = (Path(__file__).resolve().parents[2] / + 'components/utilities/asan/SConscript') + env = Environment(tools=['mingw'], CFLAGS=cflags, CXXFLAGS=cxxflags, + LINKFLAGS=linkflags) + groups = [] + module = ModuleType('building') + module.Import = lambda *names: None + module.Return = lambda *names: None + module.GetCurrentDir = lambda: str(script.parent) + + def get_depend(name): + if isinstance(name, list): + return all(get_depend(item) for item in name) + return enabled if name == 'RT_USING_ASAN' else enabled and wrappers + + module.GetDepend = get_depend + module.Glob = lambda pattern: ['asan.c', 'asan_heap.c', 'asan_libc.c'] + module.SrcRemove = lambda src, names: src.__setitem__( + slice(None), [name for name in src if name not in names]) + module.SConscript = lambda name: [] + + # Execute the real builder functions without importing BSP setup or + # registering SCons command-line options. Stub unrelated group actions. + builder = script.parents[3] / 'tools/building.py' + names = {'_AppendGroupFlags', '_PretreatListParameters', 'DefineGroup'} + tree = ast.parse(builder.read_text(), filename=str(builder)) + selected = ast.Module(body=[node for node in tree.body + if isinstance(node, ast.FunctionDef) and + node.name in names], type_ignores=[]) + module.Env = env + module.Projects = groups + module.CLVar = CLVar + module.File = lambda sources: sources + module.GetOption = lambda name: False + module.utils = SimpleNamespace(GCCC99Patch=lambda value: value) + module.rtconfig = SimpleNamespace(PLATFORM=platform) + module.operator = operator + module.os = os + exec(compile(selected, str(builder), 'exec'), module.__dict__) + self.define_group = module.DefineGroup + if earlier_flags: + module.DefineGroup('previous', [], ['RT_USING_ASAN'], + CXXFLAGS=' -fno-exceptions -fno-rtti') + with patch.dict('sys.modules', {'building': module}): + exec(compile(script.read_text(), str(script), 'exec'), + dict(env=env, rtconfig=SimpleNamespace(PLATFORM=platform))) + return env, [group for group in groups if group['name'] == 'asan'] + + def test_arguments_are_separate_for_each_bsp_representation(self): + for kind in (str, list, CLVar): + for other_kind in (str, list, CLVar): + for link_kind in (str, list, CLVar): + for wrappers in (False, True): + self.check_arguments(kind, other_kind, link_kind, wrappers) + + def check_arguments(self, kind, other_kind, link_kind, wrappers): + with self.subTest(cflags=kind, cxxflags=other_kind, + linkflags=link_kind, wrappers=wrappers): + cflags = '-O2 -g' if kind is str else kind(['-O2', '-g']) + cxxflags = ('-O2 -g' if other_kind is str else + other_kind(['-O2', '-g'])) + linkflags = ('-Wl,--gc-sections' if link_kind is str else + link_kind(['-Wl,--gc-sections'])) + env, groups = self.prepare(cflags, cxxflags, + wrappers=wrappers, + linkflags=linkflags) + for flag in ('CFLAGS', 'CXXFLAGS'): + tokens = [str(token) for token in + env.subst_list('$' + flag)[0]] + self.assertIn('-O2', tokens) + self.assertIn('-g', tokens) + self.assertIn('-fsanitize=kernel-address', tokens) + self.assertIn('-fno-omit-frame-pointer', tokens) + self.assertEqual( + sum(t.startswith('-fno-builtin-') for t in tokens), + 13 if wrappers else 0) + self.assertEqual('asan_libc.c' in groups[0]['src'], + wrappers) + tokens = [str(t) for t in + env.subst_list('$LINKFLAGS')[0]] + self.assertEqual( + sum(t.startswith('-Wl,--wrap=') for t in tokens), + 13 if wrappers else 0) + runtime = env.Clone(CFLAGS=env['CFLAGS'] + + groups[0]['LOCAL_CFLAGS']) + self.assertEqual(str(runtime.subst_list('$CFLAGS')[0][-1]), + '-fno-sanitize=kernel-address') + + def test_structured_arguments_preserve_spaces_and_nested_lists(self): + for wrappers in (False, True): + for kind in (list, CLVar): + with self.subTest(kind=kind, wrappers=wrappers): + flags = kind(['-O2', ['-g', '-DMESSAGE=hello world'], + '-include', 'directory with spaces/header.h']) + linkflags = kind(['-Wl,--gc-sections', + ['-Ldirectory with spaces', '-nostartfiles']]) + before = Environment(tools=['mingw'], CFLAGS=flags, + CXXFLAGS=flags, LINKFLAGS=linkflags) + env, groups = self.prepare(flags, flags, wrappers=wrappers, + linkflags=linkflags) + for name in ('CFLAGS', 'CXXFLAGS', 'LINKFLAGS'): + original = [str(token) for token in + before.subst_list('$' + name)[0]] + actual = [str(token) for token in + env.subst_list('$' + name)[0]] + self.assertEqual(actual[:len(original)], original) + self.assertIn('-fsanitize=kernel-address', actual) + runtime = env.Clone(CFLAGS=env['CFLAGS'] + + groups[0]['LOCAL_CFLAGS']) + self.assertEqual( + str(runtime.subst_list('$CFLAGS')[0][-1]), + '-fno-sanitize=kernel-address') + + def test_disabled_component_preserves_environment(self): + env, groups = self.prepare(['-O2'], CLVar('-g'), enabled=False) + self.assertEqual(env['CFLAGS'], ['-O2']) + self.assertEqual(env['CXXFLAGS'], CLVar('-g')) + self.assertEqual(env['LINKFLAGS'], '-Wl,--gc-sections') + self.assertEqual(groups, []) + + def test_later_groups_can_append_multiple_options(self): + flags = ['-O2', ['-g', '-DMESSAGE=hello world']] + env, groups = self.prepare(flags, CLVar(flags), + linkflags=CLVar('-Wl,--gc-sections'), earlier_flags=True) + self.define_group('later', [], ['RT_USING_ASAN'], CFLAGS=' -ffunction-sections -fdata-sections', + CXXFLAGS=' -fno-exceptions -fno-rtti', + LINKFLAGS=' -static -nostartfiles') + for name, options in ( + ('CFLAGS', ['-ffunction-sections', '-fdata-sections']), + ('CXXFLAGS', ['-fno-exceptions', '-fno-rtti']), + ('LINKFLAGS', ['-static', '-nostartfiles'])): + actual = [str(token) for token in env.subst_list('$' + name)[0]] + for option in options: + self.assertIn(option, actual) + if name != 'LINKFLAGS': + self.assertIn('-DMESSAGE=hello world', actual) + runtime = env.Clone(CFLAGS=env['CFLAGS'] + groups[0]['LOCAL_CFLAGS']) + self.assertEqual(str(runtime.subst_list('$CFLAGS')[0][-1]), + '-fno-sanitize=kernel-address') + + def test_unsupported_toolchain_is_rejected(self): + with self.assertRaisesRegex(RuntimeError, 'GCC and GNU ld'): + self.prepare('', '', platform='armcc') + + +if __name__ == '__main__': + unittest.main() diff --git a/tools/testcases/test_asan_config.py b/tools/testcases/test_asan_config.py index a774a0b8cf35..17c15cb944f9 100644 --- a/tools/testcases/test_asan_config.py +++ b/tools/testcases/test_asan_config.py @@ -17,6 +17,27 @@ class AsanConfigTest(unittest.TestCase): + def test_libc_wrappers_require_the_runtime(self): + bsp = Path(__file__).resolve().parents[2] / 'bsp/qemu-vexpress-a9' + previous = Path.cwd() + try: + os.chdir(bsp) + config = kconfiglib.Kconfig('Kconfig', warn=False) + 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) + wrappers = config.syms['RT_ASAN_USING_LIBC'] + wrappers.set_value(2) + self.assertEqual(wrappers.tri_value, 0) + config.syms['RT_USING_ASAN'].set_value(2) + self.assertEqual(wrappers.tri_value, 2) + config.syms['RT_USING_ASAN'].set_value(0) + self.assertEqual(wrappers.tri_value, 0) + finally: + os.chdir(previous) + def test_heap_lock_combinations(self): bsp = Path(__file__).resolve().parents[2] / 'bsp/qemu-vexpress-a9' previous = Path.cwd() diff --git a/tools/testcases/test_asan_libc_host.py b/tools/testcases/test_asan_libc_host.py new file mode 100644 index 000000000000..a18081274b1f --- /dev/null +++ b/tools/testcases/test_asan_libc_host.py @@ -0,0 +1,261 @@ +#!/usr/bin/env python3 +# Copyright (c) 2006-2026, RT-Thread Development Team +# SPDX-License-Identifier: Apache-2.0 + +"""Check formatter side effects using the actual libc wrapper and GNU ld.""" + +from pathlib import Path +import shutil +import subprocess +import tempfile +import unittest + + +RTTHREAD_HEADER = r''' +#ifndef TEST_RTTHREAD_H +#define TEST_RTTHREAD_H +#include +#include +#include +#define RT_ASAN_USING_LIBC +#define RT_TRUE 1 +#define RT_FALSE 0 +typedef int rt_bool_t; +typedef size_t rt_size_t; +typedef uint32_t rt_uint32_t; +#endif +''' + +FORMAT_ONCE_FIXTURE = r''' +#include +#include +#include +#include "asan.h" +static char storage[10]; +static unsigned int calls; +static size_t native_limit; +rt_bool_t rt_asan_accessible_size(const void *ptr, rt_size_t *size) +{ + uintptr_t address = (uintptr_t)ptr; + uintptr_t begin = (uintptr_t)storage; + if (address < begin || address > begin + sizeof(storage)) return RT_FALSE; + *size = begin + sizeof(storage) - address; + return RT_TRUE; +} +rt_bool_t rt_asan_check_read(const void *ptr, rt_size_t size) +{ + (void)ptr; (void)size; return RT_TRUE; +} +rt_bool_t rt_asan_check_write(void *ptr, rt_size_t size) +{ + rt_size_t available; + return !rt_asan_accessible_size(ptr, &available) || size <= available; +} +/* This is the real formatter reached through GNU ld's __real_vsnprintf. + * Count calls independently of the wrapper, without overlapping arguments. + */ +int vsnprintf(char *dst, size_t size, const char *fmt, va_list args) +{ + const char text[] = "abc"; + size_t i; + assert(strcmp(fmt, "abc%n") == 0); + calls++; + native_limit = size; + *va_arg(args, int *) = 3; + if (size) + { + for (i = 0; i < 3 && i + 1 < size; i++) dst[i] = text[i]; + dst[i] = 0; + } + return 3; +} +int va_format(char *, size_t, int, const char *, ...); +int main(void) +{ + int count = 0; + assert(snprintf(storage, 100, "abc%n", &count) == 3); + assert(count == 3 && calls == 1 && native_limit == sizeof(storage)); + assert(strcmp(storage, "abc") == 0); + calls = 0; count = 0; + assert(snprintf(NULL, 0, "abc%n", &count) == 3); + assert(count == 3 && calls == 1 && native_limit == 0); + calls = 0; count = 0; + assert(sprintf(storage, "abc%n", &count) == 3); + assert(count == 3 && calls == 1 && native_limit == sizeof(storage)); + calls = 0; count = 0; + assert(va_format(storage, 100, 1, "abc%n", &count) == 3); + assert(count == 3 && calls == 1 && native_limit == sizeof(storage)); + calls = 0; count = 0; + assert(va_format(storage, 0, 0, "abc%n", &count) == 3); + assert(count == 3 && calls == 1 && native_limit == sizeof(storage)); + return 0; +} +''' + +VA_CALLER_FIXTURE = r''' +#include +#include +/* Separate object: --wrap only redirects unresolved references. */ +int va_format(char *dst, size_t size, int bounded, const char *fmt, ...) +{ + int result; + va_list args; + va_start(args, fmt); + result = bounded ? vsnprintf(dst, size, fmt, args) : vsprintf(dst, fmt, args); + va_end(args); + return result; +} +''' + +VARARGS_FIXTURE = r''' +#include +#include +#include +#include +#include +#include +#include "asan.h" +static char source[16] __attribute__((aligned(16))); +static char output[256]; +static unsigned int reports; +int __real_vsnprintf(char *, size_t, const char *, va_list); +rt_bool_t rt_asan_accessible_size(const void *ptr, rt_size_t *size) +{ + uintptr_t address = (uintptr_t)ptr, begin = (uintptr_t)source; + if (address >= begin && address <= begin + sizeof(source)) + { + *size = address < begin + 10 ? begin + 10 - address : 0; + return RT_TRUE; + } + begin = (uintptr_t)output; + if (address >= begin && address <= begin + sizeof(output)) + { + *size = begin + sizeof(output) - address; + return RT_TRUE; + } + return RT_FALSE; +} +rt_bool_t rt_asan_check_read(const void *ptr, rt_size_t size) +{ + rt_size_t available; + if (size && rt_asan_accessible_size(ptr, &available) && size > available) + { + reports++; + return RT_FALSE; + } + return RT_TRUE; +} +rt_bool_t rt_asan_check_write(void *ptr, rt_size_t size) +{ + return rt_asan_check_read(ptr, size); +} +static void parity(const char *fmt, ...) +{ + char expected[256]; + va_list args, copy; + int native, wrapped; + va_start(args, fmt); + va_copy(copy, args); + native = __real_vsnprintf(expected, sizeof(expected), fmt, copy); + va_end(copy); + wrapped = vsnprintf(output, sizeof(output), fmt, args); + va_end(args); + assert(wrapped == native); + if (native >= 0) assert(strcmp(output, expected) == 0); +} +int main(void) +{ + const char *integers = "%jd/%ju/%zd/%zu/%td/%tu/%hhd/%hhu/%hd/%hu/%ld/%lu/%lld/%llu/%s"; + const char *floats = "%.2a/%.2A/%.2e/%.2E/%.2f/%.2F/%.2g/%.2G/%.2Lf/%c/%p/%s"; + strcpy((char *)source, "abc"); + parity(integers, (intmax_t)-1, (uintmax_t)2, (ptrdiff_t)-3, (size_t)4, + (ptrdiff_t)-5, (size_t)6, (int)-7, (unsigned int)8, (int)-9, + (unsigned int)10, -11L, 12UL, -13LL, 14ULL, source); + parity(floats, 1.25, 1.25, 1.25, 1.25, 1.25, 1.25, 1.25, 1.25, + (long double)1.25, 'x', (void *)source, source); + parity("%*.*s/%s", -5, 3, source, source); + parity("%+020lld/%#018llx/%.0d/%-8s", LLONG_MIN, ULLONG_MAX, 0, source); + parity("%1$.*2$s", source, 2); + parity("%2$s/%1$d", 7, source); + parity("%ls/%s", L"wide", source); + parity("%m/%s", source); /* Unknown extension: preserve host libc behavior. */ + assert(reports == 0); + memset(source, 'a', 10); + strcpy(output, "keep"); + assert(snprintf(output, sizeof(output), integers, (intmax_t)-1, (uintmax_t)2, + (ptrdiff_t)-3, (size_t)4, (ptrdiff_t)-5, (size_t)6, (int)-7, + (unsigned int)8, (int)-9, (unsigned int)10, -11L, 12UL, -13LL, + 14ULL, source) == -1); + assert(reports == 1 && strcmp(output, "keep") == 0); + assert(snprintf(output, sizeof(output), floats, 1.25, 1.25, 1.25, 1.25, + 1.25, 1.25, 1.25, 1.25, (long double)1.25, 'x', (void *)source, + source) == -1); + assert(reports == 2 && strcmp(output, "keep") == 0); + /* Positional syntax anywhere must be discovered before consuming args. + * Zero precision avoids touching the poisoned source in native libc. + */ + assert(snprintf(output, sizeof(output), "%1$.*2$s", source + 10, 0) == 0); + assert(reports == 2); + /* A positional conversion after an ordinary literal is still pre-scanned. */ + assert(snprintf(output, sizeof(output), "prefix/%1$.*2$s", source + 10, 0) == 7); + assert(reports == 2); + /* Every %n length consumes the correct pointer and validates its extent. */ +#define CHECK_N(type, fmt) do { \ + unsigned int before = reports; \ + type value = 0; \ + assert(snprintf(NULL, 0, "a" fmt, &value) == 1); \ + assert(value == 1 && reports == before); \ + assert(snprintf(NULL, 0, fmt, (type *)(source + 16)) == -1); \ + assert(reports == before + 1); \ +} while (0) + CHECK_N(signed char, "%hhn"); + CHECK_N(short, "%hn"); + CHECK_N(int, "%n"); + CHECK_N(long, "%ln"); + CHECK_N(long long, "%lln"); + CHECK_N(intmax_t, "%jn"); + CHECK_N(ptrdiff_t, "%zn"); + CHECK_N(ptrdiff_t, "%tn"); +#undef CHECK_N + return 0; +} +''' + + +@unittest.skipUnless(shutil.which('gcc'), 'requires host GCC and GNU ld') +class AsanLibcHostTest(unittest.TestCase): + def compile_and_run(self, fixture, caller=None): + component = (Path(__file__).resolve().parents[2] / + 'components/utilities/asan') + with tempfile.TemporaryDirectory(prefix='asan-libc-test-') as directory: + work = Path(directory) + (work / 'rtthread.h').write_text(RTTHREAD_HEADER) + (work / 'check.c').write_text(fixture) + names = ('memcpy memmove memset strlen strnlen strcpy strncpy strcat ' + 'strncat sprintf vsprintf snprintf vsnprintf').split() + command = ['gcc', '-std=gnu99', '-O2', '-fno-builtin', + '-U_FORTIFY_SOURCE', '-D_FORTIFY_SOURCE=0', + '-fsanitize=undefined', '-fno-sanitize-recover=undefined', + '-I' + str(work), '-I' + str(component), + str(component / 'asan_libc.c'), str(work / 'check.c'), + '-o', str(work / 'check')] + if caller is not None: + (work / 'caller.c').write_text(caller) + command.append(str(work / 'caller.c')) + command.extend('-Wl,--wrap=' + name for name in names) + result = subprocess.run(command, capture_output=True, text=True, + timeout=30) + self.assertEqual(result.returncode, 0, result.stdout + result.stderr) + result = subprocess.run([str(work / 'check')], capture_output=True, + text=True, timeout=30) + self.assertEqual(result.returncode, 0, result.stdout + result.stderr) + + def test_native_formatter_executes_once(self): + self.compile_and_run(FORMAT_ONCE_FIXTURE, VA_CALLER_FIXTURE) + + def test_host_varargs_and_format_fallback(self): + self.compile_and_run(VARARGS_FIXTURE) + + +if __name__ == '__main__': + unittest.main()