Skip to contentSkip to conversation

userfaultfd/pagemap: pre-existing fixes

PATCH Applied Kiryl Shutsemau (Meta) 8 participants 26 messages May 29 – Nov 11

Subsystems (8)
lore
Share & tools
lore

Kiryl Shutsemau (Meta)

author reviewer
#0 lore raw

cover userfaultfd/pagemap: pre-existing fixes

These are pre-existing bug fixes that were carried at the front of the userfaultfd RWP working-set-tracking series up to v5 [1]. Per review feedback that fixes should not sit in the middle of a feature series, they are split out and sent on their own; the RWP series is reposted rebased on top of this.

All six were flagged by the Sashiko AI review of the RWP series and carry Reported-by: Sashiko AI review <[email protected]>. They are independent of RWP, apply to mm-new directly, and carry Cc: stable@.

1: fs/proc/task_mmu: a missing huge_ptep_modify_prot_start() in
make_uffd_wp_huge_pte() can lose hardware Dirty/Accessed updates when PAGEMAP_SCAN write-protects a hugetlb PTE.

2: fs/proc/task_mmu: pagemap_scan_hugetlb_entry() compares the range
against HPAGE_SIZE rather than the hstate page size, so it never write-protects gigantic hugetlb pages.

3: fs/proc/task_mmu: PAGEMAP_SCAN with PM_SCAN_WP_MATCHING over an
unpopulated hugetlb range self-deadlocks -- pagemap_scan_pte_hole() calls uffd_wp_range() while walk_hugetlb_range() holds the hugetlb vma lock for read, and hugetlb_change_protection() then takes it for write. Install the marker inline instead.

4: mm/huge_memory: change_non_present_huge_pmd() drops pmd_swp_uffd_wp
on a device-private PMD permission downgrade, silently losing the uffd-wp marker.

5: userfaultfd: must_wait() applies pte_write() to a locklessly read
PTE without checking pte_present(), so swap/migration entries decode random offset bits and a thread can stay parked on a stale fault.

6: userfaultfd: __VMA_UFFD_FLAGS feeds VMA_UFFD_MINOR_BIT (41) to
mk_vma_flags() unconditionally, an out-of-bounds write into the single-word vma_flags_t on 32-bit. Build the mask from config-gated per-mode masks so an unavailable bit is never materialised.

[1] https://lore.kernel.org/all/[email protected]/

Kiryl Shutsemau (Meta) (6):
fs/proc/task_mmu: fix make_uffd_wp_huge_pte() prot-update race fs/proc/task_mmu: use huge_page_size() in pagemap_scan_hugetlb_entry() fs/proc/task_mmu: fix hugetlb self-deadlock in pagemap_scan_pte_hole() mm/huge_memory: preserve pmd_swp_uffd_wp on device-private PMD
downgrade
userfaultfd: gate must_wait writability check on pte_present()
userfaultfd: build __VMA_UFFD_FLAGS from config-gated masks

 fs/proc/task_mmu.c            | 73 ++++++++++++++++++++++++++++++++---
 include/linux/mm.h            | 39 +++++++++++++++++++
 include/linux/userfaultfd_k.h |  4 +-
 mm/huge_memory.c              |  2 +
 mm/userfaultfd.c              | 20 ++++++++++
 5 files changed, 130 insertions(+), 8 deletions(-)

base-commit: 449a5df98f8dffa9b037e3b6838fc5af327df072

Signature · 1 line
2.54.0
author reviewer
#1 lore raw

PATCH 1/6 fs/proc/task_mmu: fix make_uffd_wp_huge_pte() prot-update race

make_uffd_wp_huge_pte() arms the UFFD_WP bit on a present HugeTLB PTE by calling huge_ptep_modify_prot_commit() with a ptent snapshot that was fetched without the corresponding huge_ptep_modify_prot_start(). The start helper is what atomically clears the entry so the kernel-owned snapshot stays consistent until the commit; without it, the hardware may set Dirty or Accessed in the live PTE between the original read and the commit, and huge_ptep_modify_prot_commit() (whose generic implementation just calls set_huge_pte_at()) then writes the stale snapshot back over the live hardware bits, losing the update.

The non-hugetlb sibling make_uffd_wp_pte() does this correctly via ptep_modify_prot_start() / ptep_modify_prot_commit(). Mirror that pattern for the present-PTE branch. The migration case stays as-is -- migration entries are non-present, so there's no hardware update to race against.

Fixes 52526ca7fdb9 ("fs/proc/task_mmu: implement IOCTL to get and optionally clear info about PTEs")
---
 fs/proc/task_mmu.c | 12 ++++++++----
 1 file changed, 8 insertions(+), 4 deletions(-)
fs/proc/task_mmu.c +8 -4
index 1e3a15bf46f4..e21a38ac745b 100644
@@ -2610,12 +2610,16 @@ static void make_uffd_wp_huge_pte(struct vm_area_struct *vma,
 	if (softleaf_is_hwpoison(entry) || softleaf_is_marker(entry))
 		return;
 
-	if (softleaf_is_migration(entry))
+	if (softleaf_is_migration(entry)) {
 		set_huge_pte_at(vma->vm_mm, addr, ptep,
 				pte_swp_mkuffd_wp(ptent), psize);
-	else
-		huge_ptep_modify_prot_commit(vma, addr, ptep, ptent,
-					     huge_pte_mkuffd_wp(ptent));
+	} else {
+		pte_t old_pte, new_pte;
+
+		old_pte = huge_ptep_modify_prot_start(vma, addr, ptep);
+		new_pte = huge_pte_mkuffd_wp(old_pte);
+		huge_ptep_modify_prot_commit(vma, addr, ptep, old_pte, new_pte);
+	}
 }
 #endif /* CONFIG_HUGETLB_PAGE */
 
Signature · 1 line
2.54.0
maintainer Reviewed-by on v1 1/6
#2 lore raw

Re: [PATCH 1/6] fs/proc/task_mmu: fix make_uffd_wp_huge_pte() prot-update race

Quoted 3 lines · Kiryl Shutsemau (Meta)

make_uffd_wp_huge_pte() arms the UFFD_WP bit on a present HugeTLB PTE by calling huge_ptep_modify_prot_commit() with a ptent snapshot that was fetched without the corresponding huge_ptep_modify_prot_start().

The

Quoted 6 lines

start helper is what atomically clears the entry so the kernel-owned snapshot stays consistent until the commit; without it, the hardware may set Dirty or Accessed in the live PTE between the original read and the commit, and huge_ptep_modify_prot_commit() (whose generic implementation just calls set_huge_pte_at()) then writes the stale snapshot back over the live hardware bits, losing the update.

Very pedantic nit - this is a mammoth paragraph :P maybe add a linebreak where I did above?

Quoted 8 lines

The non-hugetlb sibling make_uffd_wp_pte() does this correctly via ptep_modify_prot_start() / ptep_modify_prot_commit(). Mirror that pattern for the present-PTE branch. The migration case stays as-is -- migration entries are non-present, so there's no hardware update to race against.

Fixes 52526ca7fdb9 ("fs/proc/task_mmu: implement IOCTL to get and optionally clear info about PTEs")

I wonder if better as a separate patch? Not sure what Andrew thinks about it, I do recall him complaining about separate-hotfixes-as-part-of-a-series :>)

Quoted 3 lines

LGTM, so:

Quoted 30 lines
---
 fs/proc/task_mmu.c | 12 ++++++++----
 1 file changed, 8 insertions(+), 4 deletions(-)
fs/proc/task_mmu.c +8 -5
index 1e3a15bf46f4..e21a38ac745b 100644
@@ -2610,12 +2610,16 @@ static void make_uffd_wp_huge_pte(struct vm_area_struct *vma,
 	if (softleaf_is_hwpoison(entry) || softleaf_is_marker(entry))
 		return;
 
-	if (softleaf_is_migration(entry))
+	if (softleaf_is_migration(entry)) {
 		set_huge_pte_at(vma->vm_mm, addr, ptep,
 				pte_swp_mkuffd_wp(ptent), psize);
-	else
-		huge_ptep_modify_prot_commit(vma, addr, ptep, ptent,
-					     huge_pte_mkuffd_wp(ptent));
+	} else {
+		pte_t old_pte, new_pte;
+
+		old_pte = huge_ptep_modify_prot_start(vma, addr, ptep);
+		new_pte = huge_pte_mkuffd_wp(old_pte);
+		huge_ptep_modify_prot_commit(vma, addr, ptep, old_pte, new_pte);
+	}
 }
 #endif /* CONFIG_HUGETLB_PAGE */
 
--
Quoted 2 lines

2.54.0

Cheers, Lorenzo

maintainer
#3 lore raw

Re: [PATCH 1/6] fs/proc/task_mmu: fix make_uffd_wp_huge_pte() prot-update race

Quoted 5 lines · Lorenzo Stoakes
Quoted 1 line · Kiryl Shutsemau (Meta)
Fixes 52526ca7fdb9 ("fs/proc/task_mmu: implement IOCTL to get and optionally clear info about PTEs")

I wonder if better as a separate patch? Not sure what Andrew thinks about it, I do recall him complaining about separate-hotfixes-as-part-of-a-series :>)

Ahhh it's a series of Fixes: :) ok ignore this then!

Cheers, Lorenzo

reviewer Reviewed-by on v1 1/6
#4 lore raw

Re: [PATCH 1/6] fs/proc/task_mmu: fix make_uffd_wp_huge_pte() prot-update race

In reply to Kiryl Shutsemau (Meta)
Quoted 16 lines · Kiryl Shutsemau (Meta)

make_uffd_wp_huge_pte() arms the UFFD_WP bit on a present HugeTLB PTE by calling huge_ptep_modify_prot_commit() with a ptent snapshot that was fetched without the corresponding huge_ptep_modify_prot_start(). The start helper is what atomically clears the entry so the kernel-owned snapshot stays consistent until the commit; without it, the hardware may set Dirty or Accessed in the live PTE between the original read and the commit, and huge_ptep_modify_prot_commit() (whose generic implementation just calls set_huge_pte_at()) then writes the stale snapshot back over the live hardware bits, losing the update.

The non-hugetlb sibling make_uffd_wp_pte() does this correctly via ptep_modify_prot_start() / ptep_modify_prot_commit(). Mirror that pattern for the present-PTE branch. The migration case stays as-is -- migration entries are non-present, so there's no hardware update to race against.

Quoted 5 lines
Fixes 52526ca7fdb9 ("fs/proc/task_mmu: implement IOCTL to get and optionally clear info about PTEs")

---

LGTM

Quoted 28 lines
 fs/proc/task_mmu.c | 12 ++++++++----
 1 file changed, 8 insertions(+), 4 deletions(-)
fs/proc/task_mmu.c +8 -4
index 1e3a15bf46f4..e21a38ac745b 100644
@@ -2610,12 +2610,16 @@ static void make_uffd_wp_huge_pte(struct vm_area_struct *vma,
 	if (softleaf_is_hwpoison(entry) || softleaf_is_marker(entry))
 		return;
 
-	if (softleaf_is_migration(entry))
+	if (softleaf_is_migration(entry)) {
 		set_huge_pte_at(vma->vm_mm, addr, ptep,
 				pte_swp_mkuffd_wp(ptent), psize);
-	else
-		huge_ptep_modify_prot_commit(vma, addr, ptep, ptent,
-					     huge_pte_mkuffd_wp(ptent));
+	} else {
+		pte_t old_pte, new_pte;
+
+		old_pte = huge_ptep_modify_prot_start(vma, addr, ptep);
+		new_pte = huge_pte_mkuffd_wp(old_pte);
+		huge_ptep_modify_prot_commit(vma, addr, ptep, old_pte, new_pte);
+	}
 }
 #endif /* CONFIG_HUGETLB_PAGE */
 
#5 lore raw

Re: [PATCH 1/6] fs/proc/task_mmu: fix make_uffd_wp_huge_pte() prot-update race

In reply to Kiryl Shutsemau (Meta)

Hello Kiryl,

Quoted 5 lines · Kiryl Shutsemau (Meta)

The non-hugetlb sibling make_uffd_wp_pte() does this correctly via ptep_modify_prot_start() / ptep_modify_prot_commit(). Mirror that pattern for the present-PTE branch. The migration case stays as-is -- migration entries are non-present, so there's no hardware update to race against.

I am seeing this on next-20261109 (arm64, PAGE_TABLE_CHECK) while the pagemap_ioctl kselftest runs next to a workload that migrates pages:

    WARNING: mm/page_table_check.c:206 at __page_table_check_ptes_set+0x168/0x178, CPU#21: pagemap_ioctl/894691
    Call trace:
     __page_table_check_ptes_set+0x168/0x178 (P)
     __set_ptes_anysz+0x50/0x240
     make_uffd_wp_pte+0x104/0x1c8
     pagemap_scan_pmd_entry+0x1b98/0x1df8
     walk_pgd_range+0x7b4/0xd00
     __walk_page_range+0x108/0x510
     walk_page_range_mm_unsafe+0x150/0x3e8
     do_pagemap_cmd+0x418/0x7d0
     __arm64_sys_ioctl+0xc08/0x1338

given you've touched this recently, I am wondering if you've seen it.

--breno

author reviewer
#6 lore raw

Re: [PATCH 1/6] fs/proc/task_mmu: fix make_uffd_wp_huge_pte() prot-update race

Quoted 24 lines · Breno Leitao

Hello Kiryl,

Quoted 5 lines · Kiryl Shutsemau (Meta)

The non-hugetlb sibling make_uffd_wp_pte() does this correctly via ptep_modify_prot_start() / ptep_modify_prot_commit(). Mirror that pattern for the present-PTE branch. The migration case stays as-is -- migration entries are non-present, so there's no hardware update to race against.

I am seeing this on next-20261109 (arm64, PAGE_TABLE_CHECK) while the pagemap_ioctl kselftest runs next to a workload that migrates pages:

    WARNING: mm/page_table_check.c:206 at __page_table_check_ptes_set+0x168/0x178, CPU#21: pagemap_ioctl/894691
    Call trace:
     __page_table_check_ptes_set+0x168/0x178 (P)
     __set_ptes_anysz+0x50/0x240
     make_uffd_wp_pte+0x104/0x1c8
     pagemap_scan_pmd_entry+0x1b98/0x1df8
     walk_pgd_range+0x7b4/0xd00
     __walk_page_range+0x108/0x510
     walk_page_range_mm_unsafe+0x150/0x3e8
     do_pagemap_cmd+0x418/0x7d0
     __arm64_sys_ioctl+0xc08/0x1338
Quoted 1 line

given you've touched this recently, I am wondering if you've seen it.

I have not. But I think this patch should help:

https://lore.kernel.org/all/[email protected]

Signature · 1 line
  Kiryl Shutsemau / Kirill A. Shutemov
author reviewer
#7 lore raw

PATCH 2/6 fs/proc/task_mmu: use huge_page_size() in pagemap_scan_hugetlb_entry()

In reply to Kiryl Shutsemau (Meta)

The partial-page check compares against HPAGE_SIZE (PMD_SIZE), which is wrong for gigantic hugetlb hstates (e.g. 1G). The walker hands the callback a huge_page_size()-sized range, never start + HPAGE_SIZE, so the comparison always declares it partial and aborts the WP. Compare against the actual hstate's page size.

Fixes 52526ca7fdb9 ("fs/proc/task_mmu: implement IOCTL to get and optionally clear info about PTEs")
---
 fs/proc/task_mmu.c | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)
fs/proc/task_mmu.c +1 -1
index e21a38ac745b..1489c67e88f7 100644
@@ -2960,7 +2960,7 @@ static int pagemap_scan_hugetlb_entry(pte_t *ptep, unsigned long hmask,
 	if (~categories & PAGE_IS_WRITTEN)
 		goto out_unlock;
 
-	if (end != start + HPAGE_SIZE) {
+	if (end != start + huge_page_size(hstate_vma(vma))) {
 		/* Partial HugeTLB page WP isn't possible. */
 		pagemap_scan_backout_range(p, start, end);
 		p->arg.walk_end = start;
Signature · 1 line
2.54.0
maintainer Reviewed-by on v1 2/6
#8 lore raw

Re: [PATCH 2/6] fs/proc/task_mmu: use huge_page_size() in pagemap_scan_hugetlb_entry()

Quoted 10 lines · Kiryl Shutsemau (Meta)

The partial-page check compares against HPAGE_SIZE (PMD_SIZE), which is wrong for gigantic hugetlb hstates (e.g. 1G). The walker hands the callback a huge_page_size()-sized range, never start + HPAGE_SIZE, so the comparison always declares it partial and aborts the WP. Compare against the actual hstate's page size.

Fixes 52526ca7fdb9 ("fs/proc/task_mmu: implement IOCTL to get and optionally clear info about PTEs")

LGTM, so:

Quoted 20 lines
---
 fs/proc/task_mmu.c | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)
fs/proc/task_mmu.c +1 -2
index e21a38ac745b..1489c67e88f7 100644
@@ -2960,7 +2960,7 @@ static int pagemap_scan_hugetlb_entry(pte_t *ptep, unsigned long hmask,
 	if (~categories & PAGE_IS_WRITTEN)
 		goto out_unlock;
 
-	if (end != start + HPAGE_SIZE) {
+	if (end != start + huge_page_size(hstate_vma(vma))) {
 		/* Partial HugeTLB page WP isn't possible. */
 		pagemap_scan_backout_range(p, start, end);
 		p->arg.walk_end = start;
--

2.54.0

reviewer Reviewed-by on v1 2/6
#9 lore raw

Re: [PATCH 2/6] fs/proc/task_mmu: use huge_page_size() in pagemap_scan_hugetlb_entry()

In reply to Kiryl Shutsemau (Meta)
Quoted 11 lines · Kiryl Shutsemau (Meta)

The partial-page check compares against HPAGE_SIZE (PMD_SIZE), which is wrong for gigantic hugetlb hstates (e.g. 1G). The walker hands the callback a huge_page_size()-sized range, never start + HPAGE_SIZE, so the comparison always declares it partial and aborts the WP. Compare against the actual hstate's page size.

Fixes 52526ca7fdb9 ("fs/proc/task_mmu: implement IOCTL to get and optionally clear info about PTEs")

---

Quoted 16 lines
 fs/proc/task_mmu.c | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)
fs/proc/task_mmu.c +1 -1
index e21a38ac745b..1489c67e88f7 100644
@@ -2960,7 +2960,7 @@ static int pagemap_scan_hugetlb_entry(pte_t *ptep, unsigned long hmask,
 	if (~categories & PAGE_IS_WRITTEN)
 		goto out_unlock;
 
-	if (end != start + HPAGE_SIZE) {
+	if (end != start + huge_page_size(hstate_vma(vma))) {
 		/* Partial HugeTLB page WP isn't possible. */
 		pagemap_scan_backout_range(p, start, end);
 		p->arg.walk_end = start;
author reviewer
#10 lore raw

PATCH 3/6 fs/proc/task_mmu: fix hugetlb self-deadlock in pagemap_scan_pte_hole()

In reply to Kiryl Shutsemau (Meta)

A PAGEMAP_SCAN ioctl requesting PM_SCAN_WP_MATCHING on a hugetlb VMA hangs the calling thread, unkillably, as soon as the scan reaches an unpopulated part of the range:

do_pagemap_scan()
walk_page_range()
walk_hugetlb_range()
hugetlb_vma_lock_read() # take the vma lock for read ...
pagemap_scan_pte_hole() # ... ->pte_hole() for a hole
uffd_wp_range()
change_protection()
hugetlb_change_protection()
hugetlb_vma_lock_write() # ... and block taking it for write

walk_hugetlb_range() holds the hugetlb vma lock for read across the whole walk. A present entry goes to ->hugetlb_entry(); an unpopulated one goes to ->pte_hole(), i.e. pagemap_scan_pte_hole(). To write-protect the hole that handler calls uffd_wp_range(), which on a hugetlb VMA reaches hugetlb_change_protection() and takes the same vma lock for write. The thread then blocks in down_write() waiting for the read lock it is itself holding.

The populated path avoids this: pagemap_scan_hugetlb_entry() write-protects the entry inline under the page-table lock and never enters hugetlb_change_protection().

Do the same for holes. Fault in the page table and install the uffd-wp marker directly with make_uffd_wp_huge_pte() under the page-table lock, rather than routing through uffd_wp_range(). That is the same sequence hugetlb_change_protection() runs for an unpopulated entry, minus the vma write lock -- which is safe to skip because PMD sharing is disabled on uffd-wp VMAs (hugetlb_unshare_all_pmds() runs at registration), leaving nothing for that lock to serialise against.

Fixes 52526ca7fdb9 ("fs/proc/task_mmu: implement IOCTL to get and optionally clear info about PTEs")
Assisted-by Claude:claude-opus-4-8
---
 fs/proc/task_mmu.c | 59 +++++++++++++++++++++++++++++++++++++++++++++-
 1 file changed, 58 insertions(+), 1 deletion(-)
fs/proc/task_mmu.c +58 -1
index 1489c67e88f7..06fb94a965ff 100644
@@ -2977,8 +2977,62 @@ static int pagemap_scan_hugetlb_entry(pte_t *ptep, unsigned long hmask,
 
 	return ret;
 }
+
+/*
+ * Write-protect the unpopulated hugetlb entries covering [addr, end) by
+ * installing uffd-wp markers inline, exactly as pagemap_scan_hugetlb_entry()
+ * does for populated entries.
+ *
+ * walk_hugetlb_range() currently calls ->pte_hole() once per huge page, so the
+ * loop normally runs a single iteration; it is written to cover the full range
+ * in case the walker ever coalesces adjacent holes.
+ *
+ * The obvious route -- uffd_wp_range() -> hugetlb_change_protection() --
+ * cannot be used here: it takes hugetlb_vma_lock_write(), but the page-table
+ * walker (walk_hugetlb_range()) already holds hugetlb_vma_lock_read() on the
+ * same VMA, so the scanning thread would deadlock against itself. PMD sharing
+ * is disabled on uffd-wp VMAs (hugetlb_unshare_all_pmds() at registration), so
+ * the vma lock guards nothing that matters for these entries anyway.
+ */
+static int pagemap_scan_hugetlb_hole_wp(struct vm_area_struct *vma,
+					unsigned long addr, unsigned long end)
+{
+	struct hstate *h = hstate_vma(vma);
+	unsigned long psize = huge_page_size(h);
+	struct mm_struct *mm = vma->vm_mm;
+	spinlock_t *ptl;
+	pte_t *ptep;
+	pte_t pte;
+
+	for (addr = ALIGN_DOWN(addr, psize); addr < end; addr += psize) {
+		ptep = huge_pte_alloc(mm, vma, addr, psize);
+		if (!ptep)
+			return -ENOMEM;
+
+		i_mmap_lock_write(vma->vm_file->f_mapping);
+		ptl = huge_pte_lock(h, mm, ptep);
+		pte = huge_ptep_get(mm, addr, ptep);
+		make_uffd_wp_huge_pte(vma, addr, ptep, pte);
+		/*
+		 * A none entry has no cached translation, so installing the
+		 * marker needs no TLB flush. Flush only if a fault populated
+		 * the entry between huge_pte_alloc() and the page table lock.
+		 */
+		if (!huge_pte_none(pte))
+			flush_hugetlb_tlb_range(vma, addr, addr + psize);
+		spin_unlock(ptl);
+		i_mmap_unlock_write(vma->vm_file->f_mapping);
+	}
+
+	return 0;
+}
 #else
 #define pagemap_scan_hugetlb_entry NULL
+static int pagemap_scan_hugetlb_hole_wp(struct vm_area_struct *vma,
+					unsigned long addr, unsigned long end)
+{
+	return 0;
+}
 #endif
 
 static int pagemap_scan_pte_hole(unsigned long addr, unsigned long end,
@@ -2998,7 +3052,10 @@ static int pagemap_scan_pte_hole(unsigned long addr, unsigned long end,
 	if (~p->arg.flags & PM_SCAN_WP_MATCHING)
 		return ret;
 
-	err = uffd_wp_range(vma, addr, end - addr, true);
+	if (is_vm_hugetlb_page(vma))
+		err = pagemap_scan_hugetlb_hole_wp(vma, addr, end);
+	else
+		err = uffd_wp_range(vma, addr, end - addr, true);
 	if (err < 0)
 		ret = err;
 
Signature · 1 line
2.54.0
author reviewer
#11 lore raw

PATCH 4/6 mm/huge_memory: preserve pmd_swp_uffd_wp on device-private PMD downgrade

In reply to Kiryl Shutsemau (Meta)

change_non_present_huge_pmd() rewrites a writable device-private PMD swap entry into a readable one without carrying pmd_swp_uffd_wp() across. The PTE-level change_softleaf_pte() does this correctly; mirror that here, matching what copy_huge_pmd() does for the fork path. Without the carry, a plain mprotect() over a UFFD_WP-marked device-private THP strips the bit and the trap is bypassed on swap-in.

Fixes 368076f52ebe ("mm/huge_memory: add device-private THP support to PMD operations")
---
 mm/huge_memory.c | 2 ++
 1 file changed, 2 insertions(+)
mm/huge_memory.c +2 -0
index 42b86e8ab7c0..b7c895b1d366 100644
@@ -2663,6 +2663,8 @@ static void change_non_present_huge_pmd(struct mm_struct *mm,
 	} else if (softleaf_is_device_private_write(entry)) {
 		entry = make_readable_device_private_entry(swp_offset(entry));
 		newpmd = swp_entry_to_pmd(entry);
+		if (pmd_swp_uffd_wp(*pmd))
+			newpmd = pmd_swp_mkuffd_wp(newpmd);
 	} else {
 		newpmd = *pmd;
 	}
Signature · 1 line
2.54.0
Reviewed-by on v1 4/6
#12 lore raw

Re: [PATCH 4/6] mm/huge_memory: preserve pmd_swp_uffd_wp on device-private PMD downgrade

Quoted 21 lines · Kiryl Shutsemau (Meta)

change_non_present_huge_pmd() rewrites a writable device-private PMD swap entry into a readable one without carrying pmd_swp_uffd_wp() across. The PTE-level change_softleaf_pte() does this correctly; mirror that here, matching what copy_huge_pmd() does for the fork path. Without the carry, a plain mprotect() over a UFFD_WP-marked device-private THP strips the bit and the trap is bypassed on swap-in.

Fixes 368076f52ebe ("mm/huge_memory: add device-private THP support to PMD operations")
---
 mm/huge_memory.c | 2 ++
 1 file changed, 2 insertions(+)
mm/huge_memory.c +0 -0
index 42b86e8ab7c0..b7c895b1d366 100644

mm/huge_memory.c static void change_non_present_huge_pmd(struct mm_struct *mm, · line 2670

Quoted 8 lines
mm/huge_memory.c +2 -0
@@ -2663,6 +2663,8 @@ static void change_non_present_huge_pmd(struct mm_struct *mm,
 	} else if (softleaf_is_device_private_write(entry)) {
 		entry = make_readable_device_private_entry(swp_offset(entry));
 		newpmd = swp_entry_to_pmd(entry);
+		if (pmd_swp_uffd_wp(*pmd))
+			newpmd = pmd_swp_mkuffd_wp(newpmd);
 	} else {
 		newpmd = *pmd;
 	}

Balbir

author reviewer
#13 lore raw

PATCH 5/6 userfaultfd: gate must_wait writability check on pte_present()

In reply to Kiryl Shutsemau (Meta)

userfaultfd_must_wait() and userfaultfd_huge_must_wait() read the PTE without taking the page table lock and then apply pte_write() / huge_pte_write() to it. Those accessors decode bits from the present encoding only; on a swap or migration entry they read the offset bits that happen to share the same position and return an undefined result.

The intent of the check is "is this fault still WP-blocked?". A non-marker swap entry means the page is in transit -- the userfault context the original fault delivered against is no longer the same, and the swap-in or migration completion path will re-deliver a fresh fault if userspace still needs to handle it. Worst case under the current code the garbage write bit says "wait", and the thread stays asleep until a UFFDIO_WAKE that may never arrive.

Gate the writability check on pte_present() so the lockless re-check only inspects present-PTE bits when the entry is actually present. The non-present, non-marker case returns "don't wait" and lets the fault path retry.

Fixes 369cd2121be4 ("userfaultfd: hugetlbfs: userfaultfd_huge_must_wait for hugepmd ranges")
Fixes 63b2d4174c4a ("userfaultfd: wp: add the writeprotect API to userfaultfd ioctl")
---
 mm/userfaultfd.c | 20 ++++++++++++++++++++
 1 file changed, 20 insertions(+)
mm/userfaultfd.c +20 -0
index 35b206cc9aa6..f6d2a1c67019 100644
@@ -2535,6 +2535,15 @@ static inline bool userfaultfd_huge_must_wait(struct userfaultfd_ctx *ctx,
 	/* UFFD PTE markers require userspace to resolve the fault. */
 	if (pte_is_uffd_marker(pte))
 		return true;
+	/*
+	 * Concurrent migration may have replaced the present PTE with a
+	 * non-marker swap entry between fault delivery and this lockless
+	 * re-check. huge_pte_write() on a swap entry decodes random offset
+	 * bits, so gate it on pte_present(). The migration completion path
+	 * will re-deliver the fault if it still needs userspace.
+	 */
+	if (!pte_present(pte))
+		return false;
 	/*
 	 * If VMA has UFFD WP faults enabled and WP fault, wait for userspace to
 	 * resolve the fault.
@@ -2621,6 +2630,17 @@ static inline bool userfaultfd_must_wait(struct userfaultfd_ctx *ctx,
 	/* UFFD PTE markers require userspace to resolve the fault. */
 	if (pte_is_uffd_marker(ptent))
 		goto out;
+	/*
+	 * Concurrent swap-out / migration may have replaced the present PTE
+	 * with a non-marker swap entry between fault delivery and this
+	 * lockless re-check. pte_write() on a swap entry decodes random
+	 * offset bits, so gate it on pte_present(). The page-in path will
+	 * re-deliver the fault if it still needs userspace.
+	 */
+	if (!pte_present(ptent)) {
+		ret = false;
+		goto out;
+	}
 	/*
 	 * If VMA has UFFD WP faults enabled and WP fault, wait for userspace to
 	 * resolve the fault.
Signature · 1 line
2.54.0
maintainer Reviewed-by on v1 5/6
#14 lore raw

Re: [PATCH 5/6] userfaultfd: gate must_wait writability check on pte_present()

Quoted 19 lines · Kiryl Shutsemau (Meta)

userfaultfd_must_wait() and userfaultfd_huge_must_wait() read the PTE without taking the page table lock and then apply pte_write() / huge_pte_write() to it. Those accessors decode bits from the present encoding only; on a swap or migration entry they read the offset bits that happen to share the same position and return an undefined result.

The intent of the check is "is this fault still WP-blocked?". A non-marker swap entry means the page is in transit -- the userfault context the original fault delivered against is no longer the same, and the swap-in or migration completion path will re-deliver a fresh fault if userspace still needs to handle it. Worst case under the current code the garbage write bit says "wait", and the thread stays asleep until a UFFDIO_WAKE that may never arrive.

Gate the writability check on pte_present() so the lockless re-check only inspects present-PTE bits when the entry is actually present. The non-present, non-marker case returns "don't wait" and lets the fault path retry.

Quoted 5 lines
Fixes 369cd2121be4 ("userfaultfd: hugetlbfs: userfaultfd_huge_must_wait for hugepmd ranges")
Fixes 63b2d4174c4a ("userfaultfd: wp: add the writeprotect API to userfaultfd ioctl")

One tiny nit is maybe could mention softleaf :P but it's not important!

LGTM, so:

Quoted 45 lines
---
 mm/userfaultfd.c | 20 ++++++++++++++++++++
 1 file changed, 20 insertions(+)
mm/userfaultfd.c +20 -1
index 35b206cc9aa6..f6d2a1c67019 100644
@@ -2535,6 +2535,15 @@ static inline bool userfaultfd_huge_must_wait(struct userfaultfd_ctx *ctx,
 	/* UFFD PTE markers require userspace to resolve the fault. */
 	if (pte_is_uffd_marker(pte))
 		return true;
+	/*
+	 * Concurrent migration may have replaced the present PTE with a
+	 * non-marker swap entry between fault delivery and this lockless
+	 * re-check. huge_pte_write() on a swap entry decodes random offset
+	 * bits, so gate it on pte_present(). The migration completion path
+	 * will re-deliver the fault if it still needs userspace.
+	 */
+	if (!pte_present(pte))
+		return false;
 	/*
 	 * If VMA has UFFD WP faults enabled and WP fault, wait for userspace to
 	 * resolve the fault.
@@ -2621,6 +2630,17 @@ static inline bool userfaultfd_must_wait(struct userfaultfd_ctx *ctx,
 	/* UFFD PTE markers require userspace to resolve the fault. */
 	if (pte_is_uffd_marker(ptent))
 		goto out;
+	/*
+	 * Concurrent swap-out / migration may have replaced the present PTE
+	 * with a non-marker swap entry between fault delivery and this
+	 * lockless re-check. pte_write() on a swap entry decodes random
+	 * offset bits, so gate it on pte_present(). The page-in path will
+	 * re-deliver the fault if it still needs userspace.
+	 */
+	if (!pte_present(ptent)) {
+		ret = false;
+		goto out;
+	}
 	/*
 	 * If VMA has UFFD WP faults enabled and WP fault, wait for userspace to
 	 * resolve the fault.
--

2.54.0

maintainer Reviewed-by on v1 5/6
#15 lore raw

Re: [PATCH 5/6] userfaultfd: gate must_wait writability check on pte_present()

In reply to Kiryl Shutsemau (Meta)
Quoted 19 lines · Kiryl Shutsemau (Meta)

userfaultfd_must_wait() and userfaultfd_huge_must_wait() read the PTE without taking the page table lock and then apply pte_write() / huge_pte_write() to it. Those accessors decode bits from the present encoding only; on a swap or migration entry they read the offset bits that happen to share the same position and return an undefined result.

The intent of the check is "is this fault still WP-blocked?". A non-marker swap entry means the page is in transit -- the userfault context the original fault delivered against is no longer the same, and the swap-in or migration completion path will re-deliver a fresh fault if userspace still needs to handle it. Worst case under the current code the garbage write bit says "wait", and the thread stays asleep until a UFFDIO_WAKE that may never arrive.

Gate the writability check on pte_present() so the lockless re-check only inspects present-PTE bits when the entry is actually present. The non-present, non-marker case returns "don't wait" and lets the fault path retry.

Quoted 5 lines
Fixes 369cd2121be4 ("userfaultfd: hugetlbfs: userfaultfd_huge_must_wait for hugepmd ranges")
Fixes 63b2d4174c4a ("userfaultfd: wp: add the writeprotect API to userfaultfd ioctl")
Quoted 3 lines
---
 mm/userfaultfd.c | 20 ++++++++++++++++++++
 1 file changed, 20 insertions(+)
Signature · 2 lines
Sincerely yours,
Mike.
author reviewer
#16 lore raw

PATCH 6/6 userfaultfd: build __VMA_UFFD_FLAGS from config-gated masks

In reply to Kiryl Shutsemau (Meta)

The VMA flags bitmap is a single word today: NUM_VMA_FLAG_BITS is BITS_PER_LONG, so on 32-bit vma_flags_t holds only 32 bits. (The bitmap type exists so this can grow past BITS_PER_LONG later; until it does, anything declared above the first word is out of range on 32-bit.) The bit enum nevertheless declares some bits unconditionally above BITS_PER_LONG -- VMA_UFFD_MINOR_BIT is 41, with VM_UFFD_MINOR == VM_NONE on 32-bit so no VMA actually carries the bit.

__VMA_UFFD_FLAGS feeds VMA_UFFD_MINOR_BIT to mk_vma_flags() unconditionally. On 32-bit that becomes __set_bit(41, &one_long), a write one word past the end of the single-word bitmap. The compiler folds the out-of-bounds store with wraparound (1UL << (41 % 32) == bit 9) into the first word; bit 9 is already in __VMA_UFFD_FLAGS so the mask happens to come out right today, but it is an out-of-bounds write all the same, and any high-numbered bit whose mod-BITS_PER_LONG position is otherwise unused would silently OR an extra bit into the mask.

Rather than feed bit numbers that may not exist on the current build to mk_vma_flags(), build the mask from whole per-mode masks that collapse to EMPTY_VMA_FLAGS when their feature is unavailable. Add mk_vma_flags_from_masks() for that, and define VMA_UFFD_MISSING / _WP / _MINOR alongside the VM_UFFD_* flags, gating VMA_UFFD_MINOR on the same config as VM_UFFD_MINOR (which implies 64BIT, where bit 41 fits). An out-of-range bit is then never materialised, on any arch, and the in-range fast path stays a compile-time constant.

Fixes 9ea35a25d51b ("mm: introduce VMA flags bitmap type")
Assisted-by Claude:claude-opus-4-8
---
 include/linux/mm.h            | 39 +++++++++++++++++++++++++++++++++++
 include/linux/userfaultfd_k.h |  4 ++--
 2 files changed, 41 insertions(+), 2 deletions(-)
include/linux/mm.h +39 -0
index 0f2612a70fb1..485df9c2dbdd 100644
@@ -496,6 +496,21 @@ enum {
 #else
 #define VM_UFFD_MINOR	VM_NONE
 #endif
+
+/*
+ * vma_flags_t masks for the userfaultfd VMA flags. VMA_UFFD_MINOR is gated on
+ * the same config as VM_UFFD_MINOR -- which implies 64BIT, where the bit fits
+ * -- so an out-of-range bit is never fed to mk_vma_flags() on a build whose
+ * bitmap cannot hold it.
+ */
+#define VMA_UFFD_MISSING	mk_vma_flags(VMA_UFFD_MISSING_BIT)
+#define VMA_UFFD_WP		mk_vma_flags(VMA_UFFD_WP_BIT)
+#ifdef CONFIG_HAVE_ARCH_USERFAULTFD_MINOR
+#define VMA_UFFD_MINOR		mk_vma_flags(VMA_UFFD_MINOR_BIT)
+#else
+#define VMA_UFFD_MINOR		EMPTY_VMA_FLAGS
+#endif
+
 #ifdef CONFIG_64BIT
 #define VM_ALLOW_ANY_UNCACHED	INIT_VM_FLAG(ALLOW_ANY_UNCACHED)
 #define VM_SEALED		INIT_VM_FLAG(SEALED)
@@ -1238,6 +1253,30 @@ static __always_inline void vma_flags_set_mask(vma_flags_t *flags,
 #define vma_flags_set(flags, ...) \
 	vma_flags_set_mask(flags, mk_vma_flags(__VA_ARGS__))
 
+static __always_inline vma_flags_t __mk_vma_flags_from_masks(size_t count,
+		const vma_flags_t *masks)
+{
+	vma_flags_t flags = EMPTY_VMA_FLAGS;
+	size_t i;
+
+	for (i = 0; i < count; i++)
+		vma_flags_set_mask(&flags, masks[i]);
+	return flags;
+}
+
+/*
+ * Combine pre-computed vma_flags_t masks into one value, e.g.:
+ *
+ * vma_flags_t flags = mk_vma_flags_from_masks(VMA_UFFD_WP, VMA_UFFD_MINOR);
+ *
+ * Unlike mk_vma_flags(), which takes bit numbers, this takes whole masks --
+ * each of which may be EMPTY_VMA_FLAGS when its feature is unavailable -- so a
+ * bit that does not exist on the current build is never materialised.
+ */
+#define mk_vma_flags_from_masks(...)					\
+	__mk_vma_flags_from_masks(COUNT_ARGS(__VA_ARGS__),		\
+		(const vma_flags_t []){__VA_ARGS__})
+
 /* Clear all of the to-clear flags in flags, non-atomically. */
 static __always_inline void vma_flags_clear_mask(vma_flags_t *flags,
 		vma_flags_t to_clear)
include/linux/userfaultfd_k.h +2 -2
index 3ec8e1071673..68edac4dcd78 100644
@@ -23,8 +23,8 @@
 /* The set of all possible UFFD-related VM flags. */
 #define __VM_UFFD_FLAGS (VM_UFFD_MISSING | VM_UFFD_WP | VM_UFFD_MINOR)
 
-#define __VMA_UFFD_FLAGS mk_vma_flags(VMA_UFFD_MISSING_BIT, VMA_UFFD_WP_BIT, \
-				      VMA_UFFD_MINOR_BIT)
+#define __VMA_UFFD_FLAGS mk_vma_flags_from_masks(VMA_UFFD_MISSING, VMA_UFFD_WP, \
+						 VMA_UFFD_MINOR)
 
 /*
  * CAREFUL: Check include/uapi/asm-generic/fcntl.h when defining
Signature · 1 line
2.54.0
maintainer Reviewed-by on v1 6/6
#17 lore raw

Re: [PATCH 6/6] userfaultfd: build __VMA_UFFD_FLAGS from config-gated masks

Quoted 26 lines · Kiryl Shutsemau (Meta)

The VMA flags bitmap is a single word today: NUM_VMA_FLAG_BITS is BITS_PER_LONG, so on 32-bit vma_flags_t holds only 32 bits. (The bitmap type exists so this can grow past BITS_PER_LONG later; until it does, anything declared above the first word is out of range on 32-bit.) The bit enum nevertheless declares some bits unconditionally above BITS_PER_LONG -- VMA_UFFD_MINOR_BIT is 41, with VM_UFFD_MINOR == VM_NONE on 32-bit so no VMA actually carries the bit.

__VMA_UFFD_FLAGS feeds VMA_UFFD_MINOR_BIT to mk_vma_flags() unconditionally. On 32-bit that becomes __set_bit(41, &one_long), a write one word past the end of the single-word bitmap. The compiler folds the out-of-bounds store with wraparound (1UL << (41 % 32) == bit 9) into the first word; bit 9 is already in __VMA_UFFD_FLAGS so the mask happens to come out right today, but it is an out-of-bounds write all the same, and any high-numbered bit whose mod-BITS_PER_LONG position is otherwise unused would silently OR an extra bit into the mask.

Rather than feed bit numbers that may not exist on the current build to mk_vma_flags(), build the mask from whole per-mode masks that collapse to EMPTY_VMA_FLAGS when their feature is unavailable. Add mk_vma_flags_from_masks() for that, and define VMA_UFFD_MISSING / _WP / _MINOR alongside the VM_UFFD_* flags, gating VMA_UFFD_MINOR on the same config as VM_UFFD_MINOR (which implies 64BIT, where bit 41 fits). An out-of-range bit is then never materialised, on any arch, and the in-range fast path stays a compile-time constant.

Quoted 1 line
Fixes 9ea35a25d51b ("mm: introduce VMA flags bitmap type")

Hmm, I think commit a06eb2f8279e ("mm/vma: convert vma_modify_flags[_uffd]() to use vma_flags_t") is more appropriate isn't it?

As that's the commit that added __VMA_UFFD_FLAGS :)

Quoted 4 lines

The change LGTM, so:

Quoted 81 lines
Assisted-by Claude:claude-opus-4-8
---
 include/linux/mm.h            | 39 +++++++++++++++++++++++++++++++++++
 include/linux/userfaultfd_k.h |  4 ++--
 2 files changed, 41 insertions(+), 2 deletions(-)
include/linux/mm.h +39 -0
index 0f2612a70fb1..485df9c2dbdd 100644
@@ -496,6 +496,21 @@ enum {
 #else
 #define VM_UFFD_MINOR	VM_NONE
 #endif
+
+/*
+ * vma_flags_t masks for the userfaultfd VMA flags. VMA_UFFD_MINOR is gated on
+ * the same config as VM_UFFD_MINOR -- which implies 64BIT, where the bit fits
+ * -- so an out-of-range bit is never fed to mk_vma_flags() on a build whose
+ * bitmap cannot hold it.
+ */
+#define VMA_UFFD_MISSING	mk_vma_flags(VMA_UFFD_MISSING_BIT)
+#define VMA_UFFD_WP		mk_vma_flags(VMA_UFFD_WP_BIT)
+#ifdef CONFIG_HAVE_ARCH_USERFAULTFD_MINOR
+#define VMA_UFFD_MINOR		mk_vma_flags(VMA_UFFD_MINOR_BIT)
+#else
+#define VMA_UFFD_MINOR		EMPTY_VMA_FLAGS
+#endif
+
 #ifdef CONFIG_64BIT
 #define VM_ALLOW_ANY_UNCACHED	INIT_VM_FLAG(ALLOW_ANY_UNCACHED)
 #define VM_SEALED		INIT_VM_FLAG(SEALED)
@@ -1238,6 +1253,30 @@ static __always_inline void vma_flags_set_mask(vma_flags_t *flags,
 #define vma_flags_set(flags, ...) \
 	vma_flags_set_mask(flags, mk_vma_flags(__VA_ARGS__))
 
+static __always_inline vma_flags_t __mk_vma_flags_from_masks(size_t count,
+		const vma_flags_t *masks)
+{
+	vma_flags_t flags = EMPTY_VMA_FLAGS;
+	size_t i;
+
+	for (i = 0; i < count; i++)
+		vma_flags_set_mask(&flags, masks[i]);
+	return flags;
+}
+
+/*
+ * Combine pre-computed vma_flags_t masks into one value, e.g.:
+ *
+ * vma_flags_t flags = mk_vma_flags_from_masks(VMA_UFFD_WP, VMA_UFFD_MINOR);
+ *
+ * Unlike mk_vma_flags(), which takes bit numbers, this takes whole masks --
+ * each of which may be EMPTY_VMA_FLAGS when its feature is unavailable -- so a
+ * bit that does not exist on the current build is never materialised.
+ */
+#define mk_vma_flags_from_masks(...)					\
+	__mk_vma_flags_from_masks(COUNT_ARGS(__VA_ARGS__),		\
+		(const vma_flags_t []){__VA_ARGS__})
+
 /* Clear all of the to-clear flags in flags, non-atomically. */
 static __always_inline void vma_flags_clear_mask(vma_flags_t *flags,
 		vma_flags_t to_clear)
include/linux/userfaultfd_k.h +2 -3
index 3ec8e1071673..68edac4dcd78 100644
@@ -23,8 +23,8 @@
 /* The set of all possible UFFD-related VM flags. */
 #define __VM_UFFD_FLAGS (VM_UFFD_MISSING | VM_UFFD_WP | VM_UFFD_MINOR)
 
-#define __VMA_UFFD_FLAGS mk_vma_flags(VMA_UFFD_MISSING_BIT, VMA_UFFD_WP_BIT, \
-				      VMA_UFFD_MINOR_BIT)
+#define __VMA_UFFD_FLAGS mk_vma_flags_from_masks(VMA_UFFD_MISSING, VMA_UFFD_WP, \
+						 VMA_UFFD_MINOR)
 
 /*
  * CAREFUL: Check include/uapi/asm-generic/fcntl.h when defining
--

2.54.0

maintainer Reviewed-by on v1 6/6
#18 lore raw

Re: [PATCH 6/6] userfaultfd: build __VMA_UFFD_FLAGS from config-gated masks

In reply to Kiryl Shutsemau (Meta)
Quoted 26 lines · Kiryl Shutsemau (Meta)

The VMA flags bitmap is a single word today: NUM_VMA_FLAG_BITS is BITS_PER_LONG, so on 32-bit vma_flags_t holds only 32 bits. (The bitmap type exists so this can grow past BITS_PER_LONG later; until it does, anything declared above the first word is out of range on 32-bit.) The bit enum nevertheless declares some bits unconditionally above BITS_PER_LONG -- VMA_UFFD_MINOR_BIT is 41, with VM_UFFD_MINOR == VM_NONE on 32-bit so no VMA actually carries the bit.

__VMA_UFFD_FLAGS feeds VMA_UFFD_MINOR_BIT to mk_vma_flags() unconditionally. On 32-bit that becomes __set_bit(41, &one_long), a write one word past the end of the single-word bitmap. The compiler folds the out-of-bounds store with wraparound (1UL << (41 % 32) == bit 9) into the first word; bit 9 is already in __VMA_UFFD_FLAGS so the mask happens to come out right today, but it is an out-of-bounds write all the same, and any high-numbered bit whose mod-BITS_PER_LONG position is otherwise unused would silently OR an extra bit into the mask.

Rather than feed bit numbers that may not exist on the current build to mk_vma_flags(), build the mask from whole per-mode masks that collapse to EMPTY_VMA_FLAGS when their feature is unavailable. Add mk_vma_flags_from_masks() for that, and define VMA_UFFD_MISSING / _WP / _MINOR alongside the VM_UFFD_* flags, gating VMA_UFFD_MINOR on the same config as VM_UFFD_MINOR (which implies 64BIT, where bit 41 fits). An out-of-range bit is then never materialised, on any arch, and the in-range fast path stays a compile-time constant.

Quoted 6 lines
Fixes 9ea35a25d51b ("mm: introduce VMA flags bitmap type")
Assisted-by Claude:claude-opus-4-8

Can you ask claude to produce more concise changelogs and better split it to paragraphs?

Quoted 80 lines
---
 include/linux/mm.h            | 39 +++++++++++++++++++++++++++++++++++
 include/linux/userfaultfd_k.h |  4 ++--
 2 files changed, 41 insertions(+), 2 deletions(-)
include/linux/mm.h +39 -0
index 0f2612a70fb1..485df9c2dbdd 100644
@@ -496,6 +496,21 @@ enum {
 #else
 #define VM_UFFD_MINOR	VM_NONE
 #endif
+
+/*
+ * vma_flags_t masks for the userfaultfd VMA flags. VMA_UFFD_MINOR is gated on
+ * the same config as VM_UFFD_MINOR -- which implies 64BIT, where the bit fits
+ * -- so an out-of-range bit is never fed to mk_vma_flags() on a build whose
+ * bitmap cannot hold it.
+ */
+#define VMA_UFFD_MISSING	mk_vma_flags(VMA_UFFD_MISSING_BIT)
+#define VMA_UFFD_WP		mk_vma_flags(VMA_UFFD_WP_BIT)
+#ifdef CONFIG_HAVE_ARCH_USERFAULTFD_MINOR
+#define VMA_UFFD_MINOR		mk_vma_flags(VMA_UFFD_MINOR_BIT)
+#else
+#define VMA_UFFD_MINOR		EMPTY_VMA_FLAGS
+#endif
+
 #ifdef CONFIG_64BIT
 #define VM_ALLOW_ANY_UNCACHED	INIT_VM_FLAG(ALLOW_ANY_UNCACHED)
 #define VM_SEALED		INIT_VM_FLAG(SEALED)
@@ -1238,6 +1253,30 @@ static __always_inline void vma_flags_set_mask(vma_flags_t *flags,
 #define vma_flags_set(flags, ...) \
 	vma_flags_set_mask(flags, mk_vma_flags(__VA_ARGS__))
 
+static __always_inline vma_flags_t __mk_vma_flags_from_masks(size_t count,
+		const vma_flags_t *masks)
+{
+	vma_flags_t flags = EMPTY_VMA_FLAGS;
+	size_t i;
+
+	for (i = 0; i < count; i++)
+		vma_flags_set_mask(&flags, masks[i]);
+	return flags;
+}
+
+/*
+ * Combine pre-computed vma_flags_t masks into one value, e.g.:
+ *
+ * vma_flags_t flags = mk_vma_flags_from_masks(VMA_UFFD_WP, VMA_UFFD_MINOR);
+ *
+ * Unlike mk_vma_flags(), which takes bit numbers, this takes whole masks --
+ * each of which may be EMPTY_VMA_FLAGS when its feature is unavailable -- so a
+ * bit that does not exist on the current build is never materialised.
+ */
+#define mk_vma_flags_from_masks(...)					\
+	__mk_vma_flags_from_masks(COUNT_ARGS(__VA_ARGS__),		\
+		(const vma_flags_t []){__VA_ARGS__})
+
 /* Clear all of the to-clear flags in flags, non-atomically. */
 static __always_inline void vma_flags_clear_mask(vma_flags_t *flags,
 		vma_flags_t to_clear)
include/linux/userfaultfd_k.h +2 -2
index 3ec8e1071673..68edac4dcd78 100644
@@ -23,8 +23,8 @@
 /* The set of all possible UFFD-related VM flags. */
 #define __VM_UFFD_FLAGS (VM_UFFD_MISSING | VM_UFFD_WP | VM_UFFD_MINOR)
 
-#define __VMA_UFFD_FLAGS mk_vma_flags(VMA_UFFD_MISSING_BIT, VMA_UFFD_WP_BIT, \
-				      VMA_UFFD_MINOR_BIT)
+#define __VMA_UFFD_FLAGS mk_vma_flags_from_masks(VMA_UFFD_MISSING, VMA_UFFD_WP, \
+						 VMA_UFFD_MINOR)
 
 /*
  * CAREFUL: Check include/uapi/asm-generic/fcntl.h when defining
Signature · 1 line
2.54.0
Signature · 2 lines
Sincerely yours,
Mike.
author reviewer
#19 lore raw

Re: [PATCH 6/6] userfaultfd: build __VMA_UFFD_FLAGS from config-gated masks

Quoted 34 lines · Mike Rapoport
Quoted 26 lines · Kiryl Shutsemau (Meta)

The VMA flags bitmap is a single word today: NUM_VMA_FLAG_BITS is BITS_PER_LONG, so on 32-bit vma_flags_t holds only 32 bits. (The bitmap type exists so this can grow past BITS_PER_LONG later; until it does, anything declared above the first word is out of range on 32-bit.) The bit enum nevertheless declares some bits unconditionally above BITS_PER_LONG -- VMA_UFFD_MINOR_BIT is 41, with VM_UFFD_MINOR == VM_NONE on 32-bit so no VMA actually carries the bit.

__VMA_UFFD_FLAGS feeds VMA_UFFD_MINOR_BIT to mk_vma_flags() unconditionally. On 32-bit that becomes __set_bit(41, &one_long), a write one word past the end of the single-word bitmap. The compiler folds the out-of-bounds store with wraparound (1UL << (41 % 32) == bit 9) into the first word; bit 9 is already in __VMA_UFFD_FLAGS so the mask happens to come out right today, but it is an out-of-bounds write all the same, and any high-numbered bit whose mod-BITS_PER_LONG position is otherwise unused would silently OR an extra bit into the mask.

Rather than feed bit numbers that may not exist on the current build to mk_vma_flags(), build the mask from whole per-mode masks that collapse to EMPTY_VMA_FLAGS when their feature is unavailable. Add mk_vma_flags_from_masks() for that, and define VMA_UFFD_MISSING / _WP / _MINOR alongside the VM_UFFD_* flags, gating VMA_UFFD_MINOR on the same config as VM_UFFD_MINOR (which implies 64BIT, where bit 41 fits). An out-of-range bit is then never materialised, on any arch, and the in-range fast path stays a compile-time constant.

Quoted 6 lines
Fixes 9ea35a25d51b ("mm: introduce VMA flags bitmap type")
Assisted-by Claude:claude-opus-4-8
Quoted 2 lines

Can you ask claude to produce more concise changelogs and better split it to paragraphs?

Will do!

Signature · 1 line
  Kiryl Shutsemau / Kirill A. Shutemov
maintainer
#20 lore raw
In reply to Kiryl Shutsemau (Meta)
Quoted 9 lines · On Fri, 29 May 2026 18:23:24 +0100 "Kiryl Shutsemau (Meta)" <[email protected]>…

These are pre-existing bug fixes that were carried at the front of the userfaultfd RWP working-set-tracking series up to v5 [1]. Per review feedback that fixes should not sit in the middle of a feature series, they are split out and sent on their own; the RWP series is reposted rebased on top of this.

All six were flagged by the Sashiko AI review of the RWP series and carry Reported-by: Sashiko AI review <[email protected]>. They are independent of RWP, apply to mm-new directly, and carry Cc: stable@.

And... this made Sashiko point at other stuff:

	https://sashiko.dev/#/patchset/[email protected]

I can't figure out why four of the scans failed - "View Raw Log" comes up empty.

It's concerning how frequently Sashiko is finding pre-existing things. What would a full scan of mm/ tell us? Gulp.

author reviewer
#21 lore raw
Quoted 8 lines · Andrew Morton
Quoted 6 lines · On Fri, 29 May 2026 18:23:24 +0100 "Kiryl Shutsemau (Meta)" <[email protected]>…

These are pre-existing bug fixes that were carried at the front of the userfaultfd RWP working-set-tracking series up to v5 [1]. Per review feedback that fixes should not sit in the middle of a feature series, they are split out and sent on their own; the RWP series is reposted rebased on top of this.

Quoted 8 lines
Quoted 3 lines

All six were flagged by the Sashiko AI review of the RWP series and carry Reported-by: Sashiko AI review <[email protected]>. They are independent of RWP, apply to mm-new directly, and carry Cc: stable@.

And... this made Sashiko point at other stuff:

	https://sashiko.dev/#/patchset/[email protected]

I can't figure out why four of the scans failed - "View Raw Log" comes up empty.

Seems to be passed now.

Quoted 2 lines

It's concerning how frequently Sashiko is finding pre-existing things. What would a full scan of mm/ tell us? Gulp.

Yeah... :/

I will look at the new batch of pre-existing issues when I have spare cycles. I am not sure I have the bandwidth to find the bottom of this rabbit hole.

What do you want me to with the main userfaultfd patchset? Sashiko failed to apply it because it is on top of the pre-existing fixes.

Signature · 1 line
  Kiryl Shutsemau / Kirill A. Shutemov
#22 lore raw
Quoted 23 lines · Kiryl Shutsemau
Quoted 8 lines · Andrew Morton
Quoted 6 lines · On Fri, 29 May 2026 18:23:24 +0100 "Kiryl Shutsemau (Meta)" <[email protected]>…

These are pre-existing bug fixes that were carried at the front of the userfaultfd RWP working-set-tracking series up to v5 [1]. Per review feedback that fixes should not sit in the middle of a feature series, they are split out and sent on their own; the RWP series is reposted rebased on top of this.

Quoted 8 lines
Quoted 3 lines

All six were flagged by the Sashiko AI review of the RWP series and carry Reported-by: Sashiko AI review <[email protected]>. They are independent of RWP, apply to mm-new directly, and carry Cc: stable@.

And... this made Sashiko point at other stuff:

	https://sashiko.dev/#/patchset/[email protected]

I can't figure out why four of the scans failed - "View Raw Log" comes up empty.

Seems to be passed now.

Quoted 2 lines

It's concerning how frequently Sashiko is finding pre-existing things. What would a full scan of mm/ tell us? Gulp.

Quoted 8 lines

Yeah... :/

I will look at the new batch of pre-existing issues when I have spare cycles. I am not sure I have the bandwidth to find the bottom of this rabbit hole.

What do you want me to with the main userfaultfd patchset? Sashiko failed to apply it because it is on top of the pre-existing fixes.

I suspect it might have a minor conflict with another incoming fix from Lorenzo as well.

Probably put it into mm-unstable early after the next release? At least I think it's not in mm-unstable yet and we are approaching the net release quickly ...

Signature · 3 lines
Cheers,

David
author reviewer
#23 lore raw
Quoted 30 lines · David Hildenbrand (Arm)
Quoted 29 lines · Kiryl Shutsemau
Quoted 8 lines · Andrew Morton
Quoted 6 lines · On Fri, 29 May 2026 18:23:24 +0100 "Kiryl Shutsemau (Meta)" <[email protected]>…

These are pre-existing bug fixes that were carried at the front of the userfaultfd RWP working-set-tracking series up to v5 [1]. Per review feedback that fixes should not sit in the middle of a feature series, they are split out and sent on their own; the RWP series is reposted rebased on top of this.

Quoted 8 lines
Quoted 3 lines

All six were flagged by the Sashiko AI review of the RWP series and carry Reported-by: Sashiko AI review <[email protected]>. They are independent of RWP, apply to mm-new directly, and carry Cc: stable@.

And... this made Sashiko point at other stuff:

	https://sashiko.dev/#/patchset/[email protected]

I can't figure out why four of the scans failed - "View Raw Log" comes up empty.

Seems to be passed now.

Quoted 2 lines

It's concerning how frequently Sashiko is finding pre-existing things. What would a full scan of mm/ tell us? Gulp.

Yeah... :/

I will look at the new batch of pre-existing issues when I have spare cycles. I am not sure I have the bandwidth to find the bottom of this rabbit hole.

Quoted 8 lines
Quoted 2 lines

What do you want me to with the main userfaultfd patchset? Sashiko failed to apply it because it is on top of the pre-existing fixes.

I suspect it might have a minor conflict with another incoming fix from Lorenzo as well.

Probably put it into mm-unstable early after the next release? At least I think it's not in mm-unstable yet and we are approaching the net release quickly ...

I guess I would need to patch man-pages that got applied. I put 7.2 there...

Signature · 1 line
  Kiryl Shutsemau / Kirill A. Shutemov
#24 lore raw
Quoted 15 lines · Kiryl Shutsemau
Quoted 14 lines · David Hildenbrand (Arm)
Quoted 12 lines · Kiryl Shutsemau

Seems to be passed now.

Yeah... :/

I will look at the new batch of pre-existing issues when I have spare cycles. I am not sure I have the bandwidth to find the bottom of this rabbit hole.

What do you want me to with the main userfaultfd patchset? Sashiko failed to apply it because it is on top of the pre-existing fixes.

Quoted 8 lines
Quoted 5 lines

I suspect it might have a minor conflict with another incoming fix from Lorenzo as well.

Probably put it into mm-unstable early after the next release? At least I think it's not in mm-unstable yet and we are approaching the net release quickly ...

I guess I would need to patch man-pages that got applied. I put 7.2 there...

We really should merge man page updates only after stuff is definitely in. :)

Signature · 3 lines
Cheers,

David
maintainer
#25 lore raw
In reply to Andrew Morton
Quoted 12 lines · Andrew Morton
Quoted 9 lines · On Fri, 29 May 2026 18:23:24 +0100 "Kiryl Shutsemau (Meta)" <[email protected]>…

These are pre-existing bug fixes that were carried at the front of the userfaultfd RWP working-set-tracking series up to v5 [1]. Per review feedback that fixes should not sit in the middle of a feature series, they are split out and sent on their own; the RWP series is reposted rebased on top of this.

All six were flagged by the Sashiko AI review of the RWP series and carry Reported-by: Sashiko AI review <[email protected]>. They are independent of RWP, apply to mm-new directly, and carry Cc: stable@.

Quoted 8 lines

And... this made Sashiko point at other stuff:

	https://sashiko.dev/#/patchset/[email protected]

I can't figure out why four of the scans failed - "View Raw Log" comes up empty.

It's concerning how frequently Sashiko is finding pre-existing things. What would a full scan of mm/ tell us? Gulp.

Yeah, but I also kinda wish it didn't do this :) It's similar to sending a series, and somebody replying saying 'well this is totally unrelated but <blah blah blah>' and expecting a reply :>)

You'd want that person to just submit a patch for the issue or a [DISCUSSION] or [BUG] thread or something, but interrupting, possibly delaying + taking people's (usually very limited) time on something unrelated is just unhelpful I think.

I'd prefer Sashiko had a passive mode that checked for existing stuff elsewhere, and it saved the tokens doing it for a new series :)

Just my 2 pence!

Cheers, Lorenzo