HWASan原理与实战

💡

本文聚焦HWAsan的基本原理和实战问题分析,力图涵盖HWASan方方面面的同时,突出重点。

对于一些必须掌握的基础,本文都有讲解。对于一些技术细节和拓展知识,也给出了引文链接,供读者随时查看。

因作者知识有限,文章难免有纰漏。欢迎通过文档评论、飞书私聊等方式交流指正,感谢!

HWASan基础知识

HWASan背景

内存错误是C/C++中最让人头疼的一类问题。为了解决这一问题,首先在编程语言层面,引入了很多机制:

  1. C++引用、智能指针。可以规避大多数非并发的内存破坏问题。

  2. 编译期并发安全检测-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基本原理

总体流程:

  1. 将所有可写的内存(堆、栈、全局数据区)以16:1的比例映射到影子内存区。所有内存块的起始地址都按照16字节对齐,保证每16字节的user memory都能映射到1字节的shadow memory。这个16就是Tag Granularity,下文详细介绍。

  2. 分配对象的时候,随机分配一个8位的tag标记到该对象的虚拟地址最高8位,同时该tag也会保存到其映射的shadow memory中。

  3. 编译器在每个内存地址的load/store之前都会插入检查指令(也就是插桩),用于确认操作的地址最高8位保存的tag与其映射的shadow memory中的tag值一致。这也是代码体积增大、运行时有开销的主要原因。

  4. 对象回收后也会重新分配一个随机值,保存到其映射的shadow memory中,方便检测use after free问题。

  5. 当分配的对象小于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需要检测两种可能:

  1. 地址tag和内存tag一致

  2. 影子内存存储的是short granule的大小,访问的地址在short granule的范围之内。

举个例子,一个17字节的内存块,影子内存的第一个字节存储随机tag 0x66,第二个字节存储的就是二进制0x01。

  1. 当内存访问的是第16个字节,地址0x0000100F时,比对发现ptr tag和mem tag均是0x66,pass。

  2. 当内存访问的是第17个字节,地址0x00001010时,比对发现mem tag是0x01,则需要再检测地址模16后(16字节对齐)是否小于等于0x01。发现等于,pass。

  3. 当内存访问的是第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报错主要信息包括:

  1. 出错地址

  2. ptr tag && mem tag

  3. ptr对应的内存块信息

  4. 错误类型

  5. 报错栈、分配栈、释放栈(UAF独有)

  6. 影子内存值

  7. 随机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)

强烈建议在设计阶段就按照上表进行检查,可大幅降低出问题的概率!

参考资料&&拓展阅读

AOSP HWASan官方文档

平总的两篇文章:wiki: HWASan源码与架构分析hwasan 内存分配器

clang HWAsan设计文档

论文:Memory Tagging and How it improves C/C++ memory safety

自己整理的hwasan部分源码:hwasan源码探究