* [PATCH] mm/ksm: fix userfaultfd protection during merge and unmerge
@ 2026-10-26 13:00 Lance Yang
2026-10-26 14:27 ` Lorenzo Stoakes (ARM)
0 siblings, 1 reply; 3+ messages in thread
From: Lance Yang @ 2026-10-26 13:00 UTC (permalink / raw)
To: akpm
Cc: david, xu.xin, chengming.zhou, ljs, liam, vbabka, rppt, surenb,
mhocko, jglisse, aarcange, peterx, kas, linux-mm, linux-kernel,
bobbypowers, bgeffon, dplotnikov, dgilbert, hughd, hannes,
kirill, cracauer, mcfadden8, gokhale2, mgorman, mike.kravetz,
rppt, xemul, riel, shli, jthoughton, corbet, pbonzini, seanjc,
sj, ziy, stable, samarth.colleges, med08elkadiri, Lance Yang
KSM loses userfaultfd protection when replace_page() builds a new PTE
from vma->vm_page_prot. Dropping its uffd bit lets a subsequent write go
through ordinary COW instead of userfaultfd write protection handling. For
RWP, a merged page also becomes readable without faulting.
Keeping the uffd bit and restoring PAGE_NONE fixes merging, but we also
have to handle unmerging. An existing KSM page can be protected with RWP
before MADV_UNMERGEABLE, so unmerging already needs fixing independently
of preserving RWP during merging.
Unsharing replaces a shared page with a private one and should leave
userfaultfd protection in place, as do_wp_page() already does for write
protection. Currently, an unshare fault reaches do_uffd_rwp() before it
can proceed to do_wp_page().
- With synchronous RWP, handle_userfault() returns VM_FAULT_SIGBUS.
break_ksm() then reports success with a KSM page still merged.
- With asynchronous RWP, protection is cleared before unsharing.
Preserve the uffd bit when merging, and restore PAGE_NONE for RWP. Let
FAULT_FLAG_UNSHARE skip RWP fault handling and keep PAGE_NONE on each
private copy as well. Unsharing can then complete with RWP still armed
for a subsequent access.
Fixes: 63b2d4174c4a ("userfaultfd: wp: add the writeprotect API to userfaultfd ioctl")
Fixes: 2d427bb01639 ("mm/userfaultfd: add RWP fault delivery and expose UFFDIO_REGISTER_MODE_RWP")
Link: https://lore.kernel.org/all/20261019010934.697895-1-samarth.colleges@gmail.com/
Link: https://lore.kernel.org/all/20261020055518.36087-1-lance.yang@linux.dev/
Cc: stable@vger.kernel.org
Co-developed-by: Samarth Narang <samarth.colleges@gmail.com>
Signed-off-by: Samarth Narang <samarth.colleges@gmail.com>
Signed-off-by: Lance Yang <lance.yang@linux.dev>
---
mm/ksm.c | 8 +++++++-
mm/memory.c | 14 ++++++++++----
2 files changed, 17 insertions(+), 5 deletions(-)
diff --git a/mm/ksm.c b/mm/ksm.c
index 910a1307ac9f..d6f736a1aa2f 100644
--- a/mm/ksm.c
+++ b/mm/ksm.c
@@ -34,6 +34,7 @@
#include <linux/memory.h>
#include <linux/mmu_notifier.h>
#include <linux/swap.h>
+#include <linux/userfaultfd_k.h>
#include <linux/ksm.h>
#include <linux/hashtable.h>
#include <linux/freezer.h>
@@ -1463,9 +1464,14 @@ static int replace_page(struct vm_area_struct *vma, struct page *page,
dec_mm_counter(mm, MM_ANONPAGES);
}
- /* Preserve soft-dirty when replacing the page. */
+ /* Preserve soft-dirty and uffd protection when replacing the page. */
if (pte_soft_dirty(orig_pte))
newpte = pte_mksoft_dirty(newpte);
+ if (pte_uffd(orig_pte)) {
+ newpte = pte_mkuffd(newpte);
+ if (userfaultfd_rwp(vma))
+ newpte = pte_modify(newpte, PAGE_NONE);
+ }
flush_cache_page(vma, addr, pte_pfn(ptep_get(ptep)));
/*
diff --git a/mm/memory.c b/mm/memory.c
index 6a93b7fa48b2..99439a7afa63 100644
--- a/mm/memory.c
+++ b/mm/memory.c
@@ -4122,8 +4122,11 @@ static vm_fault_t wp_page_copy(struct vm_fault *vmf)
if (unlikely(unshare)) {
if (pte_soft_dirty(vmf->orig_pte))
entry = pte_mksoft_dirty(entry);
- if (pte_uffd(vmf->orig_pte))
+ if (pte_uffd(vmf->orig_pte)) {
entry = pte_mkuffd(entry);
+ if (userfaultfd_rwp(vma))
+ entry = pte_modify(entry, PAGE_NONE);
+ }
} else {
entry = maybe_mkwrite(pte_mkdirty(entry), vma);
}
@@ -6659,9 +6662,12 @@ static vm_fault_t handle_pte_fault(struct vm_fault *vmf)
* VM_UFFD_RWP VMA, a protnone PTE without the uffd bit is
* NUMA hinting and must still fall through to do_numa_page().
*/
- if (userfaultfd_pte_rwp(vmf->vma, vmf->orig_pte))
- return do_uffd_rwp(vmf);
- return do_numa_page(vmf);
+ if (userfaultfd_pte_rwp(vmf->vma, vmf->orig_pte)) {
+ if (!(vmf->flags & FAULT_FLAG_UNSHARE))
+ return do_uffd_rwp(vmf);
+ } else {
+ return do_numa_page(vmf);
+ }
}
spin_lock(vmf->ptl);
--
2.49.0
^ permalink raw reply [flat|nested] 3+ messages in thread* Re: [PATCH] mm/ksm: fix userfaultfd protection during merge and unmerge 2026-10-26 13:00 [PATCH] mm/ksm: fix userfaultfd protection during merge and unmerge Lance Yang @ 2026-10-26 14:27 ` Lorenzo Stoakes (ARM) 2026-10-26 15:18 ` Lance Yang 0 siblings, 1 reply; 3+ messages in thread From: Lorenzo Stoakes (ARM) @ 2026-10-26 14:27 UTC (permalink / raw) To: Lance Yang Cc: akpm, david, xu.xin, chengming.zhou, liam, vbabka, rppt, surenb, mhocko, jglisse, aarcange, peterx, kas, linux-mm, linux-kernel, bobbypowers, bgeffon, dplotnikov, dgilbert, hughd, hannes, kirill, cracauer, mcfadden8, gokhale2, mgorman, mike.kravetz, rppt, xemul, riel, shli, jthoughton, corbet, pbonzini, seanjc, sj, ziy, stable, samarth.colleges, med08elkadiri On Mon, Oct 26, 2026 at 09:00:54PM +0800, Lance Yang wrote: > KSM loses userfaultfd protection when replace_page() builds a new PTE > from vma->vm_page_prot. Dropping its uffd bit lets a subsequent write go > through ordinary COW instead of userfaultfd write protection handling. For > RWP, a merged page also becomes readable without faulting. I feel we probably shouldn't have allowed an extension to UFFD while it's still fundamentally a broken feature :)) Which is evidenced by the SLEW of AI bug reports, and human-reported bug reports, and endless history of bug reports and it infecting the rest of mm, etc. etc. /rant > > Keeping the uffd bit and restoring PAGE_NONE fixes merging, but we also > have to handle unmerging. An existing KSM page can be protected with RWP > before MADV_UNMERGEABLE, so unmerging already needs fixing independently > of preserving RWP during merging. > > Unsharing replaces a shared page with a private one and should leave > userfaultfd protection in place, as do_wp_page() already does for write > protection. Currently, an unshare fault reaches do_uffd_rwp() before it > can proceed to do_wp_page(). > > - With synchronous RWP, handle_userfault() returns VM_FAULT_SIGBUS. > break_ksm() then reports success with a KSM page still merged. > > - With asynchronous RWP, protection is cleared before unsharing. > > Preserve the uffd bit when merging, and restore PAGE_NONE for RWP. Let > FAULT_FLAG_UNSHARE skip RWP fault handling and keep PAGE_NONE on each > private copy as well. Unsharing can then complete with RWP still armed > for a subsequent access. > > Fixes: 63b2d4174c4a ("userfaultfd: wp: add the writeprotect API to userfaultfd ioctl") > Fixes: 2d427bb01639 ("mm/userfaultfd: add RWP fault delivery and expose UFFDIO_REGISTER_MODE_RWP") > Link: https://lore.kernel.org/all/20261019010934.697895-1-samarth.colleges@gmail.com/ > Link: https://lore.kernel.org/all/20261020055518.36087-1-lance.yang@linux.dev/ > Cc: stable@vger.kernel.org > Co-developed-by: Samarth Narang <samarth.colleges@gmail.com> > Signed-off-by: Samarth Narang <samarth.colleges@gmail.com> > Signed-off-by: Lance Yang <lance.yang@linux.dev> Looks sensible to me so: Acked-by: Lorenzo Stoakes (ARM) <ljs@kernel.org> > --- > mm/ksm.c | 8 +++++++- > mm/memory.c | 14 ++++++++++---- > 2 files changed, 17 insertions(+), 5 deletions(-) > > diff --git a/mm/ksm.c b/mm/ksm.c > index 910a1307ac9f..d6f736a1aa2f 100644 > --- a/mm/ksm.c > +++ b/mm/ksm.c > @@ -34,6 +34,7 @@ > #include <linux/memory.h> > #include <linux/mmu_notifier.h> > #include <linux/swap.h> > +#include <linux/userfaultfd_k.h> > #include <linux/ksm.h> > #include <linux/hashtable.h> > #include <linux/freezer.h> > @@ -1463,9 +1464,14 @@ static int replace_page(struct vm_area_struct *vma, struct page *page, > dec_mm_counter(mm, MM_ANONPAGES); > } > > - /* Preserve soft-dirty when replacing the page. */ > + /* Preserve soft-dirty and uffd protection when replacing the page. */ > if (pte_soft_dirty(orig_pte)) > newpte = pte_mksoft_dirty(newpte); > + if (pte_uffd(orig_pte)) { > + newpte = pte_mkuffd(newpte); > + if (userfaultfd_rwp(vma)) > + newpte = pte_modify(newpte, PAGE_NONE); OK, so by definition, RWP pages are protnone, and this resets it correctly. > + } > > flush_cache_page(vma, addr, pte_pfn(ptep_get(ptep))); > /* > diff --git a/mm/memory.c b/mm/memory.c > index 6a93b7fa48b2..99439a7afa63 100644 > --- a/mm/memory.c > +++ b/mm/memory.c > @@ -4122,8 +4122,11 @@ static vm_fault_t wp_page_copy(struct vm_fault *vmf) > if (unlikely(unshare)) { > if (pte_soft_dirty(vmf->orig_pte)) > entry = pte_mksoft_dirty(entry); > - if (pte_uffd(vmf->orig_pte)) > + if (pte_uffd(vmf->orig_pte)) { > entry = pte_mkuffd(entry); > + if (userfaultfd_rwp(vma)) > + entry = pte_modify(entry, PAGE_NONE); > + } Could this be abstracted? You're doing the same pattern here, which also exists in copy_present_page() and presumably many other places. But maybe that's one for a follow up? > } else { > entry = maybe_mkwrite(pte_mkdirty(entry), vma); > } > @@ -6659,9 +6662,12 @@ static vm_fault_t handle_pte_fault(struct vm_fault *vmf) > * VM_UFFD_RWP VMA, a protnone PTE without the uffd bit is > * NUMA hinting and must still fall through to do_numa_page(). > */ > - if (userfaultfd_pte_rwp(vmf->vma, vmf->orig_pte)) > - return do_uffd_rwp(vmf); > - return do_numa_page(vmf); > + if (userfaultfd_pte_rwp(vmf->vma, vmf->orig_pte)) { > + if (!(vmf->flags & FAULT_FLAG_UNSHARE)) > + return do_uffd_rwp(vmf); > + } else { > + return do_numa_page(vmf); > + } > } > > spin_lock(vmf->ptl); > -- > 2.49.0 > -- Cheers, Lorenzo ^ permalink raw reply [flat|nested] 3+ messages in thread
* Re: [PATCH] mm/ksm: fix userfaultfd protection during merge and unmerge 2026-10-26 14:27 ` Lorenzo Stoakes (ARM) @ 2026-10-26 15:18 ` Lance Yang 0 siblings, 0 replies; 3+ messages in thread From: Lance Yang @ 2026-10-26 15:18 UTC (permalink / raw) To: Lorenzo Stoakes (ARM) Cc: akpm, david, xu.xin, chengming.zhou, liam, vbabka, rppt, surenb, mhocko, jglisse, aarcange, peterx, kas, linux-mm, linux-kernel, bobbypowers, bgeffon, dplotnikov, dgilbert, hughd, hannes, kirill, cracauer, mcfadden8, gokhale2, mgorman, mike.kravetz, rppt, xemul, riel, shli, jthoughton, corbet, pbonzini, seanjc, sj, ziy, stable, samarth.colleges, med08elkadiri On 2026/10/26 22:27, Lorenzo Stoakes (ARM) wrote: > On Mon, Oct 26, 2026 at 09:00:54PM +0800, Lance Yang wrote: >> KSM loses userfaultfd protection when replace_page() builds a new PTE >> from vma->vm_page_prot. Dropping its uffd bit lets a subsequent write go >> through ordinary COW instead of userfaultfd write protection handling. For >> RWP, a merged page also becomes readable without faulting. > > I feel we probably shouldn't have allowed an extension to UFFD while it's still > fundamentally a broken feature :)) > > Which is evidenced by the SLEW of AI bug reports, and human-reported bug > reports, and endless history of bug reports and it infecting the rest of mm, > etc. etc. /rant Heh, UFFD isn't winning you over today :D >> >> Keeping the uffd bit and restoring PAGE_NONE fixes merging, but we also >> have to handle unmerging. An existing KSM page can be protected with RWP >> before MADV_UNMERGEABLE, so unmerging already needs fixing independently >> of preserving RWP during merging. >> >> Unsharing replaces a shared page with a private one and should leave >> userfaultfd protection in place, as do_wp_page() already does for write >> protection. Currently, an unshare fault reaches do_uffd_rwp() before it >> can proceed to do_wp_page(). >> >> - With synchronous RWP, handle_userfault() returns VM_FAULT_SIGBUS. >> break_ksm() then reports success with a KSM page still merged. >> >> - With asynchronous RWP, protection is cleared before unsharing. >> >> Preserve the uffd bit when merging, and restore PAGE_NONE for RWP. Let >> FAULT_FLAG_UNSHARE skip RWP fault handling and keep PAGE_NONE on each >> private copy as well. Unsharing can then complete with RWP still armed >> for a subsequent access. >> >> Fixes: 63b2d4174c4a ("userfaultfd: wp: add the writeprotect API to userfaultfd ioctl") >> Fixes: 2d427bb01639 ("mm/userfaultfd: add RWP fault delivery and expose UFFDIO_REGISTER_MODE_RWP") >> Link: https://lore.kernel.org/all/20261019010934.697895-1-samarth.colleges@gmail.com/ >> Link: https://lore.kernel.org/all/20261020055518.36087-1-lance.yang@linux.dev/ >> Cc: stable@vger.kernel.org >> Co-developed-by: Samarth Narang <samarth.colleges@gmail.com> >> Signed-off-by: Samarth Narang <samarth.colleges@gmail.com> >> Signed-off-by: Lance Yang <lance.yang@linux.dev> > > Looks sensible to me so: > > Acked-by: Lorenzo Stoakes (ARM) <ljs@kernel.org> Really appreciated! >> --- >> mm/ksm.c | 8 +++++++- >> mm/memory.c | 14 ++++++++++---- >> 2 files changed, 17 insertions(+), 5 deletions(-) >> >> diff --git a/mm/ksm.c b/mm/ksm.c >> index 910a1307ac9f..d6f736a1aa2f 100644 >> --- a/mm/ksm.c >> +++ b/mm/ksm.c >> @@ -34,6 +34,7 @@ >> #include <linux/memory.h> >> #include <linux/mmu_notifier.h> >> #include <linux/swap.h> >> +#include <linux/userfaultfd_k.h> >> #include <linux/ksm.h> >> #include <linux/hashtable.h> >> #include <linux/freezer.h> >> @@ -1463,9 +1464,14 @@ static int replace_page(struct vm_area_struct *vma, struct page *page, >> dec_mm_counter(mm, MM_ANONPAGES); >> } >> >> - /* Preserve soft-dirty when replacing the page. */ >> + /* Preserve soft-dirty and uffd protection when replacing the page. */ >> if (pte_soft_dirty(orig_pte)) >> newpte = pte_mksoft_dirty(newpte); >> + if (pte_uffd(orig_pte)) { >> + newpte = pte_mkuffd(newpte); >> + if (userfaultfd_rwp(vma)) >> + newpte = pte_modify(newpte, PAGE_NONE); > > OK, so by definition, RWP pages are protnone, and this resets it correctly. Right, we need PAGE_NONE back as well as the uffd bit :) >> + } >> >> flush_cache_page(vma, addr, pte_pfn(ptep_get(ptep))); >> /* >> diff --git a/mm/memory.c b/mm/memory.c >> index 6a93b7fa48b2..99439a7afa63 100644 >> --- a/mm/memory.c >> +++ b/mm/memory.c >> @@ -4122,8 +4122,11 @@ static vm_fault_t wp_page_copy(struct vm_fault *vmf) >> if (unlikely(unshare)) { >> if (pte_soft_dirty(vmf->orig_pte)) >> entry = pte_mksoft_dirty(entry); >> - if (pte_uffd(vmf->orig_pte)) >> + if (pte_uffd(vmf->orig_pte)) { >> entry = pte_mkuffd(entry); >> + if (userfaultfd_rwp(vma)) >> + entry = pte_modify(entry, PAGE_NONE); >> + } > > Could this be abstracted? You're doing the same pattern here, which also exists > in copy_present_page() and presumably many other places. > > But maybe that's one for a follow up? Yeah, a helper would be nice here. I'd keep this fix small and do that as a follow-up :D Cheers, Lance >> } else { >> entry = maybe_mkwrite(pte_mkdirty(entry), vma); >> } >> @@ -6659,9 +6662,12 @@ static vm_fault_t handle_pte_fault(struct vm_fault *vmf) >> * VM_UFFD_RWP VMA, a protnone PTE without the uffd bit is >> * NUMA hinting and must still fall through to do_numa_page(). >> */ >> - if (userfaultfd_pte_rwp(vmf->vma, vmf->orig_pte)) >> - return do_uffd_rwp(vmf); >> - return do_numa_page(vmf); >> + if (userfaultfd_pte_rwp(vmf->vma, vmf->orig_pte)) { >> + if (!(vmf->flags & FAULT_FLAG_UNSHARE)) >> + return do_uffd_rwp(vmf); >> + } else { >> + return do_numa_page(vmf); >> + } >> } >> >> spin_lock(vmf->ptl); >> -- >> 2.49.0 >> > > -- > Cheers, Lorenzo ^ permalink raw reply [flat|nested] 3+ messages in thread
end of thread, other threads:[~2026-10-26 15:18 UTC | newest] Thread overview: 3+ messages (download: mbox.gz / follow: Atom feed) -- links below jump to the message on this page -- 2026-10-26 13:00 [PATCH] mm/ksm: fix userfaultfd protection during merge and unmerge Lance Yang 2026-10-26 14:27 ` Lorenzo Stoakes (ARM) 2026-10-26 15:18 ` Lance Yang
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®