pkey_alloc pkey_free系统调用及示例

pkey_alloc pkey_free系统调用及示例

我们继续按照您的要求学习 Linux 系统编程中的重要函数。这次我们介绍 pkey_alloc 和 pkey_free


1. 函数介绍

pkey_alloc 和 pkey_free 是一组与 内存保护键(Memory Protection Keys, MPK)相关的 Linux 系统调用(内核版本 >= 4.9, x86 架构)。它们是 Intel MPK (Memory Protection Keys) 特性的用户态接口。

**核心概念:内存保护键 **(Protection Keys)

想象你的内存是一间大房子,里面有各种不同的房间(内存页)。传统上,你只有一把总钥匙(页表项中的权限位 RWX)来控制进入这间房子的所有门(内存访问)。如果这把钥匙丢了或者被复制,坏人就能进入所有房间。

内存保护键(MPK)就像是给这间大房子加装了多把独立的锁(保护键):

  1. 获取钥匙 (pkey_alloc) 你向操作系统申请一把新的、独立的钥匙(保护键)。操作系统给你一个钥匙编号(pkey)。
  2. **给房间上锁 **(pkey_mprotect) 你可以使用 pkey_mprotect 系统调用,将特定的房间(内存区域)与你刚申请到的那把钥匙(pkey)关联起来。这相当于给这些房间的门加上了这把新锁。
  3. **控制钥匙 **(特殊寄存器) CPU 内部有一个特殊的寄存器(x86 上是 PKRU – Protection Key Rights User register)。这个寄存器里有 16 个插槽(对应 16 个可能的 pkey),每个插槽可以设置为允许或禁止访问。
  4. 尝试进入房间: 当程序试图访问一个与特定 pkey 关联的内存页时:
    • CPU 会检查页表项中的 pkey 编号(例如 3)。
    • 然后检查 PKRU 寄存器中对应插槽(第 3 个插槽)的权限。
    • 如果 PKRU 允许访问(例如,插槽 3 是 0b00),访问成功。
    • 如果 PKRU 禁止访问(例如,插槽 3 是 0b01 或 0b10),CPU 会立即产生一个 SIGSEGV (段错误) 信号,而无需进行昂贵的页表遍历

pkey_alloc 和 pkey_free 的作用:

  • pkey_alloc申请一个可用的内存保护键(pkey)。成功时返回一个唯一的 pkey 编号(0-15)。
  • pkey_free释放一个之前通过 pkey_alloc 申请的 pkey,使其可以被其他部分的程序再次申请使用。

优势:

  • 快速权限切换: 通过修改 PKRU 寄存器(一个非常快的操作),可以瞬间改变对大量内存区域的访问权限,而无需修改每个内存页的页表项。
  • 细粒度保护: 可以将不同的内存区域分配给不同的 pkey,实现更细粒度的内存访问控制。
  • 硬件加速: 权限检查由 CPU 硬件直接完成,性能极高。

相关文章:pkey_alloc系统调用及示例-CSDN博客 Linux 3.0 内核系统调用 preadv2系统调用及示例


2. 函数原型

#include <sys/mman.h> // 包含 MPK 相关常量和函数声明 (需要较新的 glibc)

// 分配一个保护键
int pkey_alloc(unsigned int flags, unsigned int access_rights);

// 释放一个保护键
int pkey_free(int pkey);

注意: 这些函数需要 glibc 2.27 或更高版本。在较旧的系统上,可能需要直接使用 syscall


3. 功能

  • pkey_alloc:
    • 向内核请求一个当前未被使用的内存保护键。
    • 内核会返回一个唯一的 pkey 编号(通常在 0 到 15 之间,具体取决于 CPU 实现)。
    • 这个 pkey 可以用于后续的 pkey_mprotect 调用。
  • pkey_free:
    • 将一个之前分配的 pkey 返还给内核。
    • 释放后,该 pkey 可以被后续的 pkey_alloc 调用再次分配。
    • 重要: 在调用 pkey_free 之前,应该确保没有内存区域仍然通过 pkey_mprotect 与该 pkey 关联,否则这些区域的访问权限可能会变得不可预测。

4. 参数

pkey_alloc

  • unsigned int flags: 目前必须设置为 0。保留供将来扩展。
  • unsigned int access_rights: 指定该 pkey 的初始访问权限。这是一个位掩码,定义在 <sys/mman.h> 中。
    • PKEY_DISABLE_ACCESS禁止所有访问(读、写、执行)。这是最严格的权限。
    • PKEY_DISABLE_WRITE禁止写入,但允许读取和执行。
    • 0允许所有访问(读、写、执行)。这是最宽松的权限。
    • 注意: 这个初始权限是设置在内核内部的,与 PKRU 寄存器中的权限是分开的。pkey_alloc 返回的 pkey,其在 PKRU 中的初始状态通常是允许访问的。这个 access_rights 参数更多是作为一种内核层面的标记或备用机制。

pkey_free

  • int pkey: 这是之前通过成功的 pkey_alloc 调用返回的保护键编号

5. 返回值

  • pkey_alloc:
    • 成功时: 返回一个非负整数,即新分配的保护键编号(pkey)。
    • 失败时: 返回 -1,并设置 errno(例如 ENOSPC 没有可用的 pkey,EINVAL flags 或 access_rights 无效,EOPNOTSUPP 硬件不支持)。
  • pkey_free:
    • 成功时: 返回 0。
    • 失败时: 返回 -1,并设置 errno(例如 EINVAL pkey 无效,EOPNOTSUPP 硬件不支持)。

6. 相似函数,或关联函数

  • pkey_mprotect: 用于将一个内存区域与特定的 pkey 关联起来,是使用 pkey 进行内存保护的核心函数。
  • mprotect: 传统的内存保护函数,修改内存区域的 RWX 权限。pkey_mprotect 是其增强版,增加了 pkey 功能。
  • mmap: 用于分配和映射内存区域,这些区域后续可以用 pkey_mprotect 来关联 pkey。
  • syscall(SYS_pkey_alloc, ...) / syscall(SYS_pkey_free, ...): 在 glibc 不支持时,直接调用系统调用的方式。

7. 示例代码

重要提示:

  1. 硬件和内核支持: MPK 仅在支持该特性的 CPU(如 Intel x86_64 Skylake 及更新架构)和 Linux 内核(>= 4.9)上可用。
  2. glibc 版本: 需要 glibc 2.27 或更高版本。
  3. 复杂性: 使用 MPK 需要结合 pkey_mprotect 和对 PKRU 寄存器的操作(通常通过内联汇编或专用库函数),下面的示例主要演示 pkey_alloc/free 的基本用法。

示例 1:检查支持并基本使用 pkey_alloc/free

这个例子演示了如何检查系统是否支持 MPK,然后分配和释放保护键。

// pkey_basic_example.c
#define _GNU_SOURCE // For pkey functions
#include <sys/mman.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>

// 如果 glibc 版本过低,可能需要手动定义系统调用号
#ifndef SYS_pkey_alloc
#define SYS_pkey_alloc 330
#endif
#ifndef SYS_pkey_free
#define SYS_pkey_free 331
#endif

// 用于手动调用系统调用的包装函数 (如果需要)
// #include <sys/syscall.h>
// static inline int my_pkey_alloc(unsigned int flags, unsigned int access_rights) {
//     return syscall(SYS_pkey_alloc, flags, access_rights);
// }
// static inline int my_pkey_free(int pkey) {
//     return syscall(SYS_pkey_free, pkey);
// }

// 检查系统是否支持内存保护键
int check_pkey_support() {
    int pkey;
    // 尝试分配一个 pkey 来检查支持
    pkey = pkey_alloc(0, 0);
    if (pkey == -1) {
        if (errno == EOPNOTSUPP) {
            return 0; // Not supported
        } else {
            // Other error, but might still support it in general
            // Let's assume it does and let the main code handle specific errors
            return 1;
        }
    }
    // Allocation succeeded, support confirmed. Free it.
    if (pkey_free(pkey) == -1) {
        perror("pkey_free after check");
    }
    return 1;
}

int main() {
    printf("Checking for Memory Protection Key (MPK) support...\n");

    if (!check_pkey_support()) {
        fprintf(stderr, "Memory Protection Keys (MPK) are NOT supported on this system/CPU.\n");
        fprintf(stderr, "This might be because:\n");
        fprintf(stderr, "  1. The CPU does not support MPK (e.g., older than Intel Skylake).\n");
        fprintf(stderr, "  2. The Linux kernel version is older than 4.9.\n");
        fprintf(stderr, "  3. The feature is disabled.\n");
        exit(EXIT_FAILURE);
    }

    printf("MPK support detected.\n");

    // --- 分配保护键 ---
    printf("\n--- Allocating Protection Keys ---\n");

    int pkey1, pkey2;

    // 分配第一个 pkey,初始权限为允许所有访问
    pkey1 = pkey_alloc(0, 0);
    if (pkey1 == -1) {
        perror("pkey_alloc 1 failed");
        exit(EXIT_FAILURE);
    }
    printf("Successfully allocated pkey 1: %d\n", pkey1);

    // 分配第二个 pkey,初始权限为禁止写入
    pkey2 = pkey_alloc(0, PKEY_DISABLE_WRITE);
    if (pkey2 == -1) {
        perror("pkey_alloc 2 failed");
        // Cleanup previously allocated pkey
        pkey_free(pkey1);
        exit(EXIT_FAILURE);
    }
    printf("Successfully allocated pkey 2: %d (initially write-disabled)\n", pkey2);

    // 尝试分配更多 pkey (系统通常限制为 16 个, 0-15)
    printf("\nAttempting to allocate more pkeys...\n");
    int pkeys[20];
    int allocated_count = 0;
    for (int i = 0; i < 20; i++) {
        pkeys[i] = pkey_alloc(0, 0);
        if (pkeys[i] == -1) {
            if (errno == ENOSPC) {
                printf("  Allocation %d failed: No space left (ENOSPC). Max pkeys reached.\n", i);
                break;
            } else {
                printf("  Allocation %d failed with errno %d (%s)\n", i, errno, strerror(errno));
                break;
            }
        } else {
            printf("  Allocated pkey %d: %d\n", i, pkeys[i]);
            allocated_count++;
        }
    }

    // --- 释放保护键 ---
    printf("\n--- Freeing Protection Keys ---\n");

    // 释放前两个
    if (pkey_free(pkey1) == -1) {
        perror("pkey_free pkey1 failed");
    } else {
        printf("Successfully freed pkey 1 (%d)\n", pkey1);
    }

    if (pkey_free(pkey2) == -1) {
        perror("pkey_free pkey2 failed");
    } else {
        printf("Successfully freed pkey 2 (%d)\n", pkey2);
    }

    // 释放之前批量分配的
    for (int i = 0; i < allocated_count; i++) {
        if (pkey_free(pkeys[i]) == -1) {
            printf("Failed to free pkey %d (%d)\n", i, pkeys[i]);
        } else {
            printf("Successfully freed pkey %d (%d)\n", i, pkeys[i]);
        }
    }

    printf("\nAll pkey operations completed.\n");
    return 0;
}

如何编译和测试:

# 需要较新的 glibc (>= 2.27)
gcc -o pkey_basic_example pkey_basic_example.c
./pkey_basic_example

代码解释:

  1. 定义了必要的头文件。
  2. check_pkey_support: 通过尝试调用 pkey_alloc 来粗略检查系统是否支持 MPK。如果返回 EOPNOTSUPP,则说明不支持。
  3. 在 main 函数中,首先调用 check_pkey_support
  4. 分配 pkey:
    • 调用 pkey_alloc(0, 0) 分配第一个 pkey,初始权限为允许所有访问。
    • 调用 pkey_alloc(0, PKEY_DISABLE_WRITE) 分配第二个 pkey,初始权限为禁止写入。
    • 通过一个循环尝试分配更多 pkey,直到系统报告 ENOSPC(没有空间,即达到上限)。
  5. 释放 pkey:
    • 调用 pkey_free 释放之前分配的所有 pkey。
  6. 打印相关信息。

示例 2:结合 mmappkey_mprotect 使用 pkey_alloc/free (概念性)

这个例子展示了一个更完整的、但概念性的用法,结合了分配内存、分配 pkey、关联内存与 pkey 以及通过 PKRU 控制访问。请注意:直接操作 PKRU 寄存器需要内联汇编,这比较复杂且依赖于架构。

// pkey_conceptual_example.c
#define _GNU_SOURCE
#include <sys/mman.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <signal.h>
#include <setjmp.h>

static jmp_buf jmp_env;
static volatile sig_atomic_t sigsegv_caught = 0;

// 信号处理函数
void sigsegv_handler(int sig) {
    sigsegv_caught = 1;
    longjmp(jmp_env, 1);
}

// 概念性的 PKRU 操作函数 (实际需要内联汇编)
// 这里用伪代码表示
void write_pkru(unsigned int pkru_value) {
    // In real code, this would be inline assembly like:
    // asm volatile(".byte 0x0f,0x01,0xef\n\t" : : "a" (pkru_value), "d" (0), "c" (0) : "memory");
    printf("  [Concept] Writing PKRU with value: 0x%08x\n", pkru_value);
    // WARNING: This is NOT real code, just for illustration.
    // Real code needs inline assembly.
}

unsigned int read_pkru() {
    // In real code:
    // unsigned int pkru;
    // asm volatile(".byte 0x0f,0x01,0xee\n\t" : "=a" (pkru) : "c" (0) : "rdx", "memory");
    // return pkru;
    printf("  [Concept] Reading PKRU\n");
    return 0; // Dummy return
}

int main() {
    if (sysconf(_SC_MPKEY) <= 0) { // Check if supported
         fprintf(stderr, "MPK not supported by sysconf.\n");
         exit(EXIT_FAILURE);
    }

    struct sigaction sa;
    sa.sa_handler = sigsegv_handler;
    sigemptyset(&sa.sa_mask);
    sa.sa_flags = SA_RESTART;
    if (sigaction(SIGSEGV, &sa, NULL) == -1) {
        perror("sigaction");
        exit(EXIT_FAILURE);
    }

    size_t len = 4096; // 1 page
    void *addr;

    // 1. 分配内存
    addr = mmap(NULL, len, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
    if (addr == MAP_FAILED) {
        perror("mmap");
        exit(EXIT_FAILURE);
    }
    printf("Allocated memory at %p\n", addr);

    // 2. 写入一些数据
    strcpy((char*)addr, "Initial data in protected memory.");
    printf("Written initial data.\n");

    // 3. 分配一个 pkey
    int pkey = pkey_alloc(0, 0);
    if (pkey == -1) {
        perror("pkey_alloc");
        munmap(addr, len);
        exit(EXIT_FAILURE);
    }
    printf("Allocated pkey: %d\n", pkey);

    // 4. 将内存区域与 pkey 关联 (概念性)
    // 实际需要调用 pkey_mprotect(addr, len, PROT_READ|PROT_WRITE, pkey);
    printf("--- Conceptually associating memory with pkey %d ---\n", pkey);
    // printf("Would call: pkey_mprotect(%p, %zu, PROT_READ|PROT_WRITE, %d)\n", addr, len, pkey);

    // 5. 通过修改 PKRU 来禁止访问 pkey (概念性)
    printf("\n--- Disabling access to pkey %d via PKRU ---\n", pkey);
    // 计算新的 PKRU 值以禁用 pkey 的访问
    // 每个 pkey 在 PKRU 中占 2 位: 00(allow), 01(deny access), 10(deny write), 11(deny access)
    // 假设 pkey 是 1, 那么它在 PKRU 的 bit 2-3
    // unsigned int current_pkru = read_pkru();
    unsigned int new_pkru = 0; // Start with allowing all
    // Set bits for pkey to 01 (deny access)
    // new_pkru |= (1 << (pkey * 2));
    // write_pkru(new_pkru);
    printf("  [Concept] Would set PKRU bits for pkey %d to deny access.\n", pkey);

    // 6. 尝试访问受保护的内存 (应该触发 SIGSEGV)
    printf("\n--- Attempting to access protected memory ---\n");
    sigsegv_caught = 0;

    if (setjmp(jmp_env) == 0) {
        // This block will be executed first
        printf("  Trying to read from %p...\n", addr);
        char first_char = *((char*)addr); // This should trigger SIGSEGV
        printf("  ERROR: Read succeeded (first char: %c). This should not happen!\n", first_char);
    } else {
        // This block will be executed if longjmp is called from signal handler
        if (sigsegv_caught) {
            printf("  SUCCESS: SIGSEGV caught as expected. Access denied by pkey.\n");
        } else {
            printf("  Unexpected longjmp.\n");
        }
    }

    // 7. 重新允许访问
    printf("\n--- Re-enabling access to pkey %d via PKRU ---\n", pkey);
    // write_pkru(0); // Allow all access again
    printf("  [Concept] Would reset PKRU to allow all access.\n");

    // 8. 再次尝试访问 (应该成功)
    printf("\n--- Attempting to access memory again (should succeed now) ---\n");
    printf("  Reading from %p: %s\n", addr, (char*)addr);

    // 9. 清理
    if (pkey_free(pkey) == -1) {
        perror("pkey_free");
    }
    if (munmap(addr, len) == -1) {
        perror("munmap");
    }

    printf("\nConceptual pkey example finished.\n");
    return 0;
}

**代码解释 **(概念性):

  1. 设置了 SIGSEGV 信号处理函数和 setjmp/longjmp 机制来捕获预期的段错误。
  2. 使用 mmap 分配了一块匿名内存。
  3. 向内存写入了初始数据。
  4. 调用 pkey_alloc(0, 0) 分配一个 pkey。
  5. 概念性步骤: 描述了将内存与 pkey 关联(实际需要 pkey_mprotect)和通过修改 PKRU 寄存器禁止访问的操作。这部分用 printf 和伪函数 write_pkru/read_pkru 代替,因为真实的实现需要内联汇编。
  6. 尝试读取受保护的内存。在真实场景下,这会触发 SIGSEGV,信号处理函数会设置标志并 longjmp 回来。
  7. 概念性步骤: 描述了重新允许访问(重置 PKRU)。
  8. 再次尝试访问,这次应该成功。
  9. 释放 pkey 和内存。

重要提示与注意事项:

  1. 硬件和内核依赖: MPK 是 x86_64 架构(Intel Skylake 及更新)的特性,需要 Linux 内核 4.9+。
  2. glibc 版本: 需要 glibc 2.27+ 才有原生支持。
  3. 复杂性: 真正使用 MPK 需要结合 pkey_mprotect 和对 PKRU 寄存器的精确控制(通常通过内联汇编),这比示例中展示的要复杂得多。
  4. pkey_mprotect 是关键pkey_alloc/free 只是管理 pkey 编号,真正将内存和权限联系起来的是 pkey_mprotect
  5. PKRU 操作: 直接读写 PKRU 寄存器是使用 MPK 功能的核心,但需要内联汇编知识。
  6. 错误处理: 始终检查 pkey_alloc 是否返回 EOPNOTSUPP(不支持)或 ENOSPC(pkey 耗尽)。
  7. 性能优势: MPK 的主要优势在于权限切换的极低延迟,因为它避免了修改页表的开销。
  8. 应用场景: 适用于需要快速、动态地改变大量内存区域访问权限的场景,如沙箱、内存安全库、调试器等。

总结:

pkey_alloc 和 pkey_free 是 Linux 内存保护键(MPK)机制的一部分,用于分配和回收独立的内存访问控制键。它们本身只是 pkey 生命周期管理的第一步。要真正利用 MPK 提供的快速、细粒度内存保护能力,还需要结合 pkey_mprotect 来关联内存区域,以及通过直接操作 PKRU 寄存器来动态启用或禁用访问权限。虽然使用起来比较底层和复杂,但对于需要极致内存安全和性能控制的应用来说,MPK 是一个强大的工具。

此条目发表在linux文章分类目录。将固定链接加入收藏夹。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注