传统的内存保护以进程为边界——虚拟地址空间划分了用户态与内核态的权限壁垒,页表项中的 U/S 位决定了_current_进程能否访问某块内存。但当需要在一个进程内部限制不同代码模块对内存的访问时(例如 JIT 编译器中限制编译器写入即将执行的代码页——W^X 原则),传统的页表操作显得过于沉重。Memory Protection Keys (MPK/PKU) 正是 Intel 在 Skylake 架构引入的硬件特性,用 4 位宽度的 "protection key" 标签附加在物理页上,用户态可在零系统调用的情况下开关访问权限,实现进程内部的细粒度内存隔离。

一、设计哲学:从进程级到页面级的权限切割

1.1 W^X 的执行困境

JIT 编译器面临一个经典悖论:编译阶段需要写入代码页(W),执行阶段需要禁止写入(X)。传统做法是 mprotect 在读写与可执行间切换,但每次切换都涉及:

  • 系统调用入口(约 50-100 个时钟周期)
  • TLB 击落广播(多核间 IPI 中断,数百周期)
  • 页表项修改与屏障

这在热点代码生成路径上是不可忽视的开销。

1.2 Protection Keys 的核心思想

保护键为每个物理页附加一个 4 位标签(x86 支持 16 个键,常用 1-15 给用户态),每个线程维护一个 Protection Keys Rights Register (PKRU)——一个 32 位寄存器,每两位控制一个键的访问权限:

PKRU[2k : 2k+1] → 键 k 的访问权限
  00 = 读写禁止
  10 = 只读禁止写
  11 = 读写允许 (AD=0, WD=0)

切换权限只需 wrpkru 指令——单周期、用户态安全、不触发 TLB 击落。切换成本从数千周期降到一个指令。

二、CPU 架构支持矩阵

2.1 x86 Intel PKU / AMD | ARM | RISC-V

特性 Intel PKU (Skylake+) AMD (Zen 3+) ARM MPK (v8.9+) UXI RISC-V
密钥数量 16 16 16 (4组×4) 16 待定义
PKRU 寄存器 32 位 同 系统寄存器 系统寄存器 -
用户态写 PKRU wrpkru wrpkru wrpkru - -
支持架构 SPARC KAP, Power10, SP4 - - - -

Intel PKU 的实现细节:

  • CR4.PKE=1 启用
  • 页表项第 59:62 位(即 PKEY 位)存储键值
  • 每个物理页只能绑定一个 key
  • PKRU 检查发生在地址转换的 permission check 阶段,优先级高于页表项本身的 U/S/R/W 位

2.2 Linux 内核的用户态支持

内核自 4.6 版本引入 pkey 系统调用:

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

// 为虚拟地址区域绑定保护键
int pkey_mprotect(void *addr, size_t len, int prot, int pkey);

// 读取/设置键的访问权限(wrpkru 封装)
int pkey_read(int pkey);
void pkey_write(int pkey, unsigned int access_rights);

// 释放键
int pkey_free(int pkey);

三、内核数据结构与权限检查流程

3.1 内核侧的键管理

// arch/x86/include/asm/pgtable.h
#define _PAGE_PKEY_BIT0  59
#define _PAGE_PKEY_BIT1  60
#define _PAGE_PKEY_BIT2  61
#define _PAGE_PKEY_BIT3  62
#define _PAGE_PKEY_MASK  (_AT(pteval_t, 0xF) << _PAGE_PKEY_BIT0)

每个 vm_area_struct 中通过 vma_pkey() 获取该 VMA 绑定的保护键。进程管理数组 mm->arch.pkey_allocation_map 跟踪已分配的键,避免重复分配。

3.2 MMU 检查流程

当 CPU 发起访存时,MMU 按以下顺序进行权限检查:

• 页表权限位:R/W/U/S 检查

• SMAP/SMEP:内核态不能访问用户态页面

• PKRU 检查(若 PCD=0):比较 PKRU 中对应键的 AD/WD 位

• MTE(若启用):内存标签检查,发生在 PKRU 之后

这意味着 PKRU 设置比标准页表权限更严格——即使用户态进程有 U/S 位授予的读写权限,PKRU 仍能在硬件层面拒绝访问。

四、用户态编程实战:从分配键到沙箱隔离

4.1 基础 API 编程模式

#include <sys/mman.h>
#include <sys/syscall.h>
#include <unistd.h>

// glibc 4.6+ 提供了封装,也可直接 syscall
static inline int pkey_alloc(unsigned flags, unsigned rights) {
    return syscall(SYS_pkey_alloc, flags, rights);
}

static inline int pkey_mprotect(void *addr, size_t len, int prot, int pkey) {
    return syscall(SYS_pkey_mprotect, addr, len, prot, pkey);
}

int pkey_read(int pkey);
void pkey_write(int pkey, unsigned rights);
int pkey_free(int pkey);

// 分配并初始化一个保护键
int pk = pkey_alloc(0, PKEY_DISABLE_ACCESS); // 初始禁用访问
if (pk == -1) { perror("pkey_alloc"); abort(); }

// 分配内存并绑定保护键
void *buf = mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0);
if (buf == MAP_FAILED) abort();

if (pkey_mprotect(buf, 4096, PROT_READ|PROT_WRITE, pk) == -1) {
    perror("pkey_mprotect"); abort();
}

4.2 wrpkru 的自定义封装

glibc 在某些发行版中 pkey_read/pkey_write 可能被优化掉(因为 wrpkru 需要特定 clobber 声明)。安全的内联汇编封装:

static inline void pkey_write_asm(int pk, unsigned int rights) {
    unsigned int pkru = (rights & 0x3) << (2 * pk);
    // wrpkru: EAX=EDX=0, ECX=pkru
    asm volatile(
        ".byte 0x0f, 0x01, 0xef"  // wrpkru
        :
        : "a"(0), "d"(0), "c"(pkru)
        : "memory"
    );
}

static inline unsigned int pkey_read_asm(int pk) {
    unsigned int pkru;
    // rdpkru
    asm volatile(
        ".byte 0x0f, 0x01, 0xee"  // rdpkru
        : "=a"(pkru)
        : "c"(0)
    );
    return (pkru >> (2 * pk)) & 0x3;
}

4.3 进程内沙箱模式:JIT 引擎的 W^X 实践

// JIT 代码缓冲区生命周期管理
typedef struct {
    void *code_page;
    int code_pkey;
    size_t size;
} jit_buffer_t;

jit_buffer_t* jit_buffer_create(size_t size) {
    jit_buffer_t *jb = malloc(sizeof(jit_buffer_t));
    
    // 步骤 1: 分配 RW 缓冲区
    jb->code_page = mmap(NULL, size, PROT_READ|PROT_WRITE,
                         MAP_PRIVATE|MAP_ANONYMOUS, -1, 0);
    
    // 步骤 2: 分配保护键(初始禁用写)
    jb->code_pkey = pkey_alloc(0, PKEY_DISABLE_WRITE);
    jb->size = size;
    
    // 步骤 3: 绑定保护键到缓冲区
    pkey_mprotect(jb->code_page, size, PROT_READ|PROT_WRITE, jb->code_pkey);
    
    return jb;
}

// 写入代码时暂时开放写权限
void jit_buffer_write(jit_buffer_t *jb, void *code, size_t len) {
    // 临时开放写权限
    pkey_write(jb->code_pkey, 0);  // AD=0, WD=0 → 允许读写
    
    // 写入代码(可能来自不可信的输入)
    memcpy(jb->code_page, code, len);
    
    // 恢复写禁止
    pkey_write(jb->code_pkey, PKEY_DISABLE_WRITE);
    
    // flush 指令缓存
    __builtin___clear_cache(jb->code_page, jb->code_page + len);
}

// 执行代码时仅开放读权限(可执行基础权限)
void jit_buffer_execute(jit_buffer_t *jb, void (*entry)(void)) {
    // 确保写权限已禁用
    pkey_write(jb->code_pkey, PKEY_DISABLE_WRITE);
    __builtin___clear_cache(jb->code_page, jb->code_page + jb->size);
    entry();
}

这段代码的核心防御能力在于:即使代码注入漏洞让攻击者控制写入路径,他们也无法将恶意代码写入可执行页面——写入必须通过 jit_buffer_write,而该函数在写操作完成后立即关闭写权限。

五、高级应用场景

5.1 数据库引擎的临时缓冲区隔离

在事务型数据库中,元数据页、WAL 缓冲区、数据页分别对应不同的信任级别:

// PostgreSQL 风格的 buffer pool 保护
enum db_page_kind {
    PAGE_METADATA,  // 高信任 → 由 DB 内核管理
    PAGE_WAL,       // 高信任 → WAL 写入器
    PAGE_DATA_USER, // 低信任 → 来自 SQL 解析后的数据
};

static int page_keys[3];

void db_init_protection(void) {
    // 初始化三种保护键,初始全部禁用访问
    for (int i = 0; i < 3; i++) {
        page_keys[i] = pkey_alloc(0, PKEY_DISABLE_ACCESS);
    }
}

void db_page_bind(void *page, size_t size, enum db_page_kind kind) {
    // 先设置为 PROT_NONE 以清除旧权限
    mprotect(page, size, PROT_NONE);
    pkey_mprotect(page, size, PROT_READ|PROT_WRITE, page_keys[kind]);
}

// 只有 DB 内核可以访问的元数据
void db_access_metadata(void *meta_page, void (*work)(void*)) {
    pkey_write(page_keys[PAGE_METADATA], 0);       // 开启元数据访问
    pkey_write(page_keys[PAGE_WAL], PKEY_DISABLE_ACCESS);
    pkey_write(page_keys[PAGE_DATA_USER], PKEY_DISABLE_ACCESS);
    
    work(meta_page);
    
    // 完成后关闭所有键
    for (int i = 0; i < 3; i++)
        pkey_write(page_keys[i], PKEY_DISABLE_ACCESS);
}

5.2 多租户应用中的租户数据隔离

在 Server 服务中处理多个租户数据时,PKU 可实现轻量隔离:

typedef struct {
    int pkey;
    void *tenant_data;
    size_t data_len;
} tenant_slot_t;

// 每个线程处理一个租户的上下文中
void tenant_process_data(tenant_slot_t *slot) {
    // 开启本租户数据的访问权限
    pkey_write(slot->pkey, 0);  // 允许读写
    
    // 处理业务逻辑...
    process_business_logic(slot->tenant_data, slot->data_len);
    
    // 处理完毕,关闭访问
    pkey_write(slot->pkey, PKEY_DISABLE_ACCESS);
}

六、安全与性能权衡

6.1 PKRU 劫持的防御

wrpkru 是用户态可执行的,恶意代码可能尝试重置 PKRU。防御策略:

• 控制流完整性 (CFI):确保 wrpkru 只出现在受控的封装函数

• 影子栈 + MAC:结合影子调用栈保护返回地址

• 编译器加固:GCC/Clang 的 -fhardened 标志

// 防御性封装:在 wrpkru 前审计调用者
static inline void pkey_write_checked(int pk, unsigned rights) {
    // 通过栈上 canary 验证 wrpkru 调用来源
    volatile uintptr_t ra = (uintptr_t)__builtin_return_address(0);
    if (!is_valid_pkey_writer(ra)) {
        raise(SIGILL);
    }
    pkey_write_asm(pk, rights);
}

6.2 性能开销比较

操作 时钟周期(近似) TLB 影响
wrpkru 切换键权限 1-2 无
mprotect (切换一页) 1000-5000 核心本地 TLB 刷新
mprotect (切换大区域) 50000+ 全局 IPI 广播
页表遍历 5-20 无

对 JIT 引擎而言,将 W^X 切换从 mprotect 改为 PKRU,可以让代码生成路径的开销降低数百倍。

七、与其他安全技术的互补关系

PKU 不是替代 seccomp、Landlock 或 MTE 的技术,而是与之互补:

技术 粒度 防御目标 切换成本
PKU 页面级(进程内) 进程内越权访问 1 周期
seccomp 系统调用级 减少攻击面 N/A(过滤)
Landlock 文件系统级 文件系统访问控制 N/A
MTE 缓存行级 (16B) Use-after-free, 溢出 1-5% 总体开销
CFI 基本块级 控制流劫持 3-5%

实际部署中,常见组合是:

seccomp 限制系统调用 → Landlock 限制文件访问 → PKU 限制内存访问 → MTE 检测硬件级内存安全违规

八、前沿探索:PKU 与机密计算融合

在 Intel TDX (Trust Domain Extensions) 和 AMD SEV-SNP 机密计算环境中,PKU 的价值进一步放大:

  • 安全分区中的沙箱:在一个机密 VM 内,不同信任级别的代码需要进一步隔离,PKU 提供无需 exit 到 hypervisor(即不触发世界切换)的隔离机制
  • 加密内存的细粒度访问:结合 TME (Total Memory Encryption),PKU 可以在加密内存上叠加权限标签,防御同进程内的侧信道攻击

Linux 内核社区也在积极探索 PKU 与 Landlock 的集成,让 Landlock 策略能够通过 pkey 而非 page table 操作来实施文件系统相关的内存保护。

九、总结:何时使用 PKU

Memory Protection Keys 适用于以下场景:

• JIT 编译器:W^X 切换的成本敏感场景,代价从微秒降到纳秒

• 多阶段沙箱:需要在进程内区分多个信任域(如 Rust 编译器中的编译阶段与运行阶段)

• 高安全服务:金融交易处理、加密服务中的密钥材料与非密钥代码的隔离

• Serverless 平台:在同一地址空间运行多个互不信任的函数实例

PKU 不是银弹——它不支持跨进程隔离(那是虚拟地址空间的职责),无法防御旁路攻击(那是 MTE 的领域),也无法防御代码注入在 wrpkru 被劫持时的攻击。但作为硬件辅助的"进程内沙箱原语",它以极低代价填补了一个传统安全技术长期覆盖不足的空白。


核心要点:pkey_alloc 绑定物理页,wrpkru 零成本切换权限,x86 PKU 16 个键覆盖绝大多数场景。在 W^X 场景下替代 mprotect,可获得百倍的性能提升。与 seccomp/Landlock/MTE 形成纵深防御。

点赞(0) 打赏

评论列表 共有 0 条评论

暂无评论
立即
投稿

微信公众账号

微信扫一扫加关注

发表
评论
返回
顶部