mirror of https://lore.kernel.org/lkml/
 help / color / mirror / Atom feed
* [PATCH 00/12] userfaultfd: assorted fixes (part 1)
@ 2026-11-11 17:55 Tal Zussman
  2026-11-11 17:55 ` [PATCH 01/12] userfaultfd: use anonymous memory for UFFDIO_ZEROPAGE on private file mappings Tal Zussman
                   ` (11 more replies)
  0 siblings, 12 replies; 16+ messages in thread
From: Tal Zussman @ 2026-11-11 17:55 UTC (permalink / raw)
  To: Andrew Morton, Mike Rapoport, Peter Xu, James Houghton,
	Andrea Arcangeli, Suren Baghdasaryan, Balbir Singh,
	Liam R. Howlett, Lorenzo Stoakes, Vlastimil Babka, Jann Horn,
	Pedro Falcato, Michał Mirosław, Andrei Vagin,
	Stephen Rothwell, Arnd Bergmann, Muhammad Usama Anjum,
	David Hildenbrand, Zi Yan, Matthew Brost, Joshua Hahn,
	Byungchul Park, Gregory Price, Ying Huang, Alistair Popple,
	Baolin Wang, Nico Pache, Ryan Roberts, Dev Jain, Barry Song,
	Lance Yang, Usama Arif, Kiryl Shutsemau, Chris Down, Muchun Song,
	Oscar Salvador, Mikołaj Lenczewski, Michal Hocko,
	Mike Kravetz
  Cc: linux-mm, linux-kernel, linux-fsdevel, stable, Tal Zussman

This is a first batch of userfaultfd fixes, with more to come.

These were found by a mix of regular old LLM review and by having an LLM
write a TLA+ model of uffd interactions with PTE bits and other state
and seeing what issues fell out.

Most of these fix places where mm code loses uffd-wp or RWP protection
while moving, migrating or zapping an entry. Userspace then either
misses writes and accesses, or gets pages reported that nobody touched.
There are also two hangs (UFFDIO_MOVE and MADV_GUARD_INSTALL), and
UFFDIO_ZEROPAGE modifying a file through a private mapping.

Patches 1-2:   UFFDIO_ZEROPAGE and UFFDIO_MOVE fixes, cc stable
Patches 3-5:   PAGEMAP_SCAN write-protect vs. migration and
               device-private entries
Patch 6:       UFFDIO_MOVE of the huge zero page from an RWP source
Patch 7:       mremap() losing uffd state when a move fails halfway
Patch 8:       migrate_vma replacing a uffd-protected zero page
Patches 9-11:  guard regions vs. uffd-wp
Patch 12:      UFFDIO_UNREGISTER with a misaligned hugetlb end

A few of the fixes change what userspace sees:

 - UFFDIO_ZEROPAGE on a private file mapping no longer fails with
   -EEXIST where the file already has a page, on an mm that cannot use
   the zero page (patch 1)
 - UFFDIO_MOVE returns -EEXIST for any huge destination PMD, not only
   for a present THP (patch 2)
 - MADV_GUARD_INSTALL sends UFFD_EVENT_REMOVE, and can return -ENOMEM
   or -EINVAL if the VMA goes away or changes while the lock is dropped
   (patch 11)
 - UFFDIO_UNREGISTER with a misaligned hugetlb end fails before
   unregistering anything (patch 12)

Patch 6 has little user impact on its own. It is included because,
together with patch 7, it allows removing clear_uffd_wp_pmd().

Patch 7 adds spurious minor faults on shmem and hugetlb after mremap():
when dropping uffd protection there, change_protection() makes writable
pages that were never protected read-only. UFFDIO_UNREGISTER and
UFFDIO_WRITEPROTECT already behave this way and lead to the same
spurious faults. This will be fixed in change_protection() in a separate
series.

Based on mm-new.

---
Tal Zussman (12):
      userfaultfd: use anonymous memory for UFFDIO_ZEROPAGE on private file mappings
      userfaultfd: refuse UFFDIO_MOVE into a non-present huge destination PMD
      fs/proc/task_mmu: downgrade writable migration entries in PAGEMAP_SCAN write-protect
      mm/migrate: keep the uffd bit when restoring a writable migration entry
      fs/proc/task_mmu: write-protect device-private THPs in PAGEMAP_SCAN
      mm/huge_memory: build the moved huge zero PMD from the destination VMA
      mm/mremap: resolve uffd state when the context is dropped, not during the move
      mm/migrate_device: do not migrate a uffd-protected zero page
      mm/madvise: let a guard marker carry the uffd-wp bit
      mm: set the uffd-wp bit on a guard marker when write-protecting
      mm/madvise: send UFFD_EVENT_REMOVE when installing guard regions
      mm/userfaultfd: check hugetlb end alignment in UFFDIO_UNREGISTER pre-scan

 fs/proc/task_mmu.c      | 64 +++++++++++++++++++++++++++++++++++++++++---
 include/linux/leafops.h | 18 +++++++++++++
 mm/huge_memory.c        | 31 +++-------------------
 mm/hugetlb.c            | 27 +------------------
 mm/madvise.c            | 62 +++++++++++++++++++++++++++++++++++--------
 mm/migrate.c            |  2 +-
 mm/migrate_device.c     | 23 ++++++++++++++--
 mm/mprotect.c           | 26 ++++++++++++++++--
 mm/mremap.c             | 46 +-------------------------------
 mm/userfaultfd.c        | 70 ++++++++++++++++++++++++-------------------------
 10 files changed, 215 insertions(+), 154 deletions(-)
---
base-commit: 2dc0136b1fdac153434ab02778fcdb32db5d0dae
change-id: 20261106-uffd-state-fixes-d2111e2abd48

Best regards,
--  
Tal Zussman <tz2294@columbia.edu>


^ permalink raw reply	[flat|nested] 16+ messages in thread

* [PATCH 01/12] userfaultfd: use anonymous memory for UFFDIO_ZEROPAGE on private file mappings
  2026-11-11 17:55 [PATCH 00/12] userfaultfd: assorted fixes (part 1) Tal Zussman
@ 2026-11-11 17:55 ` Tal Zussman
  2026-11-11 17:55 ` [PATCH 02/12] userfaultfd: refuse UFFDIO_MOVE into a non-present huge destination PMD Tal Zussman
                   ` (10 subsequent siblings)
  11 siblings, 0 replies; 16+ messages in thread
From: Tal Zussman @ 2026-11-11 17:55 UTC (permalink / raw)
  To: Andrew Morton, Mike Rapoport, Peter Xu, James Houghton,
	Andrea Arcangeli, Suren Baghdasaryan, Balbir Singh,
	Liam R. Howlett, Lorenzo Stoakes, Vlastimil Babka, Jann Horn,
	Pedro Falcato, Michał Mirosław, Andrei Vagin,
	Stephen Rothwell, Arnd Bergmann, Muhammad Usama Anjum,
	David Hildenbrand, Zi Yan, Matthew Brost, Joshua Hahn,
	Byungchul Park, Gregory Price, Ying Huang, Alistair Popple,
	Baolin Wang, Nico Pache, Ryan Roberts, Dev Jain, Barry Song,
	Lance Yang, Usama Arif, Kiryl Shutsemau, Chris Down, Muchun Song,
	Oscar Salvador, Mikołaj Lenczewski, Michal Hocko,
	Mike Kravetz
  Cc: linux-mm, linux-kernel, linux-fsdevel, stable, Tal Zussman

If the mm cannot map the shared zero page, UFFDIO_ZEROPAGE falls back to
mfill_atomic_pte_zeroed_folio(), which allocates a zeroed folio and
installs it through vma_uffd_ops(), no matter what kind of mapping it
is. An s390 KVM guest with storage keys or protected virtualization is
such an mm.

For a MAP_PRIVATE shmem mapping, that adds the zeroed folio to the page
cache of the file. That is wrong: a private mapping must never modify
the file it maps, and the file might even be mapped from a read-only fd.

The ioctl can also fail where it should succeed. A missing fault is only
delivered for a hole in the file, but UFFDIO_ZEROPAGE can be called on
an address that did not fault, or can race with a write to the file. If
the file has a page at that offset by then, inserting the zeroed folio
fails and the ioctl returns -EEXIST although nothing is mapped at the
destination address.

What we want is what UFFDIO_ZEROPAGE does on a private mapping when the
shared zero page can be used, and what UFFDIO_COPY does: only check that
nothing is mapped at the destination address, map zeroed memory there,
and leave the page cache alone. The zeroed folio path did that before it
was moved to vm_uffd_ops.

To fix it, pick the ops in the common folio path, as
mfill_atomic_pte_copy() did for UFFDIO_COPY: anon_uffd_ops for a private
mapping, the ops of the VMA otherwise. That makes
mfill_atomic_pte_copy() and mfill_atomic_pte_zeroed_folio() redundant,
so remove them and rename __mfill_atomic_pte() to
mfill_atomic_pte_folio().

UFFDIO_ZEROPAGE no longer modifies the file, and only fails with -EEXIST
if something is already mapped at the destination address.

Fixes: f74991b4e383 ("shmem, userfaultfd: implement shmem uffd operations using vm_uffd_ops")
Cc: stable@vger.kernel.org
Signed-off-by: Tal Zussman <tz2294@columbia.edu>
---
 mm/userfaultfd.c | 46 ++++++++++++++++------------------------------
 1 file changed, 16 insertions(+), 30 deletions(-)

diff --git a/mm/userfaultfd.c b/mm/userfaultfd.c
index d65184d9a480..61a712e1f9f5 100644
--- a/mm/userfaultfd.c
+++ b/mm/userfaultfd.c
@@ -531,15 +531,27 @@ static int mfill_copy_folio_retry(struct mfill_state *mfill_state,
 	return 0;
 }
 
-static int __mfill_atomic_pte(struct mfill_state *state,
-			      const struct vm_uffd_ops *ops)
+static int mfill_atomic_pte_folio(struct mfill_state *state)
 {
+	const struct vm_uffd_ops *ops = vma_uffd_ops(state->vma);
 	unsigned long dst_addr = state->dst_addr;
 	unsigned long src_addr = state->src_addr;
 	uffd_flags_t flags = state->flags;
 	struct folio *folio;
 	int ret;
 
+	/*
+	 * The normal page fault path for a MAP_PRIVATE mapping in a
+	 * file-backed VMA will invoke the fault, fill the hole in the file and
+	 * COW it right away. The result generates plain anonymous memory.
+	 * So when we are asked to fill a hole in a MAP_PRIVATE mapping, we'll
+	 * generate anonymous memory directly without actually filling the
+	 * hole. For the MAP_PRIVATE case the robustness check only happens in
+	 * the pagetable (to verify it's still none) and not in the page cache.
+	 */
+	if (!(state->vma->vm_flags & VM_SHARED))
+		ops = &anon_uffd_ops;
+
 	if (!ops) {
 		VM_WARN_ONCE(1, "UFFDIO_COPY for unsupported VMA");
 		return -EOPNOTSUPP;
@@ -597,32 +609,6 @@ static int __mfill_atomic_pte(struct mfill_state *state,
 	return ret;
 }
 
-static int mfill_atomic_pte_copy(struct mfill_state *state)
-{
-	const struct vm_uffd_ops *ops = vma_uffd_ops(state->vma);
-
-	/*
-	 * The normal page fault path for a MAP_PRIVATE mapping in a
-	 * file-backed VMA will invoke the fault, fill the hole in the file and
-	 * COW it right away. The result generates plain anonymous memory.
-	 * So when we are asked to fill a hole in a MAP_PRIVATE mapping, we'll
-	 * generate anonymous memory directly without actually filling the
-	 * hole. For the MAP_PRIVATE case the robustness check only happens in
-	 * the pagetable (to verify it's still none) and not in the page cache.
-	 */
-	if (!(state->vma->vm_flags & VM_SHARED))
-		ops = &anon_uffd_ops;
-
-	return __mfill_atomic_pte(state, ops);
-}
-
-static int mfill_atomic_pte_zeroed_folio(struct mfill_state *state)
-{
-	const struct vm_uffd_ops *ops = vma_uffd_ops(state->vma);
-
-	return __mfill_atomic_pte(state, ops);
-}
-
 static int mfill_atomic_pte_zeropage(struct mfill_state *state)
 {
 	struct vm_area_struct *dst_vma = state->vma;
@@ -634,7 +620,7 @@ static int mfill_atomic_pte_zeropage(struct mfill_state *state)
 
 	if (mm_forbids_zeropage(dst_vma->vm_mm) ||
 	    (dst_vma->vm_flags & VM_SHARED))
-		return mfill_atomic_pte_zeroed_folio(state);
+		return mfill_atomic_pte_folio(state);
 
 	_dst_pte = pte_mkspecial(pfn_pte(zero_pfn(dst_addr),
 					 dst_vma->vm_page_prot));
@@ -927,7 +913,7 @@ static __always_inline ssize_t mfill_atomic_pte(struct mfill_state *state)
 	if (uffd_flags_mode_is(flags, MFILL_ATOMIC_POISON))
 		return mfill_atomic_pte_poison(state);
 	if (uffd_flags_mode_is(flags, MFILL_ATOMIC_COPY))
-		return mfill_atomic_pte_copy(state);
+		return mfill_atomic_pte_folio(state);
 	if (uffd_flags_mode_is(flags, MFILL_ATOMIC_ZEROPAGE))
 		return mfill_atomic_pte_zeropage(state);
 

-- 
2.39.5


^ permalink raw reply	[flat|nested] 16+ messages in thread

* [PATCH 02/12] userfaultfd: refuse UFFDIO_MOVE into a non-present huge destination PMD
  2026-11-11 17:55 [PATCH 00/12] userfaultfd: assorted fixes (part 1) Tal Zussman
  2026-11-11 17:55 ` [PATCH 01/12] userfaultfd: use anonymous memory for UFFDIO_ZEROPAGE on private file mappings Tal Zussman
@ 2026-11-11 17:55 ` Tal Zussman
  2026-11-11 17:55 ` [PATCH 03/12] fs/proc/task_mmu: downgrade writable migration entries in PAGEMAP_SCAN write-protect Tal Zussman
                   ` (9 subsequent siblings)
  11 siblings, 0 replies; 16+ messages in thread
From: Tal Zussman @ 2026-11-11 17:55 UTC (permalink / raw)
  To: Andrew Morton, Mike Rapoport, Peter Xu, James Houghton,
	Andrea Arcangeli, Suren Baghdasaryan, Balbir Singh,
	Liam R. Howlett, Lorenzo Stoakes, Vlastimil Babka, Jann Horn,
	Pedro Falcato, Michał Mirosław, Andrei Vagin,
	Stephen Rothwell, Arnd Bergmann, Muhammad Usama Anjum,
	David Hildenbrand, Zi Yan, Matthew Brost, Joshua Hahn,
	Byungchul Park, Gregory Price, Ying Huang, Alistair Popple,
	Baolin Wang, Nico Pache, Ryan Roberts, Dev Jain, Barry Song,
	Lance Yang, Usama Arif, Kiryl Shutsemau, Chris Down, Muchun Song,
	Oscar Salvador, Mikołaj Lenczewski, Michal Hocko,
	Mike Kravetz
  Cc: linux-mm, linux-kernel, linux-fsdevel, stable, Tal Zussman

move_pages() refuses to move into a destination PMD that already maps a
THP, but checks for that with pmd_trans_huge(), which is only true for a
present THP. A device-private THP or a THP under migration is a
non-present huge PMD leaf and is not refused.

The destination PMD is then treated as if it pointed to a PTE table. It
is not empty, so pte_alloc() leaves it alone, and move_pages_ptes()
fails to map a PTE table from it and returns -EAGAIN. move_pages()
retries on -EAGAIN, with both VMA read locks held. If the source is a
THP, it is split first for nothing.

For a THP under migration, the loop ends once the migration finishes.
A device-private THP can remain in device memory indefinitely, so the
caller of UFFDIO_MOVE hangs until it receives a fatal signal, and
anything in the mm that needs to write-lock one of the VMAs blocks along
with it. This requires a driver that migrates THPs to device memory.

To fix it, check the destination with pmd_is_huge(), which is true for
any huge leaf, present or not. UFFDIO_MOVE now fails with -EEXIST in
these cases, as it already does for a present THP.

Fixes: adef440691ba ("userfaultfd: UFFDIO_MOVE uABI")
Fixes: a30b48bf1b24 ("mm/migrate_device: implement THP migration of zone device pages")
Cc: stable@vger.kernel.org
Signed-off-by: Tal Zussman <tz2294@columbia.edu>
---
 mm/userfaultfd.c | 11 ++++++-----
 1 file changed, 6 insertions(+), 5 deletions(-)

diff --git a/mm/userfaultfd.c b/mm/userfaultfd.c
index 61a712e1f9f5..ad9d243dc64b 100644
--- a/mm/userfaultfd.c
+++ b/mm/userfaultfd.c
@@ -2021,12 +2021,13 @@ static ssize_t move_pages(struct userfaultfd_ctx *ctx, unsigned long dst_start,
 
 		dst_pmdval = pmdp_get_lockless(dst_pmd);
 		/*
-		 * If the dst_pmd is mapped as THP don't override it and just
-		 * be strict. If dst_pmd changes into TPH after this check, the
-		 * move_pages_huge_pmd() will detect the change and retry
-		 * while move_pages_pte() will detect the change and fail.
+		 * If the dst_pmd is a huge leaf, present or not, don't
+		 * override it and just be strict. If dst_pmd changes into a
+		 * leaf after this check, move_pages_huge_pmd() will detect the
+		 * change and retry while move_pages_ptes() will fail to map
+		 * the PTE table and the next iteration will see the leaf.
 		 */
-		if (unlikely(pmd_trans_huge(dst_pmdval))) {
+		if (unlikely(pmd_is_huge(dst_pmdval))) {
 			err = -EEXIST;
 			break;
 		}

-- 
2.39.5


^ permalink raw reply	[flat|nested] 16+ messages in thread

* [PATCH 03/12] fs/proc/task_mmu: downgrade writable migration entries in PAGEMAP_SCAN write-protect
  2026-11-11 17:55 [PATCH 00/12] userfaultfd: assorted fixes (part 1) Tal Zussman
  2026-11-11 17:55 ` [PATCH 01/12] userfaultfd: use anonymous memory for UFFDIO_ZEROPAGE on private file mappings Tal Zussman
  2026-11-11 17:55 ` [PATCH 02/12] userfaultfd: refuse UFFDIO_MOVE into a non-present huge destination PMD Tal Zussman
@ 2026-11-11 17:55 ` Tal Zussman
  2026-11-11 17:55 ` [PATCH 04/12] mm/migrate: keep the uffd bit when restoring a writable migration entry Tal Zussman
                   ` (8 subsequent siblings)
  11 siblings, 0 replies; 16+ messages in thread
From: Tal Zussman @ 2026-11-11 17:55 UTC (permalink / raw)
  To: Andrew Morton, Mike Rapoport, Peter Xu, James Houghton,
	Andrea Arcangeli, Suren Baghdasaryan, Balbir Singh,
	Liam R. Howlett, Lorenzo Stoakes, Vlastimil Babka, Jann Horn,
	Pedro Falcato, Michał Mirosław, Andrei Vagin,
	Stephen Rothwell, Arnd Bergmann, Muhammad Usama Anjum,
	David Hildenbrand, Zi Yan, Matthew Brost, Joshua Hahn,
	Byungchul Park, Gregory Price, Ying Huang, Alistair Popple,
	Baolin Wang, Nico Pache, Ryan Roberts, Dev Jain, Barry Song,
	Lance Yang, Usama Arif, Kiryl Shutsemau, Chris Down, Muchun Song,
	Oscar Salvador, Mikołaj Lenczewski, Michal Hocko,
	Mike Kravetz
  Cc: linux-mm, linux-kernel, linux-fsdevel, Tal Zussman

PAGEMAP_SCAN with PM_SCAN_WP_MATCHING write-protects pages in a VMA
registered for async uffd-wp, and sets the uffd bit on non-present
entries as they are. An entry with the uffd bit set must not be
writable, so that the page is write-protected once it is mapped again.
change_protection() therefore downgrades writable migration and
device-private entries to readable ones first, but PAGEMAP_SCAN does
not.

For a writable migration entry at the PTE level,
remove_migration_pte() restores a writable PTE without the uffd bit. So
if a page is under migration while PAGEMAP_SCAN write-protects it, the
write protection is lost. A user tracking writes with async uffd-wp
sees the page reported as written on the next scan although nothing
wrote to it.

With CONFIG_PAGE_TABLE_CHECK, setting the uffd bit on a writable
non-present PTE or PMD also triggers a warning.

To fix it, downgrade writable migration and device-private entries
before setting the uffd bit, as change_protection() does, at the PTE,
PMD, and hugetlb levels.

Fixes: 52526ca7fdb9 ("fs/proc/task_mmu: implement IOCTL to get and optionally clear info about PTEs")
Signed-off-by: Tal Zussman <tz2294@columbia.edu>
---
 fs/proc/task_mmu.c | 54 +++++++++++++++++++++++++++++++++++++++++++++++++++---
 1 file changed, 51 insertions(+), 3 deletions(-)

diff --git a/fs/proc/task_mmu.c b/fs/proc/task_mmu.c
index 22361607020d..53780a454432 100644
--- a/fs/proc/task_mmu.c
+++ b/fs/proc/task_mmu.c
@@ -2395,6 +2395,38 @@ static unsigned long pagemap_page_category(struct pagemap_scan_private *p,
 	return categories;
 }
 
+/* Downgrade a writable migration or device-private entry to a readable one. */
+static softleaf_t make_readable_softleaf(softleaf_t entry)
+{
+	if (softleaf_is_device_private_write(entry))
+		return make_readable_device_private_entry(swp_offset(entry));
+	if (!softleaf_is_migration_write(entry))
+		return entry;
+	if (folio_test_anon(softleaf_to_folio(entry)))
+		return make_readable_exclusive_migration_entry(swp_offset(entry));
+	return make_readable_migration_entry(swp_offset(entry));
+}
+
+/*
+ * An armed entry must not come back writable, so downgrade a writable
+ * migration or device-private entry before giving it the bit, as
+ * change_softleaf_pte() does.
+ */
+static pte_t set_uffd_wp_swp_pte(pte_t pte)
+{
+	softleaf_t entry = softleaf_from_pte(pte);
+	pte_t newpte;
+
+	if (!softleaf_is_migration_write(entry) &&
+	    !softleaf_is_device_private_write(entry))
+		return pte_swp_mkuffd(pte);
+
+	newpte = softleaf_to_pte(make_readable_softleaf(entry));
+	if (pte_swp_soft_dirty(pte))
+		newpte = pte_swp_mksoft_dirty(newpte);
+	return pte_swp_mkuffd(newpte);
+}
+
 static void make_uffd_wp_pte(struct vm_area_struct *vma,
 			     unsigned long addr, pte_t *pte, pte_t ptent)
 {
@@ -2408,7 +2440,7 @@ static void make_uffd_wp_pte(struct vm_area_struct *vma,
 		set_pte_at(vma->vm_mm, addr, pte,
 			   make_pte_marker(PTE_MARKER_UFFD_WP));
 	} else {
-		ptent = pte_swp_mkuffd(ptent);
+		ptent = set_uffd_wp_swp_pte(ptent);
 		set_pte_at(vma->vm_mm, addr, pte, ptent);
 	}
 }
@@ -2467,6 +2499,22 @@ static unsigned long pagemap_thp_category(struct pagemap_scan_private *p,
 	return categories;
 }
 
+/* Same as set_uffd_wp_swp_pte(), for a non-present PMD. */
+static pmd_t set_uffd_wp_swp_pmd(pmd_t pmd)
+{
+	softleaf_t entry = softleaf_from_pmd(pmd);
+	pmd_t newpmd;
+
+	if (!softleaf_is_migration_write(entry) &&
+	    !softleaf_is_device_private_write(entry))
+		return pmd_swp_mkuffd(pmd);
+
+	newpmd = softleaf_to_pmd(make_readable_softleaf(entry));
+	if (pmd_swp_soft_dirty(pmd))
+		newpmd = pmd_swp_mksoft_dirty(newpmd);
+	return pmd_swp_mkuffd(newpmd);
+}
+
 static void make_uffd_wp_pmd(struct vm_area_struct *vma,
 			     unsigned long addr, pmd_t *pmdp)
 {
@@ -2477,7 +2525,7 @@ static void make_uffd_wp_pmd(struct vm_area_struct *vma,
 		pmd = pmd_mkuffd(old);
 		set_pmd_at(vma->vm_mm, addr, pmdp, pmd);
 	} else if (pmd_is_migration_entry(pmd)) {
-		pmd = pmd_swp_mkuffd(pmd);
+		pmd = set_uffd_wp_swp_pmd(pmd);
 		set_pmd_at(vma->vm_mm, addr, pmdp, pmd);
 	}
 }
@@ -2547,7 +2595,7 @@ static void make_uffd_wp_huge_pte(struct vm_area_struct *vma,
 
 	if (softleaf_is_migration(entry)) {
 		set_huge_pte_at(vma->vm_mm, addr, ptep,
-				pte_swp_mkuffd(ptent), psize);
+				set_uffd_wp_swp_pte(ptent), psize);
 	} else {
 		pte_t old_pte, new_pte;
 

-- 
2.39.5


^ permalink raw reply	[flat|nested] 16+ messages in thread

* [PATCH 04/12] mm/migrate: keep the uffd bit when restoring a writable migration entry
  2026-11-11 17:55 [PATCH 00/12] userfaultfd: assorted fixes (part 1) Tal Zussman
                   ` (2 preceding siblings ...)
  2026-11-11 17:55 ` [PATCH 03/12] fs/proc/task_mmu: downgrade writable migration entries in PAGEMAP_SCAN write-protect Tal Zussman
@ 2026-11-11 17:55 ` Tal Zussman
  2026-11-11 17:55 ` [PATCH 05/12] fs/proc/task_mmu: write-protect device-private THPs in PAGEMAP_SCAN Tal Zussman
                   ` (7 subsequent siblings)
  11 siblings, 0 replies; 16+ messages in thread
From: Tal Zussman @ 2026-11-11 17:55 UTC (permalink / raw)
  To: Andrew Morton, Mike Rapoport, Peter Xu, James Houghton,
	Andrea Arcangeli, Suren Baghdasaryan, Balbir Singh,
	Liam R. Howlett, Lorenzo Stoakes, Vlastimil Babka, Jann Horn,
	Pedro Falcato, Michał Mirosław, Andrei Vagin,
	Stephen Rothwell, Arnd Bergmann, Muhammad Usama Anjum,
	David Hildenbrand, Zi Yan, Matthew Brost, Joshua Hahn,
	Byungchul Park, Gregory Price, Ying Huang, Alistair Popple,
	Baolin Wang, Nico Pache, Ryan Roberts, Dev Jain, Barry Song,
	Lance Yang, Usama Arif, Kiryl Shutsemau, Chris Down, Muchun Song,
	Oscar Salvador, Mikołaj Lenczewski, Michal Hocko,
	Mike Kravetz
  Cc: linux-mm, linux-kernel, linux-fsdevel, Tal Zussman

remove_migration_pte() makes the restored PTE writable for a writable
migration entry, and only looks at the uffd bit of the entry otherwise.
A writable migration entry that has the uffd bit set is therefore
restored as a writable PTE without the uffd bit, and the page is no
longer write-protected.

An entry with the uffd bit set must be restored write-protected, whether
the entry is writable or not. remove_migration_pmd() gets this right: it
makes the PMD writable and sets the uffd bit as two independent steps,
and pmd_mkuffd() write-protects the result. To fix it, do the same at
the PTE level, where pte_mkuffd() write-protects the PTE as well.

The only known path that created such entries is PAGEMAP_SCAN. Now that
PAGEMAP_SCAN downgrades them like change_protection() does, we should no
longer see them, so this change alone should not be visible to
userspace. If another path ever creates a writable migration entry with
the uffd bit set, the page now stays write-protected instead of losing
the uffd bit.

Fixes: 52526ca7fdb9 ("fs/proc/task_mmu: implement IOCTL to get and optionally clear info about PTEs")
Signed-off-by: Tal Zussman <tz2294@columbia.edu>
---
 mm/migrate.c | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/mm/migrate.c b/mm/migrate.c
index 28a8d7734735..d200f64b5f98 100644
--- a/mm/migrate.c
+++ b/mm/migrate.c
@@ -399,7 +399,7 @@ static bool remove_migration_pte(struct folio *folio,
 
 		if (softleaf_is_migration_write(entry))
 			pte = pte_mkwrite(pte, vma);
-		else if (pte_swp_uffd(old_pte))
+		if (pte_swp_uffd(old_pte))
 			pte = pte_mkuffd(pte);
 
 		/* See do_swap_page(): restore PAGE_NONE for RWP */

-- 
2.39.5


^ permalink raw reply	[flat|nested] 16+ messages in thread

* [PATCH 05/12] fs/proc/task_mmu: write-protect device-private THPs in PAGEMAP_SCAN
  2026-11-11 17:55 [PATCH 00/12] userfaultfd: assorted fixes (part 1) Tal Zussman
                   ` (3 preceding siblings ...)
  2026-11-11 17:55 ` [PATCH 04/12] mm/migrate: keep the uffd bit when restoring a writable migration entry Tal Zussman
@ 2026-11-11 17:55 ` Tal Zussman
  2026-11-11 17:55 ` [PATCH 06/12] mm/huge_memory: build the moved huge zero PMD from the destination VMA Tal Zussman
                   ` (6 subsequent siblings)
  11 siblings, 0 replies; 16+ messages in thread
From: Tal Zussman @ 2026-11-11 17:55 UTC (permalink / raw)
  To: Andrew Morton, Mike Rapoport, Peter Xu, James Houghton,
	Andrea Arcangeli, Suren Baghdasaryan, Balbir Singh,
	Liam R. Howlett, Lorenzo Stoakes, Vlastimil Babka, Jann Horn,
	Pedro Falcato, Michał Mirosław, Andrei Vagin,
	Stephen Rothwell, Arnd Bergmann, Muhammad Usama Anjum,
	David Hildenbrand, Zi Yan, Matthew Brost, Joshua Hahn,
	Byungchul Park, Gregory Price, Ying Huang, Alistair Popple,
	Baolin Wang, Nico Pache, Ryan Roberts, Dev Jain, Barry Song,
	Lance Yang, Usama Arif, Kiryl Shutsemau, Chris Down, Muchun Song,
	Oscar Salvador, Mikołaj Lenczewski, Michal Hocko,
	Mike Kravetz
  Cc: linux-mm, linux-kernel, linux-fsdevel, Tal Zussman

PAGEMAP_SCAN with PM_SCAN_WP_MATCHING write-protects a huge PMD through
make_uffd_wp_pmd(), which handles a present THP and a migration entry
but does nothing for a device-private THP. The PMD never gets the uffd
bit.

pagemap_thp_category() reports a non-present PMD without the uffd bit as
written. A user tracking writes with async uffd-wp therefore sees every
page of a device-private THP as written on every scan, whether it was
written to or not. This requires a driver that migrates THPs to device
memory.

A device-private PMD must be write-protected like any other huge PMD. To
fix it, pass it to set_uffd_wp_swp_pmd() as well, which downgrades a
writable entry and sets the uffd bit. When the THP migrates back,
set_pmd_migration_entry() carries the uffd bit over to the migration
entry, so the write protection is kept.

Fixes: a30b48bf1b24 ("mm/migrate_device: implement THP migration of zone device pages")
Signed-off-by: Tal Zussman <tz2294@columbia.edu>
---
 fs/proc/task_mmu.c | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/fs/proc/task_mmu.c b/fs/proc/task_mmu.c
index 53780a454432..752f1b13d47a 100644
--- a/fs/proc/task_mmu.c
+++ b/fs/proc/task_mmu.c
@@ -2524,7 +2524,8 @@ static void make_uffd_wp_pmd(struct vm_area_struct *vma,
 		old = pmdp_invalidate_ad(vma, addr, pmdp);
 		pmd = pmd_mkuffd(old);
 		set_pmd_at(vma->vm_mm, addr, pmdp, pmd);
-	} else if (pmd_is_migration_entry(pmd)) {
+	} else if (pmd_is_migration_entry(pmd) ||
+		   pmd_is_device_private_entry(pmd)) {
 		pmd = set_uffd_wp_swp_pmd(pmd);
 		set_pmd_at(vma->vm_mm, addr, pmdp, pmd);
 	}

-- 
2.39.5


^ permalink raw reply	[flat|nested] 16+ messages in thread

* [PATCH 06/12] mm/huge_memory: build the moved huge zero PMD from the destination VMA
  2026-11-11 17:55 [PATCH 00/12] userfaultfd: assorted fixes (part 1) Tal Zussman
                   ` (4 preceding siblings ...)
  2026-11-11 17:55 ` [PATCH 05/12] fs/proc/task_mmu: write-protect device-private THPs in PAGEMAP_SCAN Tal Zussman
@ 2026-11-11 17:55 ` Tal Zussman
  2026-11-11 19:12   ` Kiryl Shutsemau
  2026-11-11 17:55 ` [PATCH 07/12] mm/mremap: resolve uffd state when the context is dropped, not during the move Tal Zussman
                   ` (5 subsequent siblings)
  11 siblings, 1 reply; 16+ messages in thread
From: Tal Zussman @ 2026-11-11 17:55 UTC (permalink / raw)
  To: Andrew Morton, Mike Rapoport, Peter Xu, James Houghton,
	Andrea Arcangeli, Suren Baghdasaryan, Balbir Singh,
	Liam R. Howlett, Lorenzo Stoakes, Vlastimil Babka, Jann Horn,
	Pedro Falcato, Michał Mirosław, Andrei Vagin,
	Stephen Rothwell, Arnd Bergmann, Muhammad Usama Anjum,
	David Hildenbrand, Zi Yan, Matthew Brost, Joshua Hahn,
	Byungchul Park, Gregory Price, Ying Huang, Alistair Popple,
	Baolin Wang, Nico Pache, Ryan Roberts, Dev Jain, Barry Song,
	Lance Yang, Usama Arif, Kiryl Shutsemau, Chris Down, Muchun Song,
	Oscar Salvador, Mikołaj Lenczewski, Michal Hocko,
	Mike Kravetz
  Cc: linux-mm, linux-kernel, linux-fsdevel, Tal Zussman

move_pages_huge_pmd() builds the destination PMD for the huge zero page
by copying the source PMD and clearing its uffd bit, so the destination
keeps the protection of the source.

A source VMA registered for RWP can map the huge zero page with a
PAGE_NONE PMD, and UFFDIO_MOVE does not check how the source is
registered. The destination then gets a PAGE_NONE PMD without the uffd
bit, although nothing asked for that in the destination VMA.

The next access fixes it up through the NUMA hinting fault path. Until
then, the first access to the destination takes a spurious fault and GUP
has to take the fault path. The impact on userspace is small.

The moved entry must have the protection of the destination VMA. To fix
it, build the entry from the vm_page_prot of the destination VMA, as
move_zeropage_pte() does for the small zero page, and keep marking it
soft-dirty. A destination VMA registered for RWP still gets its PMD
re-armed afterwards.

Fixes: 763f43865fc4 ("mm: preserve RWP marker across PTE rewrites")
Signed-off-by: Tal Zussman <tz2294@columbia.edu>
---
 mm/huge_memory.c | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/mm/huge_memory.c b/mm/huge_memory.c
index 0d589be136d2..09176b290f76 100644
--- a/mm/huge_memory.c
+++ b/mm/huge_memory.c
@@ -2981,9 +2981,9 @@ int move_pages_huge_pmd(struct mm_struct *mm, pmd_t *dst_pmd, pmd_t *src_pmd, pm
 		/* Follow mremap() behavior and treat the entry dirty after the move */
 		_dst_pmd = pmd_mkwrite(pmd_mkdirty(_dst_pmd), dst_vma);
 	} else {
-		src_pmdval = pmdp_huge_clear_flush(src_vma, src_addr, src_pmd);
-		_dst_pmd = move_soft_dirty_pmd(src_pmdval);
-		_dst_pmd = clear_uffd_wp_pmd(_dst_pmd);
+		pmdp_huge_clear_flush(src_vma, src_addr, src_pmd);
+		_dst_pmd = folio_mk_pmd(huge_zero_folio, dst_vma->vm_page_prot);
+		_dst_pmd = move_soft_dirty_pmd(pmd_mkspecial(_dst_pmd));
 	}
 
 	/* Re-arm RWP on the moved PMD if dst_vma is RWP-registered. */

-- 
2.39.5


^ permalink raw reply	[flat|nested] 16+ messages in thread

* [PATCH 07/12] mm/mremap: resolve uffd state when the context is dropped, not during the move
  2026-11-11 17:55 [PATCH 00/12] userfaultfd: assorted fixes (part 1) Tal Zussman
                   ` (5 preceding siblings ...)
  2026-11-11 17:55 ` [PATCH 06/12] mm/huge_memory: build the moved huge zero PMD from the destination VMA Tal Zussman
@ 2026-11-11 17:55 ` Tal Zussman
  2026-11-11 19:20   ` Kiryl Shutsemau
  2026-11-11 17:55 ` [PATCH 08/12] mm/migrate_device: do not migrate a uffd-protected zero page Tal Zussman
                   ` (4 subsequent siblings)
  11 siblings, 1 reply; 16+ messages in thread
From: Tal Zussman @ 2026-11-11 17:55 UTC (permalink / raw)
  To: Andrew Morton, Mike Rapoport, Peter Xu, James Houghton,
	Andrea Arcangeli, Suren Baghdasaryan, Balbir Singh,
	Liam R. Howlett, Lorenzo Stoakes, Vlastimil Babka, Jann Horn,
	Pedro Falcato, Michał Mirosław, Andrei Vagin,
	Stephen Rothwell, Arnd Bergmann, Muhammad Usama Anjum,
	David Hildenbrand, Zi Yan, Matthew Brost, Joshua Hahn,
	Byungchul Park, Gregory Price, Ying Huang, Alistair Popple,
	Baolin Wang, Nico Pache, Ryan Roberts, Dev Jain, Barry Song,
	Lance Yang, Usama Arif, Kiryl Shutsemau, Chris Down, Muchun Song,
	Oscar Salvador, Mikołaj Lenczewski, Michal Hocko,
	Mike Kravetz
  Cc: linux-mm, linux-kernel, linux-fsdevel, Tal Zussman

Without UFFD_FEATURE_EVENT_REMAP, the new VMA of an mremap() is not
registered with userfaultfd: mremap_userfaultfd_prep() drops the context
from it once the move succeeded. The uffd-wp bits, uffd-wp markers, and
RWP PAGE_NONE entries in the new VMA must be cleared as well.

However, move_ptes(), move_huge_pmd(), and move_huge_pte() clear them
from each entry while moving it, before we know whether the move will
succeed. If a page table allocation fails halfway through, mremap()
moves the entries back into the old VMA, which is still registered,
without their uffd state.

Writes to a WP range are then no longer reported, and an RWP range
becomes accessible without a fault. Nothing restores the protection
afterwards. This requires a page table allocation to fail in the middle
of mremap(), so it is rare.

The uffd state must only be cleared once the move can no longer fail. To
fix it, leave the entries alone during the move and clear the uffd state
of the new VMA in mremap_userfaultfd_prep(), right before dropping the
context, using userfaultfd_clear_ptes() like unregistering does. A
failed move then puts the entries back as they were.

This adds a second walk over the page tables of the new VMA, for VMAs
registered for WP or RWP without UFFD_FEATURE_EVENT_REMAP.

Like unregistering, userfaultfd_clear_ptes() also asks for the PTEs to
be made writable again where that is safe (MM_CP_TRY_CHANGE_WRITABLE).
The old code only cleared the uffd bit and left the PTE read-only. For
private anonymous memory this saves the minor fault on the first write
to each page that was protected.

However, this adds spurious minor faults on shmem and hugetlb. When
resolving, change_protection() rewrites every present entry in the range
with vma->vm_page_prot, so a page that was writable and not protected is
made read-only, and its first write after mremap() faults.
UFFDIO_UNREGISTER and UFFDIO_WRITEPROTECT already behave this way and
lead to the same spurious faults. This needs to be fixed in
change_protection() and will be done in a separate change.

The entries no longer need per-entry handling during the move, so drop
uffd_supports_page_table_move(). Where the old and new addresses are
suitably aligned, move_normal_pmd() and move_normal_pud() can move whole
page tables of these VMAs again.

clear_uffd_wp_pmd() has no users left, so remove it.

Fixes: 0cef0bb836e3 ("mm: clear uffd-wp PTE/PMD state on mremap()")
Fixes: 763f43865fc4 ("mm: preserve RWP marker across PTE rewrites")
Signed-off-by: Tal Zussman <tz2294@columbia.edu>
---
 mm/huge_memory.c | 25 -------------------------
 mm/hugetlb.c     | 27 +--------------------------
 mm/mremap.c      | 46 +---------------------------------------------
 mm/userfaultfd.c |  1 +
 4 files changed, 3 insertions(+), 96 deletions(-)

diff --git a/mm/huge_memory.c b/mm/huge_memory.c
index 09176b290f76..468a49041b95 100644
--- a/mm/huge_memory.c
+++ b/mm/huge_memory.c
@@ -2616,18 +2616,6 @@ static pmd_t move_soft_dirty_pmd(pmd_t pmd)
 	return pmd;
 }
 
-static pmd_t clear_uffd_wp_pmd(pmd_t pmd)
-{
-	if (pmd_none(pmd))
-		return pmd;
-	if (pmd_present(pmd))
-		pmd = pmd_clear_uffd(pmd);
-	else
-		pmd = pmd_swp_clear_uffd(pmd);
-
-	return pmd;
-}
-
 bool move_huge_pmd(struct vm_area_struct *vma, unsigned long old_addr,
 		  unsigned long new_addr, pmd_t *old_pmd, pmd_t *new_pmd)
 {
@@ -2663,19 +2651,6 @@ bool move_huge_pmd(struct vm_area_struct *vma, unsigned long old_addr,
 			pgtable_trans_huge_deposit(mm, new_pmd, pgtable);
 		}
 		pmd = move_soft_dirty_pmd(pmd);
-		if (vma_has_uffd_without_event_remap(vma)) {
-			/*
-			 * See __copy_present_ptes(): normalise the RWP marker
-			 * so the destination starts accessible instead of
-			 * taking a numa-hinting fault on first access. Only the
-			 * marker (protnone + uffd) needs it; leave other present
-			 * PMDs in the VMA untouched.
-			 */
-			if (pmd_present(pmd) && userfaultfd_rwp(vma) &&
-			    pmd_uffd(pmd))
-				pmd = pmd_modify(pmd, vma->vm_page_prot);
-			pmd = clear_uffd_wp_pmd(pmd);
-		}
 		set_pmd_at(mm, new_addr, new_pmd, pmd);
 		if (force_flush)
 			flush_pmd_tlb_range(vma, old_addr, old_addr + PMD_SIZE);
diff --git a/mm/hugetlb.c b/mm/hugetlb.c
index cb2de24affdc..97bd29925476 100644
--- a/mm/hugetlb.c
+++ b/mm/hugetlb.c
@@ -5168,7 +5168,6 @@ static void move_huge_pte(struct vm_area_struct *vma, unsigned long old_addr,
 			  unsigned long new_addr, pte_t *src_pte, pte_t *dst_pte,
 			  unsigned long sz)
 {
-	bool need_clear_uffd_wp = vma_has_uffd_without_event_remap(vma);
 	struct hstate *h = hstate_vma(vma);
 	struct mm_struct *mm = vma->vm_mm;
 	spinlock_t *src_ptl, *dst_ptl;
@@ -5179,31 +5178,7 @@ static void move_huge_pte(struct vm_area_struct *vma, unsigned long old_addr,
 	double_pt_lock(dst_ptl, src_ptl);
 
 	pte = huge_ptep_get_and_clear(mm, old_addr, src_pte, sz);
-
-	if (need_clear_uffd_wp && pte_is_uffd_wp_marker(pte)) {
-		huge_pte_clear(mm, new_addr, dst_pte, sz);
-	} else {
-		if (need_clear_uffd_wp) {
-			if (pte_present(pte)) {
-				/*
-				 * See __copy_present_ptes(): normalise the RWP
-				 * marker so the destination starts accessible
-				 * instead of taking a numa-hinting fault on
-				 * first access. Only the marker (protnone + uffd)
-				 * needs it; leave other present PTEs untouched.
-				 */
-				if (userfaultfd_rwp(vma) && huge_pte_uffd(pte)) {
-					pte = huge_pte_modify(pte, vma->vm_page_prot);
-					pte = arch_make_huge_pte(pte, huge_page_shift(h),
-								 vma->vm_flags);
-				}
-				pte = huge_pte_clear_uffd(pte);
-			} else {
-				pte = pte_swp_clear_uffd(pte);
-			}
-		}
-		set_huge_pte_at(mm, new_addr, dst_pte, pte, sz);
-	}
+	set_huge_pte_at(mm, new_addr, dst_pte, pte, sz);
 
 	double_pt_unlock(dst_ptl, src_ptl);
 }
diff --git a/mm/mremap.c b/mm/mremap.c
index fff0465f5e06..4564416a6994 100644
--- a/mm/mremap.c
+++ b/mm/mremap.c
@@ -198,7 +198,6 @@ static int move_ptes(struct pagetable_move_control *pmc,
 		unsigned long extent, pmd_t *old_pmd, pmd_t *new_pmd)
 {
 	struct vm_area_struct *vma = pmc->old;
-	bool need_clear_uffd_wp = vma_has_uffd_without_event_remap(vma);
 	struct mm_struct *mm = vma->vm_mm;
 	pte_t *old_ptep, *new_ptep;
 	pte_t old_pte, pte;
@@ -290,27 +289,7 @@ static int move_ptes(struct pagetable_move_control *pmc,
 		pte = get_and_clear_ptes(mm, old_addr, old_ptep, nr_ptes);
 		pte = move_pte(pte, old_addr, new_addr);
 		pte = move_soft_dirty_pte(pte);
-
-		if (need_clear_uffd_wp && pte_is_uffd_wp_marker(pte))
-			pte_clear(mm, new_addr, new_ptep);
-		else {
-			if (need_clear_uffd_wp) {
-				if (pte_present(pte)) {
-					/*
-					 * See __copy_present_ptes(): normalise
-					 * RWP PTEs so the destination starts
-					 * accessible instead of taking a
-					 * numa-hinting fault on first access.
-					 */
-					if (userfaultfd_rwp(vma) && pte_uffd(pte))
-						pte = pte_modify(pte, vma->vm_page_prot);
-					pte = pte_clear_uffd(pte);
-				} else {
-					pte = pte_swp_clear_uffd(pte);
-				}
-			}
-			set_ptes(mm, new_addr, new_ptep, pte, nr_ptes);
-		}
+		set_ptes(mm, new_addr, new_ptep, pte, nr_ptes);
 	}
 
 	lazy_mmu_mode_disable();
@@ -334,25 +313,6 @@ static inline bool arch_supports_page_table_move(void)
 }
 #endif
 
-static inline bool uffd_supports_page_table_move(struct pagetable_move_control *pmc)
-{
-	/*
-	 * If we are moving a VMA that has uffd-wp registered but with
-	 * remap events disabled (new VMA will not be registered with uffd), we
-	 * need to ensure that the uffd-wp state is cleared from all pgtables.
-	 * This means recursing into lower page tables in move_page_tables().
-	 *
-	 * We might get called with VMAs reversed when recovering from a
-	 * failed page table move. In that case, the
-	 * "old"-but-actually-"originally new" VMA during recovery will not have
-	 * a uffd context. Recursing into lower page tables during the original
-	 * move but not during the recovery move will cause trouble, because we
-	 * run into already-existing page tables. So check both VMAs.
-	 */
-	return !vma_has_uffd_without_event_remap(pmc->old) &&
-	       !vma_has_uffd_without_event_remap(pmc->new);
-}
-
 #ifdef CONFIG_HAVE_MOVE_PMD
 static bool move_normal_pmd(struct pagetable_move_control *pmc,
 			pmd_t *old_pmd, pmd_t *new_pmd)
@@ -365,8 +325,6 @@ static bool move_normal_pmd(struct pagetable_move_control *pmc,
 
 	if (!arch_supports_page_table_move())
 		return false;
-	if (!uffd_supports_page_table_move(pmc))
-		return false;
 	/*
 	 * The destination pmd shouldn't be established, free_pgtables()
 	 * should have released it.
@@ -434,8 +392,6 @@ static bool move_normal_pud(struct pagetable_move_control *pmc,
 
 	if (!arch_supports_page_table_move())
 		return false;
-	if (!uffd_supports_page_table_move(pmc))
-		return false;
 	/*
 	 * The destination pud shouldn't be established, free_pgtables()
 	 * should have released it.
diff --git a/mm/userfaultfd.c b/mm/userfaultfd.c
index ad9d243dc64b..bf42fabb5afd 100644
--- a/mm/userfaultfd.c
+++ b/mm/userfaultfd.c
@@ -3161,6 +3161,7 @@ void mremap_userfaultfd_prep(struct vm_area_struct *vma,
 		up_write(&ctx->map_changing_lock);
 	} else {
 		/* Drop uffd context if remap feature not enabled */
+		userfaultfd_clear_ptes(vma, vma->vm_start, vma->vm_end);
 		userfaultfd_reset_ctx(vma);
 	}
 }

-- 
2.39.5


^ permalink raw reply	[flat|nested] 16+ messages in thread

* [PATCH 08/12] mm/migrate_device: do not migrate a uffd-protected zero page
  2026-11-11 17:55 [PATCH 00/12] userfaultfd: assorted fixes (part 1) Tal Zussman
                   ` (6 preceding siblings ...)
  2026-11-11 17:55 ` [PATCH 07/12] mm/mremap: resolve uffd state when the context is dropped, not during the move Tal Zussman
@ 2026-11-11 17:55 ` Tal Zussman
  2026-11-11 17:55 ` [PATCH 09/12] mm/madvise: let a guard marker carry the uffd-wp bit Tal Zussman
                   ` (3 subsequent siblings)
  11 siblings, 0 replies; 16+ messages in thread
From: Tal Zussman @ 2026-11-11 17:55 UTC (permalink / raw)
  To: Andrew Morton, Mike Rapoport, Peter Xu, James Houghton,
	Andrea Arcangeli, Suren Baghdasaryan, Balbir Singh,
	Liam R. Howlett, Lorenzo Stoakes, Vlastimil Babka, Jann Horn,
	Pedro Falcato, Michał Mirosław, Andrei Vagin,
	Stephen Rothwell, Arnd Bergmann, Muhammad Usama Anjum,
	David Hildenbrand, Zi Yan, Matthew Brost, Joshua Hahn,
	Byungchul Park, Gregory Price, Ying Huang, Alistair Popple,
	Baolin Wang, Nico Pache, Ryan Roberts, Dev Jain, Barry Song,
	Lance Yang, Usama Arif, Kiryl Shutsemau, Chris Down, Muchun Song,
	Oscar Salvador, Mikołaj Lenczewski, Michal Hocko,
	Mike Kravetz
  Cc: linux-mm, linux-kernel, linux-fsdevel, Tal Zussman

migrate_vma_setup() installs a migration entry for each page it
collects, which carries the uffd bit over to the new PTE. However, a PTE
that maps the zero page is left in place without a migration entry.
migrate_vma_insert_page() later overwrites it with a new PTE that is
built only from the VMA flags, so the uffd-wp bit or the RWP PAGE_NONE
protection of the old PTE is lost. The same applies to the huge zero
PMD.

Once a driver has migrated such a zero page, writes and accesses to it
no longer fault into userfaultfd, so the manager misses them. With async
tracking, PAGEMAP_SCAN unconditionally reports the page as written or
accessed.

This requires a driver that migrates memory to a device.

To fix it, do not mark a zero page for migration if its entry has the
uffd bit set in a VMA registered for WP or RWP. The driver sees an
address that was not migrated, which drivers already have to deal with.

The uffd bit can also be set after the zero page was collected, as
write-protecting only takes the mmap lock for read. So check again under
the page table lock in migrate_vma_insert_page() and
migrate_vma_insert_huge_pmd_page().

The alternative would be to carry the uffd state over in
migrate_vma_insert_page() and migrate_vma_insert_huge_pmd_page(). Not
migrating the zero page is simpler, and a zero page that is protected by
userfaultfd in a range that a driver migrates should be rare.

Fixes: 292924b26024 ("userfaultfd: wp: apply _PAGE_UFFD_WP bit")
Fixes: a30b48bf1b24 ("mm/migrate_device: implement THP migration of zone device pages")
Signed-off-by: Tal Zussman <tz2294@columbia.edu>
---
 mm/migrate_device.c | 23 +++++++++++++++++++++--
 1 file changed, 21 insertions(+), 2 deletions(-)

diff --git a/mm/migrate_device.c b/mm/migrate_device.c
index 7919e04abc1c..ace216ea834c 100644
--- a/mm/migrate_device.c
+++ b/mm/migrate_device.c
@@ -159,7 +159,16 @@ static int migrate_vma_collect_huge_pmd(pmd_t *pmdp, unsigned long start,
 
 		folio = pmd_folio(*pmdp);
 		if (is_huge_zero_folio(folio)) {
+			/*
+			 * Nothing carries the uffd state of the zero page
+			 * over, so skip it if its uffd bit is set.
+			 */
+			bool protected = userfaultfd_protected(walk->vma) &&
+					 pmd_uffd(*pmdp);
+
 			spin_unlock(ptl);
+			if (protected)
+				return migrate_vma_collect_skip(start, end, walk);
 			return migrate_vma_collect_hole(start, end, -1, walk);
 		}
 		if (pmd_write(*pmdp))
@@ -337,6 +346,9 @@ static int migrate_vma_collect_pmd(pmd_t *pmdp,
 			pfn = pte_pfn(pte);
 			if (is_zero_pfn(pfn) &&
 			    (migrate->flags & MIGRATE_VMA_SELECT_SYSTEM)) {
+				/* See migrate_vma_collect_huge_pmd(). */
+				if (userfaultfd_protected(vma) && pte_uffd(pte))
+					goto next;
 				mpfn = MIGRATE_PFN_MIGRATE;
 				migrate->cpages++;
 				goto next;
@@ -878,10 +890,14 @@ static int migrate_vma_insert_huge_pmd_page(struct migrate_vma *migrate,
 	if (userfaultfd_missing(vma))
 		goto unlock_abort;
 
-	if (is_huge_zero_pmd(*pmdp))
+	if (is_huge_zero_pmd(*pmdp)) {
+		/* The uffd bit can have been set since the collection. */
+		if (userfaultfd_protected(vma) && pmd_uffd(*pmdp))
+			goto unlock_abort;
 		flush = true;
-	else if (!pmd_none(*pmdp))
+	} else if (!pmd_none(*pmdp)) {
 		goto unlock_abort;
+	}
 
 	add_mm_counter(vma->vm_mm, MM_ANONPAGES, HPAGE_PMD_NR);
 	folio_add_new_anon_rmap(folio, vma, addr, RMAP_EXCLUSIVE);
@@ -1081,6 +1097,9 @@ static void migrate_vma_insert_page(struct migrate_vma *migrate,
 
 		if (!is_zero_pfn(pfn))
 			goto unlock_abort;
+		/* The uffd bit can have been set since the collection. */
+		if (userfaultfd_protected(vma) && pte_uffd(orig_pte))
+			goto unlock_abort;
 		flush = true;
 	} else if (!pte_none(orig_pte))
 		goto unlock_abort;

-- 
2.39.5


^ permalink raw reply	[flat|nested] 16+ messages in thread

* [PATCH 09/12] mm/madvise: let a guard marker carry the uffd-wp bit
  2026-11-11 17:55 [PATCH 00/12] userfaultfd: assorted fixes (part 1) Tal Zussman
                   ` (7 preceding siblings ...)
  2026-11-11 17:55 ` [PATCH 08/12] mm/migrate_device: do not migrate a uffd-protected zero page Tal Zussman
@ 2026-11-11 17:55 ` Tal Zussman
  2026-11-11 17:55 ` [PATCH 10/12] mm: set the uffd-wp bit on a guard marker when write-protecting Tal Zussman
                   ` (2 subsequent siblings)
  11 siblings, 0 replies; 16+ messages in thread
From: Tal Zussman @ 2026-11-11 17:55 UTC (permalink / raw)
  To: Andrew Morton, Mike Rapoport, Peter Xu, James Houghton,
	Andrea Arcangeli, Suren Baghdasaryan, Balbir Singh,
	Liam R. Howlett, Lorenzo Stoakes, Vlastimil Babka, Jann Horn,
	Pedro Falcato, Michał Mirosław, Andrei Vagin,
	Stephen Rothwell, Arnd Bergmann, Muhammad Usama Anjum,
	David Hildenbrand, Zi Yan, Matthew Brost, Joshua Hahn,
	Byungchul Park, Gregory Price, Ying Huang, Alistair Popple,
	Baolin Wang, Nico Pache, Ryan Roberts, Dev Jain, Barry Song,
	Lance Yang, Usama Arif, Kiryl Shutsemau, Chris Down, Muchun Song,
	Oscar Salvador, Mikołaj Lenczewski, Michal Hocko,
	Mike Kravetz
  Cc: linux-mm, linux-kernel, linux-fsdevel, Tal Zussman

MADV_GUARD_INSTALL can only install guard markers in a range without
populated entries. It zaps the range and retries, restarting the syscall
if the range still isn't clear. On a file-backed VMA registered for
uffd-wp, the zap keeps uffd-wp markers and converts write-protected PTEs
into new markers, as the pages are still in the page cache and must stay
write-protected. The range never becomes clear, and installing a guard
region in write-protected shmem keeps restarting the madvise() call.
Signal handlers still run, but the call does not return until the task
is killed.

An entry that is write-protected must stay write-protected while a guard
is installed on it and after the guard is removed. PTE markers are a bit
mask, so a marker can be a guard marker and a uffd-wp marker at the same
time. To fix it, let a guard marker carry the uffd-wp bit:

 - guard install adds the guard bit to an existing uffd-wp marker
 - guard removal clears only the guard bit
 - unprotecting or unregistering clears the uffd-wp bit of a guard
   marker

The fault path, zap, fork, and pagemap already handle a marker with both
bits set. However, pte_is_uffd_marker() would let UFFDIO_COPY overwrite
the guard, so make it ignore guard markers. Anonymous VMAs drop uffd-wp
markers on zap and never get the combined marker.

MADV_GUARD_INSTALL on a uffd-wp shmem range now succeeds instead of
hanging. When the guard region is removed again, its pages are still
write-protected. While the guard is in place, pagemap reports both
PM_GUARD_REGION and PM_UFFD_WP for them.

There are two simpler alternatives, but neither works. Zapping with
ZAP_FLAG_DROP_MARKER fixes the hang but drops the write protection: the
page is still in the page cache once the guard is removed, and writes to
it are not reported. Refusing guard install on uffd-wp file VMAs in
is_valid_guard_vma() would break a combination that works on anonymous
memory, by adding a new -EINVAL for it.

Fixes: f807123d578d ("mm: allow guard regions in file-backed and read-only mappings")
Signed-off-by: Tal Zussman <tz2294@columbia.edu>
---
 include/linux/leafops.h |  4 ++++
 mm/madvise.c            | 28 ++++++++++++++++++++++++++--
 mm/mprotect.c           | 23 +++++++++++++++++++++--
 3 files changed, 51 insertions(+), 4 deletions(-)

diff --git a/include/linux/leafops.h b/include/linux/leafops.h
index 4c1476ae3234..add4968ded44 100644
--- a/include/linux/leafops.h
+++ b/include/linux/leafops.h
@@ -578,6 +578,10 @@ static inline bool pte_is_uffd_marker(pte_t pte)
 	if (!softleaf_is_marker(entry))
 		return false;
 
+	/* A guard marker stays a guard, even with the uffd-wp bit set. */
+	if (softleaf_is_guard_marker(entry))
+		return false;
+
 	/* UFFD WP, poisoned swap entries are UFFD-handled. */
 	if (softleaf_is_uffd_wp_marker(entry))
 		return true;
diff --git a/mm/madvise.c b/mm/madvise.c
index 0214efd2a014..e2c6b0f26672 100644
--- a/mm/madvise.c
+++ b/mm/madvise.c
@@ -1123,6 +1123,23 @@ static int guard_install_pte_entry(pte_t *pte, unsigned long addr,
 		return 0;
 	}
 
+	/*
+	 * Zapping keeps uffd-wp markers on file-backed VMAs, so add the guard
+	 * to the marker instead. The slot stays write-protected once the guard
+	 * is removed.
+	 */
+	if (!vma_is_anonymous(walk->vma) && pte_is_uffd_wp_marker(pteval)) {
+		const softleaf_t entry = softleaf_from_pte(pteval);
+
+		set_pte_at(walk->mm, addr, pte,
+			   make_pte_marker(softleaf_to_marker(entry) |
+					   PTE_MARKER_GUARD));
+		update_mmu_cache(walk->vma, addr, pte);
+		(*nr_pages)++;
+
+		return 0;
+	}
+
 	/* If populated return >0 so we abort the operation + zap. */
 	return 1;
 }
@@ -1259,8 +1276,15 @@ static int guard_remove_pte_entry(pte_t *pte, unsigned long addr,
 	pte_t ptent = ptep_get(pte);
 
 	if (is_guard_pte_marker(ptent)) {
-		/* Simply clear the PTE marker. */
-		pte_clear(walk->mm, addr, pte);
+		const softleaf_t entry = softleaf_from_pte(ptent);
+		const pte_marker marker = softleaf_to_marker(entry) &
+					  ~PTE_MARKER_GUARD;
+
+		/* Clear the guard bit and keep the uffd-wp bit, if set. */
+		if (marker)
+			set_pte_at(walk->mm, addr, pte, make_pte_marker(marker));
+		else
+			pte_clear(walk->mm, addr, pte);
 		update_mmu_cache(walk->vma, addr, pte);
 	}
 
diff --git a/mm/mprotect.c b/mm/mprotect.c
index ef4e7188e24d..6cc5b99b4185 100644
--- a/mm/mprotect.c
+++ b/mm/mprotect.c
@@ -249,9 +249,28 @@ static long change_softleaf_pte(struct vm_area_struct *vma,
 		 * because any access should sigbus/sigsegv
 		 * anyway.
 		 */
-		if (softleaf_is_poison_marker(entry) ||
-		    softleaf_is_guard_marker(entry))
+		if (softleaf_is_poison_marker(entry))
 			return 0;
+
+		/*
+		 * A guard marker on a file-backed VMA carries the
+		 * uffd-wp state of the slot for when the guard is
+		 * removed.
+		 */
+		if (softleaf_is_guard_marker(entry)) {
+			const pte_marker old = softleaf_to_marker(entry);
+			pte_marker marker = old;
+
+			if (uffd_prot_resolve)
+				marker &= ~PTE_MARKER_UFFD_WP;
+
+			if (marker == old)
+				return 0;
+			set_pte_at(vma->vm_mm, addr, pte,
+				   make_pte_marker(marker));
+			return 1;
+		}
+
 		/*
 		 * If this is uffd-wp pte marker and we'd like
 		 * to unprotect it, drop it; the next page

-- 
2.39.5


^ permalink raw reply	[flat|nested] 16+ messages in thread

* [PATCH 10/12] mm: set the uffd-wp bit on a guard marker when write-protecting
  2026-11-11 17:55 [PATCH 00/12] userfaultfd: assorted fixes (part 1) Tal Zussman
                   ` (8 preceding siblings ...)
  2026-11-11 17:55 ` [PATCH 09/12] mm/madvise: let a guard marker carry the uffd-wp bit Tal Zussman
@ 2026-11-11 17:55 ` Tal Zussman
  2026-11-11 17:55 ` [PATCH 11/12] mm/madvise: send UFFD_EVENT_REMOVE when installing guard regions Tal Zussman
  2026-11-11 17:55 ` [PATCH 12/12] mm/userfaultfd: check hugetlb end alignment in UFFDIO_UNREGISTER pre-scan Tal Zussman
  11 siblings, 0 replies; 16+ messages in thread
From: Tal Zussman @ 2026-11-11 17:55 UTC (permalink / raw)
  To: Andrew Morton, Mike Rapoport, Peter Xu, James Houghton,
	Andrea Arcangeli, Suren Baghdasaryan, Balbir Singh,
	Liam R. Howlett, Lorenzo Stoakes, Vlastimil Babka, Jann Horn,
	Pedro Falcato, Michał Mirosław, Andrei Vagin,
	Stephen Rothwell, Arnd Bergmann, Muhammad Usama Anjum,
	David Hildenbrand, Zi Yan, Matthew Brost, Joshua Hahn,
	Byungchul Park, Gregory Price, Ying Huang, Alistair Popple,
	Baolin Wang, Nico Pache, Ryan Roberts, Dev Jain, Barry Song,
	Lance Yang, Usama Arif, Kiryl Shutsemau, Chris Down, Muchun Song,
	Oscar Salvador, Mikołaj Lenczewski, Michal Hocko,
	Mike Kravetz
  Cc: linux-mm, linux-kernel, linux-fsdevel, Tal Zussman

UFFDIO_WRITEPROTECT and PAGEMAP_SCAN with PM_SCAN_WP_MATCHING do not
write-protect an entry that holds a guard marker. change_softleaf_pte()
ignores guard markers, and make_uffd_wp_pte() only sets the swap uffd
bit on the marker PTE, which guard removal clears along with the marker.

On a file-backed VMA, the page is still in the page cache once the guard
is removed. It faults back in writable, and writes to it are not
reported.

To fix it, set the uffd-wp bit on a guard marker of a file-backed VMA in
both paths. A guard marker can carry that bit, and guard removal then
keeps the write protection by leaving a uffd-wp marker in its place.

Move is_guard_pte_marker() to leafops.h as pte_is_guard_marker(), next
to pte_is_uffd_wp_marker(), so that make_uffd_wp_pte() can use it.

A page that is write-protected while a guard is in place is now still
write-protected after the guard is removed.

Anonymous memory is left as it is. With UFFD_FEATURE_WP_UNPOPULATED, an
entry that is write-protected while a guard is in place is still
unprotected after the guard is removed.

Fixes: f807123d578d ("mm: allow guard regions in file-backed and read-only mappings")
Signed-off-by: Tal Zussman <tz2294@columbia.edu>
---
 fs/proc/task_mmu.c      |  7 +++++++
 include/linux/leafops.h | 14 ++++++++++++++
 mm/madvise.c            | 11 ++---------
 mm/mprotect.c           |  5 ++++-
 4 files changed, 27 insertions(+), 10 deletions(-)

diff --git a/fs/proc/task_mmu.c b/fs/proc/task_mmu.c
index 752f1b13d47a..7b1c843a4fd0 100644
--- a/fs/proc/task_mmu.c
+++ b/fs/proc/task_mmu.c
@@ -2439,6 +2439,13 @@ static void make_uffd_wp_pte(struct vm_area_struct *vma,
 	} else if (pte_none(ptent)) {
 		set_pte_at(vma->vm_mm, addr, pte,
 			   make_pte_marker(PTE_MARKER_UFFD_WP));
+	} else if (!vma_is_anonymous(vma) && pte_is_guard_marker(ptent)) {
+		/* Keep the protection in the marker, as mprotect does. */
+		const softleaf_t entry = softleaf_from_pte(ptent);
+
+		set_pte_at(vma->vm_mm, addr, pte,
+			   make_pte_marker(softleaf_to_marker(entry) |
+					   PTE_MARKER_UFFD_WP));
 	} else {
 		ptent = set_uffd_wp_swp_pte(ptent);
 		set_pte_at(vma->vm_mm, addr, pte, ptent);
diff --git a/include/linux/leafops.h b/include/linux/leafops.h
index add4968ded44..831b85a184c4 100644
--- a/include/linux/leafops.h
+++ b/include/linux/leafops.h
@@ -547,6 +547,20 @@ static inline bool pte_is_marker(pte_t pte)
 	return softleaf_is_marker(softleaf_from_pte(pte));
 }
 
+/**
+ * pte_is_guard_marker() - Does this PTE entry encode a guard region marker
+ * leaf entry?
+ * @pte: PTE entry.
+ *
+ * Returns: true if this PTE is a guard marker leaf entry, otherwise false.
+ */
+static inline bool pte_is_guard_marker(pte_t pte)
+{
+	const softleaf_t entry = softleaf_from_pte(pte);
+
+	return softleaf_is_guard_marker(entry);
+}
+
 /**
  * pte_is_uffd_wp_marker() - Does this PTE entry encode a userfaultfd write
  * protect marker leaf entry?
diff --git a/mm/madvise.c b/mm/madvise.c
index e2c6b0f26672..e8b05dc899f1 100644
--- a/mm/madvise.c
+++ b/mm/madvise.c
@@ -1085,13 +1085,6 @@ static bool is_valid_guard_vma(const struct vm_area_struct *vma,
 	return true;
 }
 
-static bool is_guard_pte_marker(pte_t ptent)
-{
-	const softleaf_t entry = softleaf_from_pte(ptent);
-
-	return softleaf_is_guard_marker(entry);
-}
-
 static int guard_install_pud_entry(pud_t *pud, unsigned long addr,
 				   unsigned long next, struct mm_walk *walk)
 {
@@ -1117,7 +1110,7 @@ static int guard_install_pte_entry(pte_t *pte, unsigned long addr,
 	unsigned long *nr_pages = (unsigned long *)walk->private;
 
 	/* If there is already a guard page marker, we have nothing to do. */
-	if (is_guard_pte_marker(pteval)) {
+	if (pte_is_guard_marker(pteval)) {
 		(*nr_pages)++;
 
 		return 0;
@@ -1275,7 +1268,7 @@ static int guard_remove_pte_entry(pte_t *pte, unsigned long addr,
 {
 	pte_t ptent = ptep_get(pte);
 
-	if (is_guard_pte_marker(ptent)) {
+	if (pte_is_guard_marker(ptent)) {
 		const softleaf_t entry = softleaf_from_pte(ptent);
 		const pte_marker marker = softleaf_to_marker(entry) &
 					  ~PTE_MARKER_GUARD;
diff --git a/mm/mprotect.c b/mm/mprotect.c
index 6cc5b99b4185..3f2d6e3aca93 100644
--- a/mm/mprotect.c
+++ b/mm/mprotect.c
@@ -261,7 +261,10 @@ static long change_softleaf_pte(struct vm_area_struct *vma,
 			const pte_marker old = softleaf_to_marker(entry);
 			pte_marker marker = old;
 
-			if (uffd_prot_resolve)
+			if ((cp_flags & MM_CP_UFFD_WP) &&
+			    !vma_is_anonymous(vma))
+				marker |= PTE_MARKER_UFFD_WP;
+			else if (uffd_prot_resolve)
 				marker &= ~PTE_MARKER_UFFD_WP;
 
 			if (marker == old)

-- 
2.39.5


^ permalink raw reply	[flat|nested] 16+ messages in thread

* [PATCH 11/12] mm/madvise: send UFFD_EVENT_REMOVE when installing guard regions
  2026-11-11 17:55 [PATCH 00/12] userfaultfd: assorted fixes (part 1) Tal Zussman
                   ` (9 preceding siblings ...)
  2026-11-11 17:55 ` [PATCH 10/12] mm: set the uffd-wp bit on a guard marker when write-protecting Tal Zussman
@ 2026-11-11 17:55 ` Tal Zussman
  2026-11-11 19:25   ` Kiryl Shutsemau
  2026-11-11 17:55 ` [PATCH 12/12] mm/userfaultfd: check hugetlb end alignment in UFFDIO_UNREGISTER pre-scan Tal Zussman
  11 siblings, 1 reply; 16+ messages in thread
From: Tal Zussman @ 2026-11-11 17:55 UTC (permalink / raw)
  To: Andrew Morton, Mike Rapoport, Peter Xu, James Houghton,
	Andrea Arcangeli, Suren Baghdasaryan, Balbir Singh,
	Liam R. Howlett, Lorenzo Stoakes, Vlastimil Babka, Jann Horn,
	Pedro Falcato, Michał Mirosław, Andrei Vagin,
	Stephen Rothwell, Arnd Bergmann, Muhammad Usama Anjum,
	David Hildenbrand, Zi Yan, Matthew Brost, Joshua Hahn,
	Byungchul Park, Gregory Price, Ying Huang, Alistair Popple,
	Baolin Wang, Nico Pache, Ryan Roberts, Dev Jain, Barry Song,
	Lance Yang, Usama Arif, Kiryl Shutsemau, Chris Down, Muchun Song,
	Oscar Salvador, Mikołaj Lenczewski, Michal Hocko,
	Mike Kravetz
  Cc: linux-mm, linux-kernel, linux-fsdevel, Tal Zussman

MADV_GUARD_INSTALL zaps whatever is mapped in the range before
installing the guard markers. However, unlike MADV_DONTNEED and
MADV_REMOVE, madvise_guard_install() does not call userfaultfd_remove(),
so the userfaultfd manager is not told that the pages are gone.

On an anonymous VMA, the zap drops uffd-wp and RWP protection along with
the pages. Writes and accesses to the range after the guard is removed
are then not reported, and the manager cannot know that it has to
protect the range again.

To fix it, send UFFD_EVENT_REMOVE before zapping. Guard install on a VMA
registered with userfaultfd already takes the mmap lock instead of the
VMA lock, so we can drop the lock for the event and look the VMA up
again afterwards.

As with MADV_DONTNEED, the call can now return -ENOMEM if the VMA goes
away while the lock is dropped, or -EINVAL if it is replaced by a VMA
that does not support guard regions.

Fixes: 662df3e5c376 ("mm: madvise: implement lightweight guard page mechanism")
Closes: https://lore.kernel.org/linux-mm/3f4b0d1b.bbf84.1a1c55cb666.Coremail.18670333770@163.com/
Signed-off-by: Tal Zussman <tz2294@columbia.edu>
---
 mm/madvise.c | 23 +++++++++++++++++++++++
 1 file changed, 23 insertions(+)

diff --git a/mm/madvise.c b/mm/madvise.c
index e8b05dc899f1..6413f0e65381 100644
--- a/mm/madvise.c
+++ b/mm/madvise.c
@@ -1166,6 +1166,29 @@ static long madvise_guard_install(struct madvise_behavior *madv_behavior)
 	if (!is_valid_guard_vma(vma, /* allow_locked = */false))
 		return -EINVAL;
 
+	/*
+	 * Installing guards zaps the range, so report it to a userfaultfd
+	 * manager like MADV_DONTNEED does. The VMA can change while the mmap
+	 * lock is dropped for the event, so look it up again.
+	 */
+	if (!userfaultfd_remove(vma, range->start, range->end)) {
+		mark_mmap_lock_dropped(madv_behavior);
+		mmap_read_lock(madv_behavior->mm);
+		madv_behavior->vma = vma = vma_lookup(madv_behavior->mm,
+						      range->start);
+		if (!vma)
+			return -ENOMEM;
+		if (!is_valid_guard_vma(vma, /* allow_locked = */false))
+			return -EINVAL;
+		/*
+		 * The VMA may have been split while the lock was dropped.
+		 * Handle the part in this VMA. The remainder is handled by the
+		 * next iteration of madvise_walk_vmas().
+		 */
+		if (range->end > vma->vm_end)
+			range->end = vma->vm_end;
+	}
+
 	/*
 	 * Set atomically under read lock. All pertinent readers will need to
 	 * acquire an mmap/VMA write lock to read it. All remaining readers may

-- 
2.39.5


^ permalink raw reply	[flat|nested] 16+ messages in thread

* [PATCH 12/12] mm/userfaultfd: check hugetlb end alignment in UFFDIO_UNREGISTER pre-scan
  2026-11-11 17:55 [PATCH 00/12] userfaultfd: assorted fixes (part 1) Tal Zussman
                   ` (10 preceding siblings ...)
  2026-11-11 17:55 ` [PATCH 11/12] mm/madvise: send UFFD_EVENT_REMOVE when installing guard regions Tal Zussman
@ 2026-11-11 17:55 ` Tal Zussman
  11 siblings, 0 replies; 16+ messages in thread
From: Tal Zussman @ 2026-11-11 17:55 UTC (permalink / raw)
  To: Andrew Morton, Mike Rapoport, Peter Xu, James Houghton,
	Andrea Arcangeli, Suren Baghdasaryan, Balbir Singh,
	Liam R. Howlett, Lorenzo Stoakes, Vlastimil Babka, Jann Horn,
	Pedro Falcato, Michał Mirosław, Andrei Vagin,
	Stephen Rothwell, Arnd Bergmann, Muhammad Usama Anjum,
	David Hildenbrand, Zi Yan, Matthew Brost, Joshua Hahn,
	Byungchul Park, Gregory Price, Ying Huang, Alistair Popple,
	Baolin Wang, Nico Pache, Ryan Roberts, Dev Jain, Barry Song,
	Lance Yang, Usama Arif, Kiryl Shutsemau, Chris Down, Muchun Song,
	Oscar Salvador, Mikołaj Lenczewski, Michal Hocko,
	Mike Kravetz
  Cc: linux-mm, linux-kernel, linux-fsdevel, Tal Zussman

UFFDIO_REGISTER and UFFDIO_UNREGISTER both scan the VMAs of the range
before changing anything. For a hugetlb VMA, userfaultfd_register()
rejects a range whose start or end is not huge page aligned there.
However, userfaultfd_unregister() only checks the start.

An unaligned end only fails later, when vma_modify_flags_uffd() cannot
split the VMA there. At that point, userfaultfd_clear_vma() already
cleared the WP or RWP protection. The ioctl returns -EINVAL and the VMA
stays registered, but accesses to its pages are no longer reported. This
requires a caller passing a misaligned range.

To fix it, check the end of a registered VMA in the pre-scan as well.
The ioctl still returns -EINVAL, but no longer unregisters the VMAs
before the misaligned one first.

Fixes: cab350afcbc9 ("userfaultfd: hugetlbfs: allow registration of ranges containing huge pages")
Fixes: f369b07c8614 ("mm/uffd: reset write protection when unregister with wp-mode")
Signed-off-by: Tal Zussman <tz2294@columbia.edu>
---
 mm/userfaultfd.c | 12 ++++++++++++
 1 file changed, 12 insertions(+)

diff --git a/mm/userfaultfd.c b/mm/userfaultfd.c
index bf42fabb5afd..c0076c8b4e9d 100644
--- a/mm/userfaultfd.c
+++ b/mm/userfaultfd.c
@@ -3983,6 +3983,18 @@ static int userfaultfd_unregister(struct userfaultfd_ctx *ctx,
 		if (!vma_can_userfault(cur, cur->vm_flags, wp_async))
 			goto out_unlock;
 
+		/*
+		 * A range ending inside a registered hugetlb vma must end
+		 * huge page aligned, as in userfaultfd_register().
+		 */
+		if (cur->vm_userfaultfd_ctx.ctx && vma_is_hugetlb(cur) &&
+		    end <= cur->vm_end && end > cur->vm_start) {
+			unsigned long vma_hpagesize = vma_kernel_pagesize(cur);
+
+			if (end & (vma_hpagesize - 1))
+				goto out_unlock;
+		}
+
 		found = true;
 	} for_each_vma_range(vmi, cur, end);
 	VM_WARN_ON_ONCE(!found);

-- 
2.39.5


^ permalink raw reply	[flat|nested] 16+ messages in thread

* Re: [PATCH 06/12] mm/huge_memory: build the moved huge zero PMD from the destination VMA
  2026-11-11 17:55 ` [PATCH 06/12] mm/huge_memory: build the moved huge zero PMD from the destination VMA Tal Zussman
@ 2026-11-11 19:12   ` Kiryl Shutsemau
  0 siblings, 0 replies; 16+ messages in thread
From: Kiryl Shutsemau @ 2026-11-11 19:12 UTC (permalink / raw)
  To: Tal Zussman
  Cc: Andrew Morton, Mike Rapoport, Peter Xu, James Houghton,
	Andrea Arcangeli, Suren Baghdasaryan, Balbir Singh,
	Liam R. Howlett, Lorenzo Stoakes, Vlastimil Babka, Jann Horn,
	Pedro Falcato, Michał Mirosław, Andrei Vagin,
	Stephen Rothwell, Arnd Bergmann, Muhammad Usama Anjum,
	David Hildenbrand, Zi Yan, Matthew Brost, Joshua Hahn,
	Byungchul Park, Gregory Price, Ying Huang, Alistair Popple,
	Baolin Wang, Nico Pache, Ryan Roberts, Dev Jain, Barry Song,
	Lance Yang, Usama Arif, Chris Down, Muchun Song, Oscar Salvador,
	Mikołaj Lenczewski, Michal Hocko, Mike Kravetz, linux-mm,
	linux-kernel, linux-fsdevel

On Wed, Nov 11, 2026 at 12:55:14PM -0500, Tal Zussman wrote:
> move_pages_huge_pmd() builds the destination PMD for the huge zero page
> by copying the source PMD and clearing its uffd bit, so the destination
> keeps the protection of the source.
> 
> A source VMA registered for RWP can map the huge zero page with a
> PAGE_NONE PMD, and UFFDIO_MOVE does not check how the source is
> registered. The destination then gets a PAGE_NONE PMD without the uffd
> bit, although nothing asked for that in the destination VMA.
> 
> The next access fixes it up through the NUMA hinting fault path. Until
> then, the first access to the destination takes a spurious fault and GUP
> has to take the fault path. The impact on userspace is small.
> 
> The moved entry must have the protection of the destination VMA. To fix
> it, build the entry from the vm_page_prot of the destination VMA, as
> move_zeropage_pte() does for the small zero page, and keep marking it
> soft-dirty. A destination VMA registered for RWP still gets its PMD
> re-armed afterwards.
> 
> Fixes: 763f43865fc4 ("mm: preserve RWP marker across PTE rewrites")
> Signed-off-by: Tal Zussman <tz2294@columbia.edu>

Reviewed-by: Kiryl Shutsemau (Meta) <kas@kernel.org>

-- 
  Kiryl Shutsemau / Kirill A. Shutemov

^ permalink raw reply	[flat|nested] 16+ messages in thread

* Re: [PATCH 07/12] mm/mremap: resolve uffd state when the context is dropped, not during the move
  2026-11-11 17:55 ` [PATCH 07/12] mm/mremap: resolve uffd state when the context is dropped, not during the move Tal Zussman
@ 2026-11-11 19:20   ` Kiryl Shutsemau
  0 siblings, 0 replies; 16+ messages in thread
From: Kiryl Shutsemau @ 2026-11-11 19:20 UTC (permalink / raw)
  To: Tal Zussman
  Cc: Andrew Morton, Mike Rapoport, Peter Xu, James Houghton,
	Andrea Arcangeli, Suren Baghdasaryan, Balbir Singh,
	Liam R. Howlett, Lorenzo Stoakes, Vlastimil Babka, Jann Horn,
	Pedro Falcato, Michał Mirosław, Andrei Vagin,
	Stephen Rothwell, Arnd Bergmann, Muhammad Usama Anjum,
	David Hildenbrand, Zi Yan, Matthew Brost, Joshua Hahn,
	Byungchul Park, Gregory Price, Ying Huang, Alistair Popple,
	Baolin Wang, Nico Pache, Ryan Roberts, Dev Jain, Barry Song,
	Lance Yang, Usama Arif, Chris Down, Muchun Song, Oscar Salvador,
	Mikołaj Lenczewski, Michal Hocko, Mike Kravetz, linux-mm,
	linux-kernel, linux-fsdevel

On Wed, Nov 11, 2026 at 12:55:15PM -0500, Tal Zussman wrote:
> However, this adds spurious minor faults on shmem and hugetlb.

"Minor faults" reads as UFFDIO_REGISTER_MODE_MINOR in a uffd changelog.
Maybe "write faults"?

The RWP side keeps its behaviour: vm_page_prot does not depend on RWP,
so resolving with change_protection() gives the same PTEs as the
per-entry code did.

Reviewed-by: Kiryl Shutsemau (Meta) <kas@kernel.org>

-- 
  Kiryl Shutsemau / Kirill A. Shutemov

^ permalink raw reply	[flat|nested] 16+ messages in thread

* Re: [PATCH 11/12] mm/madvise: send UFFD_EVENT_REMOVE when installing guard regions
  2026-11-11 17:55 ` [PATCH 11/12] mm/madvise: send UFFD_EVENT_REMOVE when installing guard regions Tal Zussman
@ 2026-11-11 19:25   ` Kiryl Shutsemau
  0 siblings, 0 replies; 16+ messages in thread
From: Kiryl Shutsemau @ 2026-11-11 19:25 UTC (permalink / raw)
  To: Tal Zussman
  Cc: Andrew Morton, Mike Rapoport, Peter Xu, James Houghton,
	Andrea Arcangeli, Suren Baghdasaryan, Balbir Singh,
	Liam R. Howlett, Lorenzo Stoakes, Vlastimil Babka, Jann Horn,
	Pedro Falcato, Michał Mirosław, Andrei Vagin,
	Stephen Rothwell, Arnd Bergmann, Muhammad Usama Anjum,
	David Hildenbrand, Zi Yan, Matthew Brost, Joshua Hahn,
	Byungchul Park, Gregory Price, Ying Huang, Alistair Popple,
	Baolin Wang, Nico Pache, Ryan Roberts, Dev Jain, Barry Song,
	Lance Yang, Usama Arif, Chris Down, Muchun Song, Oscar Salvador,
	Mikołaj Lenczewski, Michal Hocko, Mike Kravetz, linux-mm,
	linux-kernel, linux-fsdevel

On Wed, Nov 11, 2026 at 12:55:19PM -0500, Tal Zussman wrote:
> On an anonymous VMA, the zap drops uffd-wp and RWP protection along with
> the pages. Writes and accesses to the range after the guard is removed
> are then not reported, and the manager cannot know that it has to
> protect the range again.

For RWP this only holds if the range is registered without MISSING.

The zap frees the pages, so with MISSING the first access after the
guard is removed is reported as a missing fault. And there is nothing to
protect again: RWP only covers present PTEs, and a page faulted back in
shows up as accessed in PAGEMAP_SCAN.

-- 
  Kiryl Shutsemau / Kirill A. Shutemov

^ permalink raw reply	[flat|nested] 16+ messages in thread

end of thread, other threads:[~2026-11-11 19:25 UTC | newest]

Thread overview: 16+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2026-11-11 17:55 [PATCH 00/12] userfaultfd: assorted fixes (part 1) Tal Zussman
2026-11-11 17:55 ` [PATCH 01/12] userfaultfd: use anonymous memory for UFFDIO_ZEROPAGE on private file mappings Tal Zussman
2026-11-11 17:55 ` [PATCH 02/12] userfaultfd: refuse UFFDIO_MOVE into a non-present huge destination PMD Tal Zussman
2026-11-11 17:55 ` [PATCH 03/12] fs/proc/task_mmu: downgrade writable migration entries in PAGEMAP_SCAN write-protect Tal Zussman
2026-11-11 17:55 ` [PATCH 04/12] mm/migrate: keep the uffd bit when restoring a writable migration entry Tal Zussman
2026-11-11 17:55 ` [PATCH 05/12] fs/proc/task_mmu: write-protect device-private THPs in PAGEMAP_SCAN Tal Zussman
2026-11-11 17:55 ` [PATCH 06/12] mm/huge_memory: build the moved huge zero PMD from the destination VMA Tal Zussman
2026-11-11 19:12   ` Kiryl Shutsemau
2026-11-11 17:55 ` [PATCH 07/12] mm/mremap: resolve uffd state when the context is dropped, not during the move Tal Zussman
2026-11-11 19:20   ` Kiryl Shutsemau
2026-11-11 17:55 ` [PATCH 08/12] mm/migrate_device: do not migrate a uffd-protected zero page Tal Zussman
2026-11-11 17:55 ` [PATCH 09/12] mm/madvise: let a guard marker carry the uffd-wp bit Tal Zussman
2026-11-11 17:55 ` [PATCH 10/12] mm: set the uffd-wp bit on a guard marker when write-protecting Tal Zussman
2026-11-11 17:55 ` [PATCH 11/12] mm/madvise: send UFFD_EVENT_REMOVE when installing guard regions Tal Zussman
2026-11-11 19:25   ` Kiryl Shutsemau
2026-11-11 17:55 ` [PATCH 12/12] mm/userfaultfd: check hugetlb end alignment in UFFDIO_UNREGISTER pre-scan Tal Zussman

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox

all inboxes | Powered by JetHome®