Skip to content

[components][asan] Add runtime AddressSanitizer for heap - #11772

Open
RogerNext wants to merge 10 commits into
RT-Thread:masterfrom
RogerNext:agent/rt-asan
Open

[components][asan] Add runtime AddressSanitizer for heap#11772
RogerNext wants to merge 10 commits into
RT-Thread:masterfrom
RogerNext:agent/rt-asan

Conversation

@RogerNext

Copy link
Copy Markdown
Contributor

Description / 描述

Add a heap-only runtime AddressSanitizer (kernel-address) 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.

为 RT-Thread 新增仅针对堆的运行时 AddressSanitizer(kernel-address)。通过 GCC 的 -fsanitize=kernel-address 对内存访问插桩,在运行时检测 堆缓冲区越界(heap-buffer-overflow)释放后使用(use-after-free),并提供 FinSH asan_info 命令进行交互式诊断。

Why / 为什么需要

RT-Thread 在 MCU 目标上缺乏运行时内存安全检查(ASan 之前仅存在于 x86 模拟器)。堆越界与 use-after-free 是最常见的嵌入式内存 bug,本组件提供目标机上的运行时检测,且报告包含触发线程名与内存块归属信息。

What / 修改了哪些文件

  • components/asan/Kconfig:注册 RT_USING_ASAN(含 shadow 大小 / track 表大小 / backtrace 开关)
  • components/asan/SConscript:runtime 自身以 -fno-sanitize=kernel-address 编译避免递归
  • components/asan/asan.c:shadow memory + 插桩访问检查 + 报告 + malloc/free/realloc hook
  • components/Kconfigrsource "asan/Kconfig"
  • src/SConscript:将 mem/memheap/slab 分配器移入非插桩编译组
  • tools/building.py:GCC 下注入 -fsanitize=kernel-address

Heap algorithm support / 堆算法支持

堆类型 越界检测 use-after-free
small mem (RT_USING_SMALL_MEM_AS_HEAP)
slab (RT_USING_SLAB_AS_HEAP) ❌(元数据走插桩 rt_memset,避免误报)
memheap (RT_USING_MEMHEAP_AS_HEAP) ❌(同上)
userheap (RT_USING_USERHEAP) 互斥(Kconfig 已排除)

Verification / 验证

  • qemu-vexpress-a9:small mem / slab / memheap 三种堆编译 + 运行验证
  • 真机 STM32F407ZGT6:asan_test_overflow(overflow by 2 bytes)、asan_test_uaf(USE-AFTER-FREE, offset +0)、asan_info 全部命中
  • 编译器兼容性:GCC 8+,ARM(M0/M3/M4/M7/M33/A7/A9/A53)与 RISC-V(rv32/rv64)实测支持 kernel-address

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.
@github-actions

Copy link
Copy Markdown

👋 感谢您对 RT-Thread 的贡献!Thank you for your contribution to RT-Thread!

为确保代码符合 RT-Thread 的编码规范,请在你的仓库中执行以下步骤运行代码格式化工作流(如果格式化CI运行失败)。
To ensure your code complies with RT-Thread's coding style, please run the code formatting workflow by following the steps below (If the formatting of CI fails to run).


🛠 操作步骤 | Steps

  1. 前往 Actions 页面 | Go to the Actions page
    点击进入工作流 → | Click to open workflow →

  2. 点击 Run workflow | Click Run workflow

  • Use workflow from 保持默认分支(通常为 master
    Keep the default branch (usually master) in Use workflow from
  • branch 输入框填写 PR 分支 agent/rt-asan
    Enter PR branch agent/rt-asan in the branch field
  • 设置需排除的文件/目录(目录请以"/"结尾)
    Set files/directories to exclude (directories should end with "/")
  1. 等待工作流完成 | Wait for the workflow to complete
    格式化后的代码将作为独立提交推送至你的分支。
    The formatting changes will be pushed to your branch as a separate commit.

完成后,提交将自动更新至 agent/rt-asan 分支,关联的 Pull Request 也会同步更新。
Once completed, commits will be pushed to the agent/rt-asan branch automatically, and the related Pull Request will be updated.

如有问题欢迎联系我们,再次感谢您的贡献!💐
If you have any questions, feel free to reach out. Thanks again for your contribution!

@github-actions github-actions Bot added Kernel PR has src relate code tools Component labels Aug 30, 2026
@github-actions

github-actions Bot commented Aug 30, 2026

Copy link
Copy Markdown

📌 Code Review Assignment

🏷️ Tag: components

Reviewers: @Maihuanyi

Changed Files (Click to expand)
  • components/utilities/Kconfig
  • components/utilities/asan/Kconfig
  • components/utilities/asan/SConscript
  • components/utilities/asan/asan.c
  • components/utilities/asan/asan.h
  • components/utilities/asan/asan_heap.c

🏷️ Tag: kernel

Reviewers: @GorrayLi @ReviewSun @hamburger-os @lianux-mm @wdfk-prog @xu18838022837

Changed Files (Click to expand)
  • src/SConscript
  • src/klibc/kstring.c
  • src/kservice.c
  • src/utest/Kconfig
  • src/utest/SConscript
  • src/utest/asan_tc.c

🏷️ Tag: workflow

Reviewers: @Rbb666 @kurisaW @supperthomas

Changed Files (Click to expand)
  • .github/utest/configs/components/asan.cfg
  • .github/workflows/utest_auto_run.yml

📊 Current Review Status (Last Updated: 2026-09-10 09:33 CST)


📝 Review Instructions

  1. 维护者可以通过单击此处来刷新审查状态: 🔄 刷新状态
    Maintainers can refresh the review status by clicking here: 🔄 Refresh Status

  2. 确认审核通过后评论 LGTM/lgtm
    Comment LGTM/lgtm after confirming approval

  3. PR合并前需至少一位维护者确认
    PR must be confirmed by at least one maintainer before merging

ℹ️ 刷新CI状态操作需要具备仓库写入权限。
ℹ️ Refresh CI status operation requires repository Write permission.

@github-actions

github-actions Bot commented Aug 30, 2026

Copy link
Copy Markdown

MemBrowse Memory Report

at32f415-start

  • ROM: .text +28 B (+0.0%, 65,428 B / 262,144 B, total: 25% used)

esp32-c3

  • drom0_0_seg: .eh_frame +16 B (+0.0%, 411,004 B / 8,388,576 B, total: 5% used)

gd32105r-start

  • CODE: .text +28 B (+0.0%, 69,344 B / 262,144 B, total: 26% used)

hc32f334

  • FLASH: .text +28 B (+0.0%, 99,556 B / 131,072 B, total: 76% used)

hpmicro-hpm5301evklite

  • ILM: .fast +80 B (+0.2%, 38,288 B / 131,072 B, total: 29% used)
  • XPI0: .fast +80 B (+0.1%, 125,048 B / 1,048,576 B, total: 12% used)

infineon-psoc6

  • flash: .text +28 B (+0.0%, 116,200 B / 262,144 B, total: 44% used)

k230

  • SRAM: .eh_frame +80 B, .text +64 B (+0.0%, 1,128,547 B / 268,300,288 B, total: 0% used)

loongson-ls1cdev

  • Code: .text +64 B (+0.0%, 363,368 B)

nordic-nrf51822

  • FLASH: .text +32 B (+0.1%, 52,040 B / 262,144 B, total: 20% used)

nuvoton-m487

  • CODE: .text +48 B (+0.0%, 356,072 B / 524,288 B, total: 68% used)

qemu-virt64-aarch64

  • Code: .eh_frame +32 B, .text +192 B (+0.0%, 1,077,348 B)

raspberry-pico-rp2040

  • FLASH: .text +32 B (+0.0%, 114,168 B / 2,097,152 B, total: 5% used)

renesas-ra2l1

  • FLASH: .text +24 B (+0.0%, 98,280 B / 262,144 B, total: 37% used)

simulator

  • Code: .rela.dyn -264 B, .rodata +32 B, .text -55 B (-0.0%, 2,673,138 B)
  • Data: .data -320 B (-0.0%, 1,609,412 B)

stm32f407-rt-spark

  • CODE: .text +28 B (+0.0%, 85,824 B / 1,048,576 B, total: 8% used)

stm32l475-atk-pandora-llvm

  • ROM: .text +36 B (+0.0%, 83,496 B / 524,288 B, total: 16% used)

wch-ch32v208w-r0

  • FLASH: .text +72 B (+0.0%, 160,024 B / 491,520 B, total: 33% used)

x86

  • Code: .text +80 B (+0.0%, 198,957 B)

xuantie-e901plus

  • ISRAM: .rodata +48 B, .text +16 B (+0.1%, 46,424 B / 131,072 B, total: 35% used)
    No memory changes detected for:
  • nxp-lpc1114

@BernardXiong

Copy link
Copy Markdown
Member

这部分以什么方式加入?直接加也并不合适,不应该把它做为一个独立的组件放到 components 目录下,可以考虑的方式是 components/utilities 下,或者还有一种是软件包方式。

@RogerNext

Copy link
Copy Markdown
Contributor Author

这部分以什么方式加入?直接加也并不合适,不应该把它做为一个独立的组件放到 components 目录下,可以考虑的方式是 components/utilities 下,或者还有一种是软件包方式。

感谢反馈,我理解后觉得放 components/utilities/ 下是合适的,会把它调整为
components/utilities/asan/

关于「软件包方式」,有一点技术约束想说明:ASan 依赖 GCC 的
-fsanitize=kernel-address 插桩,这个编译选项需要注入到整个内核与
BSP 所有 .c 文件的编译流程(tools/building.py),并且需要修改内核
src/SConscript 把 mem/memheap/slab 分配器移出插桩组(否则分配器
元数据访问会被误报)。软件包机制无法修改内核的全局编译选项和构建脚本,
因此纯软件包方式只能做「不插桩、仅靠 hook」的弱化版本,无法实现真正的
AddressSanitizer。

@Rbb666 Rbb666 closed this Sep 3, 2026
@Rbb666 Rbb666 reopened this Sep 3, 2026
@Rbb666

Rbb666 commented Sep 3, 2026

Copy link
Copy Markdown
Member

感谢PR,看到了描述中支持了ARM和RV,作者能否添加下代表性的BSP(qemu-a9)进行编译看护,并添加utest使用utest-auto-run ci看护起来?

@RogerNext

Copy link
Copy Markdown
Contributor Author

感谢PR,看到了描述中支持了ARM和RV,作者能否添加下代表性的BSP(qemu-a9)进行编译看护,并添加utest使用utest-auto-run ci看护起来?

好的,我处理一下

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.
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.
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-actions github-actions Bot added the action github action yml imporve label Sep 3, 2026
@RogerNext

Copy link
Copy Markdown
Contributor Author

感谢建议,已补充 utest 测试用例和 CI 看护。

新增测试用例components.asan_tc,共 6 个测试单元):

测试单元 测试场景
test_asan_overflow_write 堆缓冲区越界(写入 redzone)
test_asan_overflow_read 堆缓冲区越界(读取 redzone)
test_asan_no_false_positive 反例:边界内正常访问不误报
test_asan_realloc_overflow rt_realloc 扩容后的越界写
test_asan_uaf_read use-after-free (仅 small mem 堆)
test_asan_uaf_write use-after-free (仅 small mem 堆)

CI 看护

  • 新增 asan.cfg,在 qemu-vexpress-a9 上开启 RT_USING_ASAN 并运行 components.asan_tc
  • utest_auto_run.yml 的 matrix 中新增 A9 + asan.cfg 条目,同时实现「编译看护 + 运行看护」。

过程中发现并修复的一个问题rt_realloc 搬迁内存块时会触发 rt_memcpy 的插桩误报(rt_memcpy 按字批量拷贝会跨越 redzone),已通过给 rt_memcpy/rt_memset/rt_memmoveno_sanitize_address 修复,处理方式与 Linux KASAN 一致。

已在 qemu-vexpress-a9 上本地验证:6 个测试单元全部 PASSED。

Comment thread tools/building.py Outdated
# 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')

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

参数只追加到 CFLAGS,但 C++ 命令没有,需要同步设置下 CXXFLAGS。

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

已在 7010717 中同步向 CXXFLAGS 添加 -fsanitize=kernel-address -fno-omit-frame-pointer,并新增 C++ 越界回归用例。

Comment thread components/utilities/asan/asan.c Outdated
Comment on lines +452 to +458
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)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

这里有点问题,就是这两个 hook 均位于堆锁之外。若线程 A 进入 realloc 后被抢占,B 完成 realloc,再恢复 A,就会用 B 的指针处理 A 的释放操作。

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

确实存在这个问题,已在 7010717 中移除全局旧指针和成对 hook 的实现,将 ASan 分配状态更新接入堆锁保护范围。同时处理了旧块释放后、shadow 更新前被其他线程复用的窗口。

Comment thread components/utilities/asan/asan.c Outdated
Comment on lines +409 to +413
asan_track_add(p, size);
asan_unpoison_range(p, size);
if (aligned > size)
{
asan_poison_range(p + size, aligned - size);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

这块是只把向上对齐后多出来的字节设为了保护区,没有额外预留保护空间。
如果申请16 字节,就不存在被标记为不可访问的区域了吧?如果 rt_malloc(16),p[16] = 1 这种是不是就检测不到了?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

是的,原实现只标记对齐填充,16 字节这类请求会漏报。已在 7010717 中实际预留右侧保护区,并按用户请求长度设置 shadow;普通分配和对齐分配均已覆盖。补充了整齐/非整齐大小、对齐分配及 realloc 缩小后的边界测试

Integrate sanitizer allocation updates under the heap lock, reserve real redzones for normal and aligned allocations, and instrument C++ accesses. Handle realloc lifecycle and allocation-size overflow, and add regression tests that retain failures across test units.

Validation: 11 ARM/QEMU configurations passed, including all three heap backends with ASan disabled; 64-bit host UBSan checks passed. Restoring the old aligned-allocation functions makes both regression suites fail as expected.
@BernardXiong

Copy link
Copy Markdown
Member

这部分以什么方式加入?直接加也并不合适,不应该把它做为一个独立的组件放到 components 目录下,可以考虑的方式是 components/utilities 下,或者还有一种是软件包方式。

感谢反馈,我理解后觉得放 components/utilities/ 下是合适的,会把它调整为 components/utilities/asan/

关于「软件包方式」,有一点技术约束想说明:ASan 依赖 GCC 的 -fsanitize=kernel-address 插桩,这个编译选项需要注入到整个内核与 BSP 所有 .c 文件的编译流程(tools/building.py),并且需要修改内核 src/SConscript 把 mem/memheap/slab 分配器移出插桩组(否则分配器 元数据访问会被误报)。软件包机制无法修改内核的全局编译选项和构建脚本, 因此纯软件包方式只能做「不插桩、仅靠 hook」的弱化版本,无法实现真正的 AddressSanitizer。

SConscript 下是可以注入参数到全局的,可能除了POST_ACTION吧

@RogerNext

Copy link
Copy Markdown
Contributor Author

这部分以什么方式加入?直接加也并不合适,不应该把它做为一个独立的组件放到 components 目录下,可以考虑的方式是 components/utilities 下,或者还有一种是软件包方式。

感谢反馈,我理解后觉得放 components/utilities/ 下是合适的,会把它调整为 components/utilities/asan/
关于「软件包方式」,有一点技术约束想说明:ASan 依赖 GCC 的 -fsanitize=kernel-address 插桩,这个编译选项需要注入到整个内核与 BSP 所有 .c 文件的编译流程(tools/building.py),并且需要修改内核 src/SConscript 把 mem/memheap/slab 分配器移出插桩组(否则分配器 元数据访问会被误报)。软件包机制无法修改内核的全局编译选项和构建脚本, 因此纯软件包方式只能做「不插桩、仅靠 hook」的弱化版本,无法实现真正的 AddressSanitizer。

SConscript 下是可以注入参数到全局的,可能除了POST_ACTION吧

感谢指出,我确认了 DefineGroup 的实现,SConscript 中的 CFLAGS/CXXFLAGS 确实可以追加到全局编译环境。之前的表述不准确,也不能据此认定软件包只能实现不插桩的版本。
当前实现还涉及分配器排除插桩,以及堆锁内的 ASan 分配/释放集成,这些与参数注入是不同的问题。目前放在 components/utilities/asan 下;后续会想办法将全局插桩参数配置收回组件 SConscript,减少对公共构建脚本的修改。

@RogerNext

Copy link
Copy Markdown
Contributor Author

这部分以什么方式加入?直接加也并不合适,不应该把它做为一个独立的组件放到 components 目录下,可以考虑的方式是 components/utilities 下,或者还有一种是软件包方式。

感谢反馈,我理解后觉得放 components/utilities/ 下是合适的,会把它调整为 components/utilities/asan/
关于「软件包方式」,有一点技术约束想说明:ASan 依赖 GCC 的 -fsanitize=kernel-address 插桩,这个编译选项需要注入到整个内核与 BSP 所有 .c 文件的编译流程(tools/building.py),并且需要修改内核 src/SConscript 把 mem/memheap/slab 分配器移出插桩组(否则分配器 元数据访问会被误报)。软件包机制无法修改内核的全局编译选项和构建脚本, 因此纯软件包方式只能做「不插桩、仅靠 hook」的弱化版本,无法实现真正的 AddressSanitizer。

SConscript 下是可以注入参数到全局的,可能除了POST_ACTION吧

感谢指出,我确认了 DefineGroup 的实现,SConscript 中的 CFLAGS/CXXFLAGS 确实可以追加到全局编译环境。之前的表述不准确,也不能据此认定软件包只能实现不插桩的版本。 当前实现还涉及分配器排除插桩,以及堆锁内的 ASan 分配/释放集成,这些与参数注入是不同的问题。目前放在 components/utilities/asan 下;后续会想办法将全局插桩参数配置收回组件 SConscript,减少对公共构建脚本的修改。

已在 ebfd73c 中完成上述调整,将全局 CFLAGS/CXXFLAGS/LINKFLAGS 配置收回 components/utilities/asan/SConscript,移除了 tools/building.py 中的 ASan 专用逻辑。分配器和运行时的局部排除插桩设置保持有效。

Comment thread src/kservice.c
*
* @return the pointer to allocated memory or NULL if no free memory was found.
*/
rt_weak void *rt_malloc(rt_size_t size)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

其实在这里是专门留了weak的,这样可以在需要重载系统动态内存分配器的地方重新实现这些函数,这样就可以做到对内核的无侵入修改了。

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

明白了,当前直接在 kservice.c 中接入 ASan 的方式没有充分利用weak这个机制。我会按组件内覆盖弱符号的方向调整,并核对底层分配器接入、锁和原有接口行为,尽量消除 ASan 对内核源码的直接修改。

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

我已在本地按覆盖 weak 接口的方式调整:由 ASan 组件提供分配、释放、对齐分配及内存统计等接口,移除了 kservice.c 中的 ASan 专用分支。
调整过程中发现,四个 hook 设置接口、两个 slab 页分配接口,以及两个测试用堆锁接口目前不是 weak,而且依赖 kservice.c 内部的静态状态。为保持这些接口与组件接管后的堆一致,当前方案为这八个接口补充了 weak 声明。分配器排除插桩的构建设置暂时保留。
这个方案保留了现有功能,代价是仍有少量通用内核改动,且组件需要维护一套堆适配和锁管理逻辑。想请教您是否接受补齐这些通用 weak 扩展点,还是更倾向于严格保持内核不变、相应限定首版组件的支持范围?我再按确定的方向整理提交。

Move heap ownership, locking and hooks into the ASan adapter. Make hook setters, slab page APIs and test heap locks overridable, and remove ASan-specific branches from kservice.

Require a cross-CPU heap lock for SMP and add hook, heap adapter and Kconfig regression coverage. Validated 11 QEMU configurations plus SMP with ISR heap locking, eight Kconfig combinations and negative regression checks.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

action github action yml imporve Component Kernel PR has src relate code testcase tools

Projects

Status: No status

Development

Successfully merging this pull request may close these issues.

3 participants