Arm 内存标记扩展 (MTE)

为何选择 MTE?

内存安全 bug 是指以原生编程语言处理内存时遇到的错误,是常见的代码问题。这类问题会导致安全漏洞和稳定性问题。

Armv9 引入了 Arm Memory Tagging Extension (MTE),一种可让您捕获原生代码中的释放后使用和缓冲区溢出 bug 的硬件扩展。

检查支持情况

从 Android 13 开始,部分设备支持 MTE。如需检查设备是否在运行时启用了 MTE,请运行以下命令:

adb shell grep mte /proc/cpuinfo

如果结果为 Features : [...] mte,则表示设备在运行时启用了 MTE。

有些设备默认不启用 MTE,但允许开发者在启用 MTE 的情况下重新启动。这是一种不建议用于常规用途的实验性配置,因为它可能会降低设备性能或稳定性,但对于应用开发很有用。如需访问此模式,请在“设置”应用中依次前往开发者选项 >“Memory Tagging Extension”。如果系统未列出此选项,则表示您的设备不支持以这种方式启用 MTE。

MTE 操作模式

MTE 支持两种模式:SYNC 和 ASYNC。SYNC 模式可提供更可靠的诊断信息,因此更适用于开发目的,而 ASYNC 模式的高性能使其可以用于已发布的应用。

同步模式 (SYNC)

此模式针对可调试性而非性能进行了优化,可以在可接受较高性能开销的情况下用作精确的 bug 检测工具。启用后,MTE SYNC 可以作为有效的安全缓解措施。

如果标记不匹配,处理器会在收到违规的加载或存储指令时终止进程,并返回 SIGSEGV(si_code 为 SEGV_MTESERR)以及有关内存访问和故障地址的完整信息。

此模式适合在测试期间用作 HWASan 的更快替代方案,此时您无需重新编译代码;或当您的应用出现存在漏洞的受攻击面时在生产环境中使用。此外,当 ASYNC 模式(详见下文)发现 bug 时,可以通过使用运行时 API 将执行切换到 SYNC 模式,来获取准确的 bug 报告。

此外,在 SYNC 模式下运行时,Android 分配器会记录每次分配和取消分配的堆栈轨迹,并利用这些信息提供更好的错误报告,其中包含对内存错误的解释(例如释放后使用或缓冲区溢出),以及相关内存事件的堆栈轨迹(如需了解详情,请参阅了解 MTE 报告)。与 ASYNC 模式相比,此类报告可提供更多上下文信息,并简化 bug 跟踪和修复。

异步模式 (ASYNC)

此模式专门针对 bug 报告的性能(而非精确度)进行了优化,可用于对内存安全 bug 进行低开销检测。如果标记不匹配,处理器会继续执行,直到达到最近的内核条目(例如,系统调用或计时器中断)。这时,处理器会终止进程并返回 SIGSEGV(代码为 SEGV_MTEAERR),而不会记录错误地址或内存访问。

此模式适合在生产环境中对经过严格测试的代码库(已知其内存安全 bug 的密度较低)降低内存安全漏洞,这将通过在测试期间使用 SYNC 模式来实现。

启用 MTE

对于单台设备

对于实验,应用兼容性更改可用于为未在清单文件中指定任何值(或指定 "default")的应用设置 memtagMode 属性的默认值。

您可以在全局设置菜单中的“系统”>“高级”>“开发者选项”>“应用兼容性变更”下找到相关更改。设置 NATIVE_MEMTAG_ASYNCNATIVE_MEMTAG_SYNC 可为特定应用启用 MTE。

或者,也可以使用 am 命令进行设置,如下所示:

  • 对于 SYNC 模式:$ adb shell am compat enable NATIVE_MEMTAG_SYNC my.app.name
  • 对于 ASYNC 模式:$ adb shell am compat enable NATIVE_MEMTAG_ASYNC my.app.name

在 Gradle 中

您可以为 Gradle 项目的所有调试 build 启用 MTE,方法是将

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">

    <application android:memtagMode="sync" tools:replace="android:memtagMode"/>
</manifest>

转换为 app/src/debug/AndroidManifest.xml。这会将清单的 memtagMode 替换为调试 build 的同步功能。

或者,您也可以为自定义 buildType 的所有 build 启用 MTE。为此,请创建自己的 buildType 并将 XML 放入 app/src/<name of buildType>/AndroidManifest.xml 中。

对于任何兼容设备上的 APK

MTE 默认处于停用状态。如果要在应用中使用 MTE,请在 AndroidManifest.xml 中的 <application><process> 标记下设置 android:memtagMode

android:memtagMode=(off|default|sync|async)

当为 <application> 标记设置了该属性时,该属性会影响应用使用的所有进程,并且可以通过设置 <process> 标记来为各个进程替换该属性。

运行应用

启用 MTE 后,请照常使用和测试应用。如果检测到内存安全问题,应用将会崩溃,并提供类似于以下内容的 Tombstone(请注意,SYNC 模式下,返回的 SIGSEGV 代码为 SEGV_MTESERR;ASYNC 模式下,代码为 SEGV_MTEAERR):

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)
Learn more about MTE reports: https://source.android.com/docs/security/test/memory-safety/mte-report

如需了解详情,请参阅 AOSP 文档中的了解 MTE 报告。您还可以使用 Android Studio 调试应用,调试程序会在导致内存访问无效的行停止。

高级用户:在您自己的分配器中使用 MTE

如要对并非通过常规系统分配器分配的内存使用 MTE,您需要修改分配器以标记内存和指针。

您需要通过 mmap(或 mprotect)的 prot 标志中的 PROT_MTE 来分配分配器的页面。

所有标记的分配都需要 16 字节对齐,因为标记只能分配给 16 字节的区块(也称为 granule)。

然后,在返回指针之前,您需要使用 IRG 指令生成随机标记并将其存储在指针中。

请按照以下说明标记底层内存:

  • STG:标记单个 16 字节的 granule
  • ST2G:标记两个 16 字节的 granule
  • DC GVA:使用相同的标签标记缓存行

或者,以下说明还对内存进行零初始化:

  • STZG:标记和零初始化单个 16 字节的 granule
  • STZ2G:标记和零初始化两个 16 字节的 granule
  • DC GZVA:使用相同的标签标记和零初始化缓存行

请注意,较旧的 CPU 不支持这些指令,因此启用 MTE 后,您需要有条件地运行这些指令。您可以检查进程是否启用了 MTE:

#include <sys/prctl.h>

bool runningWithMte() {
      int mode = prctl(PR_GET_TAGGED_ADDR_CTRL, 0, 0, 0, 0);
      return mode != -1 && mode & PR_MTE_TCF_MASK;
}

您可以参阅 scudo 实现代码

了解详情

如需了解详情,请参阅由 Arm 撰写的 Android OS MTE 用户指南