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 标志在地址范围上启用标记内存属性。

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

当这些页面首次映射到用户地址空间时,分配标记设置为 0,并在写时复制时保留。MAP_SHARED 受到支持,分配标记可以在进程之间共享。

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

注意PROT_MTE 标志(和相应的内存类型)不能通过 mprotect() 清除。

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

标记检查故障

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

  • 忽略 - 这是默认模式。CPU(和内核)忽略标记检查故障。

  • 同步 - 内核同步引发 SIGSEGV,其中 .si_code = SEGV_MTESERR.si_addr = <故障地址>。不执行内存访问。如果 SIGSEGV 被违规线程忽略或阻止,则包含进程将以 coredump 终止。

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

  • 非对称 - 读取的处理方式与同步模式相同,而写入的处理方式与异步模式相同。

用户可以使用 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 - 异步标记检查故障模式

如果未指定任何模式,则忽略标记检查故障。如果指定了单个模式,则程序将在该模式下运行。如果指定了多个模式,则会按照以下“每个 CPU 首选标记检查模式”部分所述选择模式。

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

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

注意:信号处理程序始终在 PSTATE.TCO = 0 的情况下调用,而与中断的上下文无关。PSTATE.TCOsigreturn() 时恢复。

注意:没有可用于用户应用程序的匹配所有逻辑标记。

注意:如果用户线程标记检查模式为 PR_MTE_TCF_NONEPR_MTE_TCF_ASYNC,则不会检查内核对用户地址空间的访问(例如 read() 系统调用)。如果标记检查模式为 PR_MTE_TCF_SYNC,则内核会尽最大努力检查其用户地址访问,但不能始终保证。内核对用户地址的访问始终以 0 的有效 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. 异步

  2. 非对称

  3. 同步

请注意,用户空间无法请求多种模式并禁用非对称模式。

初始进程状态

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 未更新。

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

PTRACE_GETREGSETPTRACE_SETREGSET,其中 addr == ``NT_ARM_TAGGED_ADDR_CTRL 允许 ptrace() 访问进程的标记地址 ABI 控制和 MTE 配置,如 AArch64 标记地址 ABI 和上面的内容中所述的 prctl() 选项。相应的 regset 是一个 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

标签在核心文件中以 p_offset 的形式存储,一个字节中包含两个 4 位标签。对于 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;
}