get_user_pages and Page Pinning
get_user_pages()(universally abbreviated GUP) is how kernel code reaches into a user process’s address space and obtains the actualstruct pages behind a range of user virtual addresses — resolving each address through the process’s page tables, faulting pages in if needed, and taking a reference so they stay resident. Drivers, Direct I/O, RDMA, and io_uring all use it so that hardware or kernel code can touch a user buffer’s physical pages directly, bypassing the page tables that the CPU would otherwise use. GUP is notorious: holding a plain reference does not tell the rest of the memory-management subsystem that the page’s data is being read or written behind its back, which collided with writeback, copy-on-write, and filesystem layout changes and produced real security bugs. The fix was a distinct concept — a pin (FOLL_PIN, via thepin_user_pages()family, merged in Linux 5.6, early 2020 (Corbet, LWN)) — that is trackable and that the fault and writeback paths can interrogate. This note explains the GUP family, theFOLL_GETvsFOLL_PINvsFOLL_LONGTERMdistinction, how a pin is counted, the COW/long-term-pin hazards, and how pins are released. (API shapes are read from pinned v6.12mm.h; the design rationale is the in-treepin_user_pages.rst.)
Mental Model — A Reference vs a Pin
A normal user page lives under the kernel’s full control: reclaim can move it, compaction can migrate it to defragment memory, writeback can write it back and mark it clean, COW can replace it with a private copy. GUP hands a physical struct page to something that will touch the page outside that control — a DMA engine, an RDMA NIC, an io_uring fixed buffer. Two strengths of “hold on to this page” exist:
- A reference (
FOLL_GET) —_refcount++. Says “don’t free this frame.” Says nothing about whether the data is being accessed. Indistinguishable from the hundreds of other reasons a page is referenced. - A pin (
FOLL_PIN) — a trackable reference whose presence the kernel can query withfolio_maybe_dma_pinned(). Says “this frame’s data is owned by an out-of-band accessor; be careful before unmapping, migrating, or writing it back.”
flowchart TB U["User VA range<br/>(start, nr_pages)"] U -->|"walk page tables,<br/>fault in if absent"| PG["struct page[] returned"] PG --> Q{"Why are you holding it?"} Q -->|"manipulate struct page only<br/>(short, no data access)"| GET["FOLL_GET<br/>_refcount += 1<br/>(get_user_pages*)"] Q -->|"access page DATA<br/>(DMA / DIO / RDMA)"| PIN["FOLL_PIN<br/>(pin_user_pages*)"] PIN -->|"short term (DIO)"| P1["FOLL_PIN"] PIN -->|"long term (RDMA)"| P2["FOLL_PIN | FOLL_LONGTERM"] GET -->|"release"| PUT["put_page()"] PIN -->|"release"| UNPIN["unpin_user_pages()"]
The GUP decision tree. What it shows: GUP resolves a user VA range to physical struct pages; the caller then declares intent by choosing a wrapper. If you only manipulate page metadata, take a plain reference (get_user_pages*, FOLL_GET, release with put_page()). If you access the page’s data out of band, take a pin (pin_user_pages*, FOLL_PIN, release with unpin_user_pages()); add FOLL_LONGTERM if the pin outlives a single I/O. The insight: the wrapper you call is a contract about intent — get and pin are mutually exclusive for a given call site, and mixing the release functions (e.g. put_page() on a pinned page) corrupts the counts.
The API Family
GUP comes in matched get_* and pin_* variants. From v6.12 include/linux/mm.h:
long get_user_pages(unsigned long start, unsigned long nr_pages,
unsigned int gup_flags, struct page **pages);
long pin_user_pages(unsigned long start, unsigned long nr_pages,
unsigned int gup_flags, struct page **pages);
long get_user_pages_remote(struct mm_struct *mm, unsigned long start,
unsigned long nr_pages, unsigned int gup_flags,
struct page **pages, int *locked);
long pin_user_pages_remote(struct mm_struct *mm, unsigned long start,
unsigned long nr_pages, unsigned int gup_flags,
struct page **pages, int *locked);
int get_user_pages_fast(unsigned long start, int nr_pages,
unsigned int gup_flags, struct page **pages);
int pin_user_pages_fast(unsigned long start, int nr_pages,
unsigned int gup_flags, struct page **pages);*_user_pagesoperates oncurrent’smm;*_user_pages_remotetakes an explicitstruct mm_struct *(e.g. ptrace,process_vm_readv).*_user_pages_fastis the lockless fast path: it walks the page tables without takingmmap_lock, using a GUP-fast scheme that races safely against page-table teardown, and only falls back to the slow locked path on a miss.start/nr_pagesdescribe the user VA range; the resolvedstruct pagepointers come back in the caller-providedpages[]array; the return value is the number of pages actually pinned (which may be fewer than requested).
Uncertain
The
vmasparameter is gone in 6.12/6.18. The original 2019/5.6 prototypes (LWN 807108) had a trailingstruct vm_area_struct **vmasout-parameter; it was removed from the entire GUP family (Lorenzo Stoakes’ cleanup) and is absent from the v6.12 signatures above. Reason: I verified its absence in v6.12 source but did not confirm the exact removal release from a changelog. To resolve: pin the removal to its merge commit/release (it landed in the 6.x series, commonly cited as ~6.5, 2023). Any note or tutorial still showing avmasargument is describing a pre-6.5 kernel. uncertain
FOLL_GET and FOLL_PIN are internal flags. The doc is explicit: callers should not set FOLL_PIN at the call site — they call a pin_user_pages*() wrapper, which OR’s FOLL_PIN in for them and validates usage; the get_user_pages*() wrappers set FOLL_GET internally when the caller passes a non-NULL pages array. FOLL_LONGTERM, by contrast, is allowed at the call site, to avoid a combinatorial explosion of wrapper functions. FOLL_PIN and FOLL_GET are mutually exclusive per call, though the same struct page may simultaneously carry pins and gets from different call sites (pin_user_pages.rst).
How a Pin Is Counted — GUP_PIN_COUNTING_BIAS
The design constraints (from the doc) were: an actual per-page count is required (multiple processes may pin/unpin); false positives (“looks pinned but isn’t”) are acceptable but false negatives are not; and struct page may not grow — every field is already used (see struct page Anatomy). The solution overloads _refcount. Rather than splitting the field into bitfields, a pin adds a medium-large constant:
/* v6.12 include/linux/mm.h */
#define GUP_PIN_COUNTING_BIAS (1U << 10) /* = 1024 */A FOLL_GET reference adds 1; a FOLL_PIN pin adds 1024. Because the bias is a power of two, the low bits keep counting ordinary references and the upper bits count pins, so the two are separable by subtraction. folio_maybe_dma_pinned() then answers “is this DMA-pinned?”:
/* v6.12 include/linux/mm.h */
static inline bool folio_maybe_dma_pinned(struct folio *folio)
{
if (folio_test_large(folio))
return atomic_read(&folio->_pincount) > 0; /* exact for large folios */
return ((unsigned int)folio_ref_count(folio)) >= GUP_PIN_COUNTING_BIAS;
}The trade-offs, straight from the source and doc:
- Fuzzy for small folios. If a small page genuinely accumulates ≥1024 ordinary references,
folio_maybe_dma_pinned()returns a false positive — accepted, because callers must handle it gracefully and 1024 plain refs is rare.falseis never fuzzy (“definitely not pinned”); onlytrueis. - Limited counter range.
_refcountis 32-bit signed → 31 usable bits; subtracting the 10-bit bias leaves31 − 10 == 21bits for a pin counter that increments 10 bits at a time. With compound pages this overflowed: pinning a huge page bumps the head page’s refcount once per (head + tail) subpage, and “refcount overflows were seen in some huge page stress tests” (pin_user_pages.rst). - Large folios bypass the bias entirely. They store the pin count in a dedicated
_pincountfield in the tail-page area of the folio, so the result is exact and the overflow/false-positive problems vanish. - The zero page is special-cased.
FOLL_PINon the shared zero page only pretends to pin — it touches neither refcount nor pincount (the page is permanent), and unpin is likewise a no-op.
Two /proc/vmstat counters — nr_foll_pin_acquired and nr_foll_pin_released — track logical pins acquired/released since boot; under normal conditions they are equal except during transitions or while long-term RDMA pins are held.
Releasing Pins — unpin, Don’t put_page
Pinned pages must be released with the unpin_* family, not put_page():
/* v6.12 include/linux/mm.h */
void unpin_user_page(struct page *page);
void unpin_user_pages(struct page **pages, unsigned long npages);
void unpin_user_pages_dirty_lock(struct page **pages, unsigned long npages, bool make_dirty);
void unpin_user_page_range_dirty_lock(struct page *page, unsigned long npages, bool make_dirty);
void unpin_folio(struct folio *folio);
void unpin_user_folio(struct folio *folio, unsigned long npages);unpin_user_pages_dirty_lock(..., make_dirty=true) is the idiom after a write into the pages (e.g. an inbound DMA / RDMA receive): it marks them dirty so writeback eventually persists the new contents, then drops the pin. Calling put_page() on a pinned page subtracts 1 instead of 1024, corrupting the accounting and producing a stuck false-positive pin; calling unpin_user_page() on a merely get-referenced page does the reverse. The release function must match the acquire wrapper.
The Five Cases — Which Flags to Use
The doc enumerates five caller categories (pin_user_pages.rst):
- Direct I/O (DIO) — short-lived DMA buffers; no synchronization with
folio_mkclean()/munmap(). →FOLL_PIN(viapin_user_pages*). - RDMA — long-lived DMA buffers held indefinitely. →
FOLL_PIN | FOLL_LONGTERM. (DAX pages cannot take longterm pins — “pinning” there would lock down filesystem blocks, unsupported.) - MMU-notifier registration — driver pins via
get_user_pages*, registers an MMU notifier, and unpins on an “invalidate range” callback; or, with replayable-fault hardware, avoids pinning entirely. Because it synchronizes properly with mm/filesystem, → neither flag. struct pagemanipulation only — touches metadata, not the data the page tracks. → plainget_user_pages*, neither flag.- Pin in order to write the page’s data — even without DMA/DIO, the bare pattern “pin, write data, unpin” needs
FOLL_PIN. The doc gives the explicit correct/incorrect pair:pin_user_pages()→ write →unpin_user_pages()is correct;get_user_pages()→ write →put_page()is incorrect.
The mental ladder of restriction: FOLL_GET (metadata only, data untouched) → FOLL_PIN (short-term, data accessed) → FOLL_LONGTERM (long-term, data accessed; FOLL_PIN is a prerequisite).
Why FOLL_PIN Exists — The Hazards
The writeback / COW data hazard. Before pins, GUP just bumped _refcount, indistinguishable from any other reference. Two classes of bug followed (LWN 807108): (1) the kernel believes a page’s contents are stable and writes it back / marks it clean, while a device is still DMA-ing fresh data into it — lost or torn writes; (2) on persistent-memory/DAX, a pin deprives the filesystem of the ability to change file layout for those blocks. The “whole point” of marking folios as DMA-pinned, per the doc, is so code like folio_mkclean() and filesystem writeback can ask folio_maybe_dma_pinned() and make an informed decision before unmapping or cleaning a page it cannot safely touch.
The fork / “COW can break either way” hazard. This one produced a security CVE. When a process pins an anonymous page for DMA and then fork()s, the classic copy-on-write scheme write-protects the page in both parent and child and shares it. Whichever side writes first “breaks COW” and gets a fresh private copy — but the other side (which may be the one whose page the device is still DMA-ing into, or out of) can end up holding the new copy or the old, depending on who faulted first. The result, in the words of the fixing commit 17839856fd58 (“gup: document and work around ‘COW can break either way’ issue”, May 2020): “the get_user_pages() call might result in a page pointer that is no longer associated with the original VM, and is associated with — and controlled by — another VM having taken it over instead” — reported by Jann Horn, later assigned CVE-2020-29374 (Babka, “Patching until the COWs come home”, LWN).
The resolution is eager copy at fork for maybe-pinned anonymous pages. Rather than lazily sharing a possibly-pinned page, fork() copies it immediately for the child, so the pinned original stays exclusively with the parent and the device’s DMA can never be silently redirected to a page the child now owns. This is visible in v6.12 mm/memory.c, where copying a present anonymous PTE attempts the normal rmap-dup and, if the folio “may have been pinned,” falls into copy_present_page():
/* v6.12 mm/memory.c, copy_present_ptes() */
folio_get(folio);
if (folio_test_anon(folio)) {
/*
* If this page may have been pinned by the parent process,
* copy the page immediately for the child so that we'll always
* guarantee the pinned page won't be randomly replaced in the
* future.
*/
if (unlikely(folio_try_dup_anon_rmap_pte(folio, page, src_vma))) {
/* Page may be pinned, we have to copy. */
folio_put(folio);
err = copy_present_page(dst_vma, src_vma, dst_pte, src_pte,
addr, rss, prealloc, page);
...
}
...
}The decision uses folio_needs_cow_for_dma(), which is true only if the mm has the MMF_HAS_PINNED flag set (the address space has ever taken a pin) and folio_maybe_dma_pinned() says the folio is pinned — held under the PT lock and write_protect_seq (v6.12 include/linux/mm.h). This is exactly why a trackable pin (rather than a plain reference) is load-bearing: fork must be able to ask “is this maybe-DMA-pinned?” to know whether to break COW eagerly.
Failure Modes and Diagnostics
- Mismatched release —
put_page()on aFOLL_PINpage (orunpin_user_page()on aFOLL_GETpage) corrupts the count by ±1023, leaving a phantom pin (frame never reclaimable) or a premature free. - Forgetting
make_dirty— unpinning written-into pages withoutunpin_user_pages_dirty_lock(..., true)can drop the dirty bit and lose the data DMA’d in. FOLL_LONGTERMon DAX — fails by design; DAX pages have no separate page cache to pin against.- Long-term pins defeat compaction/migration/hotplug — a longterm-pinned page cannot be migrated, so the allocator often migrates such pinned allocations out of
ZONE_MOVABLE/ CMA into non-movable memory up front; pinning huge swaths of movable memory long-term undermines compaction and memory hotplug. - Diagnostics —
dump_page()reports the exact pincount for large folios;/proc/vmstatexposesnr_foll_pin_acquired/nr_foll_pin_released;tools/testing/selftests/mm/gup_test.cexercises the wrappers (./gup_test -a/-b).
See Also
- struct page Anatomy — why the pin count had to be overloaded onto
_refcount(small folios) or stored in_pincount(large folios); the size budget that forbade a new field. - Copy-on-Write and fork — the COW mechanism whose interaction with pinned pages produced CVE-2020-29374 and the eager-copy-at-fork fix.
- Folios and the Folio Conversion — large folios’ dedicated
_pincountthat makes pin tracking exact. - Memory Compaction and Page Migration — operations that long-term pins block, motivating migration out of movable zones.
- Linux Page Table Hierarchy — the page-table walk GUP performs to resolve user VAs to
struct pages. - The Page Fault Handler — GUP faults pages in via the same machinery when they are not yet present.
- MOC: Linux Memory Management MOC (§15, advanced/cross-cutting MM facilities).