HWASan原理与实战
💡
本文聚焦HWAsan的基本原理和实战问题分析,力图涵盖HWASan方方面面的同时,突出重点。
对于一些必须掌握的基础,本文都有讲解。对于一些技术细节和拓展知识,也给出了引文链接,供读者随时查看。
因作者知识有限,文章难免有纰漏。欢迎通过文档评论、飞书私聊等方式交流指正,感谢!
HWASan基础知识
HWASan背景
内存错误是C/C++中最让人头疼的一类问题。为了解决这一问题,首先在编程语言层面,引入了很多机制:
-
C++引用、智能指针。可以规避大多数非并发的内存破坏问题。
-
编译期并发安全检测-Wthread-safety。
但是对于遗留系统,或者对性能有极致要求的系统,内存错误仍无法避免。因此专家们又设计了一些运行时的内存错误检测系统。这就必须要提到HWASan的前身ASan。ASan的检测思路和HWAsan的一样,但它是在纯软件的条件下检测,因此编译时和运行时的开销均较大。
正是因为ASan开销过大的缺点,HWASan(HardWare AddressSanitize)应运而生。它借助一些硬件特性,例如ARMv8的TBI(Top Byte Ignore),实现更加高效的运行时检测。
软件无法解决的问题,通过硬件解决,是业界的常见思路之一。大到GPU设计初衷,小到Write Protection内存权限保护,都体现了这一点。
HWASan能够检测检测出以下常见的内存错误: Stack and heap buffer overflow/underflow Heap use after free Stack use outside scope(Stack overflow) Double free/wild free
与ASan相比,HWASan在性能方面的表现更加优异:
• Similar CPU overhead (~2x)
• Similar code size overhead (40 – 50%)
• Much smaller RAM overhead (10% – 35%)
也就是说,在相似的CPU开销下,HWAsan的内存占用更小。
HWASan基本原理
总体流程:
将所有可写的内存(堆、栈、全局数据区)以16:1的比例映射到影子内存区。所有内存块的起始地址都按照16字节对齐,保证每16字节的user memory都能映射到1字节的shadow memory。这个16就是Tag Granularity,下文详细介绍。
分配对象的时候,随机分配一个8位的tag标记到该对象的虚拟地址最高8位,同时该tag也会保存到其映射的shadow memory中。
编译器在每个内存地址的load/store之前都会插入检查指令(也就是插桩),用于确认操作的地址最高8位保存的tag与其映射的shadow memory中的tag值一致。这也是代码体积增大、运行时有开销的主要原因。
对象回收后也会重新分配一个随机值,保存到其映射的shadow memory中,方便检测use after free问题。
当分配的对象小于16字节(或有小于16字节的零头)时,多余的内存不会再分配给其它对象,此时shadow memory中保存的是对象所占内存的实际字节数(或模16的字节数),这样可以精准检测越界。
下面讲解其中的一些细节,方便后面理解HWASan报错。这些内容均可参考文档:clang HWAsan设计文档
内存标记
HWASan基于内存标记(Memory Tagging)技术。
在64位架构中,地址并不是64位全用的,因为不需要那么大的寻址空间。通常64位架构下使用的地址是48位的。这样就多出来很多剩余的bit。
内存标记就是用寻址剩余bit中的一个字节(最高8位),给每个地址打上一个随机的tag。然后给每个分配的内存单元也打上相同的tag。之后每次访问内存,都会比对地址和内存的tag是否一致,如果不一致,就意味着发生了某种内存错误。这里的8称为TS(Tag Size)。
同时,为了存储内存tag,HWASan引入了“影子内存”(Shadow Memory)的概念。这样,内存中正常存储的数据和tag就互不影响。在检测时,HWAsan可以很容易地根据实际内存地址计算出影子内存的地址,从而检查tag。
影子内存和实际内存有固定的比值,这个比值我们一般叫做TG(Tag Granularity)。在Android环境下这个值默认是16,也就是说1字节的tag对应了16字节的实际内存。
因为tag是随机生成的,所以TS的值影响检测的准确性。检测的准确性公式为(2^TS-1)/2^TS。8 bit的准确率就已经到99.6%,足够了。
TG的大小是综合tag内存占用、内存对齐以及硬件限制等因素制定的。一般就是取16(AArch64)或64(SPARC ADI)。
Short granules
简单讲,short granule就是按照16字节对齐后的剩余内存。最后剩余的小于16字节的内存块,会在影子内存里存储剩余内存的字节数。所以每次检测tag的时候,HWASan需要检测两种可能:
-
地址tag和内存tag一致
-
影子内存存储的是short granule的大小,访问的地址在short granule的范围之内。
举个例子,一个17字节的内存块,影子内存的第一个字节存储随机tag 0x66,第二个字节存储的就是二进制0x01。
-
当内存访问的是第16个字节,地址0x0000100F时,比对发现ptr tag和mem tag均是0x66,pass。
-
当内存访问的是第17个字节,地址0x00001010时,比对发现mem tag是0x01,则需要再检测地址模16后(16字节对齐)是否小于等于0x01。发现等于,pass。
-
当内存访问的是第18个字节,地址0x00001011时,比对发现mem tag是0x01,则需要再检测地址是否小于0x01。发现大于等于,fail。
如图所示:

这部分对应的汇编码,会在demo日志阅读中详解。
编译器插桩
前面提到,每次内存访问前,都要比对mem tag和ptr tag,所以编译插桩是必须的。因为要能检测堆、栈和全局变量,所以这些内存区的访问都会插桩。
插桩的代码,主要就是调用HWASan的检测函数,下文会有一个简单的例子,更多资料可以参考clang HWAsan设计文档。
malloc hook
因为HWAsan检测需要在malloc/free的过程中填充影子内存,并且要保证内存对齐,所以hook malloc也是必须的。
hook的基本原理和malloc debug/heapprofd类似,感兴趣的话可以看wiki: HWASan源码与架构分析和hwasan 内存分配器这两篇文档。
HWASan实战
HWASan版本编译
本节讲一下如何编译HWAsan编译,包括本地主机和Android。
因为现在Android用的是clang,所以我们默认编译器是clang。
基本原理
HWASan编译的基本方法就是clang编译时加上-fsanitize=hwaddress。有了这个参数以后,编译器就会自动生成插桩代码,并链接hwasan库。
这里有个小问题,如果要在x86_64主机上编译,只能用-fsanitize=address。网上搜了一下,原因可能是部分x86_64架构的CPU不支持hwasan。
Android编译的话,如果要全局开启,需要输出环境变量export SANITIZE_TARGET=hwaddress
如果要单模块开启,只要在对应的cc_binary或cc_library里配上sanitize: { hwaddress:true }即可。
这里谷歌官方文档有个坑,它说还要给libc配,亲测是不需要的。这里应该是soong里优化了,检测到这个配置,会自动链接hwasan版本的libc。有空可以研究下。
corgi打包
corgi打包也是常用hwasan打包方式,特别是给测试同学提供测试包的时候,因此这里讲一下。
的基本原理和上面是相同的,首先要保证输出环境变量:

由于个别库的hwasan编译存在些问题,所以需要带patch规避一下:https://gerrit.pt.mioffice.cn/c/platform/build/soong/+/4280898。这个patch是最方便的,一个patch搞定所有需要加白名单的模块。
还要带上这个https://gerrit.pt.mioffice.cn/c/device/xiaomi/missi/+/5292027,防止报产物分区错误。
最后千万不要忘记保存symbol,没有symbol难以分析hwasan问题。
HWASan demo日志阅读
写demo是一种很好的学习方法,它让我们能够先从最简单的case,用最直观的方式去学习一个新知识。
所以我们先从最简单的demo开始学习如何阅读hwasan报错。也推荐大家有兴趣都话能够自己动手写demo编译,看汇编码。
本节的所有代码可以看这个change:https://gerrit.pt.mioffice.cn/c/miui/frameworks/base/+/5355995
heap buffer overflow
我们写一个非常简单的heap buffer overflow的例子:
void test_heap_overflow() {
char* str = new char[1];
// Trigger buffer overflow
// 注意:这里用iostream可以避免被编译器优化。简单的赋值语句会被编译器优化掉。
std::cout << str[1] << std::endl;
}Android环境下的编译配置:
cc_binary {
name: "test_hwasan",
srcs: [
"test_hwasan.cpp",
],
shared_libs: [
"libbase",
"liblog",
],
cflags: [
"-O0", // 如果要分析汇编码,一定要加这个,否则难以理解。
],
sanitize: {
hwaddress: true
},
system_ext_specific: true,
}运行后即可看到报错(/data/tombstones下有完整报错。控制台输出的只有hwasan部分):
*** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***
Build fingerprint: 'Xiaomi/haotian/haotian:16/BP2A.250221.001/OS2.0.250421.1.WOBCNXM.PRE:user/release-keys'
Revision: '0'
ABI: 'arm64'
Timestamp: 2025-04-22 00:58:30.714910335+0800
Process uptime: 1s
Cmdline: test_hwasan 1
pid: 23480, tid: 23480, name: test_hwasan >>> test_hwasan <<<
uid: 0
tagged_addr_ctrl: 0000000000000001 (PR_TAGGED_ADDR_ENABLE)
pac_enabled_keys: 000000000000000f (PR_PAC_APIAKEY, PR_PAC_APIBKEY, PR_PAC_APDAKEY, PR_PAC_APDBKEY)
hwasan一般都是sigabort。因为是HWASan检测到问题主动退出的,而不是访问到非法内存抛SIGSEGV等其它信号
signal 6 (SIGABRT), code -1 (SI_QUEUE), fault addr --------
Abort message: '==23480==ERROR: HWAddressSanitizer: tag-mismatch on address 0x003ecd710021 at pc 0x005dcd720910
打印内存访问是读还是写(READ/WRITE)。错误地址,以及指针和内存的tag(如果影子内存是short granule,还会输出随机tag),还有出错的线程号
READ of size 1 at 0x003ecd710021 tags: 74/01(74) (ptr/mem) in thread T0
报错堆栈,可通过addr2line或stack工具解。这个信息很重要。这个栈下面正常的tombstone也会输出,只是格式有区别。
development/scripts/stack脚本目前只能识别普通tombstone的堆栈格式,所以如果用stack的话可以转后面的普通tombstone堆栈。
llvm源码库里有一个脚本可用来解hwasan trace:compiler-rt/lib/hwasan/scripts/hwasan_symbolize
#0 0x005dcd720910 (/system_ext/bin/test_hwasan+0x4910) (BuildId: 72a63cfdacbfed194f2a9770185af81a)
#1 0x007c1d724fbc (/apex/com.android.runtime/lib64/bionic/hwasan/libc.so+0x5ffbc) (BuildId: b9a1e64d04d5595394651cfb66aa2193)
打印实际访问的内存块的地址、尺寸和偏移量。和报错地址0x003ecd710021是能对得上的
[0x003ecd710020,0x003ecd710040) is a small allocated heap chunk; size: 32 offset: 1
错误类型
Cause: heap-buffer-overflow
0x003ecd710021 is located 0 bytes after a 1-byte region [0x003ecd710020,0x003ecd710021)
user内存分配栈
allocated by thread T0 here:
#0 0x007c1d82dbd8 (/apex/com.android.runtime/lib64/bionic/libclang_rt.hwasan-aarch64-android.so+0x29bd8) (BuildId: b61a77e145ba18c714d8eeccb556fc1da909bd49)
#1 0x007c1d7175c8 (/apex/com.android.runtime/lib64/bionic/hwasan/libc.so+0x525c8) (BuildId: b9a1e64d04d5595394651cfb66aa2193)
#2 0x005dcd720830 (/system_ext/bin/test_hwasan+0x4830) (BuildId: 72a63cfdacbfed194f2a9770185af81a)
#3 0x007c1d724fbc (/apex/com.android.runtime/lib64/bionic/hwasan/libc.so+0x5ffbc) (BuildId: b9a1e64d04d5595394651cfb66aa2193)
#4 0x005dcd720064 (/system_ext/bin/test_hwasan+0x4064) (BuildId: 72a63cfdacbfed194f2a9770185af81a)
线程信息
Thread: T0 0x007300002000 stack: [0x007ff478b000,0x007ff4f8b000) sz: 8388608 tls: [0x007c1eab0f80,0x007c1eab4000)
user内存对应的mem tag值。注意左边是user内存的地址。因为一个tag对应16B,所以左边每一行之间差了16^2=256 bytes。
Memory tags around the buggy address (one tag corresponds to 16 bytes):
0x003ecd70f800: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x003ecd70f900: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x003ecd70fa00: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x003ecd70fb00: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x003ecd70fc00: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x003ecd70fd00: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x003ecd70fe00: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x003ecd70ff00: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
=>0x003ecd710000: 08 00 [01] 00 00 00 00 00 00 00 00 00 00 00 00 00
0x003ecd710100: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x003ecd710200: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x003ecd710300: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x003ecd710400: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x003ecd710500: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x003ecd710600: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x003ecd710700: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x003ecd710800: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
打印short granules对应的随机tag的值。本例中我们内存只申请了1B,所以上面影子内存里存的就是short granule 0x01。下面的74说明随机tag是74。如果我们申请的内存size大于16,那么前面的影子内存就是存74。
Tags for short granules around the buggy address (one tag corresponds to 16 bytes):
0x003ecd70ff00: .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. ..
=>0x003ecd710000: bb .. [74] .. .. .. .. .. .. .. .. .. .. .. .. ..
0x003ecd710100: .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. ..
clang short granules帮助文档
See https://clang.llvm.org/docs/HardwareAssistedAddressSanitizerDesign.html#short-granules for a description of short granule tags
Registers where the failure occurred (pc 0x005dcd720910):
x0 0000007c1e9e4c10 x1 0000007c1fdf0880 x2 0000000000000001 x3 9200005dcd71cde0
x4 9200005dcd71cde8 x5 8700004fcd710008 x6 203a5d315b727473 x7 203a5d315b727473
x8 7400003ecd710021 x9 0000000000000001 x10 0000000000000000 x11 0000000000005bb8
x12 0000000000000008 x13 744f2e7bd6142454 x14 0000000000000000 x15 0000000000000010
x16 0000007c1d834008 x17 0000000000000001 x18 0000007c1fbf0000 x19 0040000e600001ea
x20 0200007400000000 x21 0000007c1e9e4c10 x22 0000000000000000 x23 0000007c1e9e5528
x24 ea00007ff4f88ed0 x25 7400003ecd710020 x26 000000000000000a x27 00000007ff4f88ed
x28 0040000e6000016a x29 0000007ff4f88f00 x30 0000005dcd720914 sp 0000007ff4f88e60
关键汇编代码分析(代码比上面的tombstone更简单,因此地址稍有不同,不影响理解。大家不要对地址就行):
0000000000004110 <_Z18test_heap_overflowv>:
...
char* str = new char[1];
4134: d2800020 mov x0, #0x1 // 传入参数1,表示分配尺寸
4138: 94000c0a bl 0x7160 <_Znam@plt> // 分配内存
413c: 910003e8 mov x8, sp
4140: 94000a2f bl 0x69fc <__hwasan_check_x8_19_short_v2> // 检测栈帧有没有被踩坏
4144: f90003e0 str x0, [sp] // 返回地址存入本地变量
std::cout << str[1] << std::endl;
4148: 910003e8 mov x8, sp
414c: 940009eb bl 0x68f8 <__hwasan_check_x8_3_short_v2> // 检测栈变量
4150: f94003e8 ldr x8, [sp] // 加载str基地址到x8
4154: 91000509 add x9, x8, #0x1 // 偏移量+1,放入x9
4158: 94000a3f bl 0x6a54 <__hwasan_check_x9_0_short_v2> // 这里报错
...
检测x9中的地址是否合法。这部分汇编码可以对着基本原理一节中的Short granule中的示意图一起看,更容易理解。
0000000000006a54 <__hwasan_check_x9_0_short_v2>:
// 从x9的第4位开始提取52位到x16
6a54: 9344dd30 sbfx x16, x9, #4, #52
// 加载影子内存里的值,也就是memtag。w16是32位。x20应该是影子内存的基址,这两条指令连起来相当于MemToShadow。ldrb仅加载一个字节
6a58: 38706a90 ldrb w16, [x20, x16]
// x9右移56 bit和x16比,也就是tag对比
6a5c: eb49e21f cmp x16, x9, lsr #56
// tag不匹配,跳转0x6a68继续比较short granule
6a60: 54000041 b.ne 0x6a68 <__hwasan_check_x9_0_short_v2+0x14>
// 检测pass,返回
6a64: d65f03c0 ret
// tag不匹配,看看memtag是不是大于15。
6a68: 71003e1f cmp w16, #0xf
// hi是无符号大于。大于15说明也不是short granule,没必要比较了,跳转0x6a8c
6a6c: 54000108 b.hi 0x6a8c <__hwasan_check_x9_0_short_v2+0x38>
// mem tag小于等于15,要检查short granule。因为内存对齐,所以ptr & 0xf即可得到访问的实际地址偏移是多少,存入x17
6a70: 92400d31 and x17, x9, #0xf
// 比较保存的short granule和要访问的地址偏移
6a74: 6b11021f cmp w16, w17
// 超出short granule范围则跳转到0x6a8c
6a78: 540000a9 b.ls 0x6a8c <__hwasan_check_x9_0_short_v2+0x38>
// 未超出short granule的地址范围。拿到ptr指向的16 B内存的最后一个字节,和ptr tag比较。匹配才真正pass。
// 这说明short granule利用因为对齐而闲置内存的最后一个byte记录了memtag。牛逼!
6a7c: b2400d30 orr x16, x9, #0xf
6a80: 39400210 ldrb w16, [x16]
6a84: eb49e21f cmp x16, x9, lsr #56
6a88: 54fffee0 b.eq 0x6a64 <__hwasan_check_x9_0_short_v2+0x10>
// 超出地址范围,准备报错
6a8c: a9b007e0 stp x0, x1, [sp, #-0x100]!
6a90: a90efbfd stp x29, x30, [sp, #0xe8]
6a94: aa0903e0 mov x0, x9
6a98: d2800001 mov x1, #0x0 // =0
6a9c: d0000010 adrp x16, 0x8000 <strlen+0x8000>
6aa0: f9412210 ldr x16, [x16, #0x240]
6aa4: d61f0200 br x16小结
HWAsan报错主要信息包括:
出错地址
ptr tag && mem tag
ptr对应的内存块信息
错误类型
报错栈、分配栈、释放栈(UAF独有)
影子内存值
随机tag和short granule
user-after-free
测试代码:
void test_uaf() {
std::cout << "Test use-after-free" << std::endl;
char* str = new char[1];
str[0] = 'a';
std::cout << "str: " << str << std::endl;
delete[] str;
std::cout << str[0] << std::endl;
}编译方法和前面一样。
*** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***
Build fingerprint: 'Xiaomi/haotian/haotian:16/BP2A.250221.001/OS2.0.250421.1.WOBCNXM.PRE:user/release-keys'
Revision: '0'
ABI: 'arm64'
Timestamp: 2025-04-22 00:37:31.360344722+0800
Process uptime: 1s
Cmdline: test_hwasan 2
pid: 23104, tid: 23104, name: test_hwasan >>> test_hwasan <<<
uid: 0
tagged_addr_ctrl: 0000000000000001 (PR_TAGGED_ADDR_ENABLE)
pac_enabled_keys: 000000000000000f (PR_PAC_APIAKEY, PR_PAC_APIBKEY, PR_PAC_APDAKEY, PR_PAC_APDBKEY)
signal 6 (SIGABRT), code -1 (SI_QUEUE), fault addr --------
Abort message: '==23104==ERROR: HWAddressSanitizer: tag-mismatch on address 0x0044a6010020 at pc 0x0063a6018b78
READ of size 1 at 0x0044a6010020 tags: 89/a8 (ptr/mem) in thread T0
#0 0x0063a6018b78 (/system_ext/bin/test_hwasan+0x4b78) (BuildId: 72a63cfdacbfed194f2a9770185af81a)
#1 0x007244485fbc (/apex/com.android.runtime/lib64/bionic/hwasan/libc.so+0x5ffbc) (BuildId: b9a1e64d04d5595394651cfb66aa2193)
[0x0044a6010020,0x0044a6010040) is a small unallocated heap chunk; size: 32 offset: 0
Cause: use-after-free
0x0044a6010020 is located 0 bytes inside a 1-byte region [0x0044a6010020,0x0044a6010021)
UAF问题才有free trace,buffer overflow没有
freed by thread T0 here:
#0 0x00724303f57c (/apex/com.android.runtime/lib64/bionic/libclang_rt.hwasan-aarch64-android.so+0x2957c) (BuildId: b61a77e145ba18c714d8eeccb556fc1da909bd49)
#1 0x0063a6018b74 (/system_ext/bin/test_hwasan+0x4b74) (BuildId: 72a63cfdacbfed194f2a9770185af81a)
#2 0x007244485fbc (/apex/com.android.runtime/lib64/bionic/hwasan/libc.so+0x5ffbc) (BuildId: b9a1e64d04d5595394651cfb66aa2193)
#3 0x0063a6018064 (/system_ext/bin/test_hwasan+0x4064) (BuildId: 72a63cfdacbfed194f2a9770185af81a)
previously allocated by thread T0 here:
#0 0x00724303fbd8 (/apex/com.android.runtime/lib64/bionic/libclang_rt.hwasan-aarch64-android.so+0x29bd8) (BuildId: b61a77e145ba18c714d8eeccb556fc1da909bd49)
#1 0x0072444785c8 (/apex/com.android.runtime/lib64/bionic/hwasan/libc.so+0x525c8) (BuildId: b9a1e64d04d5595394651cfb66aa2193)
#2 0x0063a6018ab4 (/system_ext/bin/test_hwasan+0x4ab4) (BuildId: 72a63cfdacbfed194f2a9770185af81a)
#3 0x007244485fbc (/apex/com.android.runtime/lib64/bionic/hwasan/libc.so+0x5ffbc) (BuildId: b9a1e64d04d5595394651cfb66aa2193)
#4 0x0063a6018064 (/system_ext/bin/test_hwasan+0x4064) (BuildId: 72a63cfdacbfed194f2a9770185af81a)
hwasan自身debug信息
hwasan_dev_note_heap_rb_distance: 1 1023
hwasan_dev_note_num_matching_addrs: 0
hwasan_dev_note_num_matching_addrs_4b: 0
Thread: T0 0x006900002000 stack: [0x007ffbfed000,0x007ffc7ed000) sz: 8388608 tls: [0x007244cedf80,0x007244cf1000)
Memory tags around the buggy address (one tag corresponds to 16 bytes):
..
0x0044a600ff00: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
=>0x0044a6010000: 08 00 [a8] 00 00 00 00 00 00 00 00 00 00 00 00 00
0x0044a6010100: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
...
Tags for short granules around the buggy address (one tag corresponds to 16 bytes):
0x0044a600ff00: .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. ..
=>0x0044a6010000: 11 .. [..] .. .. .. .. .. .. .. .. .. .. .. .. ..
0x0044a6010100: .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. ..不能识别的情况
以下这种情况,hwasan不能给出明确原因:
void test_unrecognized() {
gStr = new char[1];
gStr[0] = 'a';
ALOGD("gStr: %s", gStr);
delete gStr;
gStr[1] = 'b';
}报错信息:
Build fingerprint: 'Xiaomi/haotian/haotian:16/BP2A.250221.001/OS2.0.250421.1.WOBCNXM.PRE:user/release-keys'
Revision: '0'
ABI: 'arm64'
Timestamp: 2025-04-22 00:37:31.360344722+0800
Process uptime: 1s
Cmdline: test_hwasan 2
pid: 23104, tid: 23104, name: test_hwasan >>> test_hwasan <<<
uid: 0
tagged_addr_ctrl: 0000000000000001 (PR_TAGGED_ADDR_ENABLE)
pac_enabled_keys: 000000000000000f (PR_PAC_APIAKEY, PR_PAC_APIBKEY, PR_PAC_APDAKEY, PR_PAC_APDBKEY)
signal 6 (SIGABRT), code -1 (SI_QUEUE), fault addr --------
Abort message: '==23104==ERROR: HWAddressSanitizer: tag-mismatch on address 0x0044a6010020 at pc 0x0063a6018b78
READ of size 1 at 0x0044a6010020 tags: 89/a8 (ptr/mem) in thread T0
#0 0x0063a6018b78 (/system_ext/bin/test_hwasan+0x4b78) (BuildId: 72a63cfdacbfed194f2a9770185af81a)
#1 0x007244485fbc (/apex/com.android.runtime/lib64/bionic/hwasan/libc.so+0x5ffbc) (BuildId: b9a1e64d04d5595394651cfb66aa2193)
[0x0044a6010020,0x0044a6010040) is a small unallocated heap chunk; size: 32 offset: 0
Cause: use-after-free
0x0044a6010020 is located 0 bytes inside a 1-byte region [0x0044a6010020,0x0044a6010021)
UAF问题才有free trace,buffer overflow没有
freed by thread T0 here:
#0 0x00724303f57c (/apex/com.android.runtime/lib64/bionic/libclang_rt.hwasan-aarch64-android.so+0x2957c) (BuildId: b61a77e145ba18c714d8eeccb556fc1da909bd49)
#1 0x0063a6018b74 (/system_ext/bin/test_hwasan+0x4b74) (BuildId: 72a63cfdacbfed194f2a9770185af81a)
#2 0x007244485fbc (/apex/com.android.runtime/lib64/bionic/hwasan/libc.so+0x5ffbc) (BuildId: b9a1e64d04d5595394651cfb66aa2193)
#3 0x0063a6018064 (/system_ext/bin/test_hwasan+0x4064) (BuildId: 72a63cfdacbfed194f2a9770185af81a)
previously allocated by thread T0 here:
#0 0x00724303fbd8 (/apex/com.android.runtime/lib64/bionic/libclang_rt.hwasan-aarch64-android.so+0x29bd8) (BuildId: b61a77e145ba18c714d8eeccb556fc1da909bd49)
#1 0x0072444785c8 (/apex/com.android.runtime/lib64/bionic/hwasan/libc.so+0x525c8) (BuildId: b9a1e64d04d5595394651cfb66aa2193)
#2 0x0063a6018ab4 (/system_ext/bin/test_hwasan+0x4ab4) (BuildId: 72a63cfdacbfed194f2a9770185af81a)
#3 0x007244485fbc (/apex/com.android.runtime/lib64/bionic/hwasan/libc.so+0x5ffbc) (BuildId: b9a1e64d04d5595394651cfb66aa2193)
#4 0x0063a6018064 (/system_ext/bin/test_hwasan+0x4064) (BuildId: 72a63cfdacbfed194f2a9770185af81a)
hwasan自身debug信息
hwasan_dev_note_heap_rb_distance: 1 1023
hwasan_dev_note_num_matching_addrs: 0
hwasan_dev_note_num_matching_addrs_4b: 0
Thread: T0 0x006900002000 stack: [0x007ffbfed000,0x007ffc7ed000) sz: 8388608 tls: [0x007244cedf80,0x007244cf1000)
Memory tags around the buggy address (one tag corresponds to 16 bytes):
..
0x0044a600ff00: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
=>0x0044a6010000: 08 00 [a8] 00 00 00 00 00 00 00 00 00 00 00 00 00
0x0044a6010100: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
...
Tags for short granules around the buggy address (one tag corresponds to 16 bytes):
0x0044a600ff00: .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. ..
=>0x0044a6010000: 11 .. [..] .. .. .. .. .. .. .. .. .. .. .. .. ..
0x0044a6010100: .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. ..不能识别的情况
以下这种情况,hwasan不能给出明确原因:
void test_unrecognized() {
gStr = new char[1];
gStr[0] = 'a';
ALOGD("gStr: %s", gStr);
delete gStr;
gStr[1] = 'b';
}报错信息:
*** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***
Build fingerprint: 'Xiaomi/haotian/haotian:16/BP2A.250221.001/OS2.0.250421.1.WOBCNXM.PRE:user/release-keys'
Revision: '0'
ABI: 'arm64'
Timestamp: 2025-04-22 00:37:52.570543933+0800
Process uptime: 1s
Cmdline: test_hwasan 3
pid: 23132, tid: 23132, name: test_hwasan >>> test_hwasan <<<
uid: 0
tagged_addr_ctrl: 0000000000000001 (PR_TAGGED_ADDR_ENABLE)
pac_enabled_keys: 000000000000000f (PR_PAC_APIAKEY, PR_PAC_APIBKEY, PR_PAC_APDAKEY, PR_PAC_APDBKEY)
signal 6 (SIGABRT), code -1 (SI_QUEUE), fault addr --------
Abort message: '==23132==ERROR: HWAddressSanitizer: tag-mismatch on address 0x003cd10f0021 at pc 0x005bd1100c80
WRITE of size 1 at 0x003cd10f0021 tags: 20/26 (ptr/mem) in thread T0
#0 0x005bd1100c80 (/system_ext/bin/test_hwasan+0x4c80) (BuildId: 72a63cfdacbfed194f2a9770185af81a)
#1 0x007253275fbc (/apex/com.android.runtime/lib64/bionic/hwasan/libc.so+0x5ffbc) (BuildId: b9a1e64d04d5595394651cfb66aa2193)
[0x003cd10f0020,0x003cd10f0040) is a small unallocated heap chunk; size: 32 offset: 1
Thread: T0 0x006900002000 stack: [0x007feaaa8000,0x007feb2a8000) sz: 8388608 tls: [0x007253c22f80,0x007253c26000)
Memory tags around the buggy address (one tag corresponds to 16 bytes):
...
0x003cd10eff00: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
=>0x003cd10f0000: 08 00 [26] 00 1e 08 6c 08 00 00 00 00 00 00 00 00
0x003cd10f0100: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
...
Tags for short granules around the buggy address (one tag corresponds to 16 bytes):
0x003cd10eff00: .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. ..
=>0x003cd10f0000: 43 .. [..] .. .. 1e .. 6c .. .. .. .. .. .. .. ..
0x003cd10f0100: .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. ..
这个处理我认为是合理的。因为这个例子里,访问的内存只是和之前申请的内存接近,本来就不在之前分配的范围内,而且又被释放了。所以hwasan没有明确给出错误类型,只给出了基本信息,让开发者自己去分析判断。
AOSP避坑
AOSP官方给的例子不太好,应该不是最新版hwasan的输出,影子内存的地址是影子内存本身的地址,这个意义不大,估计是老版本的:
Memory tags around the buggy address (one tag corresponds to 16 bytes):
0x006f33ae1fe0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x006f33ae1ff0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
=>0x006f33ae2000: 08 00 08 00 [83] 00 00 00 00 00 00 00 00 00 00 00
0x006f33ae2010: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x006f33ae2020: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00所以不要仔细研究这个例子了。
HWAsan实战问题案例
按照时间由近及远,看以下3个实战案例:
| 案例 | 问题类型 | 问题根因 | 相关知识点 |
|---|---|---|---|
| SF signalFrameUpdate hwasan UAF问题分析 | 报heap-buffer-overflow,实际应当是UAF | 并发 | •并发问题,重点分析报错栈和报错变量 •UAF问题误报为heap overflow |
| O2S hwasan SF FrameTarget UAF问题分析 | use-after-free | 业务逻辑 | UAF问题重点:分配栈和释放栈 |
| toucheventcheck 内存破坏问题分析解决复盘 | heap-buffer-overflow | 并发 | •复现问题的小技巧•单模块开启hwasan |
经验总结
hwasan问题分析
以下是我个人总结的hwasan问题分析经验,供参考:
💡
• 必现、高概率问题,优先考虑代码逻辑问题;偶现问题,优先考虑并发问题。
• UAF问题,重点分析分配栈和释放栈。
• overflow问题,重点分析报错栈和分配栈。
• 如果怀疑问题类型判断错误,不用纠结,正常分析trace和代码逻辑即可。因为hwasan对问题类型的判断并不严谨,误报率不低。
如何避免内存错误?
• 用良好的设计,避免使用裸指针。优先级:引用+对象 > 智能指针 >> 裸指针
• 用良好的设计,避免并发问题。利用好编译期静态检测。
• 借鉴Rust Ownership的设计思路
关于如何避免并发问题,这里推荐一下我之前写的文档:【模板】小米系统软件设计评审自查表。里面列出了我认为的在设计、编码阶段一些必要的考量:
![http://minio.898311.xyz/blogimg/1767176600724_121066.png)
强烈建议在设计阶段就按照上表进行检查,可大幅降低出问题的概率!
参考资料&&拓展阅读
平总的两篇文章:wiki: HWASan源码与架构分析hwasan 内存分配器
论文:Memory Tagging and How it improves C/C++ memory safety
自己整理的hwasan部分源码:hwasan源码探究