MTE的开源软件支持,主要分为三大部分。
- Linux 内核态支持
- Linux 用户态支持
- 编译工具链支持
Linux内核对MTE的支持
在介绍内核对MTE的支持之前,我们必须要回顾 Armv8-A Top Byte Ignore (TBI)特性。
Armv8-A TCR\_EL1.bit[37] 代表 TBI0\, TCR\_EL1.bit[38] 代表 TBI1。
如果 TBI0 为0,表示所有 64 位地址都参与地址计算。这个是常规用法。
如果 TBI0 为1,表示 64 位地址 (一共8个字节)的最高位字节 bit[63:56] 将被忽略,不被用在实际地址计算中。此时, bit[55] 将代表最高位字节。
- 如果 bit[55] = 1, 则系统认为最高位字节 bit[63:56] = 0xFF。
- 如果 bit[55] = 0, 则系统认为最高位字节 bit[63:56] = 0x00
TBI0 和 TBI1 的区别,在于前者对 TTBR0\_EL1 所指向的地址空间起作用;后者对 TTBR1\_EL1 所指向的地址空间起作用。
在TBI0 / TBI1 = 1的情况下,空余出来的地址最高位字节可以被软件使用,在这个特殊字节中记录一些和该地址相关的一些信息。这个特殊字节可以称作一个标记 (Tag),而该特殊字节所在的低位 54-bit 指针可称为标记指针。
最典型的,就是苹果 iOS 系统使用的指针引用计数 和谷歌 Android 系统中的 HWASAN 程序。
那么问题就出现了。当标记指针被传递到Linux 内核系统调用的时候(比如 通过 copy\_from\_user() 传递),如果该系统调用正好针对该标记指针进行了内存访问操作,那么没有什么问题;但是对于某一类特殊的系统调用,比如 mmap\, mprotect 等,它们并没有进行实际的内存访问而仅仅只是操作了地址区间段。对于后一类系统调用, 使用一个无标记指针实际上是更合适的,以避免额外的效率损失。
Linux 内核代码因此要扩展非内存访问类型系统调用对标记指针的支持,这样可以避免无谓的应用程序接口的限制放宽。
有两种可能的做法:
- 其一,在内核进行指针检查的时候,调用 untagged_addr() 宏直接去除指针标记
- 其二,在进入内核系统调用之前,先封装一层函数以去除指针标记。但是因为有大量现存的 ioctl() 内核函数调用,我们需要对每一个 ioctl() 函数都提供一个封装,此方案的可行性就降低了。
截止到本文发布时, Linux 内核针对标记指针如何处理的开发补丁仍然还在持续讨论中。
下面的是一个简单的用于检测标记指针功能是否正常的程序。
include \<stdio.h>
include \<unistd.h>
include \<stdint.h>
include \<sys/utsname.h>
\#define SHIFT\_TAG(tag) ((uint64\_t)(tag) << 56)
\#define SET\_TAG(ptr\, tag) (((uint64\_t)(ptr) & \~SHIFT\_TAG(0xff)) | \
SHIFT_TAG(tag))int main(void)
{
struct utsname utsname;
void *ptr = &utsname;
void *tagged\_ptr = (void *)SET\_TAG(ptr\, 0x42);
int err = uname(tagged_ptr);
return err;
}
Linux用户态对MTE的支持
用户态支持主要是堆地址的标记。
堆地址标记可以通过 C 标准库和相应的内存分配函数来实现。
对于一个动态链接库的系统,技术上可以在不改变现有二进制可执行代码的前提下,提供带有堆标记特性的堆。此种情况下,只需要修改操作系统内核和 C 库。
Arm 已经提供了一个支持 MTE 的原型 Linux 内核。该内核做了以下修改:
- 用户态地址空间管理的时候,能够去除来自用户态指针的标记
- clear\_page() 和 copy\_page() 函数能够处理带有标记的虚拟地址
- 能够处理因为地址标记和地址不匹配导致的系统错误
- 支持将被暴露给用户态进程的内存映射转化成带标记的内存地址
- 支持对 MTE 架构特性的检测,以及对相应的系统寄存器进行配置以激活 MTE 功能
对于C库,Arm主要提供了以下几类函数修改:
- malloc()
- free()
- calloc()
- realloc()
顺便提一下,对内存复制和字符串相关函数也进行了相应修改,从而避免对源内存缓冲区的读取错误。
编译工具链对MTE的支持
编译工具链的支持主要体现在栈地址标记。因为在一个运行时栈中分配带有标记的内存,必须要获得编译器支持。
为了支持 MTE, 旧有的二进制可执行代码必须要重新编译。
LLVM 的 C 语言编译器 Clang 已经实现了对栈地址标记的功能。LLVM9 版本将包括此功能。
GCC 对于栈地址标记的支持正在计划中。
Clang 目前定义了两个公开的内联函数,初步实现了MTE 栈标记,可以实现一个带标记的栈帧指针。
- llvm.aarch64.irg.sp
- llvm.aarch64.tagp
Clang内部也定义了几个私有成员函数。
- int\_aarch64\_tagp
- int\_aarch64\_settag
- int\_aarch64\_settag\_zero
- int\_aarch64\_stgp