Linux 内核 SPARC M7 平台 ADI 应用数据完整性机制:原理、接口与内核实现全解析
【免费下载链接】linuxLinux kernel source tree项目地址: https://gitcode.com/GitHub_Trending/li/linux
在 Oracle SPARC M7 平台上,处理器原生提供了 Application Data Integrity(ADI,应用数据完整性)特性,可帮助任务在硬件层面检测内存使用错误(如 use-after-free、野指针、内存越界写入)。本文基于内核文档 Application Data Integrity (ADI) 展开,结合内核中arch/sparc下的实际源码,完整讲解 ADI 的工作原理、用户态启用步骤(mprotect+PROT_ADI)、辅助向量(auxiliary vector)能力上报机制、三类 ADI 相关异常陷阱(trap)的信号语义,以及内核在换页(swap)、页分配时对版本标签的保存与恢复实现,帮助读者掌握这一硬件级内存安全检查特性的端到端使用方法与底层支撑。
ADI 工作原理:三层硬件机制
ADI 允许一个任务在其地址空间的任意子集上设置"版本标签"(version tag)。一旦 ADI 对某地址范围启用,处理器在每次访存时会把指针中携带的标签与该范围中预先设置的版本进行比较;只有二者匹配才放行,不匹配则触发异常。文档指出,任务要完整启用 ADI 必须依次完成三个步骤:
- 设置用户态 PSTATE.mcde 位:作为任务整个地址空间 ADI 功能的总开关;
- 设置 TTE.mcd 位:在对需要启用 ADI 的地址范围对应的 TLB 表项上设置
TTE.mcd位,MMU 只检查那些TTE.mcd=1页的标签; - 设置版本标签:使用
stxa指令配合 MCD 专用 ASI(Address Space Identifier),对虚拟地址逐块打标签。每条stxa指令为 ADI block size 字节的范围设置一个标签,因此必须重复执行才能覆盖整页。
平台参数由 hypervisor 通过机器描述表(machine description tables)传给内核:
- ADI block size:在 SPARC M7 上等于 cache line 大小,即 64 字节;
- 版本标签位宽:M7 上 MMU 使用虚拟地址的 63–60 位(共 4 位)存放版本标签。
也就是说,一旦某块内存被设置为版本 10,之后对该内存的所有访问都必须使用 63–60 位为0xa的虚拟地址才能通过检查。从源码结构看,这一约束在用户态代码中体现为"普通地址左移adi_nbits位再右移(清零高 4 位),然后或上version << (64-nbits)"的地址构造方式。
平台能力检测:从 mdesc 解析 ADI 参数
内核启动时通过 mdesc_adi_init() 解析 hypervisor 提供的机器描述表来发现 ADI 能力:
- 若
cpu节点的hwcap-list属性中包含"adp"关键字,说明平台支持 ADI; platform节点下的adp-blksz、adp-nbits、ue-on-adp三个属性分别描述块大小、标签位宽,以及"ADI 不匹配时产生用户态错误还是精确异常"的策略(precise exception 开关,即文档中提到的 MCDPERR);- 三项属性缺一不可,否则
adi_state.enabled保持false,内核不启用 ADI。
解析结果保存在全局结构体 struct adi_config adi_state 中,并导出三个内联查询接口:
static inline bool adi_capable(void) { return adi_state.enabled; } static inline unsigned long adi_blksize(void) { return adi_state.caps.blksz; } static inline unsigned long adi_nbits(void) { return adi_state.caps.nbits; }值得注意的是源码中的一处保守限制:换页相关代码假设两个 ADI 标签能压缩进一个字节(每个标签 4 bit),因此若nbits > 4,内核会打印告警并直接禁用 ADI 支持(adi_64.c#L108-L111),以避免页被换出时出现不可预测的结果。
用户态接口:mprotect + PROT_ADI 与辅助向量
启用方式
ADI 通过带PROT_ADI标志的mprotect()调用在指定页集合上启用。PROT_ADI在 uapi 头文件中定义为0x10(uapi/asm/mman.h)。当任务第一次以PROT_ADI调用mprotect()时,内核会替任务置位PSTATE.mcde(总开关),之后该地址范围内的版本标签由用户态使用stxa指令和ASI_MCD_PRIMARY或ASI_MCD_ST_BLKINIT_PRIMARY设置。
内核侧的实现在 sparc_calc_vm_prot_bits():
static inline vm_flags_t sparc_calc_vm_prot_bits(unsigned long prot) { if (adi_capable() && (prot & PROT_ADI)) { struct pt_regs *regs; if (!current->mm->context.adi) { regs = task_pt_regs(current); regs->tstate |= TSTATE_MCDE; current->mm->context.adi = true; on_each_cpu_mask(mm_cpumask(current->mm), ipi_set_tstate_mcde, current->mm, 0); } return VM_SPARC_ADI; } else { return 0; } }可以看到,第一次启用时内核不仅置当前 CPU 上的TSTATE_MCDE,还会通过 IPI(ipi_set_tstate_mcde)同步所有可能运行该任务的 CPU 的状态,保证任意 CPU 上调度该任务时 ADI 总开关都是打开的。同时 VMA 被标记为VM_SPARC_ADI,TLB 表项中的TTE.mcd位由页表体系相应置位。
与PROT_ADI相关的 VMA 合法性检查在 sparc_validate_prot() 和 arch_validate_flags() 中,后者还施加了三条额外限制:
- 平台必须
adi_capable(); - 不允许对 PFN 映射页(
VM_PFNMAP/VM_MIXEDMAP)启用 ADI; - 不允许对可合并页(
VM_MERGEABLE,如 KSM 透明大页)启用 ADI——因为两块数据相同但 ADI 标签可能不同的页面在合并后会变得"不可合并",从源码结构看这一限制是为了保持 KSM 语义的确定性。
能力上报:AT_ADI_BLKSZ 与 AT_ADI_NBITS
内核通过 ELF 辅助向量把 ADI 能力传递给用户态(定义见 uapi/asm/auxvec.h,填充逻辑见 elf_64.h):
| 辅助向量 | 含义 |
|---|---|
AT_ADI_BLKSZ(值 48) | ADI 块大小,即版本化的粒度与对齐要求,单位字节 |
AT_ADI_NBITS(值 49) | 虚拟地址中 ADI 版本标签的位数 |
用户程序据此判断平台是否支持 ADI(adi_blksz == 0即不支持),并据此计算地址的构造方式。
完整示例程序
文档中给出了一个完整的示例程序,演示了"探测能力 → 分配共享内存 → 启用 ADI → 逐块打标签 → 构造带版本地址读写验证 → 关闭 ADI"的全流程:
#include <unistd.h> #include <stdio.h> #include <stdlib.h> #include <elf.h> #include <sys/ipc.h> #include <sys/shm.h> #include <sys/mman.h> #include <asm/asi.h> #ifndef AT_ADI_BLKSZ #define AT_ADI_BLKSZ 48 #endif #ifndef AT_ADI_NBITS #define AT_ADI_NBITS 49 #endif #ifndef PROT_ADI #define PROT_ADI 0x10 #endif #define BUFFER_SIZE 32*1024*1024UL main(int argc, char* argv[], char* envp[]) { unsigned long i, mcde, adi_blksz, adi_nbits; char *shmaddr, *tmp_addr, *end, *veraddr, *clraddr; int shmid, version; Elf64_auxv_t *auxv; adi_blksz = 0; while(*envp++ != NULL); for (auxv = (Elf64_auxv_t *)envp; auxv->a_type != AT_NULL; auxv++) { switch (auxv->a_type) { case AT_ADI_BLKSZ: adi_blksz = auxv->a_un.a_val; break; case AT_ADI_NBITS: adi_nbits = auxv->a_un.a_val; break; } } if (adi_blksz == 0) { fprintf(stderr, "Oops! ADI is not supported\n"); exit(1); } printf("ADI capabilities:\n"); printf("\tBlock size = %ld\n", adi_blksz); printf("\tNumber of bits = %ld\n", adi_nbits); if ((shmid = shmget(2, BUFFER_SIZE, IPC_CREAT | SHM_R | SHM_W)) < 0) { perror("shmget failed"); exit(1); } shmaddr = shmat(shmid, NULL, 0); if (shmaddr == (char *)-1) { perror("shm attach failed"); shmctl(shmid, IPC_RMID, NULL); exit(1); } if (mprotect(shmaddr, BUFFER_SIZE, PROT_READ|PROT_WRITE|PROT_ADI)) { perror("mprotect failed"); goto err_out; } /* Set the ADI version tag on the shm segment */ version = 10; tmp_addr = shmaddr; end = shmaddr + BUFFER_SIZE; while (tmp_addr < end) { asm volatile( "stxa %1, [%0]0x90\n\t" : : "r" (tmp_addr), "r" (version)); tmp_addr += adi_blksz; } asm volatile("membar #Sync\n\t"); /* Create a versioned address from the normal address by placing * version tag in the upper adi_nbits bits */ tmp_addr = (void *) ((unsigned long)shmaddr << adi_nbits); tmp_addr = (void *) ((unsigned long)tmp_addr >> adi_nbits); veraddr = (void *) (((unsigned long)version << (64-adi_nbits)) | (unsigned long)tmp_addr); printf("Starting the writes:\n"); for (i = 0; i < BUFFER_SIZE; i++) { veraddr[i] = (char)(i); if (!(i % (1024 * 1024))) printf("."); } printf("\n"); printf("Verifying data..."); fflush(stdout); for (i = 0; i < BUFFER_SIZE; i++) if (veraddr[i] != (char)i) printf("\nIndex %lu mismatched\n", i); printf("Done.\n"); /* Disable ADI and clean up */ if (mprotect(shmaddr, BUFFER_SIZE, PROT_READ|PROT_WRITE)) { perror("mprotect failed"); goto err_out; } if (shmdt((const void *)shmaddr) != 0) perror("Detach failure"); shmctl(shmid, IPC_RMID, NULL); exit(0); err_out: if (shmdt((const void *)shmaddr) != 0) perror("Detach failure"); shmctl(shmid, IPC_RMID, NULL); exit(1); }示例中几个关键点值得注意:
stxa %1, [%0]0x90中的0x90正是 ASI_MCD_PRIMARY,即"MCD version load/store",用于用户态读写版本标签;另有 ASI_MCD_ST_BLKINIT_PRIMARY(0x92) 用于"store + 块初始化"(置标签并清零数据块)。内核侧则使用特权 ASI ASI_MCD_REAL(0x05) 按物理地址访问标签;- 打标签循环每
adi_blksz(64)字节执行一次stxa,与"每条 stxa 覆盖一个 ADI block"的语义严格对应; - 标签设置完成后插入
membar #Sync保证顺序,再构造版本化地址:先通过<< nbits; >> nbits清掉高 4 位,再或上version << (64-nbits)。
重要约束(IMPORTANT NOTES)
文档列出的一组注意事项直接决定了 ADI 编程模型,逐条继承如下:
- 保留标签值:
0x0和0xf是保留的版本标签值——它们匹配任意虚拟地址中的标签,永不产生不匹配异常。这意味着"未打标/通配"语义天然可用,也解释了为什么默认情况下访问不会出错。 - 标签写在用户态、存储于物理内存:虽然版本标签存放在物理内存中,但打标签操作通过虚拟地址由用户态完成,前提是页已分配给该任务且 PTE 已建立。
- 页回收后旧标签失效:任务释放一个打过标签的页后,该页回到空闲页池;当此页重新分配给任务时,内核使用 block initialization ASI 清零该页,版本标签随之被清除。即使页被重新分配回同一个任务,此前设置的旧标签也不再存在——跨"释放-重分配"周期不应依赖旧标签。
- no-faulting load 不检测不匹配:对 non-faulting load(如
ldma之类带 ASI_PNF 语义的访问),ADI 标签不匹配不会被检测到。 - 内核不打标签,但保证标签随页迁移存活:内核不对用户页设置任何标签,设置版本标签完全是任务自己的责任;但内核保证页被换出到磁盘再换入、或页发生迁移(migration)时版本标签被保留。
- 任意页大小透明工作:用户态任务无需感知页大小,选定虚拟地址范围后用
mprotect()启用 ADI 并覆盖打标签即可;mprotect()会确保范围按页大小对齐且长度为页大小的整数倍。 - 只能对可写内存打标签:ADI 标签只能设置在可写内存上,例如只读映射无法打标签。
ADI 相关陷阱(Traps)与信号语义
启用 ADI 后可能出现三类新陷阱,内核均以force_sig_fault()转化为信号,实现在 traps_64.c 中:
1. 破坏性内存损坏(Disrupting memory corruption)
当 store 访问TTE.mcd=1的位置、任务运行于 ADI 使能状态(PSTATE.mcde=1)、且地址中 63:60 位的标签与对应 cache line 的标签不匹配时,触发 memory corruption trap。默认它是破坏性陷阱,先被送往 hypervisor,hypervisor 生成 sun4v 错误报告后以可恢复错误(TT=0x7e)转发给内核,内核再向任务发送 SIGSEGV:
siginfo.si_signo = SIGSEGV; siginfo.errno = 0; siginfo.si_code = SEGV_ADIDERR; siginfo.si_addr = addr; /* PC where first mismatch occurred */ siginfo.si_trapno = 0;内核对应处理点位于 traps_64.c#L2072:force_sig_fault(SIGSEGV, SEGV_ADIDERR, ...),其中si_addr携带的是首次发生不匹配的 PC。
2. 精确内存损坏(Precise memory corruption)
同样的标签不匹配场景下,若平台开启了 MCD 精确异常(MCDPERR=1,对应 mdesc 中的ue-on-adp属性),处理器会以 TT=0x1a 直接给内核发送精确异常,此时si_addr携带的是导致陷阱的实际地址(而非 PC),代码点见 traps_64.c#L2694:
siginfo.si_signo = SIGSEGV; siginfo.errno = 0; siginfo.si_code = SEGV_ADIPERR; siginfo.si_addr = addr; /* address that caused trap */ siginfo.si_trapno = 0;文档特别注明:load 上的 ADI 标签不匹配始终产生精确陷阱。
3. MCD disabled 陷阱
任务在未启用 ADI 的状态下尝试设置 ADI 版本标签时,处理器发出 MCD disabled 陷阱。该陷阱先经 hypervisor,再以 Data Access Exception(fault type = 0xa,invalid ASI)的形式传入内核。内核的处理在 sun4v_data_access_exception() 中按type分支:
case HV_FAULT_TYPE_MCD_DIS: force_sig_fault(SIGSEGV, SEGV_ACCADI, (void __user *)addr); break; case HV_FAULT_TYPE_INV_ASI: force_sig_fault(SIGILL, ILL_ILLADR, (void __user *)addr); break;即:MCD disabled 场景下任务收到
siginfo.si_signo = SIGSEGV; siginfo.errno = 0; siginfo.si_code = SEGV_ACCADI; siginfo.si_addr = addr; /* address that caused trap */ siginfo.si_trapno = 0;三类陷阱汇总:
| 场景 | si_code | si_addr 含义 |
|---|---|---|
| store 标签不匹配(破坏性,经 hypervisor,TT=0x7e) | SEGV_ADIDERR | 首次不匹配处的 PC |
| store 标签不匹配(精确,MCDPERR=1,TT=0x1a) | SEGV_ADIPERR | 触发陷阱的地址 |
| 未启用 ADI 时尝试设置标签(HV_FAULT_TYPE_MCD_DIS) | SEGV_ACCADI | 触发陷阱的地址 |
内核实现纵深:换页时标签的保存与恢复
文档承诺"内核保证页换出/换入以及页迁移时版本标签被保留",其实现完整位于 arch/sparc/kernel/adi_64.c:
- 换出前保存(adi_save_tags()):页即将被换出时,内核用特权
ldxa指令配ASI_MCD_REAL按adi_blksize()步进读取整页每个块的 4-bit 标签,两个标签(各 4 bit)打包进一个字节存入标签存储区; - 换入后恢复(adi_restore_tags()):页换回新物理页后,内核用
stxa+ASI_MCD_REAL把保存的标签逐块写回,完成后membar #Sync保证可见性; - 标签存储管理:标签缓存在
mm_struct->context.tag_store(每页可容纳的tag_storage_desc_t描述符表,一个字节存两个标签)。alloc_tag_store() 按 VMA 中的地址空洞预分配标签存储(默认TAG_STORAGE_PAGES即 8 页可覆盖8*PAGE_SIZE*2/adi_blksize()个页的标签),以减少同一 VMA 后续换页时的重复分配;del_tag_store() 按引用计数释放,第一个描述符占用的存储作为任务的持久应急标签空间不予释放。
另外两处与文档"重要约束"对应的实现:
- 空闲页清零:空闲页分配路径使用 block initialization ASI 初始化新页(见 mm/init_64.c#L3093-L3131 附近的
stxa ... ASI_MCD_REAL序列),从而清除上一任租户遗留的版本标签,兑现"页重新分配后旧标签不再存在"的语义; - 上下文切换维持总开关:
mm->context.adi标志与TSTATE_MCDE的维护散见于 mmu_context_64.h 等处,保证任务在 CPU 间迁移后 PSTATE.mcde 仍然生效。
总结
ADI 是 SPARC M7 提供的硬件级内存安全检查机制:通过"PSTATE.mcde 总开关 + TTE.mcd 页粒度使能 + stxa 逐块打标签"三层机制,把版本标签不匹配的内存访问转化为可诊断的信号。对用户态,整个特性仅依赖标准接口——mprotect(PROT_ADI)启用/关闭、stxa打标签、辅助向量AT_ADI_BLKSZ/AT_ADI_NBITS探测能力——无需感知页大小;对内核,则提供了 mdesc 能力解析(adi_64.c)、mprotect拦截(mman.h)、三类陷阱到SEGV_ADIDERR/SEGV_ADIPERR/SEGV_ACCADI的映射(traps_64.c)以及换页/迁移/页回收场景下的标签保存、恢复与清除等完整支撑。在编写启用 ADI 的应用时,需牢记0x0/0xf保留标签值、no-faulting load 不检测、只读映射不可打标、以及"页重新分配后旧标签必然失效"这几条硬约束,才能正确利用该特性定位内存错误。
【免费下载链接】linuxLinux kernel source tree项目地址: https://gitcode.com/GitHub_Trending/li/linux
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考