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#
- Fault entry
x86 identifies the access and obtains a suitably protected VMA.
- Access and fault
Permission checks and handle_mm_fault select the applicable MM path.
- COW or other handling
A write may reuse or prepare a folio and recheck the PTE.
- Return or failure
Retry, signal, OOM or successful completion follow the actual result and lock state.
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):
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):
/*
* 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):
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.
- Faulting taskx86 fault entry
Access with an absent or write-protected PTE
- x86 fault entryhandle_mm_fault
do_user_addr_fault: VMA lock or mmap lock, then find the VMA
- handle_mm_faultFolio and PTE
Allocate or copy the folio as the path requires
- Folio and PTEhandle_mm_fault
Return. Install the PTE after a recheck under the page-table lock
- handle_mm_faultx86 fault entry
Return. Return vm_fault_t flags, possibly RETRY
- x86 fault entryFaulting task
Return. Retry the instruction, or deliver a signal
Time runs downward. Dashed arrows show returns.
Read the text version
- Faulting task → x86 fault entry: Access with an absent or write-protected PTE
- x86 fault entry → handle_mm_fault: do_user_addr_fault: VMA lock or mmap lock, then find the VMA
- handle_mm_fault → Folio and PTE: Allocate or copy the folio as the path requires
- Folio and PTE → handle_mm_fault (return): Install the PTE after a recheck under the page-table lock
- handle_mm_fault → x86 fault entry (return): Return vm_fault_t flags, possibly RETRY
- x86 fault entry → Faulting task (return): Retry the instruction, or deliver a signal
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):
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):
/*
* 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.
- Before fork
Parent private page P maps frame F writable; its first byte is 7.
- After fork
Parent and child share F; both PTEs are write-protected so a write faults.
- Child writes 42
Write fault: prepare folio G and copy the contents.
- Recheck PTE
Install G only if the child PTE still matches the value seen at fault time.
- Result
Child maps G with 42; parent still maps F with 7. Allocation failure takes the cleanup branch.
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.
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#
- Why can
handle_mm_faultreturning a retry make the original VMA pointer unsafe? - Why is a read of zero from a fresh anonymous mapping not proof of a dedicated physical page?
- What must
wp_page_copydo if allocation fails or another thread changes the PTE before installation? - Why is
refcount == 1alone not a complete COW reuse proof? - How do
MAP_SHAREDandMAP_PRIVATEdiffer for writes, and why can a file fault produceSIGBUS?
Answers#
- 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.
- 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.
- 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.
- Mapcount, anonymous exclusivity, pins, folio state and concurrent changes matter in addition to references. Use the kernel's actual reuse protocol and locking.
- 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.