HWASan、ASan 与 GWP-ASan:用工具提前抓住 native 内存错误
C++ 内存错误最麻烦的地方是“犯错位置”和“崩溃位置”可能离得很远。你在 A 函数越界写,程序可能在 B 函数释放内存时才崩。
Sanitizer 的价值就是把这种隐藏错误尽早暴露出来。
Sanitizer 是什么
Sanitizer 是一类运行时检测工具。它会在编译或运行时插入额外检查,帮助发现内存错误。
常见工具:
ASan:AddressSanitizer,检测越界和 use-after-free
HWASan:HWAddressSanitizer,基于内存标记思想,适合 64 位 Arm
GWP-ASan:采样式检测,开销低,可用于生产辅助发现问题
可以把 sanitizer 想成工厂里的安全检查员。平时机器跑得更慢一点,但能在危险动作发生时立刻喊停。
ASan 能抓什么
heap-buffer-overflow
stack-buffer-overflow
use-after-free
double-free
use-after-return
示例:
void overflow() {
int values[4] = {0};
values[4] = 1;
}
普通运行可能不一定立刻崩。ASan 构建更容易直接报告越界位置。
HWASan 的特点
Android 官方 HWASan 文档说明,NDK 从 r21 开始支持 HWASan,Android 10/API 29 起支持,且只适用于 64 位 Arm 设备。文档还给出大致开销:CPU 开销约 2 倍,代码大小增加约 40% 到 50%,内存开销比经典 ASan 小。
它适合在调试设备、测试包、专项回归中使用。
不适合默认全量线上开启,因为开销仍然明显。
GWP-ASan 的定位
GWP-ASan 是低开销、采样式的 native 内存错误检测。官方文档说明 Android 14/API 34 及以上设备默认为所有 App 使用 Recoverable GWP-ASan。
它不像 ASan/HWASan 那样每次都抓,但适合在线上帮助发现 heap use-after-free、heap-buffer-overflow 这类问题。
开启 HWASan 的思路
CMake 构建可给目标增加 sanitizer 参数。
target_compile_options(player_core PRIVATE -fsanitize=hwaddress -fno-omit-frame-pointer)
target_link_options(player_core PRIVATE -fsanitize=hwaddress)
注意:具体可用性取决于 NDK 版本、设备系统版本、ABI 和运行方式。不要把 sanitizer 构建和普通 release 混在一起。
sanitizer 报告怎么看
一个报告通常包含:
错误类型:heap-use-after-free
错误地址:0x...
访问大小:read/write of size N
发生栈:当前访问在哪里
分配栈:这块内存在哪里申请
释放栈:这块内存在哪里释放
关键不是只看崩溃行,而是同时看“分配、释放、再次访问”三条线。
播放器 UAF 示例
错误设计:
class SurfaceSession {
public:
ANativeWindow* window = nullptr;
};
void renderThread(SurfaceSession* session) {
renderTo(session->window);
}
void onSurfaceDestroyed(SurfaceSession* session) {
ANativeWindow_release(session->window);
delete session;
}
如果 render 线程还在用 session,UI 线程把它 delete,就会 UAF。
修复思路:
SurfaceSession 用 shared ownership 或明确线程停止协议
surfaceDestroyed 只发命令,不直接释放 render 正在用的对象
render 线程退出后再释放 window
内存加固组合
建议这样分层。
日常开发:Debug + ASan/HWASan 专项构建
CI 回归:关键 native 模块跑 sanitizer 用例
灰度/线上:GWP-ASan、符号归档、崩溃聚合
代码层:RAII、所有权图、线程停止协议
工具不能替代设计。RAII 和线程边界仍然是第一道防线。
本章实验
写一个故意 UAF 的函数。
void uaf() {
int* value = new int(1);
delete value;
*value = 2;
}
用 sanitizer 构建运行,观察报告中的:
use-after-free
allocation stack
free stack
invalid write stack
再用 unique_ptr 或所有权重构消除问题。
小白常见误解
第一,sanitizer 不是让代码变安全的开关。它是检测工具,不是设计替代品。
第二,sanitizer 包不等于线上包。它通常更慢、更大,要单独构建、单独测试。
第三,报告里的崩溃行不一定是根因。内存错误要同时看分配栈、释放栈和非法访问栈。
第四,偶发问题如果 sanitizer 抓不到,不代表没有 bug。特别是 GWP-ASan 是采样式工具,不保证每次都命中。
工程风险与观测
建议把 sanitizer 纳入固定流程。
每周跑一次 HWASan/ASan 专项包
每次播放器核心改动后跑内存错误用例
CI 保存 sanitizer 报告
线上开启 GWP-ASan 相关崩溃聚合
报告要归档这些信息。
错误类型
ABI
设备系统版本
分配栈
释放栈
非法访问栈
修复 commit
回归用例
如果某个错误是 use-after-free,优先审计所有权和线程停止协议,而不是只在崩溃行加空指针判断。空指针判断可能只是掩盖问题,真正风险仍然存在。
发布门禁:
新增 sanitizer 报告未处理,不能发布
资源释放计数不平衡,不能发布
同类 UAF 二次出现,需要回滚相关改动并重新审计
本章结论
Sanitizer 的价值是把 native 内存错误从“偶发线上崩溃”提前变成“开发期可复现报告”。ASan/HWASan 适合强检测,GWP-ASan 适合低开销辅助发现。它们和 RAII、符号化、CI 一起,构成 native 稳定性的防线。