了解 MTE 报告

会返回代码 9 (SEGV_MTESERR) 或代码 8 (SEGV_MTEAERR) 的 SIGSEGV 崩溃属于内存标记故障。内存标记扩展 (MTE) 是一项 Android 12 及更高版本支持的 Armv9 功能。MTE 是带标记内存的硬件实现。它可提供精细的内存保护,以检测和减少内存安全 bug

使用 C/C++ 时,从对 malloc()、运算符 new() 或类似函数的调用中返回的指针只能用于访问相应分配范围内的内存,且只能在该分配处于活跃状态(未被释放或删除)时访问。MTE 在 Android 中用于检测违反此规则的行为,崩溃报告中将其称为“Buffer Overflow”(缓冲区上溢)/“Buffer Underflow”(缓冲区下溢)和“Use After Free”(释放后使用)问题。

MTE 有两种模式:同步(或“sync”)和异步(或“async”)。前者的运行速度较慢,但提供了更准确的诊断结果。后者的运行速度更快,但只能提供大致的详细信息。由于诊断信息略有不同,因此我们将分别介绍这两个模式。

同步模式 MTE

在 MTE 的同步(“sync”)模式下,SIGSEGV 会崩溃并返回代码 9 (SEGV_MTESERR)。

pid: 13935, tid: 13935, name: sanitizer-statu  >>> sanitizer-status <<<
uid: 0
tagged_addr_ctrl: 000000000007fff3
signal 11 (SIGSEGV), code 9 (SEGV_MTESERR), fault addr 0x800007ae92853a0
Cause: [MTE]: Use After Free, 0 bytes into a 32-byte allocation at 0x7ae92853a0
x0  0000007cd94227cc  x1  0000007cd94227cc  x2  ffffffffffffffd0  x3  0000007fe81919c0
x4  0000007fe8191a10  x5  0000000000000004  x6  0000005400000051  x7  0000008700000021
x8  0800007ae92853a0  x9  0000000000000000  x10 0000007ae9285000  x11 0000000000000030
x12 000000000000000d  x13 0000007cd941c858  x14 0000000000000054  x15 0000000000000000
x16 0000007cd940c0c8  x17 0000007cd93a1030  x18 0000007cdcac6000  x19 0000007fe8191c78
x20 0000005800eee5c4  x21 0000007fe8191c90  x22 0000000000000002  x23 0000000000000000
x24 0000000000000000  x25 0000000000000000  x26 0000000000000000  x27 0000000000000000
x28 0000000000000000  x29 0000007fe8191b70
lr  0000005800eee0bc  sp  0000007fe8191b60  pc  0000005800eee0c0  pst 0000000060001000

backtrace:
      #00 pc 00000000000010c0  /system/bin/sanitizer-status (test_crash_malloc_uaf()+40) (BuildId: 953fc93301472d0b72709b2b9a9f6f30)
      #01 pc 00000000000014a4  /system/bin/sanitizer-status (test(void (*)())+132) (BuildId: 953fc93301472d0b72709b2b9a9f6f30)
      #02 pc 00000000000019cc  /system/bin/sanitizer-status (main+1032) (BuildId: 953fc93301472d0b72709b2b9a9f6f30)
      #03 pc 00000000000487d8  /apex/com.android.runtime/lib64/bionic/libc.so (__libc_init+96) (BuildId: 6ab39e35a2fae7efbe9a04e9bbb14331)

deallocated by thread 13935:
      #00 pc 000000000004643c  /apex/com.android.runtime/lib64/bionic/libc.so (scudo::Allocator<scudo::AndroidConfig, &(scudo_malloc_postinit)>::quarantineOrDeallocateChunk(scudo::Options, void*, scudo::Chunk::UnpackedHeader*, unsigned long)+688) (BuildId: 6ab39e35a2fae7efbe9a04e9bbb14331)
      #01 pc 00000000000421e4  /apex/com.android.runtime/lib64/bionic/libc.so (scudo::Allocator<scudo::AndroidConfig, &(scudo_malloc_postinit)>::deallocate(void*, scudo::Chunk::Origin, unsigned long, unsigned long)+212) (BuildId: 6ab39e35a2fae7efbe9a04e9bbb14331)
      #02 pc 00000000000010b8  /system/bin/sanitizer-status (test_crash_malloc_uaf()+32) (BuildId: 953fc93301472d0b72709b2b9a9f6f30)
      #03 pc 00000000000014a4  /system/bin/sanitizer-status (test(void (*)())+132) (BuildId: 953fc93301472d0b72709b2b9a9f6f30)

allocated by thread 13935:
      #00 pc 0000000000042020  /apex/com.android.runtime/lib64/bionic/libc.so (scudo::Allocator<scudo::AndroidConfig, &(scudo_malloc_postinit)>::allocate(unsigned long, scudo::Chunk::Origin, unsigned long, bool)+1300) (BuildId: 6ab39e35a2fae7efbe9a04e9bbb14331)
      #01 pc 0000000000042394  /apex/com.android.runtime/lib64/bionic/libc.so (scudo_malloc+36) (BuildId: 6ab39e35a2fae7efbe9a04e9bbb14331)
      #02 pc 000000000003cc9c  /apex/com.android.runtime/lib64/bionic/libc.so (malloc+36) (BuildId: 6ab39e35a2fae7efbe9a04e9bbb14331)
      #03 pc 00000000000010ac  /system/bin/sanitizer-status (test_crash_malloc_uaf()+20) (BuildId: 953fc93301472d0b72709b2b9a9f6f30)
      #04 pc 00000000000014a4  /system/bin/sanitizer-status (test(void (*)())+132) (BuildId: 953fc93301472d0b72709b2b9a9f6f30)

所有 MTE 崩溃报告都包含检测到问题的点的常规寄存器转储和回溯。MTE 检测到的错误对应的“Cause:”行将包含“[MTE]”(如上例所示)以及更多详细信息。在本例中,检测到的具体错误类型是“Use after free”,“0 bytes into a 32-byte allocation at 0x7ae92853a0”说明该分配范围的大小和地址,以及我们尝试访问的内存分配中的偏移量。

MTE 崩溃报告还包含额外的回溯,而不仅包含来自检测点的回溯。

“Use After Free”错误会向崩溃转储添加“deallocated by”和“allocated by”部分,以显示取消分配此内存时(在使用此内存之前!)和之前分配此内存时的堆栈轨迹。这些信息还会告诉您哪个线程执行了分配/取消分配操作。在这个简单示例中,所有三个线程:检测线程、分配线程和取消分配线程都是一样的,但在更复杂的现实情况下,这并不一定是正确的;了解它们可能不尽相同,这是查找与并发相关的 bug 时的一条重要线索。

“Buffer Overflow”和“Buffer Underflow”错误只提供一个额外的“allocated by”堆栈轨迹,因为按照定义,它们尚未被取消分配(或者它们将显示为“Use After Free”):

Cause: [MTE]: Buffer Overflow, 0 bytes right of a 32-byte allocation at 0x7ae92853a0
[...]
backtrace:
[...]
allocated by thread 13949:

请注意,上文中使用了“right”一词,这是系统在告诉您错误访问的分配结束之后的字节数;下溢时会显示为“left”,是指分配开始前的字节数。

多个可能原因

有时 SEGV_MTESERR 报告包含以下行:

Note: multiple potential causes for this crash were detected, listing them in decreasing order of likelihood.

当错误源有多个合理的候选原因并且我们无法确定哪个是真正的原因时,就会发生这种情况。我们可按大概的可能性顺序输出最多 3 个此类候选原因,并将此分析工作交给用户。

signal 11 (SIGSEGV), code 9 (SEGV_MTESERR), fault addr 0x400007b43063db5
backtrace:
    [stack...]

Note: multiple potential causes for this crash were detected, listing them in decreasing order of probability.

Cause: [MTE]: Use After Free, 5 bytes into a 10-byte allocation at 0x7b43063db0
deallocated by thread 6663:
    [stack...]
allocated by thread 6663:
    [stack...]

Cause: [MTE]: Use After Free, 5 bytes into a 6-byte allocation at 0x7b43063db0
deallocated by thread 6663:
    [stack...]

allocated by thread 6663:
    [stack...]

在上面的示例中,我们检测到了同一内存地址中的最近两次分配,这些分配可能是无效内存访问的预期目标。如果分配会重复使用可用的内存,就可能发生这种情况。例如,如果您有如下序列:new、free、new、free、new、free、access。系统会先输出时间比较近的分配。

详细的原因确定启发法

崩溃的“Cause”应该显示所访问指针最初派生自的内存分配。遗憾的是,MTE 硬件无法将所含标记不匹配的指针转换至分配。为了说明 SEGV_MTESERR 崩溃问题,Android 会分析以下数据:

  • 故障地址(包括指针标记)。
  • 包含堆栈轨迹和内存标记的近期堆分配列表。
  • 附近当前(实时)分配及其内存标记。

故障地址中近期取消分配的任何内存(且内存标记与错误地址标记匹配)都可能会是导致“Use After Free”的原因。

任何附近的实时内存(且内存标记与错误地址标记匹配)都可能会是导致“Buffer Overflow”(或“Buffer Underflow”)的原因。

相较于距离该故障较远的分配,时间或空间上更近的分配被认为是可能性更大的原因。

由于取消分配的内存经常被重复使用,且不同标记值的数量较少(小于 16 个),因此,找到多个可能的候选原因并不罕见,没有办法自动找到真正的原因。因此,有时 MTE 报告会列出多个可能的原因。

建议应用开发者在研究可能原因时从最可能的那个原因开始。通常可以根据堆栈轨迹轻松滤除不相关的原因。

异步模式 MTE

在 MTE 的异步(“async”)模式下,SIGSEGV 会崩溃并返回代码 8 (SEGV_MTEAERR)。

当某程序执行无效的内存访问时,SEGV_MTEAERR 故障不会立即发生。此问题会在该事件结束后不久被检测到,而此时该程序已终止。此点通常是下一个系统调用,但也可能是一个计时器中断;简而言之,会是任何用户空间向内核的转换。

SEGV_MTEAERR 故障不会保留内存地址(它始终显示为“-------”)。回溯对应于检测到相应条件的时刻(即在下一次系统调用或其他上下文切换时),而不是执行无效访问的时间。

这意味着,异步 MTE 崩溃中的“主”回溯通常不相关。因此,与同步模式故障相比,异步模式故障的调试难度要高得多。对它们的最佳理解是,显示给定线程中附近代码里存在内存 bug。Tombstone 文件底部的日志可能会提供相关提示,让您了解实际发生的情况。否则,建议采取的措施是在同步模式下重现错误,然后使用同步模式提供的较好的诊断信息!

高级主题

在后台,内存标记的运作方式是为每个堆分配分配一个随机的 4 位 (0..15) 标记值。此值存储在与所分配的堆内存对应的特殊元数据区域中。系统会为从 malloc() 或运算符 new() 等函数返回的堆指针的最高有效字节分配相同的值。

在此过程中启用标记检查时,对于每次内存访问,CPU 都会自动将指针的顶部字节与内存标记进行比较。如果标记不匹配,CPU 会发出一个导致崩溃的错误。

由于可能的标记值数量有限,因此这种方法具有概率性。不应使用指定指针访问的任何内存位置(例如超出边界或取消分配后 [“悬空指针”])都可能具有不同的标记值,并会导致崩溃。大约有 7% 的概率不会检测到某个 bug 的任何单个出现情况。由于标记值是随机分配的,因此该 bug 下次出现时,检测到它的概率只有 93% 左右。

标记值可显示在故障地址字段和寄存器转储中,如下所示。此部分可用于检查代码是否设置为正常设置,以及查看具有相同标记值的附近其他内存分配,因为它们或许是导致报告中已列错误之外某个错误的可能原因。我们预计这主要适用于致力实现 MTE 本身或其他低级别系统组件的人员,而不是开发者。

signal 11 (SIGSEGV), code 9 (SEGV_MTESERR), fault addr 0x0800007ae92853a0
Cause: [MTE]: Use After Free, 0 bytes into a 32-byte allocation at 0x7ae92853a0
    x0  0000007cd94227cc  x1  0000007cd94227cc  x2  ffffffffffffffd0  x3  0000007fe81919c0
    x4  0000007fe8191a10  x5  0000000000000004  x6  0000005400000051  x7  0000008700000021
    x8  0800007ae92853a0  x9  0000000000000000  x10 0000007ae9285000  x11 0000000000000030
    x12 000000000000000d  x13 0000007cd941c858  x14 0000000000000054  x15 0000000000000000
    x16 0000007cd940c0c8  x17 0000007cd93a1030  x18 0000007cdcac6000  x19 0000007fe8191c78
    x20 0000005800eee5c4  x21 0000007fe8191c90  x22 0000000000000002  x23 0000000000000000
    x24 0000000000000000  x25 0000000000000000  x26 0000000000000000  x27 0000000000000000
    x28 0000000000000000  x29 0000007fe8191b70
    lr  0000005800eee0bc  sp  0000007fe8191b60  pc  0000005800eee0c0  pst 0000000060001000

崩溃报告中还会新增一个特殊的“Memory tags”部分,用于显示故障地址周围的内存标记。在下面的示例中,指针标记“4”与内存标记“a”不匹配。

Memory tags around the fault address (0x0400007b43063db5), one tag per 16 bytes:
  0x7b43063500: 0  f  0  2  0  f  0  a  0  7  0  8  0  7  0  e
  0x7b43063600: 0  9  0  8  0  5  0  e  0  f  0  c  0  f  0  4
  0x7b43063700: 0  b  0  c  0  b  0  2  0  1  0  4  0  7  0  8
  0x7b43063800: 0  b  0  c  0  3  0  a  0  3  0  6  0  b  0  a
  0x7b43063900: 0  3  0  4  0  f  0  c  0  3  0  e  0  0  0  c
  0x7b43063a00: 0  3  0  2  0  1  0  8  0  9  0  4  0  3  0  4
  0x7b43063b00: 0  5  0  2  0  5  0  a  0  d  0  6  0  d  0  2
  0x7b43063c00: 0  3  0  e  0  f  0  a  0  0  0  0  0  0  0  4
=>0x7b43063d00: 0  0  0  a  0  0  0  e  0  d  0 [a] 0  f  0  e
  0x7b43063e00: 0  7  0  c  0  9  0  a  0  d  0  2  0  0  0  c
  0x7b43063f00: 0  0  0  6  0  b  0  8  0  3  0  0  0  5  0  e
  0x7b43064000: 0  d  0  2  0  7  0  a  0  7  0  a  0  d  0  8
  0x7b43064100: 0  b  0  2  0  b  0  4  0  1  0  6  0  d  0  4
  0x7b43064200: 0  1  0  6  0  f  0  2  0  f  0  6  0  5  0  c
  0x7b43064300: 0  1  0  4  0  d  0  6  0  f  0  e  0  1  0  8
  0x7b43064400: 0  f  0  4  0  3  0  2  0  1  0  2  0  5  0  6

Tombstone 中的某些部分既会显示所有寄存器值周围的内存内容,也会显示它们的标记值。

memory near x10 ([anon:scudo:primary]):
0000007b4304a000 7e82000000008101 000003e9ce8b53a0  .......~.S......
0700007b4304a010 0000200000006001 0000000000000000  .`... ..........
0000007b4304a020 7c03000000010101 000003e97c61071e  .......|..a|....
0200007b4304a030 0c00007b4304a270 0000007ddc4fedf8  p..C{.....O.}...
0000007b4304a040 84e6000000008101 000003e906f7a9da  ................
0300007b4304a050 ffffffff00000042 0000000000000000  B...............
0000007b4304a060 8667000000010101 000003e9ea858f9e  ......g.........
0400007b4304a070 0000000100000001 0000000200000002  ................
0000007b4304a080 f5f8000000010101 000003e98a13108b  ................
0300007b4304a090 0000007dd327c420 0600007b4304a2b0   .'.}......C{...
0000007b4304a0a0 88ca000000010101 000003e93e5e5ac5  .........Z^>....
0a00007b4304a0b0 0000007dcc4bc500 0300007b7304cb10  ..K.}......s{...
0000007b4304a0c0 0f9c000000010101 000003e9e1602280  ........."`.....
0900007b4304a0d0 0000007dd327c780 0700007b7304e2d0  ..'.}......s{...
0000007b4304a0e0 0d1d000000008101 000003e906083603  .........6......
0a00007b4304a0f0 0000007dd327c3b8 0000000000000000  ..'.}...........