A thread can reach a valid, resident page and still stop before modifying it. With Linux userfaultfd write-protect mode, a registered page can be marked so that a write generates a userfaultfd page-fault event. A userspace handler receives that event, performs its bookkeeping, removes the protection, and lets the blocked thread continue.
The mechanism sits between ordinary page permissions and application-level memory accounting. The page remains part of the process address space; the kernel redirects the write fault into a file-descriptor protocol instead of forcing the application to build the same control path around mprotect() and SIGSEGV.
Registration defines which faults enter the protocol
A userfaultfd object does not intercept memory activity merely because the descriptor exists. Userspace first negotiates the API with UFFDIO_API, then registers a virtual-memory range with UFFDIO_REGISTER and UFFDIO_REGISTER_MODE_WP.
Registration and protection are separate operations. Registration says that write-protect faults in the range belong to this userfaultfd context. UFFDIO_WRITEPROTECT with UFFDIO_WRITEPROTECT_MODE_WP then arms protection on selected page-aligned subranges.
struct uffdio_writeprotect wp = {
.range = {
.start = (unsigned long)addr,
.len = len,
},
.mode = UFFDIO_WRITEPROTECT_MODE_WP,
};
ioctl(uffd, UFFDIO_WRITEPROTECT, &wp);The protected range can be smaller than the registered range. This makes the state granular: one descriptor can supervise a broad mapping while only selected pages are armed for write interception at a given moment.
The fault message identifies a protected write
When a thread writes to an armed page in synchronous write-protect mode, the thread blocks and the userfaultfd becomes readable. The handler reads a struct uffd_msg. For this fault class, msg.arg.pagefault.flags contains UFFD_PAGEFAULT_FLAG_WP; a write-protect fault also represents a write access.
writer thread handler thread
| |
| store to WP page |
v |
blocked | read(uffd)
v
WP fault message
|
record page state
|
clear WP
|<-----------------------------+
v
store continuesThe event is not a copy of the write payload. It reports the faulting access and address context. Any application-specific action, such as marking a migration bitmap or recording a first-write transition, belongs to the handler’s policy.
Removing protection is the fault resolution
For a synchronous write-protect fault, userspace resolves the blocked access by issuing UFFDIO_WRITEPROTECT again with the WP bit cleared for the relevant range.
struct uffdio_writeprotect unprotect = {
.range = {
.start = page_start,
.len = page_size,
},
.mode = 0,
};
ioctl(uffd, UFFDIO_WRITEPROTECT, &unprotect);By default, clearing protection also wakes threads waiting on faults in that range. UFFDIO_WRITEPROTECT_MODE_DONTWAKE can separate permission changes from wakeup when an application needs to batch related state transitions.
This ordering is the useful property: bookkeeping can happen while the faulting write is still stopped. Once protection is cleared, later writes to that page proceed normally until userspace arms it again.
Missing pages and protected pages are different states
Write-protect mode does not collapse every memory fault into one event type. A page can be absent, present and writable, or present and write-protected, and userfaultfd registration modes describe different parts of that state space.
A range may be registered with both UFFDIO_REGISTER_MODE_MISSING and UFFDIO_REGISTER_MODE_WP. In that configuration, a missing-page fault still requires population through an operation such as UFFDIO_COPY or UFFDIO_ZEROPAGE, while a write-protect fault requires the protection transition.
The distinction also matters when installing a page. UFFDIO_COPY_MODE_WP can populate a missing page while leaving the resulting mapping write-protected, avoiding a window in which the new page becomes writable before tracking is armed.
Behavior for unpopulated PTEs has additional memory-type and feature details. Current kernel documentation calls out UFFD_FEATURE_WP_UNPOPULATED for write-protect tracking of unpopulated anonymous memory. Code that depends on that behavior must negotiate the feature rather than infer it from the presence of basic WP support.
Synchronous WP creates an explicit ordering point
Dirty-page tracking often needs more than a final dirty bit. A migration or checkpointing component may need a precise point at which a page changes from protected-and-accounted to writable-after-accounting.
Synchronous userfaultfd WP provides that point because the writer cannot complete the protected store before userspace resolves the fault. The handler can associate the page with its own epoch, bitmap, or transfer state before releasing the write.
That guarantee has a cost. Every intercepted first write involves a page fault, a userspace event, scheduling of the handler, and a resolution operation. The mechanism is therefore a control primitive, not a claim that trapping every write is inexpensive.
Linux also provides asynchronous userfaultfd WP when UFFD_FEATURE_WP_ASYNC is available and enabled. In that mode, the kernel automatically resolves a protected write and does not deliver the synchronous fault message. That variant serves dirty-state observation with a different coordination contract; it does not provide the same userspace stop point before the write proceeds.
File-descriptor delivery changes the integration boundary
The event channel is a file descriptor, so a memory manager can integrate write faults with poll() or epoll() rather than route them through a process signal handler. That matters for software already organized around event loops or dedicated fault-handler threads.
It does not make memory faults ordinary byte-stream I/O. The descriptor carries a kernel-defined protocol, and the memory mapping remains subject to virtual-memory rules, feature negotiation, page alignment, and registration lifetime. The application must also coordinate unregistration, mapping changes, and handler shutdown so no thread is left waiting on a fault that no component will resolve.
The architectural boundary is narrow but useful: userfaultfd write protection converts selected memory writes into explicit synchronization events controlled from userspace. The page-table protection stops the access, the descriptor reports it, and an ioctl releases it. That sequence gives memory-management software a place to attach state transitions without replacing the process address space or treating every write as a signal-driven exception.