Page faults, copy-on-write, and anonymous memory

Trace anonymous page faults and copy-on-write through retry and failure paths.

14 min readChapter 29 of 168Intermediate → advanced
In this chapter

Before you start#

Read address spaces and page tables, tasks and fork, and locks and sleeping. The source walkthrough uses upstream v6.12 x86-64 and generic MM code. The optional exercise creates one child and one small private anonymous mapping in its own process; it does not inspect privileged page tables or create memory pressure. The source-only state table is a complete alternative.

Learning goals#

Trace a conditional user fault from x86 entry through VMA validation and handle_mm_fault; distinguish retry, signal and allocation outcomes; explain anonymous zero-page and copy-on-write possibilities; and track a page, mappings and references through a parent/child write without assuming that one count proves exclusivity.

A fault is a question, not always a failure#

Mental model: a VMA answers “may this address be used, and under what policy?”; a page-table entry answers “what translation and permissions are installed right now?” A permitted address can therefore fault because its PTE is absent or deliberately read-only. The kernel resolves that mismatch when possible; the CPU retries the instruction after a successful resolution.

A CPU page fault reports that an access could not be completed using the current translation and permissions. It can be expected: a valid anonymous VMA may not yet have physical backing, a private writable mapping may temporarily have a read-only PTE for copy-on-write, or a file-backed page may need to be fetched. It can also reflect an unmapped address, disallowed access, hardware problem or backing-store failure. A successful mmap establishes a virtual mapping and policy; it does not guarantee that all pages are resident or that a later access cannot fail.

The fault handler receives an address and architecture-specific error information. On x86, the fault code distinguishes relevant properties such as user versus kernel access, write versus read and instruction fetch, with additional features under configuration. The handler must find an appropriate VMA, check whether its permissions allow the attempted access and coordinate with concurrent changes to that VMA. Kernel faults have their own exception-fixup or failure rules; they are not all equivalent to a user process accessing an invalid pointer.

A VMA is metadata, while the page-table entries describe current translation state. A vm_fault object carries fault-specific context such as the VMA, address, flags and page/PTE-related state under the relevant path. A folio can contain one or more base pages. Reverse mapping, or rmap, helps the kernel find mappings of a page or folio; anon_vma participates in anonymous reverse mapping. The VMA lock, mmap lock, page-table lock, folio lock and references have different purposes. A reference keeps an object alive under its contract but does not alone protect every field from concurrent changes.

Source walkthrough#

A conditional user fault
  1. Fault entry

    x86 identifies the access and obtains a suitably protected VMA.

  2. Access and fault

    Permission checks and handle_mm_fault select the applicable MM path.

  3. COW or other handling

    A write may reuse or prepare a folio and recheck the PTE.

  4. Return or failure

    Retry, signal, OOM or successful completion follow the actual result and lock state.

Source map. A v6.12 source map, not a captured page fault. Architecture, VMA state and retry conditions determine the path.

In v6.12 arch/x86/mm/fault.c, do_user_addr_fault includes a per-VMA-lock fast path when configuration and fault conditions permit. If that path is not usable or requires retry, it reaches the mmap-lock path, obtains and validates a VMA, and calls the generic MM handler. This excerpt from that path is upstream v6.12 (SPDX GPL-2.0; surrounding access checks and error handling omitted):

c
fault = handle_mm_fault(vma, address, flags, regs);

The call passes the VMA, faulting address, fault flags and saved register context to generic memory management. The returned vm_fault_t is a bitmask of outcomes, not a physical address or a promise that the original VMA pointer remains valid. The source comments say that on the relevant retry path the mmap lock has already been dropped; the caller must follow the return flags and reacquire or retry as required. A fatal signal can change the control flow. The per-VMA-lock and mmap-lock paths have different lock-release conditions; do not unconditionally unlock both or reuse a VMA after a helper may have dropped protection.

In arch/x86/mm/fault.c, SPDX GPL-2.0, the retry branch after handle_mm_fault reads as follows (the lines between the call and this branch omitted):

c
	/*
	 * If we need to retry the mmap_lock has already been released,
	 * and if there is a fatal signal pending there is no guarantee
	 * that we made any progress. Handle this case first.
	 */
	if (unlikely(fault & VM_FAULT_RETRY)) {
		flags |= FAULT_FLAG_TRIED;
		goto retry;
	}

The comment says the mmap lock is already released on retry. The branch sets FAULT_FLAG_TRIED and jumps back to the retry label earlier in the function, so the fault is attempted again rather than continued with the old VMA. The comment also mentions a pending fatal signal, whose handling sits in a branch the excerpt omits.

In v6.12 mm/memory.c, handle_mm_fault includes this excerpt (SPDX GPL-2.0-only; checks and alternative hugetlb path omitted):

c
ret = __handle_mm_fault(vma, address, flags);

The generic handler sanitizes flags and checks architecture access permission before the relevant normal path. It handles a hugetlb mapping through a different branch, and user faults interact with memory-cgroup OOM accounting. Immediately after the possible fault-handler call, the source warns that the VMA must not be dereferenced because the mmap lock might have been dropped and the VMA destroyed. That lifetime warning is central: a pointer that was protected before a blocking fault is not automatically valid after retry or completion. See the pinned v6.12 implementation.

The architecture interprets the result and context. An access outside a valid VMA or contrary to permitted access can result in SIGSEGV for a userspace task; an error associated with a backing object or other fault conditions can result in SIGBUS. OOM-related fault outcomes have their own handling. The precise signal, code and retry depend on fault type, architecture and path; do not map every nonzero vm_fault_t to one universal signal. A kernel-mode fault can be fixed up by an exception table in an allowed user-copy path or can lead to an oops if not recoverable. The page-table documentation and source are starting points, not a complete architecture-independent fault ABI.

From access to retry or signal
From access to retry or signal1. Faulting task to x86 fault entry: Access with an absent or write-protected PTE. 2. x86 fault entry to handle_mm_fault: do_user_addr_fault: VMA lock or mmap lock, then find the VMA. 3. handle_mm_fault to Folio and PTE: Allocate or copy the folio as the path requires. 4. Folio and PTE to handle_mm_fault (return): Install the PTE after a recheck under the page-table lock. 5. handle_mm_fault to x86 fault entry (return): Return vm_fault_t flags, possibly RETRY. 6. x86 fault entry to Faulting task (return): Retry the instruction, or deliver a signal.Faulting taskx86 fault entryhandle_mm_faultFolio and PTEAccess with anabsent orwrite-protected PTEdo_user_addr_fault:VMA lock or mmaplock, then find theVMAAllocate or copy thefolio as the pathrequiresInstall the PTEafter a recheckunder the page-tablelockReturn vm_fault_tflags, possiblyRETRYRetry theinstruction, ordeliver a signal
  1. Faulting taskx86 fault entry

    Access with an absent or write-protected PTE

  2. x86 fault entryhandle_mm_fault

    do_user_addr_fault: VMA lock or mmap lock, then find the VMA

  3. handle_mm_faultFolio and PTE

    Allocate or copy the folio as the path requires

  4. Folio and PTEhandle_mm_fault

    Return. Install the PTE after a recheck under the page-table lock

  5. handle_mm_faultx86 fault entry

    Return. Return vm_fault_t flags, possibly RETRY

  6. x86 fault entryFaulting task

    Return. Retry the instruction, or deliver a signal

Time runs downward. Dashed arrows show returns.

Read the text version
  1. Faulting task → x86 fault entry: Access with an absent or write-protected PTE
  2. x86 fault entry → handle_mm_fault: do_user_addr_fault: VMA lock or mmap lock, then find the VMA
  3. handle_mm_fault → Folio and PTE: Allocate or copy the folio as the path requires
  4. Folio and PTE → handle_mm_fault (return): Install the PTE after a recheck under the page-table lock
  5. handle_mm_fault → x86 fault entry (return): Return vm_fault_t flags, possibly RETRY
  6. x86 fault entry → Faulting task (return): Retry the instruction, or deliver a signal
Conceptual model. Conceptual Linux v6.12 x86-64 fault model; the retry, signal and error branches are simplified, not a captured trace.

Anonymous read and write faults#

For some eligible anonymous read faults, Linux can map a shared zero page rather than allocate a distinct writable page immediately. A later write cannot modify the global shared zero page as private application data; the kernel must establish suitable private writable backing. Configuration, VMA flags, userfaultfd state, architecture and huge-page paths affect whether a particular fault takes this route. A read of an untouched anonymous range returning zero is not proof that a dedicated physical page was allocated for it.

A write fault to an anonymous private mapping may allocate and initialize a folio, prepare anonymous reverse-mapping relationships, charge accounting where required and install a PTE under the appropriate page-table lock. Allocation can fail, the VMA can change while a lock is dropped, or another fault can install a mapping first. The implementation must recheck the observed PTE and discard or retry its prepared resource when another actor won the race. A fault counter alone does not reveal which branch occurred. Dirty and accessed bits record architecture/kernel-managed state, but their exact meaning and update timing must be read from the relevant architecture and MM code.

Fork and copy-on-write#

For a private writable mapping, fork normally arranges that parent and child have logically separate address spaces while initially sharing suitable backing. The kernel write-protects relevant parent and child PTEs so that a later write faults and can create a private copy or use an eligible reuse path. This is copy-on-write: the user-visible guarantee is separation of private writes, not a requirement to copy every page eagerly at fork. Shared mappings have different semantics. Whether an individual page is copied or reused depends on the current mapping, folio state and the kernel's exclusivity rules.

In v6.12 wp_page_copy, the code prepares anonymous state and allocates a new folio. The following excerpt is from mm/memory.c (SPDX GPL-2.0-only; zero-page selection and surrounding statements omitted):

c
new_folio = folio_prealloc(mm, vma, vmf->address, pfn_is_zero);
if (!new_folio)
	goto oom;

The allocation failure follows an OOM-related cleanup path rather than installing a nonexistent page. On the non-zero-page copy path, the code can copy from the old page, then recheck the PTE under the page-table lock before committing. If another thread changed the entry while the allocation or copy was in progress, the prepared folio may not be installed and must be released according to the path. MMU notifier and TLB ordering can also matter. A speculative allocation and a successful PTE replacement are different states.

In mm/memory.c, v6.12, SPDX GPL-2.0-only, wp_page_copy rechecks the PTE after retaking the page-table lock (the preceding allocation and copy code omitted):

c
	/*
	 * Re-check the pte - we dropped the lock
	 */
	vmf->pte = pte_offset_map_lock(mm, vmf->pmd, vmf->address, &vmf->ptl);
	if (likely(vmf->pte && pte_same(ptep_get(vmf->pte), vmf->orig_pte))) {

The new folio was prepared while the lock was dropped, so the pte_same check against orig_pte decides whether it can be installed. If the entry changed, this branch does not commit the copy. The excerpt does not show the else path or the cleanup that releases the prepared folio, so it does not show what the racing mapping looks like afterward.

A tempting shortcut is “if the page's reference count is one, reuse it.” That is not a complete v6.12 COW proof. Mapcount, references, anonymous exclusivity, pins, compound/folio state and races all affect whether reuse is legal. The exact implementation has special handling and checks; inspect do_wp_page, wp_page_reuse and wp_page_copy for the selected path. A FOLL_PIN long-term or DMA-related pin has different lifetime and accounting implications from an ordinary transient page reference. GUP, pinned pages and fork/COW require their documented coordination; they are not exposed by a simple /proc counter as proof of exclusivity.

A worked parent-child transition#

Suppose a parent has a private anonymous mapping P and writes the byte 7 at its first offset. Before fork, P's PTE is writable and maps a physical page F under the simplified example. After fork, parent and child have separate mm objects. Their private mappings initially refer to F, with relevant PTEs protected so a write triggers the COW path. F can have multiple mappings and references; no exact numeric refcount is assumed because the kernel may hold other references.

The child writes 42. If the copy path is selected and allocation and copying succeed, it obtains a new folio G, copies or initializes the necessary contents, rechecks the original PTE under the relevant lock and installs a writable child mapping to G. The parent continues to map F and reads 7; the child reads 42 from G. A concurrent unmap or another fault can change the PTE before installation, so the handler cannot blindly commit the prepared folio without rechecking. If allocation fails, the goto oom branch and its cleanup apply; the outcome is not a half-installed successful copy. This is a conceptual state sequence, not a measured PFN trace.

Parent keeps F, child gets G
  1. Before fork

    Parent private page P maps frame F writable; its first byte is 7.

  2. After fork

    Parent and child share F; both PTEs are write-protected so a write faults.

  3. Child writes 42

    Write fault: prepare folio G and copy the contents.

  4. Recheck PTE

    Install G only if the child PTE still matches the value seen at fault time.

  5. Result

    Child maps G with 42; parent still maps F with 7. Allocation failure takes the cleanup branch.

Conceptual model. Conceptual copy-on-write state sequence, not a measured PFN trace; reuse of a sole-owner page is a different path.

A reuse path can be valid when the kernel establishes the necessary anonymous exclusivity and other conditions. Merely reading a reference count of one from outside the proper locks is not enough to establish them. The same physical page frame number appearing in two privileged observations at different times also would not prove object identity, because frames can be freed and reused. This exercise intentionally does not read restricted pagemap data or try to identify host PFNs.

File mappings and user-controlled faults#

A file mapping can be MAP_SHARED or MAP_PRIVATE. Shared writes, subject to permissions and filesystem semantics, modify shared file-backed state; private writes are isolated through COW semantics and need not update the underlying file. A file may be truncated concurrently, and faults beyond the valid backing range can fail with a bus error rather than simply allocate anonymous memory. Filesystem callbacks, page-cache state and I/O errors affect the path; do not treat every SIGBUS as proof of one particular hardware fault.

userfaultfd, when configured and permitted, can delegate specified fault handling to userspace. Its registration mode, permissions and security policy are significant. The fault path can drop and reacquire locks or return to userspace for retry under documented flags; a handler must not keep an unprotected VMA pointer across that transition. Similarly, GUP and FOLL_PIN serve different access and lifetime needs from a normal fault. They do not make all pages permanently writable or exempt an application from COW and DMA synchronization. These advanced interfaces are not enabled or exercised by the host lab.

Practice#

Source-only route: draw the parent and child mapping of F before and after the child's write, including read-only PTEs, the possible new folio G, PTE recheck, and allocation-failure cleanup. Add a separate VM_FAULT_RETRY row showing that the fault handler may have dropped the mmap lock and that the VMA must be reacquired or otherwise protected before reuse. State explicitly that no PFN or actual fault was measured.

Bounded own-process host route: run this Python program as an ordinary user on Linux. It maps one base-page-sized private anonymous range, writes 7, creates exactly one child, waits for that child and closes the mapping. It does not inspect pagemap or change any host memory setting.

sh
python3 - <<'PY'
import mmap, os, resource
page = os.sysconf('SC_PAGE_SIZE')
region = mmap.mmap(-1, page, flags=mmap.MAP_PRIVATE | mmap.MAP_ANONYMOUS,
                   prot=mmap.PROT_READ | mmap.PROT_WRITE)
try:
    region[0] = 7
    before = resource.getrusage(resource.RUSAGE_SELF).ru_minflt
    pid = os.fork()
    if pid == 0:
        try:
            region[0] = 42
            os._exit(0 if region[0] == 42 else 2)
        except BaseException:
            os._exit(3)
    waited, status = os.waitpid(pid, 0)
    after = resource.getrusage(resource.RUSAGE_SELF).ru_minflt
    print('child exited successfully:', waited == pid and os.WIFEXITED(status) and os.WEXITSTATUS(status) == 0)
    print('parent byte:', region[0], 'parent minor-fault delta:', after - before)
finally:
    region.close()
PY

Normally the child reports success through its exit status and the parent byte remains 7. The minor-fault delta is for the parent, includes unrelated Python activity and does not count the child's faults. It is not guaranteed to equal one per page; THP, zero-page behavior, accounting and runtime activity vary. A fork or mapping failure is an actual error to record, not a reason to create repeated children or induce pressure. The child uses _exit, so it does not run the parent's cleanup; the kernel releases its resources on exit. The parent closes its mapping in finally. No temporary file needs removal.

For a counterexample, a private mapping's unchanged parent byte does not tell you whether the kernel copied a base page, used a larger folio, or later reused memory. Deliver the source-only transition table and labeled host output if performed. Do not attempt to bypass PFN restrictions or cause an OOM to test a fault failure.

Knowledge check#

  1. Why can handle_mm_fault returning a retry make the original VMA pointer unsafe?
  2. Why is a read of zero from a fresh anonymous mapping not proof of a dedicated physical page?
  3. What must wp_page_copy do if allocation fails or another thread changes the PTE before installation?
  4. Why is refcount == 1 alone not a complete COW reuse proof?
  5. How do MAP_SHARED and MAP_PRIVATE differ for writes, and why can a file fault produce SIGBUS?

Answers#

  1. The handler can drop mmap protection, allowing another thread to remove the VMA. The caller must follow the returned flags and reacquire or otherwise establish valid protection before reuse.
  2. An eligible read fault can use a shared zero page, and other implementation choices exist. The VMA and returned value do not expose the exact physical allocation.
  3. It follows the appropriate failure or retry cleanup rather than installing an invalid or stale prepared folio. PTE state must be rechecked under its required lock before committing.
  4. Mapcount, anonymous exclusivity, pins, folio state and concurrent changes matter in addition to references. Use the kernel's actual reuse protocol and locking.
  5. Shared writes affect shared file-backed state under the mapping and filesystem rules; private writes are isolated by COW. An invalidated or unavailable backing range and I/O-related conditions can produce a bus error, depending on the path.

Checkpoint#

Deliver the F/G state timeline and retry/lifetime branch, with labeled own-process output if run. Pass when you distinguish VMA permission from PTE state, explain copy versus reuse and one allocation/race failure, and state why the observation is not a PFN trace. If uncertain, retrace do_user_addr_fault, handle_mm_fault and wp_page_copy and revise the table. This is not certification.

Sources#

A little further beneath the surface.

Check your understanding with the checkpoint. Marking a chapter as read saves a bookmark, not a proficiency assessment.