$RodHat_
Console Tips

userfaultfd: handle your own page faults from userspace

Published by

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

Normally when you access unmapped memory the kernel sends you SIGSEGV and you die. That’s the correct behavior most of the time. But there’s a whole class of problems where you want to say: no, hold on, I’ll supply that page myself, just wait a second. Demand paging from a network backend. Post-copy VM live migration where the guest is running before all its RAM has arrived. Checkpoint-restore where memory is hydrated lazily from a snapshot. For all of these, you need the faulting thread to stall while a handler thread fetches or constructs the missing page, then resumes transparently.

That is exactly what userfaultfd(2) does. It’s been in the kernel since 4.3 (2015). CRIU and QEMU both depend on it. Almost nobody writes userspace code that uses it directly, which is a shame, because the API is actually reasonable.

The model

userfaultfd() returns a file descriptor. You register one or more virtual memory ranges with it using UFFDIO_REGISTER. From that point on, any thread that faults on a missing page in a registered region does not get SIGSEGV. Instead, it stalls in the kernel and a uffd_msg appears on the uffd file descriptor. A dedicated handler thread reads those messages and resolves each fault with UFFDIO_COPY (copy a page from a source buffer) or UFFDIO_ZEROPAGE (zero-fill it). The faulting thread resumes only after the fault is resolved. No signal handlers, no longjmp, no threading acrobatics.

The crucial invariant: the handler thread must not itself fault on the registered region or you deadlock. Keep your handler’s stack and working memory in a separate, unregistered allocation.

Basic usage

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

static long page_size;

/* Negotiate the API version and feature set with the kernel. */
static int uffd_api_handshake(int uffd)
{
    struct uffdio_api api = {
        .api = UFFD_API,
        .features = 0,
    };
    return ioctl(uffd, UFFDIO_API, &api);
}

/* Register a VA range for missing-page fault interception. */
static int uffd_register(int uffd, void *addr, size_t len)
{
    struct uffdio_register reg = {
        .range  = { .start = (uint64_t)(uintptr_t)addr, .len = len },
        .mode   = UFFDIO_REGISTER_MODE_MISSING,
    };
    return ioctl(uffd, UFFDIO_REGISTER, &reg);
}

/* Resolve a fault by copying one page from 'src' to the faulting address. */
static int uffd_supply_page(int uffd, uint64_t fault_addr, void *src)
{
    uint64_t page_start = fault_addr & ~((uint64_t)page_size - 1);
    struct uffdio_copy copy = {
        .dst  = page_start,
        .src  = (uint64_t)(uintptr_t)src,
        .len  = (uint64_t)page_size,
        .mode = 0,
    };
    return ioctl(uffd, UFFDIO_COPY, &copy);
}

The handler thread runs a read loop:

typedef struct { int uffd; } HandlerArg;

static void *fault_handler(void *arg)
{
    int uffd = ((HandlerArg *)arg)->arg.uffd;

    /* Allocate the page buffer OUTSIDE the registered region. */
    char *page = mmap(NULL, page_size, PROT_READ | PROT_WRITE,
                      MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);

    for (;;) {
        struct uffd_msg msg;
        ssize_t n = read(uffd, &msg, sizeof(msg));
        if (n <= 0) break;

        if (msg.event != UFFD_EVENT_PAGEFAULT)
            continue;

        uint64_t addr = msg.arg.pagefault.address;

        /* This is where you'd fetch real data: from a file, network, etc.
           Here we just fill with a recognizable pattern. */
        memset(page, 0xAB, page_size);

        if (uffd_supply_page(uffd, addr, page) < 0) {
            perror("UFFDIO_COPY");
            break;
        }
    }

    munmap(page, page_size);
    return NULL;
}

int main(void)
{
    page_size = sysconf(_SC_PAGE_SIZE);

    /* Create the uffd. O_CLOEXEC always; O_NONBLOCK if you want
       poll()/select() in the handler instead of blocking read(). */
    int uffd = (int)syscall(SYS_userfaultfd, O_CLOEXEC | O_NONBLOCK);
    if (uffd < 0) { perror("userfaultfd"); return 1; }

    if (uffd_api_handshake(uffd) < 0) { perror("UFFDIO_API"); return 1; }

    /* Allocate a lazy region: MAP_ANONYMOUS but we will supply pages
       on demand rather than letting the kernel zero them. */
    size_t region_size = 4 * (size_t)page_size;
    char *region = mmap(NULL, region_size, PROT_READ | PROT_WRITE,
                        MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);

    if (uffd_register(uffd, region, region_size) < 0) {
        perror("UFFDIO_REGISTER"); return 1;
    }

    /* Start the handler thread before anyone touches the region. */
    HandlerArg harg = { .uffd = uffd };
    pthread_t tid;
    pthread_create(&tid, NULL, fault_handler, &harg);

    /* Now access the region. Each page fault blocks the accessor until
       the handler supplies the page. */
    for (size_t i = 0; i < region_size; i += page_size) {
        printf("region[%zu] = 0x%02x\n", i, (unsigned char)region[i]);
    }

    pthread_join(tid, NULL);
    munmap(region, region_size);
    close(uffd);
    return 0;
}

Each read() on the uffd blocks until a fault arrives (or returns immediately if O_NONBLOCK and the queue is empty, in which case use poll()). The handler calls UFFDIO_COPY with a source page from any writable buffer it controls, and the faulting thread is unblocked. The pattern is identical whether “fetch data” means reading from a local file, a network socket, a custom compressor, or a key-value store.

Privileges and the unprivileged sysctl

Pre-5.11, userfaultfd() required either root or CAP_SYS_PTRACE on most distros. Linux 5.2 added the unprivileged_userfaultfd sysctl:

cat /proc/sys/vm/unprivileged_userfaultfd
# 0 on most distros by default
# 1 to allow any user to call userfaultfd()

sysctl vm.unprivileged_userfaultfd=1

Linux 5.11 added UFFD_USER_MODE_ONLY as a flag to userfaultfd(). With this flag, the uffd only intercepts faults from user-mode accesses (not kernel-mode, e.g. copy_from_user). Distros started allowing unprivileged userfaultfd with this restriction as a reasonable compromise. If you’re on 5.11+, use it:

int uffd = (int)syscall(SYS_userfaultfd,
                        O_CLOEXEC | O_NONBLOCK | UFFD_USER_MODE_ONLY);

Check your kernel’s sysctl and version before assuming you can call this unprivileged.

Write-protect mode: dirty page tracking

UFFDIO_REGISTER_MODE_MISSING intercepts accesses to unmapped pages. UFFDIO_REGISTER_MODE_WP (landed in Linux 5.7) intercepts the first write to a mapped page. This is the mechanism post-copy live migration uses to track which pages the guest has modified since migration started.

/* Register a region for write-protect fault interception.
   The region must already be mapped. */
struct uffdio_register reg = {
    .range = { .start = (uint64_t)(uintptr_t)addr, .len = len },
    .mode  = UFFDIO_REGISTER_MODE_WP,
};
ioctl(uffd, UFFDIO_REGISTER, &reg);

/* Then write-protect the whole region to start tracking. */
struct uffdio_writeprotect wp = {
    .range = { .start = (uint64_t)(uintptr_t)addr, .len = len },
    .mode  = UFFDIO_WRITEPROTECT_MODE_WP,  /* enable WP */
};
ioctl(uffd, UFFDIO_WRITEPROTECT, &wp);

When a thread writes to a write-protected page, a UFFD_EVENT_PAGEFAULT arrives with msg.arg.pagefault.flags & UFFD_PAGEFAULT_FLAG_WP set. The handler records which page was dirtied and then removes WP for that page:

/* In the handler, after recording the dirty address: */
struct uffdio_writeprotect clear = {
    .range = {
        .start = fault_addr & ~((uint64_t)page_size - 1),
        .len   = (uint64_t)page_size,
    },
    .mode = 0,  /* clear WP, allow future writes without faulting */
};
ioctl(uffd, UFFDIO_WRITEPROTECT, &clear);

You can combine both modes on the same registration: UFFDIO_REGISTER_MODE_MISSING | UFFDIO_REGISTER_MODE_WP. Missing-page faults supply the page; write faults track dirtiness. QEMU’s postcopy migration implementation does this: it starts with the guest RAM un-transferred and WP-enabled, brings over pages on demand as the guest faults them, and tracks writes so it knows what needs to be re-synchronized.

Other events

The uffd reports more than just page faults. After calling UFFDIO_API with UFFD_FEATURE_EVENT_FORK, you get UFFD_EVENT_FORK when a forked child inherits a registered VMA. UFFD_EVENT_REMAP fires on mremap(). UFFD_EVENT_REMOVE fires on madvise(MADV_REMOVE). This is how CRIU tracks memory state across process tree snapshots without stopping every thread simultaneously.

/* Request fork and remap events during API negotiation. */
struct uffdio_api api = {
    .api      = UFFD_API,
    .features = UFFD_FEATURE_EVENT_FORK | UFFD_FEATURE_EVENT_REMAP,
};
ioctl(uffd, UFFDIO_API, &api);

The child’s uffd in a fork event is in msg.arg.fork.ufd. Your handler has to read from both uffd file descriptors from that point on (poll them together).

What this is not

userfaultfd is not a general memory-error handler. If you just want to catch SIGSEGV on specific ranges for debugging or sanitization, a SIGSEGV handler with sigaction(SA_SIGINFO) that checks si_addr is simpler and does not require a handler thread. userfaultfd is specifically for the case where a fault should stall the accessor, asynchronously fetch data from somewhere, and resume transparently. The stalling behavior is the whole point.

It also does not work on all VMA types. Shared anonymous mappings (MAP_SHARED | MAP_ANONYMOUS) are supported. File-backed mappings require the kernel to have been built with CONFIG_USERFAULTFD (it usually is) and the specific VMA type to be whitelisted. Private file-backed mappings work on most kernels since 5.3.

Checking support

# Config must be set
zcat /proc/config.gz | grep USERFAULTFD
# CONFIG_USERFAULTFD=y

# Verify the syscall number on your arch
grep userfaultfd /usr/include/x86_64-linux-gnu/asm/unistd_64.h
# #define __NR_userfaultfd 323

# Quick smoke test: does it return an fd or ENOSYS?
python3 -c "
import ctypes, os
libc = ctypes.CDLL(None)
fd = libc.syscall(323, 0o2000000)  # O_CLOEXEC
print('uffd fd:', fd, '/ errno:', ctypes.get_errno() if fd < 0 else 'ok')
os.close(fd) if fd >= 0 else None
"

The memfd_create post covers sealed anonymous file descriptors for immutable IPC. The pidfd post covers stable process handles. The io_uring post shows how to batch async I/O with registered buffers, which pairs well with a uffd-backed demand-paging backend. Kerrisk’s The Linux Programming Interface has the mmap() and virtual memory chapter that gives the mental model userfaultfd builds on top of. The actual uffd deep dive is in the kernel tree at Documentation/admin-guide/mm/userfaultfd.rst.