A thread can hold a writable virtual memory mapping and still block when it attempts to modify a particular page. Linux userfaultfd write-protect mode lets user space register a memory range, apply write protection to pages in that range, and receive a page-fault event when a protected page is written. The VMA can remain logically writable while page-table state creates a narrower interception boundary.

This mechanism is not a general authorization system. It is a memory-fault control interface. Its security relevance comes from the placement of the decision point: a write can be suspended before the protected page changes, allowing a separate handler to record state, coordinate migration, preserve a snapshot boundary, or reject progress by leaving the fault unresolved.

Registration and protection are separate operations

A userfaultfd object first goes through the UFFDIO_API handshake. User space requests the features it intends to use and receives the feature and ioctl capabilities exposed by the running kernel. A memory range can then be registered with UFFDIO_REGISTER_MODE_WP.

Registration alone does not necessarily write-protect every page. Protection is applied with UFFDIO_WRITEPROTECT and UFFDIO_WRITEPROTECT_MODE_WP over a range inside a registered region.

Conceptually, the state transition is:

writable mapping
      |
UFFDIO_REGISTER_MODE_WP
      |
registered range
      |
UFFDIO_WRITEPROTECT + MODE_WP
      |
write-protected page-table state

A later write to a protected page produces a page-fault message with UFFD_PAGEFAULT_FLAG_WP; write faults also carry UFFD_PAGEFAULT_FLAG_WRITE. In synchronous mode, the faulting thread remains blocked until user space resolves the condition.

Removing protection releases the blocked writer

The handler resolves a synchronous write-protect fault by issuing UFFDIO_WRITEPROTECT for the relevant range with UFFDIO_WRITEPROTECT_MODE_WP cleared. The kernel restores write access for that protected state and the blocked operation can continue.

That sequence creates a coordination point before mutation:

writer             kernel               handler
  |                   |                    |
  |---- write -------> |                    |
  |                 WP fault                |
  |                   |---- event --------> |
  |                   |                    |
  |                blocked          inspect / record
  |                   |                    |
  |                   |<--- unprotect -----|
  |<---- resume -------|                    |

The handler does not receive a copy of an already-completed write. It receives notification because the write encountered protection. This makes the interface suitable for state tracking that must run before the writer proceeds.

The same property also creates a liveness dependency. If the handler fails to service a synchronous fault, the writer can remain blocked. A design that places critical memory behind this boundary must treat handler availability, event draining, and shutdown sequencing as part of the operational model.

Missing pages have distinct semantics

Write protection of present pages and handling of missing pages are separate concerns. A range may be registered with both UFFDIO_REGISTER_MODE_WP and UFFDIO_REGISTER_MODE_MISSING, but the resulting fault paths are not interchangeable.

For anonymous memory, historical write-protect behavior did not apply protection to absent page-table entries. A write to an unpopulated address could therefore follow the missing-page path rather than generate the expected write-protect event. Kernels exposing UFFD_FEATURE_WP_UNPOPULATED can extend write protection to unpopulated anonymous entries when that feature is requested.

This distinction matters for any policy that assumes every first write will cross the same interception point. The assumption is valid only when the memory type, registration modes, page population state, and negotiated kernel features support that behavior.

For missing-mode faults, user space can supply page contents with operations such as UFFDIO_COPY or map a zero page where supported. When missing and write-protect modes are combined, the application must deliberately manage the order in which a page is populated and write protection is retained or removed.

Memory type and kernel capability define the usable boundary

Write-protect support has expanded across kernel versions and memory types. Anonymous memory, shmem, and hugetlbfs do not have identical historical support. Software cannot safely infer capability from a kernel version string alone; the UFFDIO_API feature negotiation exists to expose the actual interface available to the process.

Feature bits also distinguish behaviors such as write protection for shmem and hugetlbfs, protection of unpopulated anonymous entries, and asynchronous write-protect tracking. Requesting unsupported feature bits causes the API negotiation to fail rather than silently approximating the requested behavior.

This is an important enforcement property. A program that requires a specific fault boundary should fail closed at initialization when the required feature is absent, instead of continuing with a weaker memory-tracking assumption.

Access to userfaultfd is itself policy-controlled

Creating a userfaultfd that handles only user-mode page faults can be requested with UFFD_USER_MODE_ONLY. Broader fault handling through the userfaultfd() system call is subject to kernel policy involving CAP_SYS_PTRACE or the vm.unprivileged_userfaultfd setting.

Linux also exposes /dev/userfaultfd on systems configured for it. Access to that device can be controlled through ordinary filesystem permissions, and a descriptor created through that interface is not governed by vm.unprivileged_userfaultfd in the same manner as the system-call path.

These creation rules are separate from write-protect registration. Possession of a writable mapping does not by itself imply that a process can create every class of userfaultfd context. Deployment policy determines which processes may establish the interception mechanism.

Asynchronous mode changes the enforcement meaning

UFFD_FEATURE_WP_ASYNC changes write-protect mode from a blocking notification mechanism into asynchronous dirty tracking. In that mode, a write clears the write-protect tracking state and the kernel allows progress without delivering the normal blocking event to a handler.

The distinction is fundamental. Synchronous mode can place handler work before writer progress. Asynchronous mode records that a write occurred without putting a user-space decision on the critical path.

A system using asynchronous mode therefore cannot claim that a monitor approved each mutation. It can use the page-table tracking state to identify pages written since protection was armed, but that is an observation property rather than per-write mediation.

Page-granular mediation is not an application transaction boundary

A write-protect fault is generated by memory access at page-table granularity. It does not know whether the instruction belongs to a database transaction, a cryptographic operation, a language-level object update, or a multi-page invariant.

One logical operation can touch several pages and produce several faults. Multiple threads can also contend around the same protected range. Once protection is removed, later writes may proceed without another event until the range is armed again.

For that reason, userfaultfd write protection is strongest when the protected unit and re-arming policy are explicit. It can create a precise pre-write page boundary, but higher-level atomicity still belongs to the application or runtime coordinating those pages.

The security property depends on who controls re-arming

The kernel enforces the page-table transition and fault delivery, but user space decides when protection is applied and when it is removed. A handler that unprotects a broad range may permit more subsequent mutation than a handler that releases only the faulting page and promptly re-arms it.

This separates mechanism from policy. userfaultfd supplies a kernel-enforced stop on selected writes while protection is active. It does not define which writes are legitimate, how long protection remains absent, or what state must be recorded before progress resumes.

The resulting boundary is therefore conditional but useful: while synchronous write protection is armed on a supported page, a matching write cannot silently pass through that page-table state. Once user space removes protection, ordinary writable-mapping semantics return until protection is established again.