$RodHat_
Console Tips

userfaultfd(2): handle your own page faults from userspace

Published by

userfaultfd(2): handle your own page faults from userspace
Photo: AI-generated — no human photographer / RodHat AI Cover

When a thread touches an unmapped page, the kernel handles the fault, populates the page, and the thread resumes with no idea anything happened. That is the normal contract. userfaultfd(2) breaks it on purpose: you register a memory range, and when any thread faults on a page in that range, the kernel freezes the faulting thread and wakes your fault-handler thread instead. Your thread decides what goes in the page, copies it in via an ioctl, and the faulting thread continues.

This is not a general-purpose allocator hook. The per-fault overhead of a blocking read() makes it useless for high-frequency heap operations. What it is for: lazy page delivery where the source of page content is remote, compressed, or checkpoint-derived. CRIU (the kernel-level checkpoint/restore tool) uses it for post-copy migration: restore the process on the destination host first, let it run, pull pages from the source host as the restored process faults on them. QEMU uses the same approach for live VM migration. Some garbage collector implementations use the write-protect variant to implement write barriers without hot mprotect() calls.

The kernel has had this since 4.3 (late 2015). Almost nobody reaches for it directly. But if you need to control what goes in a page the first time a thread touches it, this is the right interface.

The API in four steps

  1. userfaultfd(O_CLOEXEC) creates the fd.
  2. ioctl(uffd, UFFDIO_API, &api) handshakes the kernel API version and negotiates feature flags.
  3. ioctl(uffd, UFFDIO_REGISTER, &reg) registers a memory range for fault interception.
  4. A thread blocks on read(uffd, &msg, sizeof(msg)), wakes on each fault, calls ioctl(uffd, UFFDIO_COPY, ...) to inject a page, and the faulting thread resumes.
#define _GNU_SOURCE
#include <sys/ioctl.h>
#include <sys/mman.h>
#include <sys/syscall.h>
#include <linux/userfaultfd.h>
#include <pthread.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <stdint.h>
#include <errno.h>

#define NPAGES 4

static long   page_size;
static int    uffd;
static char  *region;

/* Page content the handler injects: all 'X'. */
static char   fill_buf[4096]; /* assumes 4096-byte pages; use page_size in prod */

static void *fault_handler(void *arg)
{
    for (;;) {
        struct uffd_msg msg;
        ssize_t n = read(uffd, &msg, sizeof(msg));
        if (n == 0) break;         /* uffd closed */
        if (n < 0) { perror("read uffd"); break; }

        if (msg.event != UFFD_EVENT_PAGEFAULT) continue;

        /* Align the fault address down to the page boundary. */
        uintptr_t page = msg.arg.pagefault.address & ~(uintptr_t)(page_size - 1);

        fprintf(stderr, "fault @ %#lx  flags=%#llx\n",
                (unsigned long)page,
                (unsigned long long)msg.arg.pagefault.flags);

        struct uffdio_copy uc = {
            .dst  = page,
            .src  = (uintptr_t)fill_buf,
            .len  = (uint64_t)page_size,
            .mode = 0,
        };
        if (ioctl(uffd, UFFDIO_COPY, &uc) < 0)
            perror("UFFDIO_COPY");
    }
    return NULL;
}

int main(void)
{
    page_size = sysconf(_SC_PAGESIZE);
    memset(fill_buf, 'X', sizeof(fill_buf));

    /* Step 1 */
    uffd = (int)syscall(SYS_userfaultfd, O_CLOEXEC);
    if (uffd < 0) { perror("userfaultfd"); return 1; }

    /* Step 2: handshake. features=0 means no optional extensions. */
    struct uffdio_api api = { .api = UFFD_API, .features = 0 };
    if (ioctl(uffd, UFFDIO_API, &api) < 0) { perror("UFFDIO_API"); return 1; }

    /* Step 3: allocate a region and register it.
       The range must be anonymous and have no pages present yet. */
    region = mmap(NULL, NPAGES * page_size, PROT_READ | PROT_WRITE,
                  MAP_ANONYMOUS | MAP_PRIVATE, -1, 0);
    if (region == MAP_FAILED) { perror("mmap"); return 1; }

    struct uffdio_register reg = {
        .range = { .start = (uintptr_t)region,
                   .len   = (uint64_t)(NPAGES * page_size) },
        .mode  = UFFDIO_REGISTER_MODE_MISSING,
    };
    if (ioctl(uffd, UFFDIO_REGISTER, &reg) < 0) { perror("UFFDIO_REGISTER"); return 1; }

    /* Step 4: start the handler thread */
    pthread_t thr;
    pthread_create(&thr, NULL, fault_handler, NULL);

    /* Touch all four pages; each will stall until the handler responds. */
    for (int i = 0; i < NPAGES; i++) {
        char *p = region + i * page_size;
        printf("page %d byte[0] = '%c'\n", i, p[0]);
    }

    close(uffd);   /* causes handler's read() to return 0 */
    pthread_join(thr, NULL);
    munmap(region, NPAGES * page_size);
    return 0;
}

Compile with -lpthread. Run it: you’ll see four fault @ lines from the handler thread, interleaved with four page N byte[0] = 'X' from main. The handler thread owns the kernel, briefly, for each page delivery.

What UFFDIO_COPY does

UFFDIO_COPY copies len bytes from your src buffer (in your process’s address space) into the faulted page at dst, marks the destination page present in the faulting thread’s page table, and unblocks that thread. From the faulting thread’s perspective, it’s atomic: it stalls in the kernel and comes back with a fully populated page. There is no window where it sees a partial write.

UFFDIO_ZEROPAGE does the same without needing a src buffer; the kernel fills the page with zeros. Useful when most of a sparse region should read as zero on first access.

Privilege

Before Linux 5.11, userfaultfd() required CAP_SYS_PTRACE or the sysctl vm.unprivileged_userfaultfd=1. The concern: a misbehaving fault handler can freeze a thread indefinitely by never responding, which is useful for some kernel race-condition exploits.

Since 5.11, the flag UFFD_USER_MODE_ONLY lets an unprivileged process register anonymous and shmem ranges without any special capability. File-backed mappings still require privilege (CRIU needs this case, so it runs as root). For IPC and lazy-alloc patterns over anonymous mmap, UFFD_USER_MODE_ONLY is sufficient:

/* Linux 5.11+, no CAP_SYS_PTRACE needed for anonymous ranges */
uffd = (int)syscall(SYS_userfaultfd, O_CLOEXEC | UFFD_USER_MODE_ONLY);

On older kernels, that flag doesn’t exist. Check the sysctl or run as root:

sysctl vm.unprivileged_userfaultfd          # 0 = root-only, 1 = open
sysctl -w vm.unprivileged_userfaultfd=1     # persistent in /etc/sysctl.d/

Write-protect faults

UFFDIO_REGISTER_MODE_MISSING fires on missing pages. The write-protect mode, UFFDIO_REGISTER_MODE_WP, fires when a thread tries to write a present but write-protected page. This is how some garbage collectors implement write barriers cheaply: register a heap region in WP mode, mark pages write-protected with ioctl(uffd, UFFDIO_WRITEPROTECT, ...), and the handler records writes and clears the protection per-page without ever calling mprotect() from a signal handler.

To use WP mode, request UFFD_FEATURE_PAGEFAULT_FLAG_WP in the UFFDIO_API handshake and check the returned features bitmask before trusting it:

struct uffdio_api api = {
    .api      = UFFD_API,
    .features = UFFD_FEATURE_PAGEFAULT_FLAG_WP,
};
ioctl(uffd, UFFDIO_API, &api);
/* api.features now contains what the kernel actually supports */
if (!(api.features & UFFD_FEATURE_PAGEFAULT_FLAG_WP)) {
    fprintf(stderr, "kernel too old for WP faults\n");
    return 1;
}

Production handler: use poll, not blocking read

The example above uses blocking read() because it is simple. In production, create the fd with O_CLOEXEC | O_NONBLOCK and poll() it:

struct pollfd pfd = { .fd = uffd, .events = POLLIN };
while (poll(&pfd, 1, -1) > 0) {
    struct uffd_msg msg;
    if (read(uffd, &msg, sizeof(msg)) < 0 && errno == EAGAIN) continue;
    /* handle msg */
}

This lets you multiplex the uffd alongside other event sources in the same thread: a signal pipe, a control socket, a shutdown eventfd. For a handler that only deals with uffd events, blocking read is fine and one thread is enough.

This is not mprotect with SIGSEGV

The signal-handler approach is a common alternative: call mprotect(PROT_NONE) on a range, handle SIGSEGV, re-protect inside the handler. It works, but signal handlers run under severe restrictions (async-signal-safe functions only), can’t block, and are a pain to get right with multiple threads all faulting concurrently.

The uffd handler thread is a normal thread. It can call malloc, open files, block on a network socket, log to stderr. The faulting thread stalls in the kernel waiting for the ioctl response; it is not spinning and it doesn’t hold any locks the handler might need (unless you wrote the handler that way, in which case that’s on you).


The memfd post is the natural pairing: create a sealed memfd with the page content you want to inject, then use its fd as the src in UFFDIO_COPY without keeping a separate buffer in memory. The pidfd post covers moving fds between processes; if your fault handler lives in a separate supervisor process, pidfd_getfd() gets it access to the faulting process’s uffd. The timerfd/signalfd/eventfd post covers the poll()-based event loop pattern that a production fault handler should use.