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_MTEp_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;
}