[PATCH v4 04/12] mm/memory: Batch set uffd-wp markers during zapping
From: Dev Jain
Date: Tue May 26 2026 - 02:37:45 EST
In preparation for the next patch, enable batch setting of uffd-wp ptes.
The code paths passing nr > 1 to zap_install_uffd_wp_if_needed() produce
that nr through either folio_pte_batch or swap_pte_batch, therefore
batching is correct:
1) all ptes belong to the same type of VMA (anonymous or non-anonymous,
wp-armed or non-wp-armed)
2) all ptes being marked with uffd-wp or all being not marked (same is the
case with the pte_swp_uffd_wp_any check)
3) uffd_supports_wp_marker() is independent of the function parameters
Note that we will have to use set_pte_at() in a loop instead of set_ptes()
since the latter cannot handle present->non-present conversion for
nr_pages > 1.
Rename the function to cond_install_uffd_wp_ptes, and convert the
documentation to kerneldoc format.
Move the function to memory.c since this has grown too long to be kept
in mm_inline.h, while retaining the inline hint.
Rename pte->ptep and pteval->pte.
Signed-off-by: Dev Jain <dev.jain@xxxxxxx>
---
include/linux/mm.h | 4 ++
include/linux/mm_inline.h | 53 -------------------------
mm/memory.c | 81 ++++++++++++++++++++++++++++++---------
mm/rmap.c | 2 +-
4 files changed, 67 insertions(+), 73 deletions(-)
diff --git a/include/linux/mm.h b/include/linux/mm.h
index 31e27ff6a35fa..3169bd6d69f5a 100644
--- a/include/linux/mm.h
+++ b/include/linux/mm.h
@@ -5216,4 +5216,8 @@ void map_anon_folio_pte_nopf(struct folio *folio, pte_t *pte,
struct vm_area_struct *vma, unsigned long addr,
bool uffd_wp);
+bool cond_install_uffd_wp_ptes(struct vm_area_struct *vma, unsigned long addr,
+ pte_t *ptep, pte_t pte, unsigned long nr_ptes);
+
+
#endif /* _LINUX_MM_H */
diff --git a/include/linux/mm_inline.h b/include/linux/mm_inline.h
index a171070e15f05..1a65c2bda2398 100644
--- a/include/linux/mm_inline.h
+++ b/include/linux/mm_inline.h
@@ -566,59 +566,6 @@ static inline pte_marker copy_pte_marker(
return dstm;
}
-/*
- * If this pte is wr-protected by uffd-wp in any form, arm the special pte to
- * replace a none pte. NOTE! This should only be called when *pte is already
- * cleared so we will never accidentally replace something valuable. Meanwhile
- * none pte also means we are not demoting the pte so tlb flushed is not needed.
- * E.g., when pte cleared the caller should have taken care of the tlb flush.
- *
- * Must be called with pgtable lock held so that no thread will see the none
- * pte, and if they see it, they'll fault and serialize at the pgtable lock.
- *
- * Returns true if an uffd-wp pte was installed, false otherwise.
- */
-static inline bool
-pte_install_uffd_wp_if_needed(struct vm_area_struct *vma, unsigned long addr,
- pte_t *pte, pte_t pteval)
-{
- bool arm_uffd_pte = false;
-
- if (!uffd_supports_wp_marker())
- return false;
-
- /* The current status of the pte should be "cleared" before calling */
- WARN_ON_ONCE(!pte_none(ptep_get(pte)));
-
- /*
- * NOTE: userfaultfd_wp_unpopulated() doesn't need this whole
- * thing, because when zapping either it means it's dropping the
- * page, or in TTU where the present pte will be quickly replaced
- * with a swap pte. There's no way of leaking the bit.
- */
- if (vma_is_anonymous(vma) || !userfaultfd_wp(vma))
- return false;
-
- /* A uffd-wp wr-protected normal pte */
- if (unlikely(pte_present(pteval) && pte_uffd_wp(pteval)))
- arm_uffd_pte = true;
-
- /*
- * A uffd-wp wr-protected swap pte. Note: this should even cover an
- * existing pte marker with uffd-wp bit set.
- */
- if (unlikely(pte_swp_uffd_wp_any(pteval)))
- arm_uffd_pte = true;
-
- if (unlikely(arm_uffd_pte)) {
- set_pte_at(vma->vm_mm, addr, pte,
- make_pte_marker(PTE_MARKER_UFFD_WP));
- return true;
- }
-
- return false;
-}
-
static inline bool vma_has_recency(const struct vm_area_struct *vma)
{
if (vma->vm_flags & (VM_SEQ_READ | VM_RAND_READ))
diff --git a/mm/memory.c b/mm/memory.c
index 0c9d9c2cbf0e0..767c033e95da9 100644
--- a/mm/memory.c
+++ b/mm/memory.c
@@ -1599,6 +1599,67 @@ static inline bool zap_drop_markers(struct zap_details *details)
return details->zap_flags & ZAP_FLAG_DROP_MARKER;
}
+/**
+ * cond_install_uffd_wp_ptes - install uffd-wp marker after clearing PTEs
+ * that mapped consecutive pages of the same
+ * large folio.
+ * @vma: The VMA the pages are mapped into.
+ * @addr: Address the first page of this batch is mapped at.
+ * @ptep: Page table pointer for the first entry of this batch.
+ * @pte: old value of the entry pointed to by ptep.
+ * @nr_ptes: Number of entries to clear (batch size).
+ *
+ * If the ptes were wr-protected by uffd-wp in any form, arm special ptes to
+ * replace none ptes. NOTE! This should only be called when *pte is already
+ * cleared so we will never accidentally replace something valuable. Meanwhile
+ * none pte also means we are not demoting the pte so tlb flushed is not needed.
+ * E.g., when pte cleared the caller should have taken care of the tlb flush.
+ *
+ * Must be called with pgtable lock held so that no thread will see the none
+ * pte, and if they see it, they'll fault and serialize at the pgtable lock.
+ *
+ * Returns true if uffd-wp ptes were installed, false otherwise.
+ */
+inline bool cond_install_uffd_wp_ptes(struct vm_area_struct *vma, unsigned long addr,
+ pte_t *ptep, pte_t pte, unsigned long nr_ptes)
+{
+ bool arm_uffd_pte = false;
+
+ if (!uffd_supports_wp_marker())
+ return false;
+
+ /* The current status of the pte should be "cleared" before calling */
+ WARN_ON_ONCE(!pte_none(ptep_get(ptep)));
+
+ /*
+ * NOTE: userfaultfd_wp_unpopulated() doesn't need this whole
+ * thing, because when zapping either it means it's dropping the
+ * page, or in TTU where the present pte will be quickly replaced
+ * with a swap pte. There's no way of leaking the bit.
+ */
+ if (vma_is_anonymous(vma) || !userfaultfd_wp(vma))
+ return false;
+
+ /* A uffd-wp wr-protected normal pte */
+ if (unlikely(pte_present(pte) && pte_uffd_wp(pte)))
+ arm_uffd_pte = true;
+
+ /*
+ * A uffd-wp wr-protected swap pte. Note: this should even cover an
+ * existing pte marker with uffd-wp bit set.
+ */
+ if (unlikely(pte_swp_uffd_wp_any(pte)))
+ arm_uffd_pte = true;
+
+ if (likely(!arm_uffd_pte))
+ return false;
+
+ for (int i = 0; i < nr_ptes; ++i, ++ptep, addr += PAGE_SIZE)
+ set_pte_at(vma->vm_mm, addr, ptep, make_pte_marker(PTE_MARKER_UFFD_WP));
+
+ return true;
+}
+
/*
* This function makes sure that we'll replace the none pte with an uffd-wp
* swap special pte marker when necessary. Must be with the pgtable lock held.
@@ -1610,29 +1671,11 @@ zap_install_uffd_wp_if_needed(struct vm_area_struct *vma,
unsigned long addr, pte_t *pte, int nr,
struct zap_details *details, pte_t pteval)
{
- bool was_installed = false;
-
- if (!uffd_supports_wp_marker())
- return false;
-
- /* Zap on anonymous always means dropping everything */
- if (vma_is_anonymous(vma))
- return false;
-
if (zap_drop_markers(details))
return false;
- for (;;) {
- /* the PFN in the PTE is irrelevant. */
- if (pte_install_uffd_wp_if_needed(vma, addr, pte, pteval))
- was_installed = true;
- if (--nr == 0)
- break;
- pte++;
- addr += PAGE_SIZE;
- }
+ return cond_install_uffd_wp_ptes(vma, addr, pte, pteval, nr);
- return was_installed;
}
static __always_inline void zap_present_folio_ptes(struct mmu_gather *tlb,
diff --git a/mm/rmap.c b/mm/rmap.c
index 12bbee57f20da..6a0b43856d6c0 100644
--- a/mm/rmap.c
+++ b/mm/rmap.c
@@ -2288,7 +2288,7 @@ static bool try_to_unmap_one(struct folio *folio, struct vm_area_struct *vma,
* we may want to replace a none pte with a marker pte if
* it's file-backed, so we don't lose the tracking info.
*/
- pte_install_uffd_wp_if_needed(vma, address, pvmw.pte, pteval);
+ cond_install_uffd_wp_ptes(vma, address, pvmw.pte, pteval, 1);
/* Update high watermark before we lower rss */
update_hiwater_rss(mm);
--
2.34.1