内存保护键¶
内存保护键提供了一种强制执行基于页面的保护机制,但当应用程序更改保护域时,无需修改页表。
- Pkeys 用户空间 (PKU) 是一项可以在以下设备上找到的功能
英特尔服务器 CPU,Skylake 及更高版本
英特尔客户端 CPU,Tiger Lake(第 11 代 Core)及更高版本
未来的 AMD CPU
实现权限覆盖扩展 (FEAT_S1POE) 的 arm64 CPU
x86_64¶
Pkeys 的工作原理是在每个页表条目中为“保护键”分配 4 个之前保留的位,从而给出 16 个可能的键。
每个键的保护都使用每个 CPU 用户可访问的寄存器 (PKRU) 定义。 这些寄存器中的每一个都是一个 32 位寄存器,为 16 个键中的每一个存储两位(禁用访问和禁用写入)。
PKRU 是一个 CPU 寄存器,因此本质上是线程本地的,可能会给每个线程一组与其他线程不同的保护。
有两个指令(RDPKRU/WRPKRU)用于读取和写入该寄存器。 该功能仅在 64 位模式下可用,即使理论上在 PAE PTE 中有空间。 这些权限仅在数据访问时强制执行,对指令提取没有影响。
arm64¶
Pkeys 在每个页表条目中使用 3 位来编码“保护键索引”,从而给出 8 个可能的键。
每个键的保护都使用每个 CPU 可写用户系统寄存器 (POR_EL0) 定义。 这是一个 64 位寄存器,用于编码每个保护键索引的读取、写入和执行覆盖权限。
POR_EL0 是一个 CPU 寄存器,因此本质上是线程本地的,可能会给每个线程一组与其他线程不同的保护。
与 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() 权限时,si_code 将设置为 SEGV_ACCERR。
请注意,来自 kthread 的内核访问(例如 io_uring)将使用保护键寄存器的默认值,因此与用户空间的寄存器值或 mprotect() 不一致。