pkey_mprotect系统调用及示例

pkey_mprotect系统调用及示例

我们来深入学习 pkey_mprotect 系统调用

相关文章:pkey_mprotect系统调用及示例-CSDN博客 pkey_mprotect系统调用及示例 pkey_alloc pkey_free系统调用及示例 preadv2系统调用及示例

1. 函数介绍

在 Linux 系统中,内存保护是一个核心的安全机制。我们使用 mprotect 系统调用(或者 C 标准库的 mprotect 函数)来设置一块内存区域的访问权限,比如:

  • 只读 (PROT_READ)
  • 可写 (PROT_WRITE)
  • 可执行 (PROT_EXEC)

例如,你可以将包含程序代码的内存页设置为“只读可执行”,防止程序意外修改自己的代码;或者将包含数据的内存页设置为“只读”,防止意外写入。

然而,mprotect 有一个限制:整个进程都遵循同一套内存保护规则。如果一个进程有权限修改某块内存的保护属性(通常需要特殊权限),它就可以修改任何内存页的权限。

Memory Protection Keys (MPK) 是 Intel 和 ARM 等 CPU 架构引入的一种更细粒度的内存保护机制。它允许将内存区域与一个密钥 (Protection Key) 关联起来。这个密钥就像一把锁,控制着与之关联的内存区域是否可以被访问。

pkey_mprotect 系统调用就是用来在设置内存区域访问权限的同时,将这块内存与一个特定的保护密钥 (pkey) 关联起来

简单来说

  • mprotect(addr, len, prot): 设置 addr 开始的 len 字节内存的权限为 prot
  • pkey_mprotect(addr, len, prot, pkey): 做同样的事,外加将这块内存和密钥 pkey 绑定。

要访问一块由 pkey_mprotect 保护的内存,不仅需要满足 prot 指定的权限(如 PROT_READ),当前线程的密钥权限掩码 (PKRU) 中对应的 pkey 也必须允许这种访问。

这提供了一种额外的、硬件加速的、线程级的内存访问控制。即使程序通过 mprotect 获得了写权限,如果线程的 pkey 设置禁止写入,访问仍然会失败。

典型应用场景

  • 沙箱/安全容器:为不同来源或信任级别的代码分配不同的 pkey,防止它们互相干扰或越权访问。
  • 调试器/分析器:保护关键数据结构不被被调试的程序意外修改。
  • 高性能库:在复杂的内存管理库中,使用 pkey 来防止用户代码错误地访问库的内部数据。

2. 函数原型

#define _GNU_SOURCE // 启用 GNU 扩展以使用 pkey_mprotect
#include <sys/mman.h> // 包含 pkey_mprotect 函数声明

int pkey_mprotect(void *addr, size_t len, int prot, int pkey);

注意:此函数需要 glibc 2.27 或更高版本,并且运行在支持 Memory Protection Keys 的 CPU 上(如 Intel Haswell 及更新的处理器,或支持相应特性的 ARM 处理器)。

3. 功能

为从 addr 开始、长度为 len 字节的内存区域设置访问权限 (prot),并将其与保护密钥 (pkey) 关联。

4. 参数

  • addr:
    • void * 类型。
    • 指向要修改保护属性的内存区域的起始地址。这个地址必须是页对齐的(通常是 4KB 边界)。
  • len:
    • size_t 类型。
    • 要修改保护属性的内存区域的长度(字节数)。
  • prot:
    • int 类型。
    • 指定要设置的内存访问权限。可以是以下值的按位或 (|) 组合:
      • PROT_NONE: 内存无法访问。
      • PROT_READ: 页面可读。
      • PROT_WRITE: 页面可写。
      • PROT_EXEC: 页面可执行。
  • pkey:
    • int 类型。
    • 指定要与该内存区域关联的保护密钥
    • 有效的 pkey 值是 0 到 PKEY_MAX (通常是 15) 之间的整数。
    • pkey 为 -1 时,表示不改变该内存区域当前关联的 pkey(如果有的话)。
    • 特殊的 pkey 值 PKEY_DISABLE_ACCESS 和 PKEY_DISABLE_WRITE 可用于 pkey_set 函数,而不是 pkey_mprotect

5. 返回值

  • 成功: 返回 0。
  • 失败: 返回 -1,并设置全局变量 errno 来指示具体的错误原因。

6. 错误码 (errno)

pkey_mprotect 可能返回的错误码与 mprotect 相同或类似,并增加了一些与 pkey 相关的:

  • EINVALaddr 不是页对齐的,或者 prot 或 pkey 参数无效。
  • ENOMEM: 地址范围 (addr to addr+len) 无效,或者包含了未映射的页面。
  • EFAULTaddr 指向了调用进程无法访问的内存地址。
  • EACCES: 调用进程没有权限修改指定内存区域的保护属性。
  • ENOSYS: 系统调用不被当前内核或硬件支持(例如,CPU 不支持 MPK)。

7. 相似函数或关联函数

  • mprotect: 不使用保护密钥的标准内存保护函数。
  • pkey_alloc: 分配一个新的保护密钥。
  • pkey_free: 释放一个之前分配的保护密钥。
  • pkey_get: 获取当前线程的密钥权限掩码 (PKRU) 的值。
  • pkey_set: 设置当前线程的密钥权限掩码 (PKRU) 的值。
  • mmap: 用于分配和映射内存区域。
  • sysconf(_SC_PAGESIZE): 获取系统页大小,用于确保地址对齐。

8. 示例代码

下面的示例演示了如何使用 pkey_mprotect 来保护内存区域。请注意,此代码需要在支持 MPK 的硬件和内核上运行。

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

// 全局变量用于信号处理
volatile sig_atomic_t segv_caught = 0;

// 信号处理函数,捕获 SIGSEGV
void sigsegv_handler(int sig) {
    segv_caught = 1;
    write(STDOUT_FILENO, "Caught SIGSEGV (Segmentation Fault)!\n", 38);
}

int main() {
    char *buffer;
    int pkey, pkru_orig;
    size_t page_size = sysconf(_SC_PAGESIZE);
    struct sigaction sa;

    printf("--- Demonstrating pkey_mprotect ---\n");
    printf("Page size: %zu bytes\n", page_size);

    // 1. 检查系统是否支持 pkey
    // 尝试分配一个 pkey 来测试支持
    pkey = pkey_alloc(0, 0);
    if (pkey == -1) {
        if (errno == ENOSYS) {
            printf("Error: Memory Protection Keys (pkey) are not supported on this system/CPU.\n");
            printf("This example requires MPK support (e.g., Intel Haswell+).\n");
            exit(EXIT_FAILURE);
        } else {
            perror("pkey_alloc");
            exit(EXIT_FAILURE);
        }
    }
    printf("1. Allocated a protection key: %d\n", pkey);

    // 2. 分配一块内存 (使用 mmap 以确保页对齐)
    buffer = mmap(NULL, page_size, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
    if (buffer == MAP_FAILED) {
        perror("mmap");
        pkey_free(pkey);
        exit(EXIT_FAILURE);
    }
    printf("2. Allocated %zu bytes of memory at %p using mmap.\n", page_size, buffer);

    // 3. 初始化内存
    strcpy(buffer, "Initial data in protected memory.");
    printf("3. Initialized memory: '%s'\n", buffer);

    // 4. 设置信号处理函数来捕获段错误
    memset(&sa, 0, sizeof(sa));
    sa.sa_handler = sigsegv_handler;
    sigemptyset(&sa.sa_mask);
    if (sigaction(SIGSEGV, &sa, NULL) == -1) {
        perror("sigaction");
        munmap(buffer, page_size);
        pkey_free(pkey);
        exit(EXIT_FAILURE);
    }

    // 5. 使用 pkey_mprotect 设置权限并关联 pkey
    // 设置为只读,并与 pkey 关联
    printf("4. Calling pkey_mprotect to set memory to READ-ONLY and associate with pkey %d...\n", pkey);
    if (pkey_mprotect(buffer, page_size, PROT_READ, pkey) == -1) {
        perror("pkey_mprotect");
        munmap(buffer, page_size);
        pkey_free(pkey);
        exit(EXIT_FAILURE);
    }
    printf("   pkey_mprotect succeeded.\n");

    // 6. 尝试读取 (应该成功)
    printf("5. Attempting to READ from protected memory...\n");
    segv_caught = 0;
    printf("   Data read: '%s'\n", buffer);
    if (!segv_caught) {
        printf("   Read successful.\n");
    } else {
        printf("   Unexpected SIGSEGV on read!\n");
    }

    // 7. 尝试写入 (应该失败并触发 SIGSEGV)
    printf("6. Attempting to WRITE to READ-ONLY protected memory...\n");
    segv_caught = 0;
    buffer[0] = 'X'; // 尝试修改
    // 如果没有触发 SIGSEGV,说明可能没有保护生效(例如 pkey 允许写)
    if (segv_caught) {
        printf("   Write failed as expected (SIGSEGV caught).\n");
    } else {
        printf("   Write succeeded (unexpected, check pkey settings or hardware support).\n");
    }

    // 8. 修改线程的 pkey 权限掩码 (PKRU) 来允许写入
    printf("7. Getting current PKRU register value...\n");
    pkru_orig = pkey_get();
    if (pkru_orig == -1) {
        perror("pkey_get");
    } else {
        printf("   Current PKRU value: 0x%08x\n", pkru_orig);
        // 计算新的 PKRU 值,允许 pkey 的读写
        // 每个 pkey 占用 2 位:
        // Bits 0-1: pkey 0 (AD=Allow Disable access, WD=Write Disable)
        // Bits 2-3: pkey 1
        // ...
        // Bits 30-31: pkey 15
        // AD=0, WD=0 意味着允许访问和写入
        // AD=1, WD=0 意味着禁止访问
        // AD=0, WD=1 意味着允许访问但禁止写入
        // AD=1, WD=1 意味着禁止访问
        // 假设我们的 pkey 是 1 (这只是示例,实际 pkey 号可能不同)
        // 我们需要将 pkey 1 的 AD 和 WD 位都设为 0
        // pkey 1 的位是 2-3
        int new_pkru = pkru_orig & ~(3 << (pkey * 2)); // 清除 pkey 的两位
        printf("   Setting PKRU to allow RW for pkey %d. New PKRU value: 0x%08x\n", pkey, new_pkru);
        if (pkey_set(new_pkru) == -1) {
            perror("pkey_set");
        }
    }

    // 9. 再次尝试写入 (现在应该成功,因为 pkey 允许了)
    printf("8. Attempting to WRITE again after modifying PKRU...\n");
    segv_caught = 0;
    buffer[0] = 'Y'; // 尝试修改
    if (segv_caught) {
        printf("   Write failed (SIGSEGV caught), even after PKRU change.\n");
        printf("   This might indicate the pkey is not correctly associated or hardware issue.\n");
    } else {
        printf("   Write succeeded after PKRU change.\n");
        printf("   Data is now: '%s'\n", buffer);
    }

    // 10. 清理资源
    printf("\n--- Cleaning up ---\n");
    // 恢复原始的 PKRU 值 (好习惯)
    if (pkey_set(pkru_orig) == -1) {
        perror("pkey_set (restore)");
    } else {
        printf("Restored original PKRU value.\n");
    }

    if (munmap(buffer, page_size) == -1) {
        perror("munmap");
    } else {
        printf("Unmapped memory.\n");
    }

    if (pkey_free(pkey) == -1) {
        perror("pkey_free");
    } else {
        printf("Freed protection key %d.\n", pkey);
    }

    printf("\n--- Summary ---\n");
    printf("1. pkey_mprotect(addr, len, prot, pkey) sets memory permissions AND associates it with a pkey.\n");
    printf("2. Access requires BOTH standard permissions (prot) AND pkey permission (via PKRU register).\n");
    printf("3. pkey_alloc() gets a new key, pkey_free() releases it.\n");
    printf("4. pkey_get() reads PKRU, pkey_set() writes PKRU to control access per thread.\n");
    printf("5. It provides fine-grained, hardware-accelerated memory access control.\n");
    printf("6. Requires CPU and kernel support for Memory Protection Keys (MPK).\n");

    return 0;
}

9. 编译和运行

# 假设代码保存在 pkey_mprotect_example.c 中
# 需要较新的 glibc (>= 2.27) 和支持 MPK 的 CPU
gcc -o pkey_mprotect_example pkey_mprotect_example.c

# 运行程序
# 注意:如果系统不支持 MPK,程序会在开始时就退出并提示。
./pkey_mprotect_example

10. 预期输出 (在支持 MPK 的系统上)

--- Demonstrating pkey_mprotect ---
Page size: 4096 bytes
1. Allocated a protection key: 1
2. Allocated 4096 bytes of memory at 0x7f8b8c000000 using mmap.
3. Initialized memory: 'Initial data in protected memory.'
4. Calling pkey_mprotect to set memory to READ-ONLY and associate with pkey 1...
   pkey_mprotect succeeded.
5. Attempting to READ from protected memory...
   Data read: 'Initial data in protected memory.'
   Read successful.
6. Attempting to WRITE to READ-ONLY protected memory...
Caught SIGSEGV (Segmentation Fault)!
   Write failed as expected (SIGSEGV caught).
7. Getting current PKRU register value...
   Current PKRU value: 0x55555555
   Setting PKRU to allow RW for pkey 1. New PKRU value: 0x55555551
8. Attempting to WRITE again after modifying PKRU...
   Write succeeded after PKRU change.
   Data is now: 'Ynitial data in protected memory.'

--- Cleaning up ---
Restored original PKRU value.
Unmapped memory.
Freed protection key 1.

--- Summary ---
1. pkey_mprotect(addr, len, prot, pkey) sets memory permissions AND associates it with a pkey.
2. Access requires BOTH standard permissions (prot) AND pkey permission (via PKRU register).
3. pkey_alloc() gets a new key, pkey_free() releases it.
4. pkey_get() reads PKRU, pkey_set() writes PKRU to control access per thread.
5. It provides fine-grained, hardware-accelerated memory access control.
6. Requires CPU and kernel support for Memory Protection Keys (MPK).

在不支持 MPK 的系统上的输出:

--- Demonstrating pkey_mprotect ---
Page size: 4096 bytes
Error: Memory Protection Keys (pkey) are not supported on this system/CPU.
This example requires MPK support (e.g., Intel Haswell+).

11. 总结

pkey_mprotect 是一个利用现代 CPU 硬件特性(Memory Protection Keys)来提供更精细内存访问控制的系统调用。

  • 核心优势
    • 额外保护层:在传统的 mprotect 权限之上增加了一层由硬件支持的、基于密钥的访问控制。
    • 线程级控制:每个线程可以通过 pkey_set 独立控制自己对不同 pkey 保护区域的访问权限。
    • 高性能:由 CPU 硬件直接处理,检查开销极小。
  • 工作流程
    1. 使用 pkey_alloc 获取一个 pkey
    2. 使用 pkey_mprotect 将内存区域与 pkey 和访问权限 (prot) 关联。
    3. 使用 pkey_get 和 pkey_set 控制当前线程的 PKRU 寄存器,决定哪些 pkey 允许访问/写入。
    4. 当线程尝试访问内存时,CPU 会同时检查 mprotect 权限和 PKRU 中对应的 pkey 权限。
  • 使用前提
    • CPU 支持 MPK (如 Intel RDPID + Protection Keys for User-mode pages)。
    • Linux 内核支持 (通常 4.9+)。
    • glibc 版本足够新 (2.27+)。
  • 典型应用:沙箱、安全容器、调试器、高性能库。

对于 Linux 编程新手来说,pkey_mprotect 是一个高级特性,理解其概念和潜在用途有助于学习现代系统安全和内存管理的前沿技术。但在日常开发中,mprotect 仍然是管理内存权限的主要工具。

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

发表回复

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