内存保护键¶
内存保护键提供了一种强制实施基于页的保护机制,而无需在应用程序更改保护域时修改页表。
- Pkeys 用户空间 (PKU) 是一项可在以下处理器上找到的功能:
英特尔服务器 CPU,Skylake 及更新版本
英特尔客户端 CPU,Tiger Lake(第 11 代酷睿)及更新版本
未来的 AMD CPU
实现权限覆盖扩展 (FEAT_S1POE) 的 arm64 CPU
x86_64¶
Pkeys 的工作原理是:在每个页表中,将 4 个先前保留的位专门用于一个“保护键”,从而提供 16 个可能的键。
每个键的保护由一个 per-CPU 用户可访问寄存器 (PKRU) 定义。每个 PKRU 都是一个 32 位寄存器,为 16 个键中的每个键存储两位(访问禁用和写入禁用)。
作为 CPU 寄存器,PKRU 本质上是线程局部的,可能使每个线程拥有与其他所有线程不同的保护集。
有两条指令(RDPKRU/WRPKRU)用于读写寄存器。即使理论上 PAE PTE 中有空间,该功能也仅在 64 位模式下可用。这些权限仅在数据访问时强制执行,对指令获取没有影响。
arm64¶
Pkeys 在每个页表项中使用 3 位来编码一个“保护键索引”,从而提供 8 个可能的键。
每个键的保护由一个 per-CPU 用户可写系统寄存器 (POR_EL0) 定义。这是一个 64 位寄存器,用于编码每个保护键索引的读、写和执行覆盖权限。
作为 CPU 寄存器,POR_EL0 本质上是线程局部的,可能使每个线程拥有与其他所有线程不同的保护集。
与 x86_64 不同,保护键权限也适用于指令获取。
系统调用¶
有 3 个系统调用直接与 pkeys 交互
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);
在使用 pkey 之前,必须先用 pkey_alloc() 分配它。应用程序直接向特定于架构的 CPU 寄存器写入,以更改由键覆盖的内存的访问权限。在此示例中,这被一个名为 pkey_set() 的 C 函数封装。
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);
ret = pkey_mprotect(ptr, PAGE_SIZE, real_prot, pkey);
... application runs here
现在,如果应用程序需要更新“ptr”处的数据,它可以获得访问权限,进行更新,然后移除其写访问权限
pkey_set(pkey, 0); // clear PKEY_DISABLE_WRITE
*ptr = foo; // assign something
pkey_set(pkey, PKEY_DISABLE_WRITE); // set PKEY_DISABLE_WRITE again
现在当它释放内存时,它也会释放 pkey,因为它不再被使用
munmap(ptr, PAGE_SIZE);
pkey_free(pkey);
注意
pkey_set() 是对写入 CPU 寄存器的封装。示例实现可以在 tools/testing/selftests/mm/pkey-{arm64,powerpc,x86}.h 中找到
行为¶
内核尝试使保护键的行为与普通的 mprotect() 一致。例如,如果你这样做
mprotect(ptr, size, PROT_NONE);
something(ptr);
你可以预期使用保护键时也会有相同的效果
pkey = pkey_alloc(0, PKEY_DISABLE_WRITE | PKEY_DISABLE_READ);
pkey_mprotect(ptr, size, PROT_READ|PROT_WRITE, pkey);
something(ptr);
无论 something() 是对“ptr”的直接访问,例如
*ptr = foo;
还是内核代表应用程序进行访问,例如使用 read() 时,都应该如此
read(fd, ptr, 1);
在这两种情况下,内核都会发送 SIGSEGV,但是当违反保护键时 si_code 将设置为 SEGV_PKERR,而当违反普通 mprotect() 权限时则设置为 SEGV_ACCERR。
请注意,kthread 的内核访问(例如 io_uring)将使用保护键寄存器的默认值,因此与用户空间中寄存器的值或 mprotect() 不一致。