A page fault normally crosses from a process into the kernel and returns only after the kernel has resolved the virtual-memory condition or delivered an error. Linux userfaultfd can insert a user-space component into that path for explicitly registered address ranges. The kernel reports selected faults through a file descriptor, blocks the faulting execution context when the mode requires it, and accepts an ioctl that resolves the fault.
This is a Linux virtual-memory interface, not a C or POSIX memory guarantee. Its behavior depends on negotiated kernel features, the registered range, its mapping type, and the registration mode.
Registration defines the fault boundary
Calling userfaultfd() creates the descriptor, but the descriptor alone does not redirect faults. A process first negotiates the API with UFFDIO_API, then registers an address interval with UFFDIO_REGISTER. The requested mode determines which fault classes enter the protocol.
UFFDIO_REGISTER_MODE_MISSING covers accesses to pages that are absent from a supported registered mapping. UFFDIO_REGISTER_MODE_WP provides userfaultfd write-protection faults. UFFDIO_REGISTER_MODE_MINOR, where supported for the mapping type and kernel, reports minor faults for pages whose backing content exists but whose page-table state still needs completion.
These modes are not interchangeable. A missing-page handler supplies content or zeroes a page. A write-protection handler controls a write barrier on an already mapped page. A minor-fault handler can coordinate page-table installation without treating the backing data as absent.
Registration is therefore a capability boundary over an address interval, not a process-wide replacement for the kernel page-fault machinery. Faults outside registered ranges continue through their ordinary kernel paths.
Fault delivery is a descriptor protocol
The userfaultfd descriptor can be monitored with poll(), select(), or epoll(). A read returns one or more struct uffd_msg records. For a page-fault event, the record carries the fault address and flags that identify relevant properties such as a write access or a write-protection fault.
The event is notification, not resolution. For fault modes that stall execution, the thread that touched the address remains blocked until another execution context performs an operation that makes the fault resolvable, or until the surrounding mapping state changes in a way that terminates that wait.
This separation has an important concurrency consequence: the faulting thread cannot also be the only handler responsible for servicing its own blocked fault. Designs commonly dedicate another thread or process to reading the descriptor and issuing resolution ioctls. That handler must also avoid touching registered memory in a manner that creates a dependency cycle through the same service path.
Resolution changes memory state, not the event record
For a missing fault, UFFDIO_COPY copies bytes into the faulting range and installs the resulting page state atomically with respect to the fault resolution operation. UFFDIO_ZEROPAGE, on mappings that support it, resolves a missing page with zero-filled content. These operations act on page-aligned ranges even when the reported fault address points somewhere inside a page.
For minor faults, UFFDIO_CONTINUE tells the kernel to continue fault handling using the existing backing content. For write-protection mode, UFFDIO_WRITEPROTECT applies or removes the userfaultfd write-protection state over a range.
A successful resolution ioctl does not imply that every later access to the range is fault-free. Another page can still be missing, write protection can remain on adjacent pages, and mapping changes can invalidate assumptions held by the handler. The protocol operates on current virtual-memory state, not on a permanent promise about the address interval.
Mapping mutation creates a second event plane
Virtual address ranges are not static identities. munmap(), remapping operations, process creation, and other mapping changes can alter the relationship between an address and its backing object. userfaultfd includes feature negotiation and event mechanisms for selected non-page-fault changes, with availability depending on the requested API features and kernel support.
A handler that caches only raw fault addresses can therefore become stale. The address is meaningful relative to the process memory map at the time the event is interpreted. If the application permits concurrent mapping mutation, the handler needs a synchronization contract that connects its own metadata to those mutations.
This differs from a storage request keyed by an immutable object identifier. A virtual address is a location in a mutable address space. Treating it as durable identity can make a delayed fault response populate a range whose application-level role has already changed.
Copying page data does not define application ownership
UFFDIO_COPY provides a kernel operation for installing page contents, but it does not establish an application-level ownership model for the source data. The handler remains responsible for the lifetime and consistency of whatever source buffer or backing store it uses to produce the page.
The same boundary appears in snapshot and migration designs. userfaultfd can expose first access, missing content, or write attempts under supported modes, but it does not by itself define snapshot consistency. If other threads can mutate related state, the application still needs ordering rules that determine which version of that state belongs to a given page response.
Write-protection mode is especially useful as a synchronization signal because a write can be intercepted before the protected page is modified. The observable event still does not prescribe policy. A handler may record dirtiness, copy old contents, remove protection, or combine those actions, but the consistency guarantee comes from the complete protocol around the ioctl sequence.
Handler failure is part of the memory contract
Moving resolution into user space also moves a liveness dependency. A thread blocked on a userfaultfd-managed fault can make no forward progress while the required handler action is absent. If the handler waits on a lock held by the faulting thread, the result can be a deadlock even though the kernel interface itself is operating as specified.
Resource pressure can create similar cycles. A handler that allocates memory, performs filesystem I/O, or calls into a runtime with its own page-touch behavior may depend indirectly on memory covered by the same fault service. The kernel exposes the mechanism; it cannot guarantee that the surrounding dependency graph is acyclic.
Closing the descriptor, unregistering ranges, or changing mappings also affects outstanding and future faults according to the kernel interface rules. Shutdown therefore needs an explicit ordering between stopping producers of new accesses, draining or abandoning fault work, changing registrations, and releasing handler resources.
The descriptor separates detection from policy
The central property of userfaultfd is not merely that page faults become visible to an application. It separates kernel detection of selected virtual-memory events from a user-space decision about page content, page-table continuation, or write protection. That separation creates a programmable boundary with precise ioctl operations and equally precise liveness obligations.
The interface is strongest when address-range ownership, mapping mutation, handler concurrency, and shutdown order are treated as parts of one protocol. Without those constraints, a technically valid fault event can still arrive at a handler whose metadata is stale or whose dependencies prevent it from resolving the blocked execution context.