diff --git a/.github/utest/configs/components/asan.cfg b/.github/utest/configs/components/asan.cfg new file mode 100644 index 000000000000..751ef66bc9b4 --- /dev/null +++ b/.github/utest/configs/components/asan.cfg @@ -0,0 +1,8 @@ +# dependencies +CONFIG_RT_CONSOLEBUF_SIZE=1024 +CONFIG_RT_USING_CI_ACTION=y + +CONFIG_RT_USING_ASAN=y +CONFIG_RT_ASAN_SHADOW_SIZE=65536 +CONFIG_RT_ASAN_TRACK_MAX=512 +CONFIG_RT_UTEST_ASAN=y diff --git a/.github/workflows/utest_auto_run.yml b/.github/workflows/utest_auto_run.yml index 9a4915737b6a..087b747af820 100644 --- a/.github/workflows/utest_auto_run.yml +++ b/.github/workflows/utest_auto_run.yml @@ -152,6 +152,8 @@ jobs: config_file: "components/dfs.cfg" - platform: { UTEST: "A9", RTT_BSP: "bsp/qemu-vexpress-a9", QEMU_ARCH: "arm", QEMU_MACHINE: "vexpress-a9", SD_FILE: "sd.bin", KERNEL: "standard", "SMP_RUN":"" } config_file: "components/libc.cfg" + - platform: { UTEST: "A9", RTT_BSP: "bsp/qemu-vexpress-a9", QEMU_ARCH: "arm", QEMU_MACHINE: "vexpress-a9", SD_FILE: "sd.bin", KERNEL: "standard", "SMP_RUN":"" } + config_file: "components/asan.cfg" env: TEST_QEMU_ARCH: ${{ matrix.platform.QEMU_ARCH }} diff --git a/components/utilities/Kconfig b/components/utilities/Kconfig index c32cd692bde6..d2dc05c0bbc1 100644 --- a/components/utilities/Kconfig +++ b/components/utilities/Kconfig @@ -244,5 +244,6 @@ config RT_USING_RESOURCE_ID rsource "libadt/Kconfig" rsource "rt-link/Kconfig" +rsource "asan/Kconfig" endmenu diff --git a/components/utilities/asan/Kconfig b/components/utilities/asan/Kconfig new file mode 100644 index 000000000000..3001632e5e9b --- /dev/null +++ b/components/utilities/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/utilities/asan/SConscript b/components/utilities/asan/SConscript new file mode 100644 index 000000000000..11db7c4cbaff --- /dev/null +++ b/components/utilities/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/utilities/asan/asan.c b/components/utilities/asan/asan.c new file mode 100644 index 000000000000..fb52e406ab50 --- /dev/null +++ b/components/utilities/asan/asan.c @@ -0,0 +1,587 @@ +/* + * 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 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 */ + +/* 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)) + +/* ---- 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(); + + asan_report_count++; + + 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 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 */ + } +#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 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); +#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/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__ */ diff --git a/src/SConscript b/src/SConscript index 7b2dec5e4ce1..4f6dc3fd078a 100644 --- a/src/SConscript +++ b/src/SConscript @@ -28,6 +28,17 @@ if GetDepend('RT_USING_SMP') == False: else: SrcRemove(src, ['cpu_up.c', 'scheduler_up.c']) +# AddressSanitizer: heap allocators keep their metadata (headers, free lists) +# inside the heap region, so instrumenting them would report their own header +# accesses as false positives. Move them to a separate non-instrumented group. +asan_alloc_src = [] +if GetDepend('RT_USING_ASAN'): + for alloc_name in ['mem.c', 'memheap.c', 'slab.c']: + matched = [x for x in src if os.path.basename(x.rstr()) == alloc_name] + if matched: + asan_alloc_src += matched + SrcRemove(src, [alloc_name]) + LOCAL_CFLAGS = '' LINKFLAGS = '' @@ -59,6 +70,13 @@ else: LINKFLAGS=LINKFLAGS, LOCAL_CFLAGS=LOCAL_CFLAGS, CPPDEFINES=['__RTTHREAD__'], LOCAL_CPPDEFINES=['__RT_KERNEL_SOURCE__']) +# AddressSanitizer: build heap allocators without instrumentation. +if GetDepend('RT_USING_ASAN') and asan_alloc_src: + group = group + DefineGroup('KernelAlloc', asan_alloc_src, depend=['RT_USING_ASAN'], + CPPPATH=inc, CPPDEFINES=['__RTTHREAD__'], + LOCAL_CPPDEFINES=['__RT_KERNEL_SOURCE__'], + LOCAL_CFLAGS=' -fno-sanitize=kernel-address') + list = os.listdir(cwd) for item in list: if os.path.isfile(os.path.join(cwd, item, 'SConscript')): diff --git a/src/klibc/kstring.c b/src/klibc/kstring.c index b6d553ffa34d..aac9894a33b1 100644 --- a/src/klibc/kstring.c +++ b/src/klibc/kstring.c @@ -10,6 +10,18 @@ #include +/* + * AddressSanitizer: rt_memcpy/rt_memset/rt_memmove copy word-at-a-time and may + * legally read/write a few bytes past the requested byte count (word-aligned + * bulk loops). When instrumented these accesses fall into poisoned redzones and + * raise false positives, so disable instrumentation for them. + */ +#ifdef RT_USING_ASAN +#define RT_KLIB_NO_ASAN __attribute__((no_sanitize_address)) +#else +#define RT_KLIB_NO_ASAN +#endif + #if defined(RT_KLIBC_USING_LIBC_MEMSET) || \ defined(RT_KLIBC_USING_LIBC_MEMCPY) || \ defined(RT_KLIBC_USING_LIBC_MEMMOVE) || \ @@ -36,6 +48,7 @@ * @return The address of source memory. */ #ifndef RT_KLIBC_USING_USER_MEMSET +RT_KLIB_NO_ASAN void *rt_memset(void *s, int c, size_t count) { #if defined(RT_KLIBC_USING_LIBC_MEMSET) @@ -121,6 +134,7 @@ RTM_EXPORT(rt_memset); * @return The address of destination memory */ #ifndef RT_KLIBC_USING_USER_MEMCPY +RT_KLIB_NO_ASAN void *rt_memcpy(void *dst, const void *src, size_t count) { #if defined(RT_KLIBC_USING_LIBC_MEMCPY) @@ -211,6 +225,7 @@ RTM_EXPORT(rt_memcpy); * @return The address of destination memory. */ #ifndef RT_KLIBC_USING_USER_MEMMOVE +RT_KLIB_NO_ASAN void *rt_memmove(void *dest, const void *src, size_t n) { #ifdef RT_KLIBC_USING_LIBC_MEMMOVE diff --git a/src/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); diff --git a/tools/building.py b/tools/building.py index 125da8521e76..44ea243d1ae7 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/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') + attach_global_macros = GetOption('global-macros') if attach_global_macros: attach_global_macros = attach_global_macros.split(',')