diff --git a/.github/utest/configs/components/asan.cfg b/.github/utest/configs/components/asan.cfg new file mode 100644 index 000000000000..a3c7bd0a404d --- /dev/null +++ b/.github/utest/configs/components/asan.cfg @@ -0,0 +1,10 @@ +# dependencies +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/.github/workflows/utest_auto_run.yml b/.github/workflows/utest_auto_run.yml index 9a4915737b6a..087b747af820 100644 --- a/.github/workflows/utest_auto_run.yml +++ b/.github/workflows/utest_auto_run.yml @@ -152,6 +152,8 @@ jobs: config_file: "components/dfs.cfg" - platform: { UTEST: "A9", RTT_BSP: "bsp/qemu-vexpress-a9", QEMU_ARCH: "arm", QEMU_MACHINE: "vexpress-a9", SD_FILE: "sd.bin", KERNEL: "standard", "SMP_RUN":"" } config_file: "components/libc.cfg" + - platform: { UTEST: "A9", RTT_BSP: "bsp/qemu-vexpress-a9", QEMU_ARCH: "arm", QEMU_MACHINE: "vexpress-a9", SD_FILE: "sd.bin", KERNEL: "standard", "SMP_RUN":"" } + config_file: "components/asan.cfg" env: TEST_QEMU_ARCH: ${{ matrix.platform.QEMU_ARCH }} diff --git a/components/utilities/Kconfig b/components/utilities/Kconfig index c32cd692bde6..d2dc05c0bbc1 100644 --- a/components/utilities/Kconfig +++ b/components/utilities/Kconfig @@ -244,5 +244,6 @@ config RT_USING_RESOURCE_ID rsource "libadt/Kconfig" rsource "rt-link/Kconfig" +rsource "asan/Kconfig" endmenu diff --git a/components/utilities/asan/Kconfig b/components/utilities/asan/Kconfig new file mode 100644 index 000000000000..21c33d8472cc --- /dev/null +++ b/components/utilities/asan/Kconfig @@ -0,0 +1,110 @@ +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 libraries outside the optional libc wrappers + are not checked. + + 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 + 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_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 + 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..a41d74e06789 --- /dev/null +++ b/components/utilities/asan/SConscript @@ -0,0 +1,37 @@ +from building import * +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 +# 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') + +# Do not extend in place: DefineGroup retains the runtime source list. +group = group + SConscript('utest/SConscript') + +Return('group') diff --git a/components/utilities/asan/asan.c b/components/utilities/asan/asan.c new file mode 100644 index 000000000000..d14e08591881 --- /dev/null +++ b/components/utilities/asan/asan.c @@ -0,0 +1,714 @@ +/* + * 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; + +/* 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) +{ + 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) + { + asan_track_copy(&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) + { + asan_track_copy(&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; + asan_track_copy(&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"); +} + +/* ---- 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) \ + { \ + 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; + volatile char *owner = asan_tracks[i].owner; + if (self) + { + const volatile char *name = self->parent.name; + while (n < RT_NAME_MAX - 1 && name[n]) + { + owner[n] = name[n]; + n++; + } + } + 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_track_copy(&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(); + asan_track_copy(&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(); + asan_track_copy(&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..ccd6fdc64246 --- /dev/null +++ b/components/utilities/asan/asan.h @@ -0,0 +1,78 @@ +/* + * 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); + +/* 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. + */ +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/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 new file mode 100644 index 000000000000..67d84640ff14 --- /dev/null +++ b/components/utilities/asan/utest/Kconfig @@ -0,0 +1,18 @@ +menu "AddressSanitizer" + +config RT_UTEST_ASAN + bool "AddressSanitizer Test" + 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 new file mode 100644 index 000000000000..b7e9e106475f --- /dev/null +++ b/components/utilities/asan/utest/SConscript @@ -0,0 +1,15 @@ +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'] + +# 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..4bc7680b2597 --- /dev/null +++ b/components/utilities/asan/utest/asan_cpp_test.cpp @@ -0,0 +1,50 @@ +/* + * Copyright (c) 2026, RT-Thread Development Team + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include "asan.h" +#include "utest.h" +#ifdef RT_ASAN_USING_LIBC +#include +#include +#endif + +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)); +} + +#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/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/examples/test/mem_align_test.c b/examples/test/mem_align_test.c new file mode 100644 index 000000000000..9dd84aebb65d --- /dev/null +++ b/examples/test/mem_align_test.c @@ -0,0 +1,90 @@ +/* + * Copyright (c) 2026, RT-Thread Development Team + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include "utest.h" + +/* These tests also run without ASan: size arithmetic must be safe in both modes. */ +static void test_align_valid(void) +{ + static const rt_size_t sizes[] = { 1, 13, 16, 31, 32 }; + static const rt_size_t aligns[] = { 1, 2, sizeof(void *), 16, 64, 256 }; + rt_size_t i; + rt_size_t j; + + for (i = 0; i < sizeof(sizes) / sizeof(sizes[0]); i++) + { + for (j = 0; j < sizeof(aligns) / sizeof(aligns[0]); j++) + { + char *p = (char *)rt_malloc_align(sizes[i], aligns[j]); + uassert_not_null(p); + if (!p) + { + return; + } + uassert_true(((rt_uintptr_t)p & (aligns[j] - 1)) == 0); + rt_memset(p, 0x5a, sizes[i]); + uassert_int_equal(p[0], 0x5a); + uassert_int_equal(p[sizes[i] - 1], 0x5a); + rt_free_align(p); + } + } + rt_free_align(RT_NULL); +} + +static void test_align_invalid(void) +{ + static const rt_size_t aligns[] = { 0, 3, 6, 12, (rt_size_t)-1 }; + rt_size_t i; + void *p; + + for (i = 0; i < sizeof(aligns) / sizeof(aligns[0]); i++) + { + p = rt_malloc_align(16, aligns[i]); + uassert_null(p); + rt_free_align(p); + } + p = rt_malloc_align(0, 64); + uassert_null(p); + rt_free_align(p); +} + +static void test_align_overflow(void) +{ + static const rt_size_t requests[][2] = { + { (rt_size_t)-1, 64 }, + { (rt_size_t)-3, 64 }, + { (rt_size_t)-64, 64 }, + { (rt_size_t)-256, 256 }, + { (rt_size_t)-1 - sizeof(void *), 1 }, + { (rt_size_t)-1 - RT_ALIGN_SIZE, 1 }, + { (rt_size_t)-1 / 2 + 1, (rt_size_t)-1 / 2 + 1 }, + }; + rt_size_t i; + + for (i = 0; i < sizeof(requests) / sizeof(requests[0]); i++) + { + void *p = rt_malloc_align(requests[i][0], requests[i][1]); + uassert_null(p); + rt_free_align(p); + } +} + +static void testcase(void) +{ + rt_size_t failures = 0; + + /* Each unit resets the framework counters, so retain earlier failures. */ + UTEST_UNIT_RUN(test_align_valid); + failures += utest_handle_get()->failed_num; + UTEST_UNIT_RUN(test_align_invalid); + failures += utest_handle_get()->failed_num; + UTEST_UNIT_RUN(test_align_overflow); + failures += utest_handle_get()->failed_num; + uassert_int_equal(failures, 0); +} + +UTEST_TC_EXPORT(testcase, "core.mem_align", RT_NULL, RT_NULL, 1000); diff --git a/src/SConscript b/src/SConscript index 7b2dec5e4ce1..4f6dc3fd078a 100644 --- a/src/SConscript +++ b/src/SConscript @@ -28,6 +28,17 @@ if GetDepend('RT_USING_SMP') == False: else: SrcRemove(src, ['cpu_up.c', 'scheduler_up.c']) +# AddressSanitizer: heap allocators keep their metadata (headers, free lists) +# inside the heap region, so instrumenting them would report their own header +# accesses as false positives. Move them to a separate non-instrumented group. +asan_alloc_src = [] +if GetDepend('RT_USING_ASAN'): + for alloc_name in ['mem.c', 'memheap.c', 'slab.c']: + matched = [x for x in src if os.path.basename(x.rstr()) == alloc_name] + if matched: + asan_alloc_src += matched + SrcRemove(src, [alloc_name]) + LOCAL_CFLAGS = '' LINKFLAGS = '' @@ -59,6 +70,13 @@ else: LINKFLAGS=LINKFLAGS, LOCAL_CFLAGS=LOCAL_CFLAGS, CPPDEFINES=['__RTTHREAD__'], LOCAL_CPPDEFINES=['__RT_KERNEL_SOURCE__']) +# AddressSanitizer: build heap allocators without instrumentation. +if GetDepend('RT_USING_ASAN') and asan_alloc_src: + group = group + DefineGroup('KernelAlloc', asan_alloc_src, depend=['RT_USING_ASAN'], + CPPPATH=inc, CPPDEFINES=['__RTTHREAD__'], + LOCAL_CPPDEFINES=['__RT_KERNEL_SOURCE__'], + LOCAL_CFLAGS=' -fno-sanitize=kernel-address') + list = os.listdir(cwd) for item in list: if os.path.isfile(os.path.join(cwd, item, 'SConscript')): diff --git a/src/klibc/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 b6d553ffa34d..082cec84408b 100644 --- a/src/klibc/kstring.c +++ b/src/klibc/kstring.c @@ -10,6 +10,18 @@ #include +/* + * 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 +#endif + #if defined(RT_KLIBC_USING_LIBC_MEMSET) || \ defined(RT_KLIBC_USING_LIBC_MEMCPY) || \ defined(RT_KLIBC_USING_LIBC_MEMMOVE) || \ @@ -36,8 +48,12 @@ * @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_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) @@ -121,8 +137,13 @@ 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_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) @@ -211,8 +232,13 @@ 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) { +#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 @@ -527,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/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'] 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 new file mode 100644 index 000000000000..17c15cb944f9 --- /dev/null +++ b/tools/testcases/test_asan_config.py @@ -0,0 +1,70 @@ +#!/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_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() + 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() 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()