AArch64 Linux 中的内存标记扩展 (MTE)¶
- 作者:Vincenzo Frascino <vincenzo.frascino@arm.com>
Catalin Marinas <catalin.marinas@arm.com>
日期:2020-02-25
本文档介绍了 AArch64 Linux 中提供的内存标记扩展功能。
简介¶
基于 ARMv8.5 的处理器引入了内存标记扩展 (MTE) 功能。MTE 构建在 ARMv8.0 虚拟地址标记 TBI(忽略最高字节)功能之上,并允许软件访问物理地址空间中每个 16 字节粒度的 4 位分配标记。此内存范围必须使用 Normal-Tagged 内存属性进行映射。逻辑标记源自用于内存访问的虚拟地址的第 59-56 位。启用 MTE 的 CPU 将比较逻辑标记与分配标记,并可能在不匹配时引发异常,具体取决于系统寄存器配置。
用户空间支持¶
当选择 CONFIG_ARM64_MTE
并且硬件支持内存标记扩展时,内核通过 HWCAP2_MTE
向用户空间通告该功能。
PROT_MTE¶
要访问分配标记,用户进程必须使用 mmap()
和 mprotect()
的新 prot
标志在地址范围上启用 Tagged 内存属性。
PROT_MTE
- 页面允许访问 MTE 分配标记。
当此类页面首次映射到用户地址空间时,分配标记设置为 0,并在写时复制时保留。MAP_SHARED
受到支持,并且分配标记可以在进程之间共享。
注意:PROT_MTE
仅在 MAP_ANONYMOUS
和基于 RAM 的文件映射(tmpfs
、memfd
)上受到支持。将其传递给其他类型的映射将导致这些系统调用返回 -EINVAL
。
注意:PROT_MTE
标志(和相应的内存类型)无法通过 mprotect()
清除。
注意:具有 MADV_DONTNEED
和 MADV_FREE
的 madvise()
内存范围可能在系统调用之后的任何时间点清除(设置为 0)分配标记。
标记检查错误¶
当在地址范围上启用 PROT_MTE
并且在访问时发生逻辑标记和分配标记之间的不匹配时,存在三种可配置的行为
忽略 - 这是默认模式。CPU(和内核)忽略标记检查错误。
同步 - 内核同步引发
SIGSEGV
,其中.si_code = SEGV_MTESERR
且.si_addr = <fault-address>
。不执行内存访问。如果SIGSEGV
被冒犯线程忽略或阻止,则包含进程将以coredump
终止。异步 - 内核在冒犯线程中异步引发
SIGSEGV
,在发生一个或多个标记检查错误之后,其中.si_code = SEGV_MTEAERR
且.si_addr = 0
(故障地址未知)。非对称 - 读取的处理方式与同步模式相同,而写入的处理方式与异步模式相同。
用户可以使用 prctl(PR_SET_TAGGED_ADDR_CTRL, flags, 0, 0, 0)
系统调用,按线程选择上述模式,其中 flags
在 PR_MTE_TCF_MASK
位字段中包含以下值的任意数量
PR_MTE_TCF_NONE
- 忽略标记检查错误(如果与其他选项组合则忽略)
PR_MTE_TCF_SYNC
- 同步标记检查错误模式PR_MTE_TCF_ASYNC
- 异步标记检查错误模式
如果未指定任何模式,则忽略标记检查错误。如果指定了单个模式,则程序将以该模式运行。如果指定了多个模式,则按以下“按 CPU 首选标记检查模式”部分中所述选择模式。
可以使用 prctl(PR_GET_TAGGED_ADDR_CTRL, 0, 0, 0, 0)
系统调用读取当前标记检查错误配置。如果请求了多种模式,则将报告所有模式。
还可以通过使用 MSR TCO, #1
设置 PSTATE.TCO
位来禁用用户线程的标记检查。
注意:信号处理程序始终以 PSTATE.TCO = 0
调用,而与中断上下文无关。在 sigreturn()
上还原 PSTATE.TCO
。
注意:没有适用于用户应用程序的匹配所有逻辑标记。
注意:如果用户线程标记检查模式为 PR_MTE_TCF_NONE
或 PR_MTE_TCF_ASYNC
,则不检查内核对用户地址空间的访问(例如,read()
系统调用)。如果标记检查模式为 PR_MTE_TCF_SYNC
,则内核会尽最大努力检查其用户地址访问,但不能始终保证这一点。无论用户配置如何,内核对用户地址的访问始终以有效的 PSTATE.TCO
值为零执行。
按 CPU 首选的标记检查模式¶
在某些 CPU 上,MTE 在更严格的标记检查模式下的性能与不太严格的标记检查模式下的性能相似。这使得在请求不太严格的检查模式时,在这些 CPU 上启用更严格的检查以获得更严格检查的错误检测优势而不会降低性能是值得的。为了支持这种情况,特权用户可以将更严格的标记检查模式配置为 CPU 的首选标记检查模式。
每个 CPU 的首选标记检查模式由 /sys/devices/system/cpu/cpu<N>/mte_tcf_preferred
控制,特权用户可以在其中写入值 async
、sync
或 asymm
。每个 CPU 的默认首选模式是 async
。
为了允许程序可能在 CPU 的首选标记检查模式下运行,用户程序可以在 prctl(PR_SET_TAGGED_ADDR_CTRL, flags, 0, 0, 0)
系统调用的 flags
参数中设置多个标记检查错误模式位。如果请求了同步和异步模式,则内核也可以选择非对称模式。如果 CPU 的首选标记检查模式位于任务提供的标记检查模式集中,则将选择该模式。否则,内核将使用以下偏好顺序从任务模式集中的任务模式中选择一种模式
异步
非对称
同步
请注意,用户空间无法请求多种模式并同时禁用非对称模式。
初始进程状态¶
在 execve()
上,新进程具有以下配置
PR_TAGGED_ADDR_ENABLE
设置为 0(禁用)未选择任何标记检查模式(忽略标记检查错误)
PR_MTE_TAG_MASK
设置为 0(排除所有标记)PSTATE.TCO
设置为 0未在任何初始内存映射上设置
PROT_MTE
在 fork()
上,新进程继承父进程的配置和内存映射属性,但 MADV_WIPEONFORK
的 madvise()
范围除外,这些范围将清除数据和标记(设置为 0)。
ptrace()
接口¶
PTRACE_PEEKMTETAGS
和 PTRACE_POKEMTETAGS
允许示踪器从被跟踪者的地址空间读取标记或将被跟踪者的地址空间设置为标记。ptrace()
系统调用按 ptrace(request, pid, addr, data)
调用,其中
request
-PTRACE_PEEKMTETAGS
或PTRACE_POKEMTETAGS
之一。pid
- 被跟踪者的 PID。addr
- 被跟踪者地址空间中的地址。data
- 指向struct iovec
的指针,其中iov_base
指向示踪器地址空间中长度为iov_len
的缓冲区。
示踪器 iov_base
缓冲区中的标记表示为每个字节一个 4 位标记,并对应于被跟踪者地址空间中的 16 字节 MTE 标记粒度。
注意:如果 addr
未与 16 字节粒度对齐,则内核将使用相应的对齐地址。
ptrace()
返回值
0 - 已复制标记,示踪器的
iov_len
已更新为传输的标记数。如果无法访问被跟踪者或示踪器空间中请求的地址范围,或者该范围没有有效标记,则此值可能小于请求的iov_len
。-EPERM
- 无法跟踪指定的进程。-EIO
- 无法访问被跟踪者的地址范围(例如,无效地址),并且没有复制任何标记。iov_len
未更新。-EFAULT
- 在访问示踪器的内存(struct iovec
或iov_base
缓冲区)时发生错误,并且没有复制任何标记。iov_len
未更新。-EOPNOTSUPP
- 被跟踪者的地址没有有效标记(从未使用PROT_MTE
标志映射)。iov_len
未更新。
注意:上述请求没有瞬态错误,因此用户程序不应在系统调用返回非零值时重试。
具有 addr == ``NT_ARM_TAGGED_ADDR_CTRL
的 PTRACE_GETREGSET
和 PTRACE_SETREGSET
允许 ptrace()
访问进程的标记地址 ABI 控制和 MTE 配置,如 AArch64 TAGGED ADDRESS ABI 和上面所述的 prctl()
选项中所述。相应的 regset
是 8 字节(sizeof(long))
)的 1 个元素。
核心转储支持¶
使用 PROT_MTE
映射的用户内存的分配标记作为额外的 PT_AARCH64_MEMTAG_MTE
段转储在核心文件中。此类段的程序头定义为
p_type
:PT_AARCH64_MEMTAG_MTE
p_flags
:0
p_offset
:段文件偏移量
p_vaddr
:段虚拟地址,与相应的
PT_LOAD
段相同p_paddr
:0
p_filesz
:文件中的段大小,计算为
p_mem_sz / 32
(两个 4 位标记覆盖 32 字节的内存)p_memsz
:内存中的段大小,与相应的
PT_LOAD
段相同p_align
:0
这些标记作为字节中的两个 4 位标记存储在核心文件中 p_offset
处。对于 16 字节的标记粒度,4K 页面需要在核心文件中使用 128 字节。
正确使用示例¶
MTE 示例代码
/*
* To be compiled with -march=armv8.5-a+memtag
*/
#include <errno.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/auxv.h>
#include <sys/mman.h>
#include <sys/prctl.h>
/*
* From arch/arm64/include/uapi/asm/hwcap.h
*/
#define HWCAP2_MTE (1 << 18)
/*
* From arch/arm64/include/uapi/asm/mman.h
*/
#define PROT_MTE 0x20
/*
* From include/uapi/linux/prctl.h
*/
#define PR_SET_TAGGED_ADDR_CTRL 55
#define PR_GET_TAGGED_ADDR_CTRL 56
# define PR_TAGGED_ADDR_ENABLE (1UL << 0)
# define PR_MTE_TCF_SHIFT 1
# define PR_MTE_TCF_NONE (0UL << PR_MTE_TCF_SHIFT)
# define PR_MTE_TCF_SYNC (1UL << PR_MTE_TCF_SHIFT)
# define PR_MTE_TCF_ASYNC (2UL << PR_MTE_TCF_SHIFT)
# define PR_MTE_TCF_MASK (3UL << PR_MTE_TCF_SHIFT)
# define PR_MTE_TAG_SHIFT 3
# define PR_MTE_TAG_MASK (0xffffUL << PR_MTE_TAG_SHIFT)
/*
* Insert a random logical tag into the given pointer.
*/
#define insert_random_tag(ptr) ({ \
uint64_t __val; \
asm("irg %0, %1" : "=r" (__val) : "r" (ptr)); \
__val; \
})
/*
* Set the allocation tag on the destination address.
*/
#define set_tag(tagged_addr) do { \
asm volatile("stg %0, [%0]" : : "r" (tagged_addr) : "memory"); \
} while (0)
int main()
{
unsigned char *a;
unsigned long page_sz = sysconf(_SC_PAGESIZE);
unsigned long hwcap2 = getauxval(AT_HWCAP2);
/* check if MTE is present */
if (!(hwcap2 & HWCAP2_MTE))
return EXIT_FAILURE;
/*
* Enable the tagged address ABI, synchronous or asynchronous MTE
* tag check faults (based on per-CPU preference) and allow all
* non-zero tags in the randomly generated set.
*/
if (prctl(PR_SET_TAGGED_ADDR_CTRL,
PR_TAGGED_ADDR_ENABLE | PR_MTE_TCF_SYNC | PR_MTE_TCF_ASYNC |
(0xfffe << PR_MTE_TAG_SHIFT),
0, 0, 0)) {
perror("prctl() failed");
return EXIT_FAILURE;
}
a = mmap(0, page_sz, PROT_READ | PROT_WRITE,
MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
if (a == MAP_FAILED) {
perror("mmap() failed");
return EXIT_FAILURE;
}
/*
* Enable MTE on the above anonymous mmap. The flag could be passed
* directly to mmap() and skip this step.
*/
if (mprotect(a, page_sz, PROT_READ | PROT_WRITE | PROT_MTE)) {
perror("mprotect() failed");
return EXIT_FAILURE;
}
/* access with the default tag (0) */
a[0] = 1;
a[1] = 2;
printf("a[0] = %hhu a[1] = %hhu\n", a[0], a[1]);
/* set the logical and allocation tags */
a = (unsigned char *)insert_random_tag(a);
set_tag(a);
printf("%p\n", a);
/* non-zero tag access */
a[0] = 3;
printf("a[0] = %hhu a[1] = %hhu\n", a[0], a[1]);
/*
* If MTE is enabled correctly the next instruction will generate an
* exception.
*/
printf("Expecting SIGSEGV...\n");
a[16] = 0xdd;
/* this should not be printed in the PR_MTE_TCF_SYNC mode */
printf("...haven't got one\n");
return EXIT_FAILURE;
}