From: Kiryl Shutsemau <kas@kernel.org>
To: Pedro Falcato <pfalcato@suse.de>
Cc: Lance Yang <lance.yang@linux.dev>,
usama.arif@linux.dev, hughd@google.com,
akpm@linux-foundation.org, baohua@kernel.org,
baolin.wang@linux.alibaba.com, david@kernel.org,
dev.jain@arm.com, liam@infradead.org,
linux-kernel@vger.kernel.org, linux-mm@kvack.org,
ljs@kernel.org, nico.pache@linux.dev, ryan.roberts@arm.com,
ziy@nvidia.com, nphamcs@gmail.com, hannes@cmpxchg.org,
riel@surriel.com, shakeel.butt@linux.dev, kernel-team@meta.com,
stable@vger.kernel.org, willy@infradead.org
Subject: Re: [PATCH] mm/huge_memory: transfer the pmd dirty bit to the folio on zap
Date: Thu, 20 Aug 2026 14:20:26 +0100 [thread overview]
Message-ID: <aob9PF_H6CCJMdkk@thinkstation> (raw)
In-Reply-To: <aobp2CtWZBlw4w7E@pedro-suse.lan>
On Thu, Aug 20, 2026 at 01:12:18PM +0100, Pedro Falcato wrote:
> +CC willy
>
> On Thu, Aug 20, 2026 at 02:13:37PM +0800, Lance Yang wrote:
> >
> > On Wed, Aug 19, 2026 at 05:35:41PM +0100, Pedro Falcato wrote:
> > >On Wed, Aug 19, 2026 at 03:31:40PM +0100, Kiryl Shutsemau wrote:
> > >> On Wed, Aug 19, 2026 at 03:12:22AM -0700, Usama Arif wrote:
> > >> > zap_huge_pmd_folio() propagates the pmd young bit to the folio for the
> > >> > file case, but not the dirty bit. The pte path does propagate it, in
> > >> > zap_present_folio_ptes() and so does the pmd split path, in
> > >> > __split_huge_pmd_locked().
> > >> >
> > >> > For most file mappings the omission is harmless, because writing to a
> > >> > shared file mapping goes through page_mkwrite(), which dirties the
> > >> > folio. tmpfs is different: it has no page_mkwrite(), and
> > >> > vma_wants_writenotify() is false for it, so a *read* fault on a
> > >> > MAP_SHARED tmpfs mapping installs a writable pmd via do_read_fault().
> > >> > do_read_fault() does not call fault_dirty_shared_page(), so subsequent
> > >> > stores through that mapping set only the hardware dirty bit in the pmd
> > >> > and never call folio_mark_dirty().
> > >> >
> > >> > A shmem folio allocated by a fault
> > >> > is marked uptodate but not dirty (see the clear: block in
> > >> > shmem_get_folio_gfp()), so PG_dirty is never set at all.
> > >> >
> > >> > Unmapping such a folio - munmap(), or exit_mmap() when the process dies
> > >> > - then loses the only record that it was written, because zap_huge_pmd()
> > >> > drops the pmd without transferring the dirty bit. Reclaim afterwards
> > >> > sees a clean shmem folio: the whole swap-out block in
> > >> > shrink_folio_list() is inside "if (folio_test_dirty(folio))", so
> > >> > pageout() is skipped and the folio falls into __remove_mapping().
> > >> > There, folio_is_file_lru() is false for a swapbacked folio, so no shadow
> > >> > entry is created and __filemap_remove_folio(folio, NULL) simply empties
> > >> > the i_pages slot. The data is freed without ever being written to swap,
> > >> > and the next fault on that index returns a freshly zeroed folio.
> > >> >
> > >> > This is silent data loss for any process that keeps state in a
> > >> > MAP_SHARED tmpfs segment across an unmap - for example a cache handed
> > >> > from one process generation to the next through /dev/shm. It requires
> > >> > the folio to be PMD-mapped, so it only shows up once shmem THP is
> > >> > enabled (which is what we did in Meta fleet and started noticing crashes);
> > >> > with THP off the pte path transfers the dirty bit correctly.
> > >> > It also only becomes visible when swap is enabled, because with no swap
> > >> > device shmem folios (which are on the anon LRU) are not scanned by
> > >> > reclaim at all, so the clean folio is never dropped.
> > >> >
> > >> > Reproduced on x86_64 with a tmpfs mounted huge=within_size: read-fault a
> > >> > 2MB-backed region, write a known pattern through the resulting mapping,
> > >> > munmap, force reclaim of the cgroup, then re-map and read back. Without
> > >> > this patch the region reads back as zeros and vmstat shows zswpout 0 -
> > >> > the data was discarded rather than swapped. With this patch the region
> > >> > reads back correctly and the pages are swapped out as expected. With
> > >> > huge=never, or when the first touch is a write, the test passes either
> > >> > way.
> > >>
> > >> +Hugh.
> > >>
> > >> Oopsie.
> > >>
> > >> I'm confused why it took a decade to discover the bug...
> > >> Maybe read ahead of write for shmem is too rare, I donno.
> > >>
> > >> >
> > >> > Fixes: 800d8c63b2e9 ("shmem: add huge pages support")
> > >>
> > >> This would be more precise: b5072380eb61 ("thp: support file pages in zap_huge_pmd()")
> > >>
> > >> Reviewed-by: Kiryl Shutsemau <kas@kernel.org>
> > >>
> > >> > Cc: <stable@vger.kernel.org>
> > >> > Signed-off-by: Usama Arif <usama.arif@linux.dev>
> > >> > ---
> > >> > mm/huge_memory.c | 2 ++
> > >> > 1 file changed, 2 insertions(+)
> > >> >
> > >> > diff --git a/mm/huge_memory.c b/mm/huge_memory.c
> > >> > index ced400f72d43a..afbb5974bd225 100644
> > >> > --- a/mm/huge_memory.c
> > >> > +++ b/mm/huge_memory.c
> > >> > @@ -2449,6 +2449,8 @@ static void zap_huge_pmd_folio(struct mm_struct *mm, struct vm_area_struct *vma,
> > >> > add_mm_counter(mm, mm_counter_file(folio),
> > >> > -HPAGE_PMD_NR);
> > >> >
> > >> > + if (is_present && pmd_dirty(pmdval))
> > >> > + folio_mark_dirty(folio);
> > >>
> > >> Unrelated to your patch, but noticed while looking at it: we drop the rmap
> > >> here under the pmd lock, while the TLB flush is deferred to
> > >> tlb_finish_mmu(). The pte path handles this with
> > >> tlb_delay_rmap()/force_flush (5df397dec7c4), but there's no pmd equivalent:
> > >> tlb_flush_rmap_batch() only knows folio_remove_rmap_ptes(), and
> > >> zap_huge_pmd() uses tlb_remove_page_size(), which takes no delay_rmap.
> > >>
> > >> Doesn't matter for shmem, but xfs & friends do get PMD-order folios, and
> > >> do_set_pmd() makes the pmd dirty+writable once page_mkwrite() has run. So
> > >> folio_mkclean() can clean the folio while another CPU still stores through a
> > >> stale TLB entry -- silently lost write, no PG_dirty left behind.
> > >
> > >Where do you see page_mkwrite being called in the same path as do_set_pmd()?
> > >Per my understanding of the code, this Should Not Happen, and it really Should
> > >Not Happen for many, many reasons (write amplification being the main one).
> >
> > Hmm.. that happens on an initial shared write fault. For non-DAX XFS, the
> > path starts with an empty PMD.
> >
> > TL;DR
> >
> > With an empty PMD and PMD-order THP allowed, __handle_mm_fault() first
> > tries create_huge_pmd(). VM_FAULT_FALLBACK sends the fault to
> > handle_pte_fault():
> >
> > static vm_fault_t __handle_mm_fault(struct vm_area_struct *vma,
> > unsigned long address, unsigned int flags)
> > {
> > ...
> > if (pmd_none(*vmf.pmd) &&
> > thp_vma_allowable_order(vma, vm_flags, TVA_PAGEFAULT, PMD_ORDER)) {
> > ret = create_huge_pmd(&vmf);
> > if (ret & VM_FAULT_FALLBACK)
> > goto fallback;
> > else
> > return ret;
> > }
> > ...
> > fallback:
> > return handle_pte_fault(&vmf);
> > }
> >
> > create_huge_pmd() dispatches to the filesystem's huge_fault callback:
> >
> > static inline vm_fault_t create_huge_pmd(struct vm_fault *vmf)
> > {
> > struct vm_area_struct *vma = vmf->vma;
> > ...
> > if (vma->vm_ops->huge_fault)
> > return vma->vm_ops->huge_fault(vmf, PMD_ORDER);
> > return VM_FAULT_FALLBACK;
> > }
> >
> > For non-DAX XFS, that callback returns VM_FAULT_FALLBACK:
> >
> > static vm_fault_t
> > xfs_filemap_huge_fault(
> > struct vm_fault *vmf,
> > unsigned int order)
> > {
> > if (!IS_DAX(file_inode(vmf->vma->vm_file)))
> > return VM_FAULT_FALLBACK;
> > ...
> > }
> >
> > XFS installs the huge-fault, regular-fault, and page_mkwrite callbacks
> > in the same vm_ops:
> >
> > static const struct vm_operations_struct xfs_file_vm_ops = {
> > .fault = xfs_filemap_fault,
> > .huge_fault = xfs_filemap_huge_fault,
> > ...
> > .page_mkwrite = xfs_filemap_page_mkwrite,
> > ...
> > };
> >
> > On the fallback path, handle_pte_fault() leaves an empty PMD without a
> > PTE and calls do_pte_missing():
> >
> > static vm_fault_t handle_pte_fault(struct vm_fault *vmf)
> > {
> > ...
> > if (unlikely(pmd_none(*vmf->pmd))) {
> > /*
> > * Leave __pte_alloc() until later: because vm_ops->fault may
> > * want to allocate huge page, and if we expose page table
> > * for an instant, it will be difficult to retract from
> > * concurrent faults and from rmap lookups.
> > */
> > vmf->pte = NULL;
> > vmf->flags &= ~FAULT_FLAG_ORIG_PTE_VALID;
> > ...
> > }
> >
> > if (!vmf->pte)
> > return do_pte_missing(vmf);
> > ...
> > }
> >
> > For a file VMA, do_pte_missing() calls do_fault():
> >
> > static vm_fault_t do_pte_missing(struct vm_fault *vmf)
> > {
> > if (vma_is_anonymous(vmf->vma))
> > return do_anonymous_page(vmf);
> > else
> > return do_fault(vmf);
> > }
> >
> > do_fault() sends FAULT_FLAG_WRITE + VM_SHARED to do_shared_fault():
> >
> > static vm_fault_t do_fault(struct vm_fault *vmf)
> > {
> > struct vm_area_struct *vma = vmf->vma;
> > ...
> > if (!vma->vm_ops->fault) {
> > ...
> > } else if (!(vmf->flags & FAULT_FLAG_WRITE))
> > ret = do_read_fault(vmf);
> > else if (!(vma->vm_flags & VM_SHARED))
> > ret = do_cow_fault(vmf);
> > else
> > ret = do_shared_fault(vmf);
> > ...
> > }
> >
> > do_shared_fault() first calls __do_fault():
> >
> > static vm_fault_t do_shared_fault(struct vm_fault *vmf)
> > {
> > struct vm_area_struct *vma = vmf->vma;
> > vm_fault_t ret, tmp;
> > struct folio *folio;
> > ...
> > ret = __do_fault(vmf);
> > ...
> > }
> >
> > __do_fault() invokes the regular fault callback:
> >
> > static vm_fault_t __do_fault(struct vm_fault *vmf)
> > {
> > struct vm_area_struct *vma = vmf->vma;
> > struct folio *folio;
> > vm_fault_t ret;
> > ...
> > ret = vma->vm_ops->fault(vmf);
> > ...
> > return ret;
> > }
> >
> > For non-DAX XFS, xfs_filemap_fault() reaches filemap_fault():
> >
> > static vm_fault_t
> > xfs_filemap_fault(
> > struct vm_fault *vmf)
> > {
> > struct inode *inode = file_inode(vmf->vma->vm_file);
> > ...
> > return filemap_fault(vmf);
> > }
> >
> > Once that returns the folio, do_shared_fault() calls do_page_mkwrite()
> > and then finish_fault():
> >
> > static vm_fault_t do_shared_fault(struct vm_fault *vmf)
> > {
> > struct vm_area_struct *vma = vmf->vma;
> > vm_fault_t ret, tmp;
> > struct folio *folio;
> > ...
> > folio = page_folio(vmf->page);
> > ...
> > if (vma->vm_ops->page_mkwrite) {
> > folio_unlock(folio);
> > tmp = do_page_mkwrite(vmf, folio);
> > ...
> > }
> >
> > ret |= finish_fault(vmf);
> > ...
> > }
> >
> > do_page_mkwrite() calls the XFS callback installed above and restores
> > the original fault flags:
> >
> > static vm_fault_t do_page_mkwrite(struct vm_fault *vmf, struct folio *folio)
> > {
> > vm_fault_t ret;
> > unsigned int old_flags = vmf->flags;
> >
> > vmf->flags = FAULT_FLAG_WRITE|FAULT_FLAG_MKWRITE;
> > ...
> > ret = vmf->vma->vm_ops->page_mkwrite(vmf);
> > /* Restore original flags so that caller is not surprised */
> > vmf->flags = old_flags;
> > ...
> > }
> >
> > So finish_fault() still sees FAULT_FLAG_WRITE. With an empty PMD, no
> > fallback requirement, and a PMD-mappable folio, it tries do_set_pmd():
> >
> > vm_fault_t finish_fault(struct vm_fault *vmf)
> > {
> > ...
> > if (pmd_none(*vmf->pmd)) {
> > if (!needs_fallback && folio_test_pmd_mappable(folio)) {
> > ret = do_set_pmd(vmf, folio, page);
> > if (ret != VM_FAULT_FALLBACK)
> > return ret;
> > }
> > ...
> > }
> > ...
> > }
> >
> > After its checks pass, do_set_pmd() takes FAULT_FLAG_WRITE from vmf and
> > installs a dirty+writable PMD:
> >
> > vm_fault_t do_set_pmd(struct vm_fault *vmf, struct folio *folio, struct page *page)
> > {
> > struct vm_area_struct *vma = vmf->vma;
> > bool write = vmf->flags & FAULT_FLAG_WRITE;
> > unsigned long haddr = vmf->address & HPAGE_PMD_MASK;
> > pmd_t entry;
> > ...
> > entry = folio_mk_pmd(folio, vma->vm_page_prot);
> > if (write)
> > entry = maybe_pmd_mkwrite(pmd_mkdirty(entry), vma);
> > ...
> > set_pmd_at(vma->vm_mm, haddr, vmf->pmd, entry);
> > ...
> > }
> >
> > maybe_pmd_mkwrite() sets write permission for VM_WRITE:
> >
> > pmd_t maybe_pmd_mkwrite(pmd_t pmd, struct vm_area_struct *vma)
> > {
> > if (likely(vma->vm_flags & VM_WRITE))
> > pmd = pmd_mkwrite(pmd, vma);
> > return pmd;
> > }
>
> Thanks, this makes sense!
>
> >
> > >Namely, see the comment in wp_huge_pmd():
> > > /* COW or write-notify handled on pte level: split pmd. */
> > >
> > >if file huge pages get mapped writable, that's a bug.
> >
> > That comment is about a different path. __handle_mm_fault() calls
> > wp_huge_pmd() only when a write/unshare fault hits an existing PMD THP
> > which is not writable:
>
> No. That is simply a bug. There's little reason you wouldn't try do un-WP
> a huge PMD if the idea would be to do PMD granularity for write notifications.
> It isn't, naturally, because that results in horrible write amplification.
Write notification is already folio-granular. Installing PTE instead of
PMD changes nothing.
> The fix IMO is to make it so write faults on shared mappings with page_mkwrite
> never create a PMD.
Why? Write batching from large folios is a win.
> I don't think it makes sense to add rmap flushing hacks
> for PMDs, when the common case (read + write) instantly and purposefully
> breaks down to the PTE level (such that you really aren't supposed to get
> the above; you'll notice that as soon as the folio gets cleaned, it will
> get broken by the next write fault, period).
If you consider delayed rmap a hack (I don't), it has to fixed on PTE
level too.
> See the attached patch. I know willy has been working on related stuff, so
> perhaps he might want to pick it up.
As I said the patch doesn't do what you expect it to do. PG_dirty is on
folio and we writeback folios, not PTEs.
Also, needs_fallback is not just "no PMD": finish_fault() then forces
nr_pages = 1, so it is 512 faults and 512 ->page_mkwrite calls per 2M folio
instead of one.
--
Kiryl Shutsemau / Kirill A. Shutemov
next prev parent reply other threads:[~2026-08-20 13:20 UTC|newest]
Thread overview: 17+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-08-19 10:12 [PATCH] mm/huge_memory: transfer the pmd dirty bit to the folio on zap Usama Arif
2026-08-19 14:13 ` David Hildenbrand (Arm)
2026-08-19 14:13 ` David Hildenbrand (Arm)
2026-08-19 14:31 ` Kiryl Shutsemau
2026-08-19 16:17 ` Lance Yang
2026-08-19 16:32 ` Usama Arif
2026-08-20 13:05 ` Usama Arif
2026-08-19 16:35 ` Pedro Falcato
2026-08-20 6:13 ` Lance Yang
2026-08-20 12:12 ` Pedro Falcato
2026-08-20 13:20 ` Kiryl Shutsemau [this message]
2026-08-20 14:10 ` Pedro Falcato
2026-08-19 20:33 ` Hugh Dickins
2026-08-19 15:10 ` Lance Yang
2026-08-19 15:31 ` Zi Yan
2026-08-19 16:09 ` Lorenzo Stoakes (ARM)
2026-08-20 2:06 ` Baolin Wang
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=aob9PF_H6CCJMdkk@thinkstation \
--to=kas@kernel.org \
--cc=akpm@linux-foundation.org \
--cc=baohua@kernel.org \
--cc=baolin.wang@linux.alibaba.com \
--cc=david@kernel.org \
--cc=dev.jain@arm.com \
--cc=hannes@cmpxchg.org \
--cc=hughd@google.com \
--cc=kernel-team@meta.com \
--cc=lance.yang@linux.dev \
--cc=liam@infradead.org \
--cc=linux-kernel@vger.kernel.org \
--cc=linux-mm@kvack.org \
--cc=ljs@kernel.org \
--cc=nico.pache@linux.dev \
--cc=nphamcs@gmail.com \
--cc=pfalcato@suse.de \
--cc=riel@surriel.com \
--cc=ryan.roberts@arm.com \
--cc=shakeel.butt@linux.dev \
--cc=stable@vger.kernel.org \
--cc=usama.arif@linux.dev \
--cc=willy@infradead.org \
--cc=ziy@nvidia.com \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox