资讯详情

资讯详情

Linux 内核 Memory Protection Keys(PKU)权威指南:x86_64 与 arm64 用户态内存保护密钥机制深度解析

Linux 内核 Memory Protection KeysPKU权威指南x86_64 与 arm64 用户态内存保护密钥机制深度解析【免费下载链接】linuxLinux kernel source tree项目地址: https://gitcode.com/GitHub_Trending/li/linux导读Memory Protection Keys内存保护密钥简称 Pkeys / PKU是 Linux 内核提供的、用于在不修改页表的前提下为不同内存区域动态切换访问权限的机制应用将页表项中保留的若干位标记为保护密钥随后仅需读写一个 per-CPU 的寄存器即可瞬时启用或禁用对整片密钥区域的读/写/执行权限。本指南以内核文档 Documentation/core-api/protection-keys.rst 为骨架结合内核源码mm/mprotect.c、arch/x86/mm/pkeys.c、arch/arm64相关实现与自测程序tools/testing/selftests/mm/系统讲解 x86_64 与 arm64 两种架构的密钥编码、PKRU/POR_EL0 寄存器语义、pkey_alloc()/pkey_free()/pkey_mprotect()三个系统调用、以及故障信号行为。读完本文你将能够在自己的应用中安全地分配密钥、设置密钥保护并正确处理SEGV_PKERR信号。一、什么是 Memory Protection Keys动机与核心思想传统的内存保护依赖mprotect()修改页表项中的权限位而页表是共享的、修改代价高昂需要 TLB shootdown 等操作。当应用需要在可写/只读等保护域之间频繁切换时每次都改页表既不高效也不够灵活。Memory Protection Keys 提供了另一条路径在不修改页表的前提下为每个页表项打上一个密钥编号权限的开关由 CPU 寄存器中的位决定。核心思路分两步打标静态、低频通过pkey_mprotect()将页表项中原本保留的若干位编码为保护密钥编号此操作与普通mprotect()一样需要经过内核、修改页表项切换动态、高频应用直接读写 per-CPU 的用户可访问寄存器x86_64 为PKRUarm64 为POR_EL0瞬间改变该密钥对应内存的访问权限全程不经过内核、不触碰页表。由于权限寄存器的状态天然是 per-CPU、per-thread 的不同线程可以持有同一块内存的不同访问视图——这正是保护域隔离的天然载体。这一特性常用于 JIT 运行时代码页写后改只读、沙箱、内存安全库等场景。二、架构差异x86_64 与 arm64 的密钥编码与权限寄存器同一套概念在两个架构上有不同的硬件实现内核文档分别做了说明下面结合架构代码逐一展开。2.1 x86_644 位密钥 32 位 PKRU 寄存器页表项编码每个 PTE 中原本保留的 4 位被用于编码protection key因此共有16 个可用密钥编号 015。权限寄存器 PKRU每个 CPU 有一个用户可访问的32 位 PKRU 寄存器每 2 位对应一个密钥分别表示Access Disable禁止访问即禁止读与Write Disable禁止写16 个密钥 × 2 位 32 位。读写指令内核提供RDPKRU/WRPKRU两条指令读取和写入该寄存器。可用模式该特性仅存在于 64 位模式尽管理论上 PAE 页表也有空间且权限仅作用于数据访问不影响指令取指。内核自测程序 tools/testing/selftests/mm/pkey-x86.h 给出了这两条指令的直接内联汇编实现static inline u64 __read_pkey_reg(void) { unsigned int eax, edx; unsigned int ecx 0; asm volatile(.byte 0x0f,0x01,0xee\n\t /* RDPKRU */ : a (eax), d (edx) : c (ecx)); return eax; } static inline void __write_pkey_reg(u64 pkey_reg) { unsigned int eax pkey_reg; unsigned int ecx 0; unsigned int edx 0; asm volatile(.byte 0x0f,0x01,0xef\n\t /* WRPKRU */ : : a (eax), c (ecx), d (edx)); }同一头文件中还定义了 x86 的密钥常量与容量信息#define PKEY_DISABLE_ACCESS 0x1 #define PKEY_DISABLE_WRITE 0x2 #define NR_PKEYS 16 #define NR_RESERVED_PKEYS 2 /* pkey-0 and exec-only-pkey */ #define PKEY_BITS_PER_PKEY 2支持的 CPUIntel 服务器 CPUSkylake 及以后、Intel 客户端 CPUTiger Lake第 11 代酷睿及以后、以及未来的 AMD CPU。CPU 特性检测x86 上需要同时具备PKUCPUID leaf 0x7, ECX bit 3和OSPKEECX bit 4两个特性位才算可用pkey-x86.h中的cpu_has_pkeys()正是这样检测的内核在启动时也依赖这两个特性位来使能 PKU 支持。2.2 arm643 位密钥索引 64 位 POR_EL0 寄存器Permission Overlay Extension页表项编码arm64 使用页表项中3 位编码protection key index因此共有8 个可用密钥编号 07。权限寄存器 POR_EL0每个 CPU 有一个用户可写的64 位系统寄存器POR_EL0每个密钥索引用 4 位编码 read/write/execute 三种overlay叠加权限8 个密钥 × 4 位 32 位使用量。线程本地性与 x86 一样POR_EL0是 CPU 寄存器天然线程本地不同线程可持有不同保护视图。关键差异与 x86_64 不同arm64 的保护密钥权限同样作用于指令取指instruction fetch。arm64 的硬件实现基于Permission Overlay ExtensionFEAT_S1POE。内核自测程序 tools/testing/selftests/mm/pkey-arm64.h 给出了POR_EL0的读写与权限编码细节/* POR_EL0 通过系统寄存器操作指令访问S3_3_c10_c2_4 */ static inline u64 __read_pkey_reg(void) { u64 pkey_reg 0; asm volatile(mrs %0, S3_3_c10_c2_4 : r (pkey_reg)); return pkey_reg; } /* 每个密钥占 4 位权限取值如下 */ #define POE_NONE 0x0 /* 无权限 */ #define POE_X 0x2 /* 仅执行 */ #define POE_RX 0x3 /* 读 执行 */ #define POE_RWX 0x7 /* 读 写 执行 */ #define PKEY_BITS_PER_PKEY 4set_pkey_bits()展示了如何把 Linux 的PKEY_DISABLE_ACCESS/PKEY_DISABLE_WRITE标志翻译为 arm64 的 overlay 权限PKEY_DISABLE_ACCESS对应POE_X仅可执行、禁读PKEY_DISABLE_WRITE对应POE_RX可读执行、禁写。这也印证了文档所述arm64 的权限同时作用于指令取指——因为POE_X这类仅执行状态本身就是指令级权限。小结两个架构对照表维度x86_64arm64页表项编码位4 位3 位密钥数量168权限寄存器PKRU32 位POR_EL064 位每密钥位数2 位AD/WD4 位R/W/X overlay是否作用于指令取指否是硬件扩展PKU/OSPKEPermission Overlay ExtensionFEAT_S1POE三、三个系统调用pkey_alloc / pkey_free / pkey_mprotect文档定义了与 pkeys 直接交互的 3 个系统调用int pkey_alloc(unsigned long flags, unsigned long init_access_rights); int pkey_free(int pkey); int pkey_mprotect(unsigned long start, size_t len, unsigned long prot, int pkey);3.1 pkey_alloc()分配密钥pkey_alloc()用于在使用前先分配一个密钥。其内核实现位于 mm/mprotect.cSYSCALL_DEFINE2(pkey_alloc, ...)关键行为如下flags参数目前必须为 0否则返回-EINVAL内核注释明确 No flags supported yetinit_access_rights是初始访问权限必须是PKEY_ACCESS_MASK的子集即只能是PKEY_DISABLE_ACCESS、PKEY_DISABLE_WRITE或二者的组合见 include/uapi/asm-generic/mman-common.h#define PKEY_DISABLE_ACCESS 0x1 #define PKEY_DISABLE_WRITE 0x2 #define PKEY_ACCESS_MASK (PKEY_DISABLE_ACCESS | PKEY_DISABLE_WRITE)内核在mmap_write_lock保护下通过mm_pkey_alloc()分配一个空闲密钥若耗尽返回 -1则返回-ENOSPC随后调用架构钩子arch_set_user_pkey_access(pkey, init_val)将初始权限写入寄存器x86 实现见 arch/x86/kernel/fpu/xstate.cx86 的 PKRU 状态同时作为 XSAVE 扩展状态的一部分被保存/恢复。3.2 pkey_mprotect()给内存打上密钥pkey_mprotect()与普通mprotect()的唯一区别是多了pkey参数用于把[start, startlen)区域绑定到指定密钥。内核实现mm/mprotect.c非常直白SYSCALL_DEFINE4(pkey_mprotect, unsigned long, start, size_t, len, unsigned long, prot, int, pkey) { return do_mprotect_pkey(start, len, prot, pkey); }而普通mprotect()仅仅是do_mprotect_pkey(start, len, prot, -1)——传-1表示不使用 pkey。这说明两者共享同一套 VMA 权限变更核心逻辑pkey 只是作为额外的vm_flags位VM_PKEY_SHIFT写入 VMA 并在建立页表时写入 PTE。此外x86 还通过arch_override_mprotect_pkey()见 arch/x86/mm/pkeys.c 与 arch/x86/include/asm/pkeys.h实现execute-only仅执行映射当应用对某个 pkey 请求PROT_EXEC时内核会分配一个专门的密钥并设置PKEY_DISABLE_ACCESS从而得到可执行但不可读的页面——这也是 x86 上exec-only pkey保留密钥的来历对应pkey-x86.h中NR_RESERVED_PKEYS为 2pkey-0 与 exec-only pkey。3.3 pkey_free()释放密钥pkey_free()释放密钥供后续复用实现见 mm/mprotect.c。内核注释提醒释放时不会检查是否仍有 VMA 引用该密钥We could provide warnings or errors if any VMA still has the pkey set here因此应用必须先munmap()相关区域再释放否则释放后密钥可能被重新分配给其他用途造成语义混乱。注意这三个系统调用仅在CONFIG_ARCH_HAS_PKEYS配置开启时编译mm/mprotect.c 的#ifdef条件x86_64 与 arm64 均满足。四、完整使用流程从分配密钥到释放文档给出了一个完整的最小示例下面结合源码注释与寄存器操作展开讲解。4.1 分配密钥并绑定内存int real_prot PROT_READ | PROT_WRITE; pkey pkey_alloc(0, PKEY_DISABLE_WRITE); /* 初始即禁止写 */ ptr mmap(NULL, PAGE_SIZE, PROT_NONE, MAP_ANONYMOUS | MAP_PRIVATE, -1, 0); /* 先以 PROT_NONE 映射 */ ret pkey_mprotect(ptr, PAGE_SIZE, real_prot, pkey); /* 绑定密钥并赋予 RW */ ... /* 应用正常运行 */关键点pkey_alloc(0, PKEY_DISABLE_WRITE)flags传 0初始权限为禁写——即使后续pkey_mprotect()赋予PROT_WRITE只要寄存器中该密钥仍带PKEY_DISABLE_WRITE写操作就会被拒绝mmap(..., PROT_NONE, ...)后再用pkey_mprotect()赋予真实权限这是把页表权限与密钥绑定解耦的标准写法一旦绑定改变该区域读写权限不再需要调用内核——直接改寄存器即可见下节。4.2 通过寄存器切换权限无需系统调用当应用需要更新ptr指向的数据时它先清除写禁止位、完成写入、再恢复写禁止位pkey_set(pkey, 0); /* 清除 PKEY_DISABLE_WRITE允许写 */ *ptr foo; /* 赋值 */ pkey_set(pkey, PKEY_DISABLE_WRITE); /* 重新设置 PKEY_DISABLE_WRITE */这里pkey_set()是对写入 CPU 寄存器的 C 封装x86 上即WRPKRUarm64 上即msr POR_EL0。文档明确指出可参考的实现位于tools/testing/selftests/mm/pkey-{arm64,powerpc,x86}.h——本文 2.1、2.2 节引用的__read_pkey_reg()/__write_pkey_reg()正是pkey_set()的底层构件powerpc版本见 tools/testing/selftests/mm/pkey-powerpc.h用于 64 位 PowerPC 的 AMR 寄存器实现。由于写寄存器是普通用户态指令这段开关写权限的开销远低于一次mprotect()系统调用这正是 PKU 在高频保护域切换场景下的性能价值所在。4.3 释放内存与密钥munmap(ptr, PAGE_SIZE); pkey_free(pkey);先解除映射、再释放密钥避免释放后仍有 VMA 引用该密钥的悬空状态内核对此并不做强制检查见 3.3 节。4.4 自测程序中的完整实践内核自带的自测套件 tools/testing/selftests/mm/protection_keys.c 对上述全流程进行了系统验证它覆盖了 pkey 分配/释放、pkey_mprotect()绑定、寄存器读写、SIGSEGV信号处理expected_pkey_fault()以及 exec-only 密钥的读故障验证见pkey-x86.h中的expect_fault_on_read_execonly_key()等场景是学习与验证 PKU 行为的最佳参考用例与配套的 tools/testing/selftests/mm/pkey_sighandler_tests.c 一起重点测试了信号处理路径中的密钥寄存器状态保存/恢复。五、行为语义与 mprotect() 的一致性及故障信号5.1 与普通 mprotect() 行为一致内核努力让保护密钥的语义与普通mprotect()保持一致。例如下面这段普通写法mprotect(ptr, size, PROT_NONE); something(ptr); /* 访问 ptr 会触发 SIGSEGV */应当与下面的 pkey 写法有完全相同的效果pkey pkey_alloc(0, PKEY_DISABLE_WRITE | PKEY_DISABLE_READ); pkey_mprotect(ptr, size, PROT_READ | PROT_WRITE, pkey); something(ptr); /* 访问 ptr 同样触发 SIGSEGV */无论something()是应用直接访问如*ptr foo;还是内核代表应用进行访问如read(fd, ptr, 1);两种情况下内核都会发送SIGSEGV。5.2 通过 si_code 区分两种故障两种故障虽然都产生SIGSEGV但si_code不同违反保护密钥权限→si_code为SEGV_PKERRx86 上即 PKU 故障arm64 的 Permission Overlay 故障同样映射为此值违反普通 mprotect() 页表权限→si_code为SEGV_ACCERR。这为信号处理器提供了精确的故障分类依据应用可以在 handler 中检查siginfo-si_code区分密钥寄存器权限不足与页表权限不足从而决定是临时提升寄存器权限如在 JIT 中补充写权限还是真正报错终止。x86 上SEGV_PKERR故障时还会在siginfo中携带出错的 pkey 编号si_pkey自测头文件pkey-x86.h中定义的si_pkey_offset即用于在信号帧中解析该字段。5.3 kthread 场景的注意事项文档特别提醒来自 kthread如 io_uring 的工作线程的内核访问会使用保护密钥寄存器的默认值因此不会与用户空间的寄存器值或mprotect()保持一致。换言之当内核线程替应用执行 I/O 时它看到的权限是基于页表含 pkey 绑定位但寄存器为默认值的视图而非应用当前线程寄存器中的动态视图。在设计依赖 PKU 的并发 I/O 路径时这一点需要纳入考量。六、可用性与配置前提x86_64需要 Intel Skylake 及以后的服务器 CPU、或 Tiger Lake第 11 代酷睿及以后的客户端 CPU未来的 AMD CPU 也在支持之列且 CPU 需同时具备PKU与OSPKE特性位。arm64需要实现Permission Overlay ExtensionFEAT_S1POE的 CPU。内核配置pkey 相关系统调用在CONFIG_ARCH_HAS_PKEYS开启时可用x86_64 与 arm64 均支持用户态需要内核头文件中的PKEY_DISABLE_ACCESS/PKEY_DISABLE_WRITE等 UAPI 常量定义于 include/uapi/asm-generic/mman-common.h。pkey-0 的保留语义密钥 0 是默认密钥所有未显式绑定 pkey 的内存都归于密钥 0x86 上除密钥 0 外还保留exec-only pkey见pkey-x86.h的NR_RESERVED_PKEYS 2arm64 则仅保留密钥 0NR_RESERVED_PKEYS 1见pkey-arm64.h。应用可用的密钥数为总密钥数减去保留数。七、总结Memory Protection Keys 为 Linux 应用提供了一套页表打标 寄存器开关的两层内存保护模型pkey_alloc()/pkey_mprotect()/pkey_free()负责低频的密钥生命周期与内存绑定RDPKRU/WRPKRUx86_64或POR_EL0arm64负责高频的权限切换。它在语义上与mprotect()保持一致同时通过SEGV_PKERR与SEGV_ACCERR的区分让应用能够精确识别权限故障来源。无论是 x86_64 的 16 密钥、2 位/密钥编码还是 arm64 的 8 密钥、4 位 R/W/X overlay 编码内核都以统一的系统调用接口向用户空间呈现具体架构差异被封装在 arch/x86/mm/pkeys.c、arch/arm64实现及tools/testing/selftests/mm/自测代码之中——这也是理解与移植 PKU 应用时最值得深入阅读的第一手资料。【免费下载链接】linuxLinux kernel source tree项目地址: https://gitcode.com/GitHub_Trending/li/linux创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →