
Linux 内核 SPARC M7 平台 ADI 应用数据完整性机制原理、接口与内核实现全解析【免费下载链接】linuxLinux kernel source tree项目地址: https://gitcode.com/GitHub_Trending/li/linux在 Oracle SPARC M7 平台上处理器原生提供了 Application Data IntegrityADI应用数据完整性特性可帮助任务在硬件层面检测内存使用错误如 use-after-free、野指针、内存越界写入。本文基于内核文档 Application Data Integrity (ADI) 展开结合内核中arch/sparc下的实际源码完整讲解 ADI 的工作原理、用户态启用步骤mprotectPROT_ADI、辅助向量auxiliary vector能力上报机制、三类 ADI 相关异常陷阱trap的信号语义以及内核在换页swap、页分配时对版本标签的保存与恢复实现帮助读者掌握这一硬件级内存安全检查特性的端到端使用方法与底层支撑。ADI 工作原理三层硬件机制ADI 允许一个任务在其地址空间的任意子集上设置版本标签version tag。一旦 ADI 对某地址范围启用处理器在每次访存时会把指针中携带的标签与该范围中预先设置的版本进行比较只有二者匹配才放行不匹配则触发异常。文档指出任务要完整启用 ADI 必须依次完成三个步骤设置用户态 PSTATE.mcde 位作为任务整个地址空间 ADI 功能的总开关设置 TTE.mcd 位在对需要启用 ADI 的地址范围对应的 TLB 表项上设置TTE.mcd位MMU 只检查那些TTE.mcd1页的标签设置版本标签使用stxa指令配合 MCD 专用 ASIAddress 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关键字说明平台支持 ADIplatform节点下的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 头文件中定义为0x10uapi/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还会通过 IPIipi_set_tstate_mcde同步所有可能运行该任务的 CPU 的状态保证任意 CPU 上调度该任务时 ADI 总开关都是打开的。同时 VMA 被标记为VM_SPARC_ADITLB 表项中的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值 48ADI 块大小即版本化的粒度与对齐要求单位字节AT_ADI_NBITS值 49虚拟地址中 ADI 版本标签的位数用户程序据此判断平台是否支持 ADIadi_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_PRIMARY0x92 用于store 块初始化置标签并清零数据块。内核侧则使用特权 ASI ASI_MCD_REAL0x05 按物理地址访问标签打标签循环每adi_blksz64字节执行一次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.mcd1的位置、任务运行于 ADI 使能状态PSTATE.mcde1、且地址中 63:60 位的标签与对应 cache line 的标签不匹配时触发 memory corruption trap。默认它是破坏性陷阱先被送往 hypervisorhypervisor 生成 sun4v 错误报告后以可恢复错误TT0x7e转发给内核内核再向任务发送 SIGSEGVsiginfo.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#L2072force_sig_fault(SIGSEGV, SEGV_ADIDERR, ...)其中si_addr携带的是首次发生不匹配的 PC。2. 精确内存损坏Precise memory corruption同样的标签不匹配场景下若平台开启了 MCD 精确异常MCDPERR1对应 mdesc 中的ue-on-adp属性处理器会以 TT0x1a 直接给内核发送精确异常此时si_addr携带的是导致陷阱的实际地址而非 PC代码点见 traps_64.c#L2694siginfo.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 Exceptionfault type 0xainvalid 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_codesi_addr 含义store 标签不匹配破坏性经 hypervisorTT0x7eSEGV_ADIDERR首次不匹配处的 PCstore 标签不匹配精确MCDPERR1TT0x1aSEGV_ADIPERR触发陷阱的地址未启用 ADI 时尝试设置标签HV_FAULT_TYPE_MCD_DISSEGV_ACCADI触发陷阱的地址内核实现纵深换页时标签的保存与恢复文档承诺内核保证页换出/换入以及页迁移时版本标签被保留其实现完整位于 arch/sparc/kernel/adi_64.c换出前保存adi_save_tags()页即将被换出时内核用特权ldxa指令配ASI_MCD_REAL按adi_blksize()步进读取整页每个块的 4-bit 标签两个标签各 4 bit打包进一个字节存入标签存储区换入后恢复adi_restore_tags()页换回新物理页后内核用stxaASI_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),仅供参考