用户态缺页处理

本文档阐述了,如何利用 Linux 的 userfaultfd 机制,在用户空间处理缺页异常(page fault)。

1. 整体概述

代码主要流程如下:

  1. 内存映射
    在
    main() 中使用 mmap 分配了 2 页内存(每页 4KB)。
  2. 注册 userfaultfd
    调用
    register_uffd() 将映射区域注册到 userfaultfd,使得对这些区域的访问触发缺页异常。
  3. 缺页处理线程
    启动一个专门处理缺页事件的线程(
    fault_handler_thread),该线程通过 poll()、read() 捕获缺页事件,然后利用 UFFDIO_COPY 将预先准备的数据填充到缺页区域。
  4. 触发缺页
    在
    main() 中通过读取映射内存触发缺页事件,最终观察到填充的数据。

2. main() 函数解析

int main() {
    void *page;
    page = mmap(NULL, 0x2000, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
    if (page == MAP_FAILED)
        fatal("mmap");

    printf("[+] mmap two pages at 0x%llx\n", (long long unsigned int)page);
    register_uffd(page, 0x2000);

    char buf[0x100];
    puts("[*] reading from page#1");
    strcpy(buf, (char *)(page));
    printf("[+] 0x0000: %s\n", buf);
    puts("[*] reading from page#2");
    strcpy(buf, (char *)(page + 0x1000));
    printf("[+] 0x1000: %s\n", buf);
    puts("[*] reading from page#1");
    strcpy(buf, (char *)(page));
    printf("[+] 0x0000: %s\n", buf);
    puts("[*] reading from page#2");
    strcpy(buf, (char *)(page + 0x1000));
    printf("[+] 0x1000: %s\n", buf);

    getchar();
    return 0;
}

主要步骤

  • 内存映射
    使用
    mmap 分配了 0x2000 字节(2 页)的内存,设置了读写权限。映射类型为匿名且私有。
  • 打印映射地址
    打印分配内存的起始地址,便于调试。
  • 注册 userfaultfd
    调用
    register_uffd(page, 0x2000) 将分配的 2 页内存区域注册到 userfaultfd。
  • 触发缺页
    通过对映射区域内存的读取(使用
    strcpy 拷贝数据到缓冲区),第一次访问将触发缺页异常。代码中读取两次每个页面,借此观察缺页处理线程的填充行为(根据缺页次数返回不同的字符串)。
  • 暂停程序
    使用
    getchar() 阻塞程序退出,以便观察程序运行过程中的输出。

3. register_uffd() 函数解析

int register_uffd(void *addr, size_t len) {
    struct uffdio_api uffdio_api;
    struct uffdio_register uffdio_register;
    long uffd;
    pthread_t th;

    puts("[*] registering userfaultfd");

    uffd = syscall(__NR_userfaultfd, O_CLOEXEC | O_NONBLOCK);
    if (uffd == -1)
        fatal("userfaultfd");

    uffdio_api.api = UFFD_API;
    uffdio_api.features = 0;
    if (ioctl(uffd, UFFDIO_API, &uffdio_api) == -1)
        fatal("ioctl(UFFDIO_API)");

    uffdio_register.range.start = (unsigned long)addr;
    uffdio_register.range.len = len;
    uffdio_register.mode = UFFDIO_REGISTER_MODE_MISSING;
    if (ioctl(uffd, UFFDIO_REGISTER, &uffdio_register) == -1)
        fatal("UFFDIO_REGISTER");

    puts("[*] spawning a fault handler thread");
    if (pthread_create(&th, NULL, fault_handler_thread, (void *)uffd))
        fatal("pthread_create");

    return 0;
}

主要步骤

  • 创建 userfaultfd
    通过
    syscall(__NR_userfaultfd, O_CLOEXEC | O_NONBLOCK) 创建一个 userfaultfd 文件描述符,设置了 O_CLOEXEC(子进程不继承)和 O_NONBLOCK(非阻塞模式)。
  • 初始化 API
    使用
    ioctl(uffd, UFFDIO_API, &uffdio_api) 初始化 userfaultfd,设置 API 版本为 UFFD_API,不启用额外特性(features = 0)。
  • 注册内存区域
    设置
    uffdio_register 结构体,指定要监控的内存区域起始地址和长度,模式设置为 UFFDIO_REGISTER_MODE_MISSING(只对缺页页面进行处理)。调用 ioctl 将内存区域注册到 userfaultfd。
  • 启动缺页处理线程
    使用
    pthread_create 创建一个新的线程,线程函数为 fault_handler_thread,并将 userfaultfd 文件描述符作为参数传递给线程。

4. fault_handler_thread() 函数解析

static void *fault_handler_thread(void *arg) {
    char *dummy_page;
    static struct uffd_msg msg;
    struct uffdio_copy copy;
    struct pollfd pollfd;
    long uffd;
    static int fault_cnt = 0;

    uffd = (long)arg;

    puts("[t][*] mmaping one dummy page");
    dummy_page = mmap(NULL, 0x1000, PROT_READ | PROT_WRITE,
                      MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
    if (dummy_page == MAP_FAILED)
        fatal("mmap(dummy)");

    puts("[t][*] waiting for page fault");
    pollfd.fd = uffd;
    pollfd.events = POLLIN;

    while (poll(&pollfd, 1, -1) > 0) {
        if (pollfd.revents & POLLERR || pollfd.revents & POLLHUP)
            fatal("poll");

        // block until triggered
        if (read(uffd, &msg, sizeof(msg)) <= 0)
            fatal("read(uffd)");
        assert(msg.event == UFFD_EVENT_PAGEFAULT);
        puts("[t][+] caught page fault");
        printf("[t][+] uffd: flag=0x%llx, addr=0x%llx\n", msg.arg.pagefault.flags, msg.arg.pagefault.address);

        // craft data and copy
        puts("[t][*] writing hello world into dummy page");
        if (fault_cnt++ == 0)
            strcpy(dummy_page, "Hello, world! (1)");
        else
            strcpy(dummy_page, "Hello, world! (2)");

        puts("[t][*] copying data from dummy page to faulted page");
        copy.src = (unsigned long)dummy_page;
        copy.dst = (unsigned long)msg.arg.pagefault.address & ~0xfff;
        copy.len = 0x1000;
        copy.mode = 0;
        copy.copy = 0;
        if (ioctl(uffd, UFFDIO_COPY, &copy) == -1)
            fatal("ioctl(UFFDIO_COPY)");
    }

    return NULL;
}

主要步骤

  1. 线程初始化
  • 接收参数,将 arg 转换为 userfaultfd 文件描述符。
  • 定义相关变量:dummy_page 用于存放数据,msg 用于接收缺页事件信息,copy 用于 UFFDIO_COPY 操作,pollfd 用于事件监听,fault_cnt 用于统计缺页次数。
  1. 分配临时页面
  • 使用 mmap 分配一个大小为 0x1000(4KB)的临时页面 dummy_page,用于在缺页事件时填充数据。
  1. 等待缺页事件
  • 设置 pollfd 监听 userfaultfd 文件描述符的可读事件(POLLIN)。
  • 进入 while 循环,使用 poll() 阻塞等待缺页事件。
  1. 处理缺页事件
  • 当 poll() 返回后,通过 read() 读取 userfaultfd 的事件消息,并使用 assert 确认事件类型为 UFFD_EVENT_PAGEFAULT。
  • 打印出缺页的标志和地址信息。
  1. 填充数据
  • 根据 fault_cnt判断缺页次数:
  • 第一次填充 "Hello, world! (1)"
  • 后续填充 "Hello, world! (2)"
  • 将数据写入 dummy_page。
  1. 复制数据到缺页区域
  • 配置 copy结构体:
  • copy.src:设置为 dummy_page 的地址(数据来源)。
  • copy.dst:将缺页地址按页对齐(使用 & ~0xfff)。
  • copy.len:复制 0x1000 字节(即 4KB,一整页)。
  • 使用 ioctl(uffd, UFFDIO_COPY, &copy) 将 dummy_page 中的数据复制到触发缺页的内存页面,从而完成缺页处理。
  1. 退出线程
  • 当没有更多缺页事件时,线程将退出并返回 NULL。

5. 总结

  • 初始化与注册
  • main() 中调用 mmap 分配了 2 页内存,并通过 register_uffd() 注册到 userfaultfd。
  • 缺页事件处理
  • 注册后,对内存的读取触发缺页,缺页处理线程 fault_handler_thread() 捕获并响应,通过 UFFDIO_COPY 将预设数据写入缺页区域。
  • 效果验证
  • 通过重复读取内存页面,可以观察到缺页处理线程根据缺页次数填充了不同的字符串数据,从而验证 userfaultfd 机制的工作效果。

这种机制常用于延迟加载数据、实现用户态内存管理和内存调试等场景。上述的处理函数中,穿插的我们自己的处理代码,就可以帮助实现条件竞争。

[+] mmap two pages at 0x7f0ed2616000
[*] registering userfaultfd
[*] spawning a fault handler thread
[t][*] mmaping one dummy page
[t][*] waiting for page fault
[*] reading from page#1
[t][+] catched page fault
[t][+] uffd: flag=0x0, addr=0x7f0ed2616000
[t][*] writing hello world into dummy page
[t][*] copying data from dummy page to faulted page
[+] 0x0000: Hello, world! (1)
[*] reading from page#2
[t][+] catched page fault
[t][+] uffd: flag=0x0, addr=0x7f0ed2617000
[t][*] writing hello world into dummy page
[t][*] copying data from dummy page to faulted page
[+] 0x1000: Hello, world! (2)
[*] reading from page#1
[+] 0x0000: Hello, world! (1)
[*] reading from page#2
[+] 0x1000: Hello, world! (2)

poc

#define _GNU_SOURCE
#include <assert.h>
#include <fcntl.h>
#include <linux/userfaultfd.h>
#include <poll.h>
#include <pthread.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ioctl.h>
#include <sys/mman.h>
#include <sys/syscall.h>
#include <sys/types.h>
#include <unistd.h>

void fatal(const char *msg) {
    perror(msg);
    exit(1);
}

static void *fault_handler_thread(void *arg) {
    char *dummy_page;
    static struct uffd_msg msg;
    struct uffdio_copy copy;
    struct pollfd pollfd;
    long uffd;
    static int fault_cnt = 0;

    uffd = (long)arg;

    puts("[t][*] mmaping one dummy page");
    dummy_page = mmap(NULL, 0x1000, PROT_READ | PROT_WRITE,
                      MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
    if (dummy_page == MAP_FAILED)
        fatal("mmap(dummy)");

    puts("[t][*] waiting for page fault");
    pollfd.fd = uffd;
    pollfd.events = POLLIN;

    while (poll(&pollfd, 1, -1) > 0) {
        if (pollfd.revents & POLLERR || pollfd.revents & POLLHUP)
            fatal("poll");

        // block until triggered
        if (read(uffd, &msg, sizeof(msg)) <= 0)
            fatal("read(uffd)");
        assert(msg.event == UFFD_EVENT_PAGEFAULT);
        puts("[t][+] caught page fault");
        printf("[t][+] uffd: flag=0x%llx, addr=0x%llx\n", msg.arg.pagefault.flags, msg.arg.pagefault.address);

        // craft data and copy
        puts("[t][*] writing hello world into dummy page");
        if (fault_cnt++ == 0)
            strcpy(dummy_page, "Hello, world! (1)");
        else
            strcpy(dummy_page, "Hello, world! (2)");

        puts("[t][*] copying data from dummy page to faulted page");
        copy.src = (unsigned long)dummy_page;
        copy.dst = (unsigned long)msg.arg.pagefault.address & ~0xfff;
        copy.len = 0x1000;
        copy.mode = 0;
        copy.copy = 0;
        if (ioctl(uffd, UFFDIO_COPY, &copy) == -1)
            fatal("ioctl(UFFDIO_COPY)");
    }

    return NULL;
}

int register_uffd(void *addr, size_t len) {
    struct uffdio_api uffdio_api;
    struct uffdio_register uffdio_register;
    long uffd;
    pthread_t th;

    puts("[*] registering userfaultfd");

    uffd = syscall(__NR_userfaultfd, O_CLOEXEC | O_NONBLOCK);
    if (uffd == -1)
        fatal("userfaultfd");

    uffdio_api.api = UFFD_API;
    uffdio_api.features = 0;
    if (ioctl(uffd, UFFDIO_API, &uffdio_api) == -1)
        fatal("ioctl(UFFDIO_API)");

    uffdio_register.range.start = (unsigned long)addr;
    uffdio_register.range.len = len;
    uffdio_register.mode = UFFDIO_REGISTER_MODE_MISSING;
    if (ioctl(uffd, UFFDIO_REGISTER, &uffdio_register) == -1)
        fatal("UFFDIO_REGISTER");

    puts("[*] spawning a fault handler thread");
    if (pthread_create(&th, NULL, fault_handler_thread, (void *)uffd))
        fatal("pthread_create");

    return 0;
}

int main() {
    void *page;
    page = mmap(NULL, 0x2000, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
    if (page == MAP_FAILED)
        fatal("mmap");

    printf("[+] mmap two pages at 0x%llx\n", (long long unsigned int)page);
    register_uffd(page, 0x2000);

    char buf[0x100];
    puts("[*] reading from page#1");
    strcpy(buf, (char *)(page));
    printf("[+] 0x0000: %s\n", buf);
    puts("[*] reading from page#2");
    strcpy(buf, (char *)(page + 0x1000));
    printf("[+] 0x1000: %s\n", buf);
    puts("[*] reading from page#1");
    strcpy(buf, (char *)(page));
    printf("[+] 0x0000: %s\n", buf);
    puts("[*] reading from page#2");
    strcpy(buf, (char *)(page + 0x1000));
    printf("[+] 0x1000: %s\n", buf);

    getchar();
    return 0;
}

参考链接:

https://blog.wohin.me/posts/pawnyable-0303/