AArch64 Linux 中的内存标记扩展 (MTE)

作者:Vincenzo Frascino <vincenzo.frascino@arm.com>

Catalin Marinas <catalin.marinas@arm.com>

日期:2020-02-25

本文档描述了 AArch64 Linux 中内存标记扩展(Memory Tagging Extension)功能的提供情况。

简介

基于 ARMv8.5 的处理器引入了内存标记扩展 (MTE) 功能。MTE 构建在 ARMv8.0 虚拟地址标记 TBI(Top Byte Ignore,高字节忽略)功能之上,允许软件访问物理地址空间中每 16 字节粒度的一个 4 位分配标记。此类内存范围必须使用“常规带标记”(Normal-Tagged)内存属性进行映射。逻辑标记派生自用于内存访问的虚拟地址的第 59-56 位。启用了 MTE 的 CPU 会将逻辑标记与分配标记进行比较,并在不匹配时潜在地引发异常,具体取决于系统寄存器的配置。

用户空间支持

当选中 CONFIG_ARM64_MTE 且硬件支持内存标记扩展时,内核会通过 HWCAP2_MTE 向用户空间通告该功能。

PROT_MTE

要访问分配标记,用户进程必须在地址范围上启用带标记的内存属性,方法是为 mmap()mprotect() 使用一个新的 prot 标志

PROT_MTE - 允许访问 MTE 分配标记的页面。

当此类页面首次映射到用户地址空间时,分配标记被设置为 0,并在写时复制(copy-on-write)时保留。支持 MAP_SHARED,并且可以在进程之间共享分配标记。

注意PROT_MTE 仅在 MAP_ANONYMOUS 和基于 RAM 的文件映射(tmpfsmemfd)上受支持。将其传递给其他类型的映射将导致这些系统调用返回 -EINVAL

注意PROT_MTE 标志(及对应的内存类型)无法通过 mprotect() 清除。

注意:带有 MADV_DONTNEEDMADV_FREEmadvise() 内存范围可能在系统调用之后的任何时刻被清除分配标记(设置为 0)。

标记检查错误(Tag Check Faults)

当在地址范围上启用了 PROT_MTE 且在访问时发生逻辑标记与分配标记不匹配的情况时,有三种可配置的行为

  • Ignore(忽略) - 这是默认模式。CPU(和内核)会忽略标记检查错误。

  • Synchronous(同步) - 内核同步引发一个 SIGSEGV,其中 .si_code = SEGV_MTESERR.si_addr = <fault-address>。不执行内存访问。如果该出错线程忽略或阻塞了 SIGSEGV,包含该进程的程序将通过 coredump(核心转储)终止。

  • Asynchronous(异步) - 内核在发生一个或多个标记检查错误之后,在出错线程中异步引发一个 SIGSEGV,其中 .si_code = SEGV_MTEAERR.si_addr = 0(出错地址未知)。

  • Asymmetric(非对称) - 读操作按照同步模式处理,而写操作按照异步模式处理。

用户可以使用 prctl(PR_SET_TAGGED_ADDR_CTRL, flags, 0, 0, 0) 系统调用按线程选择上述模式,其中 flagsPR_MTE_TCF_MASK 位域中包含以下任意数量的值

  • PR_MTE_TCF_NONE  - 忽略标记检查错误

    (如果与其他选项组合,则会被忽略)

  • PR_MTE_TCF_SYNC - 同步标记检查错误模式

  • PR_MTE_TCF_ASYNC - 异步标记检查错误模式

如果未指定任何模式,则忽略标记检查错误。如果指定了单个模式,程序将以该模式运行。如果指定了多个模式,将按照下文“Per-CPU preferred tag checking modes”一节中所述选择模式。

可以使用 prctl(PR_GET_TAGGED_ADDR_CTRL, 0, 0, 0, 0) 系统调用读取当前的标记检查错误配置。如果请求了多种模式,则会报告所有模式。

也可以通过使用 MSR TCO, #1 设置 PSTATE.TCO 位来为用户线程禁用标记检查。

注意:信号处理程序总是在 PSTATE.TCO = 0 的情况下被调用,而不管被中断的上下文如何。PSTATE.TCO 会在 sigreturn() 时恢复。

注意:没有可供用户应用程序使用的全匹配逻辑标记。

注意:如果用户线程标记检查模式为 PR_MTE_TCF_NONEPR_MTE_TCF_ASYNC,内核对用户地址空间的访问(例如 read() 系统调用)将不会被检查。如果标记检查模式为 PR_MTE_TCF_SYNC,内核会尽最大努力检查其用户地址访问,但并不能总是保证这一点。无论用户配置如何,内核对用户地址的访问始终以有效值为零的 PSTATE.TCO 执行。

IRGADDGSUBG 指令中排除标记

该架构允许通过 GCR_EL1.Exclude 寄存器位域排除某些要随机生成的标记。默认情况下,Linux 会排除除 0 以外的所有标记。用户线程可以使用 prctl(PR_SET_TAGGED_ADDR_CTRL, flags, 0, 0, 0) 系统调用在随机生成的集合中启用特定标记,其中 flagsPR_MTE_TAG_MASK 位域中包含标记位图。

注意:硬件使用的是排除掩码,但 prctl() 接口提供的是包含掩码。包含掩码为 0(排除掩码为 0xffff)会导致 CPU 始终生成标记 0

每个 CPU 首选的标记检查模式

在某些 CPU 上,MTE 在更严格的标记检查模式下的性能与不太严格的标记检查模式相似。这使得当请求不太严格的检查模式时,在这些 CPU 上启用更严格的检查是值得的,以便在没有性能缺点的情况下获得更严格检查的错误检测优势。为了支持这种场景,特权用户可以将更严格的标记检查模式配置为 CPU 的首选标记检查模式。

每个 CPU 的首选标记检查模式由 /sys/devices/system/cpu/cpu<N>/mte_tcf_preferred 控制,特权用户可以向其写入值 asyncsyncasymm。每个 CPU 的默认首选模式是 async

为了让程序有可能在 CPU 的首选标记检查模式下运行,用户程序可以在传递给 prctl(PR_SET_TAGGED_ADDR_CTRL, flags, 0, 0, 0) 系统调用的 flags 参数中设置多个标记检查错误模式位。如果同时请求了同步和异步模式,内核也可能会选择非对称模式。如果 CPU 的首选标记检查模式包含在任务所提供的标记检查模式集合中,则会选择该模式。否则,内核将根据偏好顺序从任务的模式集合中选择任务模式中的一种模式

  1. 异步 (Asynchronous)

  2. 非对称 (Asymmetric)

  3. 同步 (Synchronous)

请注意,用户空间没有办法在请求多种模式的同时禁用非对称模式。

初始进程状态

execve() 时,新进程具有以下配置

  • PR_TAGGED_ADDR_ENABLE 设置为 0(禁用)

  • 未选择任何标记检查模式(忽略标记检查错误)

  • PR_MTE_TAG_MASK 设置为 0(排除所有标记)

  • PSTATE.TCO 设置为 0

  • 任何初始内存映射上均未设置 PROT_MTE

fork() 时,新进程继承父进程的配置和内存映射属性,但带有 MADV_WIPEONFORKmadvise() 范围除外,其数据和标记将被清除(设置为 0)。

ptrace() 接口

PTRACE_PEEKMTETAGSPTRACE_POKEMTETAGS 允许跟踪器从被跟踪者的地址空间读取标记或向其设置标记。ptrace() 系统调用的调用形式为 ptrace(request, pid, addr, data),其中

  • request - PTRACE_PEEKMTETAGSPTRACE_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 ioveciov_base 缓冲区)时出错,且未复制任何标记。iov_len 未更新。

  • -EOPNOTSUPP - 被跟踪者的地址没有有效标记(从未通过 PROT_MTE 标志映射)。iov_len 未更新。

注意:上述请求没有瞬态错误,因此如果系统调用返回非零值,用户程序不应重试。

带有一项 addr == NT_ARM_TAGGED_ADDR_CTRLPTRACE_GETREGSETPTRACE_SETREGSET 允许 ptrace() 访问进程的带标记地址 ABI 控制和 MTE 配置,如 AArch64 TAGGED ADDRESS ABI 及上文中所述的 prctl() 选项。对应的 regset 是 1 个 8 字节的元素(sizeof(long))。

核心转储支持

使用 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;
}