From 2b975037e07795870ecaa5a4b4ca7d3602ceb8dc Mon Sep 17 00:00:00 2001 From: hanzhijian Date: Mon, 31 Aug 2026 00:33:07 +0800 Subject: [PATCH 1/7] [components][asan] Add runtime AddressSanitizer for heap Add a heap-only AddressSanitizer (kernel-address) runtime for RT-Thread. It instruments memory accesses via GCC's -fsanitize=kernel-address and detects heap-buffer-overflow and use-after-free at runtime, with a FinSH 'asan_info' command for interactive diagnosis. ## What - components/asan/: shadow memory + instrumented-access checks + report - allocator hooks to poison/unpoison heap blocks (malloc/free/realloc) - components/Kconfig: register RT_USING_ASAN with shadow/track/backtrace opts - src/SConscript: build mem/memheap/slab allocators without instrumentation - tools/building.py: inject -fsanitize=kernel-address on GCC ## Why RT-Thread lacks runtime memory-safety checking on MCU targets (ASan only existed on the x86 simulator). Heap overflow and use-after-free are the most common embedded memory bugs; this gives on-target detection with thread and block context in the report. ## Heap algorithm support - small mem: full support (overflow + use-after-free) - slab / memheap: overflow only (their allocators reuse freed blocks for metadata written via instrumented rt_memset, so free-block poisoning is disabled to avoid false positives) - userheap: mutually exclusive (Kconfig) Verified on qemu-vexpress-a9 (small mem / slab / memheap) and on a real STM32F407ZGT6 board. --- components/Kconfig | 1 + components/asan/Kconfig | 56 ++++ components/asan/SConscript | 13 + components/asan/asan.c | 537 +++++++++++++++++++++++++++++++++++++ src/SConscript | 18 ++ tools/building.py | 6 + 6 files changed, 631 insertions(+) create mode 100644 components/asan/Kconfig create mode 100644 components/asan/SConscript create mode 100644 components/asan/asan.c diff --git a/components/Kconfig b/components/Kconfig index accc38c67434..9f19018e93f8 100644 --- a/components/Kconfig +++ b/components/Kconfig @@ -36,6 +36,7 @@ rsource "drivers/Kconfig" rsource "libc/Kconfig" rsource "net/Kconfig" rsource "mprotect/Kconfig" +rsource "asan/Kconfig" rsource "utilities/Kconfig" endif diff --git a/components/asan/Kconfig b/components/asan/Kconfig new file mode 100644 index 000000000000..3001632e5e9b --- /dev/null +++ b/components/asan/Kconfig @@ -0,0 +1,56 @@ +menuconfig RT_USING_ASAN + bool "Enable AddressSanitizer (heap overflow & use-after-free check)" + default n + depends on RT_USING_HOOK && RT_HOOK_USING_FUNC_PTR && !RT_USING_USERHEAP + 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. + + Heap algorithm support: + - small mem (RT_USING_SMALL_MEM_AS_HEAP): full support, detects + both heap-buffer-overflow and use-after-free. + - slab (RT_USING_SLAB_AS_HEAP) and memheap + (RT_USING_MEMHEAP_AS_HEAP): detects heap-buffer-overflow only. + Their allocators reuse freed blocks for internal metadata written + through instrumented rt_memset/rt_memcpy, so poisoning a whole + freed block would raise false positives; use-after-free is + therefore disabled for these two. + - userheap (RT_USING_USERHEAP): not supported (mutually exclusive). + + if RT_USING_ASAN + config RT_ASAN_SHADOW_SIZE + int "ASan shadow memory size (bytes)" + default 65536 + help + Size of the static shadow memory array. Each byte maps 8 + bytes of the heap, so the checked heap range is + RT_ASAN_SHADOW_SIZE * 8 bytes. + + config RT_ASAN_TRACK_MAX + int "Max number of tracked active allocations" + default 512 + help + Size of the allocation tracking table. Each entry records + one live block (ptr, size, owner thread). Reduce this on + memory-constrained MCUs (e.g. 128 or 64). When the table + is full, further allocations are not tracked (and thus not + diagnosed) but are still unpoisoned for correctness. + + 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/asan/SConscript b/components/asan/SConscript new file mode 100644 index 000000000000..11db7c4cbaff --- /dev/null +++ b/components/asan/SConscript @@ -0,0 +1,13 @@ +from building import * + +cwd = GetCurrentDir() +src = Glob('*.c') +CPPPATH = [cwd] + +# 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, + LOCAL_CFLAGS=' -fno-sanitize=kernel-address') + +Return('group') diff --git a/components/asan/asan.c b/components/asan/asan.c new file mode 100644 index 000000000000..e2974c0fe72c --- /dev/null +++ b/components/asan/asan.c @@ -0,0 +1,537 @@ +/* + * 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 + +#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 is poisoned/unpoisoned via the existing rt_malloc/rt_free + * hooks, which gives heap buffer overflow and use-after-free detection. + */ + +/* ---- 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 */ + +#define ASAN_SHADOW_SCALE 8 +#define ASAN_POISON 0xF8 /* whole granule poisoned */ +#define ASAN_MIN(a, b) ((a) < (b) ? (a) : (b)) + +/* + * Poisoning a freed block enables use-after-free detection. This is only safe + * for allocators whose internal metadata is written by non-instrumented code: + * small mem assigns its header fields directly, but memheap/slab write their + * internal structures (memheap item headers, slab zone structs) through + * instrumented rt_memset/rt_memcpy and place them inside freed blocks, so + * poisoning the whole block would report those allocator-internal writes as + * false positives. For those allocators only the tail redzone (overflow) is + * kept. + */ +#if defined(RT_USING_SMALL_MEM_AS_HEAP) +#define ASAN_POISON_FREED_BLOCK 1 +#else +#define ASAN_POISON_FREED_BLOCK 0 +#endif + +/* ---- allocation tracking table ---- */ +#ifndef RT_ASAN_TRACK_MAX +#define RT_ASAN_TRACK_MAX 512 +#endif + +struct asan_track +{ + rt_uintptr_t ptr; + rt_uint32_t size; + rt_uint8_t used; + char owner[RT_NAME_MAX]; +}; + +static struct asan_track asan_tracks[RT_ASAN_TRACK_MAX]; + +/* most recently freed block, for use-after-free diagnosis */ +static struct asan_track asan_last_freed; + +/* ---- helpers ---- */ +rt_inline rt_bool_t asan_addr_in_range(rt_uintptr_t addr) +{ + return addr >= asan_heap_base && addr < asan_heap_limit; +} + +/* check whether [addr, addr+size) touches any poisoned byte */ +static rt_bool_t asan_range_is_poisoned(rt_uintptr_t addr, rt_size_t size) +{ + rt_uintptr_t a = addr; + rt_uintptr_t end = addr + size; + + if (size == 0) + return RT_FALSE; + + while (a < end) + { + rt_uintptr_t off; + rt_uint8_t s; + rt_size_t n; + + if (!asan_addr_in_range(a)) + return RT_FALSE; /* outside shadow coverage: not checked */ + + off = a - asan_heap_base; + s = asan_shadow[off >> 3]; + + if (s == 0) + { + /* whole granule addressable */ + n = ASAN_SHADOW_SCALE - (off & (ASAN_SHADOW_SCALE - 1)); + } + else if (s >= ASAN_SHADOW_SCALE) + { + return RT_TRUE; /* whole granule poisoned */ + } + else + { + /* partial granule: first s bytes addressable */ + if ((off & (ASAN_SHADOW_SCALE - 1)) >= s) + return RT_TRUE; + n = s - (off & (ASAN_SHADOW_SCALE - 1)); + } + + if (n >= end - a) + return RT_FALSE; /* remaining bytes are addressable */ + a += n; + } + + return RT_FALSE; +} + +static void asan_locate_block(rt_uintptr_t addr) +{ + rt_uint32_t i; + rt_uint32_t best = RT_ASAN_TRACK_MAX; + rt_uintptr_t best_end = 0; + + /* 1. exact match: addr is inside an active block */ + for (i = 0; i < RT_ASAN_TRACK_MAX; i++) + { + if (asan_tracks[i].used && + addr >= asan_tracks[i].ptr && + addr < asan_tracks[i].ptr + asan_tracks[i].size) + { + rt_kprintf("== block : 0x%08x size %d owner %.*s (inside block, offset +%d)\n", + asan_tracks[i].ptr, asan_tracks[i].size, + RT_NAME_MAX, asan_tracks[i].owner, + addr - asan_tracks[i].ptr); + return; + } + } + + /* 2. use-after-free: addr is inside the most recently freed block */ + if (asan_last_freed.used && + addr >= asan_last_freed.ptr && + addr < asan_last_freed.ptr + asan_last_freed.size) + { + rt_kprintf("== block : 0x%08x size %d owner %.*s (USE-AFTER-FREE, offset +%d)\n", + asan_last_freed.ptr, asan_last_freed.size, + RT_NAME_MAX, asan_last_freed.owner, + addr - asan_last_freed.ptr); + return; + } + + /* 3. overflow candidate: the active block whose tail is closest below addr */ + for (i = 0; i < RT_ASAN_TRACK_MAX; i++) + { + rt_uintptr_t blk_end; + + if (!asan_tracks[i].used) + continue; + + blk_end = asan_tracks[i].ptr + asan_tracks[i].size; + if (blk_end <= addr && blk_end >= best_end) + { + best = i; + best_end = blk_end; + } + } + + if (best != RT_ASAN_TRACK_MAX) + { + rt_kprintf("== block : 0x%08x size %d owner %.*s (overflow by %d bytes)\n", + asan_tracks[best].ptr, asan_tracks[best].size, + RT_NAME_MAX, asan_tracks[best].owner, + addr - best_end); + } + 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_kprintf("\n"); + rt_kprintf("=================================================================\n"); + rt_kprintf("== ADDRESS SANITIZER: %s\n", + is_write ? "heap-buffer-overflow on WRITE" : "heap-buffer-overflow on READ"); + rt_kprintf("== address: 0x%08x size: %d\n", addr, size); + rt_kprintf("== pc : 0x%08x\n", pc); + if (self) + rt_kprintf("== thread : %.*s\n", RT_NAME_MAX, self->parent.name); + asan_locate_block(addr); +#ifdef RT_ASAN_BACKTRACE + rt_backtrace(); +#endif + rt_kprintf("=================================================================\n"); +} + +/* ---- instrumented access checks ---- */ +#define ASAN_DEFINE_CHECK(_size, _suffix) \ + void __asan_load##_suffix##_noabort(rt_uintptr_t addr) \ + { \ + if (asan_range_is_poisoned(addr, _size)) \ + asan_report(addr, _size, RT_FALSE, \ + (rt_uintptr_t)__builtin_return_address(0)); \ + } \ + void __asan_store##_suffix##_noabort(rt_uintptr_t addr) \ + { \ + if (asan_range_is_poisoned(addr, _size)) \ + asan_report(addr, _size, RT_TRUE, \ + (rt_uintptr_t)__builtin_return_address(0)); \ + } + +ASAN_DEFINE_CHECK(1, 1) +ASAN_DEFINE_CHECK(2, 2) +ASAN_DEFINE_CHECK(4, 4) +ASAN_DEFINE_CHECK(8, 8) +ASAN_DEFINE_CHECK(16, 16) + +/* variable-length variants */ +void __asan_loadN_noabort(rt_uintptr_t addr, rt_size_t size) +{ + if (asan_range_is_poisoned(addr, size)) + asan_report(addr, size, RT_FALSE, (rt_uintptr_t)__builtin_return_address(0)); +} + +void __asan_storeN_noabort(rt_uintptr_t addr, rt_size_t size) +{ + if (asan_range_is_poisoned(addr, size)) + asan_report(addr, size, RT_TRUE, (rt_uintptr_t)__builtin_return_address(0)); +} + +/* misc symbols referenced by some GCC versions */ +void __asan_init(void) {} +void __asan_handle_no_return(void) {} + +/* ---- poison / unpoison (allocator integration) ---- */ +static void asan_unpoison_range(rt_uintptr_t addr, rt_size_t size) +{ + rt_uintptr_t a = addr; + rt_uintptr_t end = addr + size; + + if (size == 0) + return; + + while (a < end) + { + rt_uintptr_t off; + rt_uint8_t *sh; + rt_size_t n; + rt_uint8_t k; + + if (!asan_addr_in_range(a)) + return; + + off = a - asan_heap_base; + sh = &asan_shadow[off >> 3]; + k = off & (ASAN_SHADOW_SCALE - 1); + n = ASAN_MIN(ASAN_SHADOW_SCALE - k, end - a); + + if (n == ASAN_SHADOW_SCALE) + *sh = 0; /* whole granule addressable */ + else + *sh = (rt_uint8_t)n; /* first n bytes addressable */ + + 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(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_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; + if (rt_thread_self()) + rt_strncpy(asan_tracks[i].owner, rt_thread_self()->parent.name, RT_NAME_MAX - 1); + else + rt_memset(asan_tracks[i].owner, 0, RT_NAME_MAX); + 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 */ +} + +static void asan_malloc_hook(void **ptr, rt_size_t size) +{ + rt_uintptr_t p; + rt_size_t aligned; + + if (!*ptr) + return; + + p = (rt_uintptr_t)*ptr; + aligned = RT_ALIGN(size, ASAN_SHADOW_SCALE); + + /* address reuse: this block was freed before, clear the stale record */ + if (asan_last_freed.used && asan_last_freed.ptr == p) + asan_last_freed.used = 0; + + asan_track_add(p, size); + asan_unpoison_range(p, size); + if (aligned > size) + asan_poison_range(p + size, aligned - size); +} + +static void asan_free_hook(void **ptr) +{ + rt_uintptr_t p; + rt_uint32_t idx; + + if (!*ptr) + return; + + p = (rt_uintptr_t)*ptr; + idx = asan_track_find(p); + if (idx == RT_ASAN_TRACK_MAX) + return; /* unknown block, skip */ + +#if ASAN_POISON_FREED_BLOCK + { + rt_size_t aligned = RT_ALIGN(asan_tracks[idx].size, ASAN_SHADOW_SCALE); + asan_poison_range(p, aligned); /* poison whole block -> use-after-free */ + } +#endif + + /* remember it for use-after-free diagnosis */ + asan_last_freed = asan_tracks[idx]; + asan_last_freed.used = 1; + + asan_tracks[idx].used = 0; +} + +/* rt_realloc frees/moves the old block and allocates a new one without going + * through rt_free/rt_malloc, so its hooks must be handled separately. */ +static rt_uintptr_t asan_realloc_old_ptr; + +static void asan_realloc_entry_hook(void **ptr, rt_size_t size) +{ + RT_UNUSED(size); + asan_realloc_old_ptr = (rt_uintptr_t)*ptr; +} + +static void asan_realloc_exit_hook(void **ptr, rt_size_t size) +{ + rt_uintptr_t p; + rt_size_t aligned; + rt_uint32_t idx; + + if (!*ptr) + return; + + p = (rt_uintptr_t)*ptr; + aligned = RT_ALIGN(size, ASAN_SHADOW_SCALE); + + /* when realloc moves the block, poison the old block so that a stale + * pointer to it is still detected as use-after-free */ + if (asan_realloc_old_ptr && asan_realloc_old_ptr != p) + { + idx = asan_track_find(asan_realloc_old_ptr); + if (idx != RT_ASAN_TRACK_MAX) + { +#if ASAN_POISON_FREED_BLOCK + rt_size_t old_aligned = RT_ALIGN(asan_tracks[idx].size, ASAN_SHADOW_SCALE); + + asan_poison_range(asan_realloc_old_ptr, old_aligned); +#endif + asan_tracks[idx].used = 0; + } + } + + /* address may have been reused internally by the allocator, drop any + * stale use-after-free record for it */ + if (asan_last_freed.used && asan_last_freed.ptr == p) + asan_last_freed.used = 0; + + /* track and unpoison the new block, poison its tail redzone */ + asan_track_add(p, size); + asan_unpoison_range(p, size); + if (aligned > size) + asan_poison_range(p + size, aligned - size); +} + +/* + * Override the weak rt_system_heap_init to capture the heap range and + * install the allocator hooks before the generic 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; + + /* + * The shadow maps one byte per ASAN_SHADOW_SCALE (8) bytes. Heap blocks + * are RT_ALIGN_SIZE (8) aligned, so align the shadow base to the same + * granularity to keep every block boundary on a shadow byte boundary. + * Otherwise (e.g. __bss_end is only 4-aligned) the partial-granule + * state cannot represent an addressable region and false positives occur + * right at block start. + */ + asan_heap_base = RT_ALIGN(begin, ASAN_SHADOW_SCALE); + asan_heap_limit = 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)); + + /* install allocator hooks */ + rt_malloc_sethook(asan_malloc_hook); + rt_free_sethook(asan_free_hook); + rt_realloc_set_entry_hook(asan_realloc_entry_hook); + rt_realloc_set_exit_hook(asan_realloc_exit_hook); + + /* run the original heap init */ + rt_system_heap_init_generic(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; + + rt_kprintf("\n-- AddressSanitizer status --\n"); + rt_kprintf("shadow : %p, %d bytes\n", asan_shadow, sizeof(asan_shadow)); + rt_kprintf("coverage : 0x%08x - 0x%08x (%d bytes)\n", + asan_heap_base, asan_heap_limit, + asan_heap_limit - asan_heap_base); + + if (asan_last_freed.used) + { + rt_kprintf("last free: 0x%08x size %d owner %.*s\n", + asan_last_freed.ptr, asan_last_freed.size, + RT_NAME_MAX, asan_last_freed.owner); + } + else + { + rt_kprintf("last free: (none)\n"); + } + + rt_kprintf("\n-- active allocations --\n"); + for (i = 0; i < RT_ASAN_TRACK_MAX; i++) + { + if (asan_tracks[i].used) + { + active++; + rt_kprintf(" 0x%08x %6d %.*s\n", + asan_tracks[i].ptr, asan_tracks[i].size, + RT_NAME_MAX, asan_tracks[i].owner); + } + } + rt_kprintf("total: %d 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/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/tools/building.py b/tools/building.py index 125da8521e76..d5250dc87e56 100644 --- a/tools/building.py +++ b/tools/building.py @@ -378,6 +378,12 @@ def PrepareBuilding(env, root_directory, has_libcpu=False, remove_components = [ if rtconfig.PLATFORM in ['gcc'] and str(env['LINKFLAGS']).find('nano.specs') != -1: env.AppendUnique(CPPDEFINES = ['_REENT_SMALL']) + # AddressSanitizer (kernel-address): instrument memory accesses. The + # runtime is provided by components/asan and does not need libasan. + if rtconfig.PLATFORM in ['gcc'] and 'RT_USING_ASAN' in BuildOptions: + env.Append(CFLAGS=' -fsanitize=kernel-address -fno-omit-frame-pointer') + env.Append(LINKFLAGS=' -fsanitize=kernel-address') + attach_global_macros = GetOption('global-macros') if attach_global_macros: attach_global_macros = attach_global_macros.split(',') From da78c40dc661654f9d10a971cb666c4f2084e271 Mon Sep 17 00:00:00 2001 From: hanzhijian Date: Mon, 31 Aug 2026 01:05:49 +0800 Subject: [PATCH 2/7] style: format asan.c with clang-format --- components/asan/asan.c | 138 ++++++++++++++++++++++++++++------------- 1 file changed, 96 insertions(+), 42 deletions(-) diff --git a/components/asan/asan.c b/components/asan/asan.c index e2974c0fe72c..d41e0fe1e311 100644 --- a/components/asan/asan.c +++ b/components/asan/asan.c @@ -32,11 +32,11 @@ /* ---- 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 */ +static rt_uint8_t asan_shadow[RT_ASAN_SHADOW_SIZE]; /* 8 bytes -> 1 byte */ -#define ASAN_SHADOW_SCALE 8 -#define ASAN_POISON 0xF8 /* whole granule poisoned */ -#define ASAN_MIN(a, b) ((a) < (b) ? (a) : (b)) +#define ASAN_SHADOW_SCALE 8 +#define ASAN_POISON 0xF8 /* whole granule poisoned */ +#define ASAN_MIN(a, b) ((a) < (b) ? (a) : (b)) /* * Poisoning a freed block enables use-after-free detection. This is only safe @@ -49,22 +49,22 @@ static rt_uint8_t asan_shadow[RT_ASAN_SHADOW_SIZE]; /* 8 bytes -> 1 byt * kept. */ #if defined(RT_USING_SMALL_MEM_AS_HEAP) -#define ASAN_POISON_FREED_BLOCK 1 +#define ASAN_POISON_FREED_BLOCK 1 #else -#define ASAN_POISON_FREED_BLOCK 0 +#define ASAN_POISON_FREED_BLOCK 0 #endif /* ---- allocation tracking table ---- */ #ifndef RT_ASAN_TRACK_MAX -#define RT_ASAN_TRACK_MAX 512 +#define RT_ASAN_TRACK_MAX 512 #endif struct asan_track { rt_uintptr_t ptr; - rt_uint32_t size; - rt_uint8_t used; - char owner[RT_NAME_MAX]; + rt_uint32_t size; + rt_uint8_t used; + char owner[RT_NAME_MAX]; }; static struct asan_track asan_tracks[RT_ASAN_TRACK_MAX]; @@ -85,16 +85,20 @@ static rt_bool_t asan_range_is_poisoned(rt_uintptr_t addr, rt_size_t size) rt_uintptr_t end = addr + size; if (size == 0) + { return RT_FALSE; + } while (a < end) { rt_uintptr_t off; - rt_uint8_t s; - rt_size_t n; + rt_uint8_t s; + rt_size_t n; if (!asan_addr_in_range(a)) + { return RT_FALSE; /* outside shadow coverage: not checked */ + } off = a - asan_heap_base; s = asan_shadow[off >> 3]; @@ -112,12 +116,16 @@ static rt_bool_t asan_range_is_poisoned(rt_uintptr_t addr, rt_size_t size) { /* partial granule: first s bytes addressable */ if ((off & (ASAN_SHADOW_SCALE - 1)) >= s) + { return RT_TRUE; + } n = s - (off & (ASAN_SHADOW_SCALE - 1)); } if (n >= end - a) + { return RT_FALSE; /* remaining bytes are addressable */ + } a += n; } @@ -163,7 +171,9 @@ static void asan_locate_block(rt_uintptr_t addr) rt_uintptr_t blk_end; if (!asan_tracks[i].used) + { continue; + } blk_end = asan_tracks[i].ptr + asan_tracks[i].size; if (blk_end <= addr && blk_end >= best_end) @@ -197,7 +207,9 @@ static void asan_report(rt_uintptr_t addr, rt_size_t size, rt_bool_t is_write, r rt_kprintf("== address: 0x%08x size: %d\n", addr, size); rt_kprintf("== pc : 0x%08x\n", pc); if (self) + { rt_kprintf("== thread : %.*s\n", RT_NAME_MAX, self->parent.name); + } asan_locate_block(addr); #ifdef RT_ASAN_BACKTRACE rt_backtrace(); @@ -206,18 +218,18 @@ static void asan_report(rt_uintptr_t addr, rt_size_t size, rt_bool_t is_write, r } /* ---- 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)); \ +#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) @@ -230,13 +242,17 @@ ASAN_DEFINE_CHECK(16, 16) 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 */ @@ -250,27 +266,35 @@ static void asan_unpoison_range(rt_uintptr_t addr, rt_size_t size) 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; + 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(ASAN_SHADOW_SCALE - k, end - a); + sh = &asan_shadow[off >> 3]; + k = off & (ASAN_SHADOW_SCALE - 1); + n = ASAN_MIN(ASAN_SHADOW_SCALE - k, end - a); if (n == ASAN_SHADOW_SCALE) + { *sh = 0; /* whole granule addressable */ + } else + { *sh = (rt_uint8_t)n; /* first n bytes addressable */ + } a += n; } @@ -282,27 +306,35 @@ static void asan_poison_range(rt_uintptr_t addr, rt_size_t size) 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; + 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(ASAN_SHADOW_SCALE - k, end - a); + sh = &asan_shadow[off >> 3]; + k = off & (ASAN_SHADOW_SCALE - 1); + n = ASAN_MIN(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; } @@ -328,13 +360,17 @@ static void asan_track_add(rt_uintptr_t ptr, rt_size_t size) { if (!asan_tracks[i].used) { - asan_tracks[i].ptr = ptr; + asan_tracks[i].ptr = ptr; asan_tracks[i].size = size; asan_tracks[i].used = 1; if (rt_thread_self()) + { rt_strncpy(asan_tracks[i].owner, rt_thread_self()->parent.name, RT_NAME_MAX - 1); + } else + { rt_memset(asan_tracks[i].owner, 0, RT_NAME_MAX); + } return; } } @@ -347,7 +383,9 @@ static rt_uint32_t asan_track_find(rt_uintptr_t ptr) 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 */ @@ -356,36 +394,46 @@ static rt_uint32_t asan_track_find(rt_uintptr_t ptr) static void asan_malloc_hook(void **ptr, rt_size_t size) { rt_uintptr_t p; - rt_size_t aligned; + rt_size_t aligned; if (!*ptr) + { return; + } p = (rt_uintptr_t)*ptr; aligned = RT_ALIGN(size, ASAN_SHADOW_SCALE); /* address reuse: this block was freed before, clear the stale record */ if (asan_last_freed.used && asan_last_freed.ptr == p) + { asan_last_freed.used = 0; + } asan_track_add(p, size); asan_unpoison_range(p, size); if (aligned > size) + { asan_poison_range(p + size, aligned - size); + } } static void asan_free_hook(void **ptr) { rt_uintptr_t p; - rt_uint32_t idx; + rt_uint32_t idx; if (!*ptr) + { return; + } p = (rt_uintptr_t)*ptr; idx = asan_track_find(p); if (idx == RT_ASAN_TRACK_MAX) + { return; /* unknown block, skip */ + } #if ASAN_POISON_FREED_BLOCK { @@ -414,11 +462,13 @@ static void asan_realloc_entry_hook(void **ptr, rt_size_t size) static void asan_realloc_exit_hook(void **ptr, rt_size_t size) { rt_uintptr_t p; - rt_size_t aligned; - rt_uint32_t idx; + rt_size_t aligned; + rt_uint32_t idx; if (!*ptr) + { return; + } p = (rt_uintptr_t)*ptr; aligned = RT_ALIGN(size, ASAN_SHADOW_SCALE); @@ -442,13 +492,17 @@ static void asan_realloc_exit_hook(void **ptr, rt_size_t size) /* address may have been reused internally by the allocator, drop any * stale use-after-free record for it */ if (asan_last_freed.used && asan_last_freed.ptr == p) + { asan_last_freed.used = 0; + } /* track and unpoison the new block, poison its tail redzone */ asan_track_add(p, size); asan_unpoison_range(p, size); if (aligned > size) + { asan_poison_range(p + size, aligned - size); + } } /* @@ -458,7 +512,7 @@ static void asan_realloc_exit_hook(void **ptr, rt_size_t size) 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; + rt_uintptr_t end = (rt_uintptr_t)end_addr; /* * The shadow maps one byte per ASAN_SHADOW_SCALE (8) bytes. Heap blocks @@ -468,9 +522,9 @@ void rt_system_heap_init(void *begin_addr, void *end_addr) * state cannot represent an addressable region and false positives occur * right at block start. */ - asan_heap_base = RT_ALIGN(begin, ASAN_SHADOW_SCALE); + asan_heap_base = RT_ALIGN(begin, ASAN_SHADOW_SCALE); asan_heap_limit = ASAN_MIN(end, asan_heap_base + - (rt_uintptr_t)sizeof(asan_shadow) * ASAN_SHADOW_SCALE); + (rt_uintptr_t)sizeof(asan_shadow) * ASAN_SHADOW_SCALE); /* * Start with everything addressable: the heap allocators store their own From 4416a46da8b6a7ea3111b1545a273f05a9fa3307 Mon Sep 17 00:00:00 2001 From: hanzhijian Date: Mon, 31 Aug 2026 10:25:29 +0800 Subject: [PATCH 3/7] components: move asan under utilities --- components/Kconfig | 1 - components/utilities/Kconfig | 1 + components/{ => utilities}/asan/Kconfig | 0 components/{ => utilities}/asan/SConscript | 0 components/{ => utilities}/asan/asan.c | 0 5 files changed, 1 insertion(+), 1 deletion(-) rename components/{ => utilities}/asan/Kconfig (100%) rename components/{ => utilities}/asan/SConscript (100%) rename components/{ => utilities}/asan/asan.c (100%) diff --git a/components/Kconfig b/components/Kconfig index 9f19018e93f8..accc38c67434 100644 --- a/components/Kconfig +++ b/components/Kconfig @@ -36,7 +36,6 @@ rsource "drivers/Kconfig" rsource "libc/Kconfig" rsource "net/Kconfig" rsource "mprotect/Kconfig" -rsource "asan/Kconfig" rsource "utilities/Kconfig" endif 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/asan/Kconfig b/components/utilities/asan/Kconfig similarity index 100% rename from components/asan/Kconfig rename to components/utilities/asan/Kconfig diff --git a/components/asan/SConscript b/components/utilities/asan/SConscript similarity index 100% rename from components/asan/SConscript rename to components/utilities/asan/SConscript diff --git a/components/asan/asan.c b/components/utilities/asan/asan.c similarity index 100% rename from components/asan/asan.c rename to components/utilities/asan/asan.c From 5f7ef13dad08e997eb0082175f8535368377b7c7 Mon Sep 17 00:00:00 2001 From: hanzhijian Date: Mon, 31 Aug 2026 10:29:57 +0800 Subject: [PATCH 4/7] components: fix asan path in comment --- tools/building.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/building.py b/tools/building.py index d5250dc87e56..44ea243d1ae7 100644 --- a/tools/building.py +++ b/tools/building.py @@ -379,7 +379,7 @@ def PrepareBuilding(env, root_directory, has_libcpu=False, remove_components = [ env.AppendUnique(CPPDEFINES = ['_REENT_SMALL']) # AddressSanitizer (kernel-address): instrument memory accesses. The - # runtime is provided by components/asan and does not need libasan. + # runtime is provided by components/utilities/asan and does not need libasan. if rtconfig.PLATFORM in ['gcc'] and 'RT_USING_ASAN' in BuildOptions: env.Append(CFLAGS=' -fsanitize=kernel-address -fno-omit-frame-pointer') env.Append(LINKFLAGS=' -fsanitize=kernel-address') From feebc5a8bb9f4d5a75ad23df0b61431dcbefc6cb Mon Sep 17 00:00:00 2001 From: hanzhijian Date: Thu, 3 Sep 2026 11:03:40 +0800 Subject: [PATCH 5/7] [asan] expose report counter and UAF capability Add rt_asan_report_count_get() to let the utest harness verify that a deliberate violation is actually detected (ASan uses the GCC _noabort variant, so a hit only prints and does not abort). Also move the use-after-free capability flag into asan.h as RT_ASAN_HAS_UAF_DETECTION so both the runtime and tests share a single source of truth. --- components/utilities/asan/asan.c | 32 ++++++++++------------ components/utilities/asan/asan.h | 46 ++++++++++++++++++++++++++++++++ 2 files changed, 60 insertions(+), 18 deletions(-) create mode 100644 components/utilities/asan/asan.h diff --git a/components/utilities/asan/asan.c b/components/utilities/asan/asan.c index d41e0fe1e311..fb52e406ab50 100644 --- a/components/utilities/asan/asan.c +++ b/components/utilities/asan/asan.c @@ -13,6 +13,8 @@ #ifdef RT_USING_ASAN +#include "asan.h" + #define DBG_TAG "asan" #define DBG_LVL DBG_INFO #include @@ -34,26 +36,18 @@ static rt_uintptr_t asan_heap_base; /* first checked ad static rt_uintptr_t asan_heap_limit; /* base + coverage */ static rt_uint8_t asan_shadow[RT_ASAN_SHADOW_SIZE]; /* 8 bytes -> 1 byte */ +/* 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) +{ + return asan_report_count; +} + #define ASAN_SHADOW_SCALE 8 #define ASAN_POISON 0xF8 /* whole granule poisoned */ #define ASAN_MIN(a, b) ((a) < (b) ? (a) : (b)) -/* - * Poisoning a freed block enables use-after-free detection. This is only safe - * for allocators whose internal metadata is written by non-instrumented code: - * small mem assigns its header fields directly, but memheap/slab write their - * internal structures (memheap item headers, slab zone structs) through - * instrumented rt_memset/rt_memcpy and place them inside freed blocks, so - * poisoning the whole block would report those allocator-internal writes as - * false positives. For those allocators only the tail redzone (overflow) is - * kept. - */ -#if defined(RT_USING_SMALL_MEM_AS_HEAP) -#define ASAN_POISON_FREED_BLOCK 1 -#else -#define ASAN_POISON_FREED_BLOCK 0 -#endif - /* ---- allocation tracking table ---- */ #ifndef RT_ASAN_TRACK_MAX #define RT_ASAN_TRACK_MAX 512 @@ -200,6 +194,8 @@ static void asan_report(rt_uintptr_t addr, rt_size_t size, rt_bool_t is_write, r { rt_thread_t self = rt_thread_self(); + asan_report_count++; + rt_kprintf("\n"); rt_kprintf("=================================================================\n"); rt_kprintf("== ADDRESS SANITIZER: %s\n", @@ -435,7 +431,7 @@ static void asan_free_hook(void **ptr) return; /* unknown block, skip */ } -#if ASAN_POISON_FREED_BLOCK +#if RT_ASAN_HAS_UAF_DETECTION { rt_size_t aligned = RT_ALIGN(asan_tracks[idx].size, ASAN_SHADOW_SCALE); asan_poison_range(p, aligned); /* poison whole block -> use-after-free */ @@ -480,7 +476,7 @@ static void asan_realloc_exit_hook(void **ptr, rt_size_t size) idx = asan_track_find(asan_realloc_old_ptr); if (idx != RT_ASAN_TRACK_MAX) { -#if ASAN_POISON_FREED_BLOCK +#if RT_ASAN_HAS_UAF_DETECTION rt_size_t old_aligned = RT_ALIGN(asan_tracks[idx].size, ASAN_SHADOW_SCALE); asan_poison_range(asan_realloc_old_ptr, old_aligned); diff --git a/components/utilities/asan/asan.h b/components/utilities/asan/asan.h new file mode 100644 index 000000000000..5c9b5586669e --- /dev/null +++ b/components/utilities/asan/asan.h @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2006-2024, RT-Thread Development Team + * + * SPDX-License-Identifier: Apache-2.0 + * + * Change Logs: + * Date Author Notes + * 2026-08-30 RT-Thread the first version + */ + +#ifndef __ASAN_H__ +#define __ASAN_H__ + +#include + +/* + * Use-after-free detection requires poisoning a whole freed block. This is only + * safe for allocators whose internal metadata is written by non-instrumented + * code (small mem). memheap/slab reuse freed blocks for metadata written via + * instrumented rt_memset/rt_memcpy, so their freed blocks must not be poisoned. + */ +#if defined(RT_USING_SMALL_MEM_AS_HEAP) +#define RT_ASAN_HAS_UAF_DETECTION 1 +#else +#define RT_ASAN_HAS_UAF_DETECTION 0 +#endif + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @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__ */ From efc3c5ad0f4d71ecdf4a808728b37ae8893503e1 Mon Sep 17 00:00:00 2001 From: hanzhijian Date: Thu, 3 Sep 2026 11:04:10 +0800 Subject: [PATCH 6/7] [klibc] exclude memcpy/memset/memmove from asan instrumentation rt_memcpy/rt_memset/rt_memmove copy word-at-a-time and may legally touch a few bytes past the requested count (word-aligned bulk loops). Under -fsanitize=kernel-address those accesses fall into poisoned heap redzones and raise false positives (notably during rt_realloc block migration). Mark them no_sanitize_address, mirroring how KASAN treats the same helpers. --- src/klibc/kstring.c | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/klibc/kstring.c b/src/klibc/kstring.c index b6d553ffa34d..aac9894a33b1 100644 --- a/src/klibc/kstring.c +++ b/src/klibc/kstring.c @@ -10,6 +10,18 @@ #include +/* + * AddressSanitizer: rt_memcpy/rt_memset/rt_memmove copy word-at-a-time and may + * legally read/write a few bytes past the requested byte count (word-aligned + * bulk loops). When instrumented these accesses fall into poisoned redzones and + * raise false positives, so disable instrumentation for them. + */ +#ifdef RT_USING_ASAN +#define RT_KLIB_NO_ASAN __attribute__((no_sanitize_address)) +#else +#define RT_KLIB_NO_ASAN +#endif + #if defined(RT_KLIBC_USING_LIBC_MEMSET) || \ defined(RT_KLIBC_USING_LIBC_MEMCPY) || \ defined(RT_KLIBC_USING_LIBC_MEMMOVE) || \ @@ -36,6 +48,7 @@ * @return The address of source memory. */ #ifndef RT_KLIBC_USING_USER_MEMSET +RT_KLIB_NO_ASAN void *rt_memset(void *s, int c, size_t count) { #if defined(RT_KLIBC_USING_LIBC_MEMSET) @@ -121,6 +134,7 @@ RTM_EXPORT(rt_memset); * @return The address of destination memory */ #ifndef RT_KLIBC_USING_USER_MEMCPY +RT_KLIB_NO_ASAN void *rt_memcpy(void *dst, const void *src, size_t count) { #if defined(RT_KLIBC_USING_LIBC_MEMCPY) @@ -211,6 +225,7 @@ RTM_EXPORT(rt_memcpy); * @return The address of destination memory. */ #ifndef RT_KLIBC_USING_USER_MEMMOVE +RT_KLIB_NO_ASAN void *rt_memmove(void *dest, const void *src, size_t n) { #ifdef RT_KLIBC_USING_LIBC_MEMMOVE From 6bf8db6cd0c7100dea022cbef204778c6b03dd37 Mon Sep 17 00:00:00 2001 From: hanzhijian Date: Thu, 3 Sep 2026 11:04:37 +0800 Subject: [PATCH 7/7] [asan] add utest testcase and CI guard Add a utest testcase (components.asan_tc) that exercises the ASan heap detection on a real target, plus a CI config that both compiles and runs it on qemu-vexpress-a9 via utest_auto_run. Test scenarios: - heap-buffer-overflow write / read - in-bounds access (no false positive) - realloc overflow - use-after-free read / write (small mem only) --- .github/utest/configs/components/asan.cfg | 8 + .github/workflows/utest_auto_run.yml | 2 + src/utest/Kconfig | 5 + src/utest/SConscript | 3 + src/utest/asan_tc.c | 236 ++++++++++++++++++++++ 5 files changed, 254 insertions(+) create mode 100644 .github/utest/configs/components/asan.cfg create mode 100644 src/utest/asan_tc.c diff --git a/.github/utest/configs/components/asan.cfg b/.github/utest/configs/components/asan.cfg new file mode 100644 index 000000000000..751ef66bc9b4 --- /dev/null +++ b/.github/utest/configs/components/asan.cfg @@ -0,0 +1,8 @@ +# dependencies +CONFIG_RT_CONSOLEBUF_SIZE=1024 +CONFIG_RT_USING_CI_ACTION=y + +CONFIG_RT_USING_ASAN=y +CONFIG_RT_ASAN_SHADOW_SIZE=65536 +CONFIG_RT_ASAN_TRACK_MAX=512 +CONFIG_RT_UTEST_ASAN=y diff --git a/.github/workflows/utest_auto_run.yml b/.github/workflows/utest_auto_run.yml index 9a4915737b6a..087b747af820 100644 --- a/.github/workflows/utest_auto_run.yml +++ b/.github/workflows/utest_auto_run.yml @@ -152,6 +152,8 @@ jobs: config_file: "components/dfs.cfg" - platform: { UTEST: "A9", RTT_BSP: "bsp/qemu-vexpress-a9", QEMU_ARCH: "arm", QEMU_MACHINE: "vexpress-a9", SD_FILE: "sd.bin", KERNEL: "standard", "SMP_RUN":"" } config_file: "components/libc.cfg" + - platform: { UTEST: "A9", RTT_BSP: "bsp/qemu-vexpress-a9", QEMU_ARCH: "arm", QEMU_MACHINE: "vexpress-a9", SD_FILE: "sd.bin", KERNEL: "standard", "SMP_RUN":"" } + config_file: "components/asan.cfg" env: TEST_QEMU_ARCH: ${{ matrix.platform.QEMU_ARCH }} diff --git a/src/utest/Kconfig b/src/utest/Kconfig index 7f478c591ffa..0a566a5769f4 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_ASAN + bool "AddressSanitizer Test" + default n + depends on RT_USING_ASAN + rsource "perf/Kconfig" rsource "../klibc/utest/Kconfig" diff --git a/src/utest/SConscript b/src/utest/SConscript index 52aca3dcf698..7768e6e6df72 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_ASAN']): + src += ['asan_tc.c'] + # Stressful testcase for scheduler (MP/UP) if GetDepend(['RT_UTEST_SCHEDULER']): src += ['sched_timeout_race_tc.c'] diff --git a/src/utest/asan_tc.c b/src/utest/asan_tc.c new file mode 100644 index 000000000000..14cf9171240c --- /dev/null +++ b/src/utest/asan_tc.c @@ -0,0 +1,236 @@ +/* + * 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 occupies [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 occupies [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; + 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(p); + + uassert_true(after > before); +} + +static void test_asan_overflow_read(void) +{ + rt_uint32_t before; + rt_uint32_t after; + volatile char v; + 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(p); + + uassert_true(after > before); +} + +static void test_asan_no_false_positive(void) +{ + rt_uint32_t before; + rt_uint32_t after; + 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(p); + + uassert_int_equal(after, before); +} + +static void test_asan_realloc_overflow(void) +{ + rt_uint32_t before; + rt_uint32_t after; + char *p; + char *q; + + p = (char *)rt_malloc(10); + uassert_not_null(p); + if (!p) + { + return; + } + + q = (char *)rt_realloc(p, 20); + uassert_not_null(q); + if (!q) + { + rt_free(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(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; + char *p; + + p = (char *)rt_malloc(10); + uassert_not_null(p); + if (!p) + { + return; + } + + rt_free(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; + char *p; + + p = (char *)rt_malloc(10); + uassert_not_null(p); + if (!p) + { + return; + } + + rt_free(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 */ + +static void testcase(void) +{ + UTEST_UNIT_RUN(test_asan_overflow_write); + UTEST_UNIT_RUN(test_asan_overflow_read); + UTEST_UNIT_RUN(test_asan_no_false_positive); + UTEST_UNIT_RUN(test_asan_realloc_overflow); +#if RT_ASAN_HAS_UAF_DETECTION + UTEST_UNIT_RUN(test_asan_uaf_read); + UTEST_UNIT_RUN(test_asan_uaf_write); +#endif +} + +UTEST_TC_EXPORT(testcase, "components.asan_tc", utest_tc_init, utest_tc_cleanup, 1000);