[PATCH v9 00/15] userfaultfd: working set tracking for VM guest memory

From: Kiryl Shutsemau

Date: Mon Jul 06 2026 - 05:10:58 EST


From: "Kiryl Shutsemau (Meta)" <kas@xxxxxxxxxx>

This series adds userfaultfd support for tracking the working set of
VM guest memory, so a VMM can identify hot pages and reclaim cold ones
to tiered or remote storage.

v1: https://lore.kernel.org/all/20260427114607.4068647-1-kas@xxxxxxxxxx/
v2: https://lore.kernel.org/all/cover.1778254670.git.kas@xxxxxxxxxx/
v3: https://lore.kernel.org/all/20260522133857.552279-1-kirill@xxxxxxxxxxxxx/
v4: https://lore.kernel.org/all/20260525113737.1942478-1-kas@xxxxxxxxxx/
v5: https://lore.kernel.org/all/20260526130509.2748441-1-kirill@xxxxxxxxxxxxx/
v6: https://lore.kernel.org/all/20260529172716.357179-1-kas@xxxxxxxxxx/
v7: https://lore.kernel.org/all/20260629120749.566063-1-kirill@xxxxxxxxxxxxx/
v8: https://lore.kernel.org/all/20260703133615.1039465-1-kirill@xxxxxxxxxxxxx/

== Changes since v8 ==

Addresses the Sashiko AI review of v8; triage summary and interdiff
were posted in the v8 thread:
https://lore.kernel.org/all/akfmUXjqaBQnxqYX@thinkstation/

- 05/15: disable fault-around on VM_UFFD_RWP VMAs. Pre-faulted
neighbours are indistinguishable from accessed pages in
PAGEMAP_SCAN (PAGE_IS_ACCESSED) and pollute the tracked working
set; each page must be populated by its own fault. Also group the
WP/RWP flags in uffd_disable_fault_around() and
uffd_disable_huge_pmd_share().
- 06/15: change_huge_pud() rejects MM_CP_UFFD_RWP_ALL next to the
existing MM_CP_UFFD_WP_ALL check. Defensive only: PUD THPs are
DAX-only and DAX cannot be RWP-registered.
- 09/15: enforce the PROT_NONE accessibility check at register time
only instead of in vma_can_userfault(), which the unregister path
also calls. Previously RWP register -> mprotect(PROT_NONE) ->
UFFDIO_UNREGISTER failed with EINVAL until access was restored or
the uffd was closed.
- 13/15: userfaultfd_register() reads ctx->features through the
userfaultfd_features() helper instead of a plain load racing the
WRITE_ONCE() in userfaultfd_set_mode(). Benign (the tested bit is
not toggleable), KCSAN hygiene.
- 15/15: fix the documentation claim that UFFDIO_API returns the
supported feature bitmask on EINVAL; the kernel returns the
structure zeroed.

The review also surfaced a pre-existing bug in
copy_hugetlb_page_range() (present-pte accessor applied to
hwpoison/migration swap entries, corrupting the swap offset); the fix
was posted separately with Fixes:/Cc: stable:
https://lore.kernel.org/all/20260703161833.57416-1-kirill@xxxxxxxxxxxxx/

114/114 of tools/testing/selftests/mm/uffd-unit-tests pass on v7.2-rc1
(47 RWP cases plus the existing UFFD groups, no regressions).

== Problem ==

A VMM managing guest memory needs to:

1. detect which pages are still being touched (working-set
tracking);
2. safely reclaim cold pages to slower tiered or remote storage;
3. fetch them back on demand when accessed again.

== Approach ==

UFFDIO_REGISTER_MODE_RWP is a new userfaultfd registration mode, in
parallel with the existing MODE_MISSING / MODE_WP / MODE_MINOR. It
uses the same mechanism on every backing -- anon, shmem, hugetlbfs:

- PAGE_NONE on the PTE (the same primitive NUMA balancing uses)
makes the page inaccessible while keeping it resident;
- the uffd PTE bit (the one MODE_WP already owns) marks the entry
as "userfaultfd-tracked" so the protnone fault path can tell an
RWP fault apart from an mprotect(PROT_NONE) or NUMA hinting
fault.

VM_UFFD_WP and VM_UFFD_RWP are mutually exclusive per VMA, so the
same PTE bit safely carries both meanings depending on the
registered VMA flag.

In sync mode, the kernel delivers a UFFD_PAGEFAULT_FLAG_RWP message
to the registered handler, and the handler resolves the fault with
UFFDIO_RWPROTECT clearing MODE_RWP. In async mode
(UFFD_FEATURE_RWP_ASYNC), the fault is auto-resolved in-place: the
kernel restores the original PTE permissions and the faulting thread
continues without a userfaultfd message ever being delivered.
Userspace then learns which pages were touched during the cycle by
reading PAGE_IS_ACCESSED out of PAGEMAP_SCAN -- that set is the
working set; everything else is a reclaim candidate.

UFFDIO_RWPROTECT is the protect/unprotect ioctl, mirroring
UFFDIO_WRITEPROTECT.

UFFDIO_SET_MODE flips RWP_ASYNC <-> sync at runtime under
mmap_write_lock() + vma_start_write(), so a VMM can run in async
mode for detection and switch to sync for race-free reclaim without
re-registering the userfaultfd.

== Typical VMM workflow ==

/* arm */
UFFDIO_API(features = RWP | RWP_ASYNC)
UFFDIO_REGISTER(MODE_RWP)

/* detection cycle (async) */
UFFDIO_RWPROTECT(range, RWP)
sleep(interval)

/* freeze the snapshot before scanning */
UFFDIO_SET_MODE(disable = RWP_ASYNC) /* sync */
PAGEMAP_SCAN(PAGE_IS_ACCESSED) -> hot pages (working set)

/* reclaim everything not in the hot set from the backing file */
fallocate(FALLOC_FL_PUNCH_HOLE, non-hot) /* or pwrite to remote */
UFFDIO_SET_MODE(enable = RWP_ASYNC) /* resume */

== Series layout ==

Patches 1 to 3 are preparatory:

1: decouple protnone helpers from CONFIG_NUMA_BALANCING.
2-3: rename _PAGE_BIT_UFFD_WP, pte_uffd_wp() and friends to drop
the _WP suffix, since the bit now carries WP and RWP meaning
depending on the VMA flag. The SCAN_PTE_UFFD enum's ftrace
output string is intentionally kept as "pte_uffd_wp" so
trace-based tooling does not silently break.

Patch 4 switches the uffd VMA-flag helpers to the vma_flags_t
accessors (vma_test_*_mask), so the VMA_UFFD_* masks are the single
place that knows which modes the build offers.

Patches 5 to 8 add the in-kernel mechanism:

5: VM_UFFD_RWP VMA flag (aliased to VM_NONE until patch 9
introduces CONFIG_USERFAULTFD_RWP together with the UAPI).
6: MM_CP_UFFD_RWP change_protection() primitive (PAGE_NONE +
uffd bit, plus a RESOLVE counterpart).
7: marker preservation across swap, device-exclusive, migration,
fork, mremap, UFFDIO_MOVE, hugetlb copy, and mprotect().
8: handle VM_UFFD_RWP in khugepaged, rmap, and GUP.

Patches 9 to 13 wire the userspace surface:

9: UFFDIO_REGISTER_MODE_RWP and UFFDIO_RWPROTECT plumbing
(introduces CONFIG_USERFAULTFD_RWP).
10: RWP fault delivery and exposure of UFFDIO_REGISTER_MODE_RWP.
11: PAGE_IS_ACCESSED in PAGEMAP_SCAN.
12: UFFD_FEATURE_RWP_ASYNC for async fault resolution.
13: UFFDIO_SET_MODE for runtime sync/async toggle.

Patches 14 and 15 are kernel tests and Documentation/. The matching
man-pages series is already upstream.

The series is also available at:

https://git.kernel.org/pub/scm/linux/kernel/git/kas/linux.git uffd/v9

Kiryl Shutsemau (Meta) (15):
mm: decouple protnone helpers from CONFIG_NUMA_BALANCING
mm: rename uffd-wp PTE bit macros to uffd
mm: rename uffd-wp PTE accessors to uffd
userfaultfd: test uffd VMA flags through the vma_flags_t API
mm: add VM_UFFD_RWP VMA flag
mm: add MM_CP_UFFD_RWP change_protection() flag
mm: preserve RWP marker across PTE rewrites
mm: handle VM_UFFD_RWP in khugepaged, rmap, and GUP
userfaultfd: add UFFDIO_REGISTER_MODE_RWP and UFFDIO_RWPROTECT
plumbing
mm/userfaultfd: add RWP fault delivery and expose
UFFDIO_REGISTER_MODE_RWP
mm/pagemap: add PAGE_IS_ACCESSED for RWP tracking
userfaultfd: add UFFD_FEATURE_RWP_ASYNC for async fault resolution
userfaultfd: add UFFDIO_SET_MODE for runtime sync/async toggle
selftests/mm: add userfaultfd RWP tests
Documentation/userfaultfd: document RWP working set tracking

Documentation/admin-guide/mm/pagemap.rst | 13 +-
Documentation/admin-guide/mm/userfaultfd.rst | 269 +++++-
Documentation/filesystems/proc.rst | 1 +
arch/arm64/Kconfig | 1 +
arch/arm64/include/asm/pgtable-prot.h | 8 +-
arch/arm64/include/asm/pgtable.h | 47 +-
arch/loongarch/Kconfig | 1 +
arch/loongarch/include/asm/pgtable.h | 4 +-
arch/powerpc/include/asm/book3s/64/pgtable.h | 8 +-
arch/powerpc/platforms/Kconfig.cputype | 1 +
arch/riscv/Kconfig | 1 +
arch/riscv/include/asm/pgtable-bits.h | 12 +-
arch/riscv/include/asm/pgtable.h | 59 +-
arch/s390/Kconfig | 1 +
arch/s390/include/asm/hugetlb.h | 12 +-
arch/s390/include/asm/pgtable.h | 4 +-
arch/x86/Kconfig | 1 +
arch/x86/include/asm/pgtable.h | 56 +-
arch/x86/include/asm/pgtable_types.h | 16 +-
fs/proc/task_mmu.c | 98 +-
include/asm-generic/hugetlb.h | 18 +-
include/asm-generic/pgtable_uffd.h | 32 +-
include/linux/huge_mm.h | 7 +
include/linux/leafops.h | 4 +-
include/linux/mm.h | 65 +-
include/linux/mm_inline.h | 4 +-
include/linux/pgtable.h | 32 +-
include/linux/swapops.h | 4 +-
include/linux/userfaultfd_k.h | 100 ++-
include/trace/events/huge_memory.h | 2 +-
include/trace/events/mmflags.h | 7 +
include/uapi/linux/fs.h | 1 +
include/uapi/linux/userfaultfd.h | 54 +-
init/Kconfig | 8 +
mm/Kconfig | 9 +
mm/debug_vm_pgtable.c | 4 +-
mm/huge_memory.c | 170 +++-
mm/hugetlb.c | 159 +++-
mm/internal.h | 4 +-
mm/khugepaged.c | 40 +-
mm/memory.c | 152 +++-
mm/migrate.c | 20 +-
mm/migrate_device.c | 8 +-
mm/mprotect.c | 70 +-
mm/mremap.c | 17 +-
mm/page_table_check.c | 8 +-
mm/rmap.c | 18 +-
mm/swapfile.c | 9 +-
mm/userfaultfd.c | 389 +++++++-
tools/include/uapi/linux/fs.h | 1 +
tools/testing/selftests/mm/uffd-unit-tests.c | 884 +++++++++++++++++++
51 files changed, 2468 insertions(+), 445 deletions(-)


base-commit: dc59e4fea9d83f03bad6bddf3fa2e52491777482
--
2.54.0