Linux-mm Archive on lore.kernel.org
 help / color / mirror / Atom feed
From: sashiko-bot@kernel.org
To: "Usama Arif" <usama.arif@linux.dev>,
	"Andrew Morton" <akpm@linux-foundation.org>,
	david@kernel.org, chrisl@kernel.org, kasong@tencent.com,
	ljs@kernel.org, ziy@nvidia.com, linux-mm@kvack.org
Cc: chenhuacai@kernel.org, ying.huang@linux.alibaba.com,
	"Baoquan He" <baoquan.he@linux.dev>,
	willy@infradead.org, youngjun.park@lge.com, hannes@cmpxchg.org,
	riel@surriel.com, shakeel.butt@linux.dev, alex@ghiti.fr,
	kas@kernel.org, baohua@kernel.org, dev.jain@arm.com,
	baolin.wang@linux.alibaba.com,
	"Nico Pache" <nico.pache@linux.dev>,
	"Liam R. Howlett" <liam@infradead.org>,
	ryan.roberts@arm.com, "Vlastimil Babka" <vbabka@kernel.org>,
	lance.yang@linux.dev, nphamcs@gmail.com,
	shikemeng@huaweicloud.com, yosry@kernel.org, qi.zheng@linux.dev,
	luizcap@redhat.com, kernel-team@meta.com
Subject: Re: [PATCH v9 31/32] mm: install PMD swap entries on swap-out
Date: Fri, 06 Nov 2026 18:43:13 +0000	[thread overview]
Message-ID: <sashiko-outbox-202086@kernel.org> (raw)
In-Reply-To: <20261105183940.2236717-32-usama.arif@linux.dev>

Thank you for your contribution! Sashiko AI review found 3 potential
issues to consider:
- [Critical] mm: rmap corruption in do_swap_page() on unmapped PMD
  swapcache folio
- [Critical] mm: stale swap data via set_pmd_swap_entry() after
  madvise_free_huge_pmd()
- [High] mm: swapped-out PMD THPs omitted from core dumps in
  follow_pmd_mask()
--

commit 5fc4ff682d479e59aca8b75c2c237658535938ae
Author: Usama Arif <usama.arif@linux.dev>

mm: install PMD swap entries on swap-out

This commit avoids splitting a PMD-mapped anonymous THP during swap-out when
the folio is already in the swap cache, replacing the PMD mapping directly
with a PMD-level swap entry via set_pmd_swap_entry().

> diff --git a/mm/huge_memory.c b/mm/huge_memory.c
> index b8d61cdf8cbfb..95fce141e114f 100644
> --- a/mm/huge_memory.c
> +++ b/mm/huge_memory.c
> @@ -5778,3 +5778,87 @@ void remove_migration_pmd(struct page_vma_mapped_walk *pvmw, struct folio *folio
>  	trace_remove_migration_pmd(address, pmd_val(pmde));
>  }
>  #endif
> +
> +#ifdef CONFIG_THP_SWAP
> +/**
> + * set_pmd_swap_entry() - Replace a PMD mapping with a PMD-level swap entry.
> + * @pvmw: Page vma mapped walk context, must have pvmw->pmd set and
> + *        pvmw->pte NULL (i.e. PMD-mapped).
> + * @folio: The folio being swapped out. Must be in the swap cache.
> + *
> + * This installs a PMD-level swap entry in place of a present PMD mapping,
> + * avoiding the need to split the PMD into PTE-level swap entries.
> + *
> + * Return: 0 on success, negative error code on failure.
> + */
> +int set_pmd_swap_entry(struct page_vma_mapped_walk *pvmw,
> +		       struct folio *folio)
> +{
[ ... ]
> +	mm_prepare_for_swap_entries(mm);
> +
> +	if (pmd_dirty(pmdval))
> +		folio_mark_dirty(folio);
> +
> +	entry = folio->swap;
> +	pmdswp = softleaf_to_pmd(entry);
> +	if (pmd_soft_dirty(pmdval))
> +		pmdswp = pmd_swp_mksoft_dirty(pmdswp);
> +	if (pmd_uffd(pmdval))
> +		pmdswp = pmd_swp_mkuffd(pmdswp);
> +	if (anon_exclusive)
> +		pmdswp = pmd_swp_mkexclusive(pmdswp);
> +	set_pmd_at(mm, haddr, pvmw->pmd, pmdswp);

[Severity: Critical]
Can installing a PMD swap entry here lead to rmap corruption in
do_swap_page() if the PMD swap entry is later split while a freshly
allocated PMD-order folio is in the swap cache?

Once set_pmd_swap_entry() installs a PMD swap entry, swapin_pmd_swap_entry()
(via MADV_WILLNEED) or do_huge_pmd_swap_page() calls swapin_sync() with
BIT(HPAGE_PMD_ORDER), which allocates an unmapped PMD-order folio in the
swap cache with !folio_test_anon(folio).

If the PMD swap entry is then split into HPAGE_PMD_NR contiguous PTE swap
entries while that unmapped PMD-order folio remains in the swap cache, for
example when madvise(MADV_WILLNEED) is followed by madvise(MADV_NOHUGEPAGE)
or prctl(PR_SET_THP_DISABLE) and a page fault enters do_huge_pmd_swap_page():

mm/huge_memory.c:do_huge_pmd_swap_page()
	if (!thp_vma_allowable_order(vma, vma->vm_flags, TVA_PAGEFAULT,
				     HPAGE_PMD_ORDER)) {
		__split_huge_pmd(vma, vmf->pmd, haddr);
		return 0;
	}

(or when do_huge_pmd_swap_page() backs out at folio_lock_or_retry() or
!pmd_same() after a concurrent split), the subsequent fault falls back to
do_swap_page().

In do_swap_page(), swap_pte_batch() batches all HPAGE_PMD_NR contiguous PTE
swap entries, so nr_pages == folio_nr_pages(folio) and the partial-mapping
check does not remove the folio from the swap cache. do_swap_page() then
installs PTEs via set_ptes() and calls folio_add_new_anon_rmap():

mm/memory.c:do_swap_page()
	} else if (!folio_test_anon(folio)) {
		VM_WARN_ON_ONCE_FOLIO(folio_nr_pages(folio) != nr_pages, folio);
		VM_WARN_ON_ONCE_FOLIO(folio_mapped(folio), folio);
		folio_add_new_anon_rmap(folio, vma, address, rmap_flags);
		folio_put_swap(folio, NULL);
	}

However, folio_add_new_anon_rmap() assumes any folio_test_pmd_mappable(folio)
folio is being PMD-mapped rather than PTE-mapped:

mm/rmap.c:folio_add_new_anon_rmap()
	} else if (!folio_test_pmd_mappable(folio)) {
		...
	} else {
		nr = folio_large_nr_pages(folio);
		/* increment count (starts at -1) */
		atomic_set(&folio->_entire_mapcount, 0);
		folio_set_large_mapcount(folio, 1, vma);
		if (IS_ENABLED(CONFIG_PAGE_MAPCOUNT))
			atomic_set(&folio->_nr_pages_mapped, ENTIRELY_MAPPED);
		if (exclusive)
			SetPageAnonExclusive(&folio->page);
		nr_pmdmapped = nr;
	}

Because the folio was mapped with PTEs via set_ptes(), this leaves
page->_mapcount at -1 on all subpages, sets _large_mapcount to 1 instead of
HPAGE_PMD_NR, sets _entire_mapcount to 0, and only sets PageAnonExclusive on
the head page. When zap_pte_range() later unmaps those PTEs via
folio_remove_rmap_ptes(), page->_mapcount underflows to -2, _large_mapcount
underflows, and _entire_mapcount is never decremented, triggering a Bad page
state bug when the folio is freed. (Similarly, if swapoff runs after the PMD
swap entry is split, unuse_pte() hits VM_WARN_ON_ONCE(folio_test_large(folio))
and calls folio_add_new_anon_rmap() while installing a single PTE.)

> diff --git a/mm/rmap.c b/mm/rmap.c
> index 9d6353e98e7c7..b76e5f85c9bcc 100644
> --- a/mm/rmap.c
> +++ b/mm/rmap.c
> @@ -2275,6 +2275,25 @@ static bool try_to_unmap_one(struct folio *folio, struct vm_area_struct *vma,
>  		}
>  
>  		if (!pvmw.pte) {
> +#ifdef CONFIG_THP_SWAP
> +			/*
> +			 * If the folio is in the swap cache and we're not
> +			 * asked to split, install a PMD-level swap entry.
> +			 */
> +			if (!(flags & TTU_SPLIT_HUGE_PMD) &&
> +			    folio_test_anon(folio) &&
> +			    folio_test_swapcache(folio)) {
> +				if (set_pmd_swap_entry(&pvmw, folio))
> +					goto walk_abort;

[Severity: High]
Does replacing the PMD with a non-present PMD swap entry cause swapped-out
THPs to be silently omitted from core dumps?

Previously, reclaiming an anonymous THP split the PMD into PTE swap entries.
When dump_user_range() (or mte_dump_tag_range() on arm64) calls
get_dump_page() with FOLL_FORCE | FOLL_DUMP | FOLL_GET, follow_page_pte()
returns NULL for a non-none, non-present PTE, which causes
__get_user_pages() to call faultin_page() and fault the swapped-out page back
in.

With a non-present PMD swap entry, follow_pmd_mask() instead calls
no_page_table():

mm/gup.c:follow_pmd_mask()
	if (pmd_none(pmdval))
		return no_page_table(vma, flags, address);
	if (!pmd_present(pmdval))
		return no_page_table(vma, flags, address);

And no_page_table() treats FOLL_DUMP on an anonymous VMA as an unpopulated
hole, returning ERR_PTR(-EFAULT):

mm/gup.c:no_page_table()
	if (flags & FOLL_DUMP) {
		if (vma->vm_ops && vma->vm_ops->may_split)
			return ERR_PTR(-ENOSYS);

		/*
		 * For anonymous or fault-less VMA, return -EFAULT to judge
		 * there is no page Mapped, so we can skip dumping them.
		 */
		if (vma_is_anonymous(vma) || !vma->vm_ops->fault)
			return ERR_PTR(-EFAULT);
	}

Because __get_user_pages() sees IS_ERR(page), it aborts without calling
faultin_page(), so get_dump_page() returns NULL and dump_user_range() skips
the entire 2MB THP range in the core dump. Should follow_pmd_mask() only call
no_page_table() when pmd_none(pmdval) and return NULL for non-present PMD
swap entries?

> diff --git a/mm/vmscan.c b/mm/vmscan.c
> index 071a6eaef1a1a..179ba94494e85 100644
> --- a/mm/vmscan.c
> +++ b/mm/vmscan.c
> @@ -1330,7 +1330,14 @@ static unsigned int shrink_folio_list(struct list_head *folio_list,
>  			enum ttu_flags flags = TTU_BATCH_FLUSH;
>  			bool was_swapbacked = folio_test_swapbacked(folio);
>  
> -			if (folio_test_pmd_mappable(folio))
> +			/*
> +			 * With THP_SWAP, PMD-mappable folios already in the
> +			 * swap cache can be unmapped with a PMD-level swap
> +			 * entry, avoiding the cost of splitting the PMD.
> +			 */
> +			if (folio_test_pmd_mappable(folio) &&
> +			    !(IS_ENABLED(CONFIG_THP_SWAP) &&
> +			      folio_test_swapcache(folio)))
>  				flags |= TTU_SPLIT_HUGE_PMD;

[Severity: Critical]
Can skipping TTU_SPLIT_HUGE_PMD for a PMD-mapped THP that is already in the
swap cache result in reclaiming the folio without writeback after
madvise(MADV_FREE), leaving the PMD swap entry pointing to stale or
unwritten swap data?

With this change and do_huge_pmd_swap_page(), an anonymous THP can be both
PMD-mapped and in the swap cache (for instance, after a read swap-in in
do_huge_pmd_swap_page() when should_try_to_free_swap() is false, where the
process then writes to the THP in memory while the swap slot holds older
data; or if an earlier reclaim pass called folio_alloc_swap() and
try_to_unmap() aborted before unmapping all mappings).

If userspace then calls madvise(MADV_FREE) on that range,
madvise_free_huge_pmd() clears both the folio dirty bit and the PMD dirty
bit without freeing the swap cache entry (unlike madvise_free_pte_range(),
which checks folio_test_swapcache(folio) and calls folio_free_swap()):

mm/huge_memory.c:madvise_free_huge_pmd()
	if (folio_test_dirty(folio))
		folio_clear_dirty(folio);
	folio_unlock(folio);

	if (pmd_young(orig_pmd) || pmd_dirty(orig_pmd)) {
		pmdp_invalidate(vma, addr, pmd);
		orig_pmd = pmd_mkold(orig_pmd);
		orig_pmd = pmd_mkclean(orig_pmd);

		set_pmd_at(mm, addr, pmd, orig_pmd);
		tlb_remove_pmd_tlb_entry(tlb, pmd, addr);
	}

	folio_mark_lazyfree(folio);

Because folio_test_swapcache(folio) is still true, folio_mark_lazyfree()
returns early without clearing PG_swapbacked:

mm/folio.c:folio_mark_lazyfree()
	if (!folio_test_anon(folio) || !folio_test_swapbacked(folio) ||
	    !folio_test_lru(folio) ||
	    folio_test_swapcache(folio) || folio_test_unevictable(folio))
		return;

When shrink_folio_list() later reclaims this folio, folio_test_swapcache()
is already true, so it skips the !folio_test_swapcache(folio) block (which
would have called folio_mark_dirty(folio)), skips TTU_SPLIT_HUGE_PMD here,
and calls try_to_unmap_one() -> set_pmd_swap_entry().

In set_pmd_swap_entry(), pmd_dirty(pmdval) is false, so the folio is not
marked dirty while a PMD swap entry is installed for folio->swap. Back in
shrink_folio_list(), folio_test_dirty(folio) is false, so pageout() is
skipped and the folio is freed without writing it out, leaving subsequent
faults to read stale pre-modification data or unwritten swap blocks from
disk.

-- 
Sashiko AI review · https://sashiko.dev/#/patchset/20261105183940.2236717-1-usama.arif@linux.dev?part=31


  reply	other threads:[~2026-11-06 18:43 UTC|newest]

Thread overview: 43+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-11-05 18:38 [PATCH v9 00/32] mm: PMD-level swap entries for anonymous THPs Usama Arif
2026-11-05 18:38 ` [PATCH v9 01/32] mm: rename pmd_to_softleaf_folio() to pmd_softleaf_to_folio() Usama Arif
2026-11-05 18:38 ` [PATCH v9 02/32] arm64: mm: add PMD swap-exclusive helpers Usama Arif
2026-11-05 18:38 ` [PATCH v9 03/32] loongarch: " Usama Arif
2026-11-05 18:38 ` [PATCH v9 04/32] powerpc: " Usama Arif
2026-11-05 18:38 ` [PATCH v9 05/32] riscv: " Usama Arif
2026-11-05 18:38 ` [PATCH v9 06/32] s390: " Usama Arif
2026-11-05 18:38 ` [PATCH v9 07/32] x86: " Usama Arif
2026-11-05 18:38 ` [PATCH v9 08/32] mm: recognize PMD swap entries in the softleaf layer Usama Arif
2026-11-05 18:38 ` [PATCH v9 09/32] mm/debug_vm_pgtable: test PMD swap-exclusive helpers Usama Arif
2026-11-05 18:38 ` [PATCH v9 10/32] mm/migrate_device: avoid leaking folio references during THP splitting Usama Arif
2026-11-05 18:38 ` [PATCH v9 11/32] mm: make PMD migration-entry splitting explicit Usama Arif
2026-11-05 18:38 ` [PATCH v9 12/32] mm: split PMD swap entries into PTE swap entries Usama Arif
2026-11-05 18:38 ` [PATCH v9 13/32] mm/swap: allow duplicating a range of " Usama Arif
2026-11-05 18:38 ` [PATCH v9 14/32] mm: handle PMD swap entries in fork path Usama Arif
2026-11-05 18:38 ` [PATCH v9 15/32] mm: swap: share swap-cache allocation error cleanup Usama Arif
2026-11-06 21:10   ` Yosry Ahmed
2026-11-06 21:39   ` Nhat Pham
2026-11-05 18:39 ` [PATCH v9 16/32] mm: zswap: reject high-order swap cache allocations backed by zswap Usama Arif
2026-11-06 16:26   ` Nhat Pham
2026-11-06 21:16   ` Yosry Ahmed
2026-11-05 18:39 ` [PATCH v9 17/32] mm: swap in PMD swap entries as whole THPs during swapoff Usama Arif
2026-11-06 18:43   ` sashiko-bot
2026-11-05 18:39 ` [PATCH v9 18/32] fs/proc: account PMD swap entries in smaps Usama Arif
2026-11-05 18:39 ` [PATCH v9 19/32] mm: handle soft-dirty and uffd-wp on PMD swap entries Usama Arif
2026-11-05 18:39 ` [PATCH v9 20/32] mm/hmm: fault PMD swap entries on demand Usama Arif
2026-11-05 18:39 ` [PATCH v9 21/32] mm: free PMD swap entries in zap_huge_pmd() Usama Arif
2026-11-05 18:39 ` [PATCH v9 22/32] mm/madvise: free PMD swap entries with MADV_FREE Usama Arif
2026-11-05 18:39 ` [PATCH v9 23/32] mm/madvise: skip PMD swap entries for MADV_COLD and MADV_PAGEOUT Usama Arif
2026-11-05 18:39 ` [PATCH v9 24/32] mm/madvise: keep PMD swap entries whole for MADV_GUARD_INSTALL/REMOVE Usama Arif
2026-11-05 18:39 ` [PATCH v9 25/32] mm/mincore: report PMD swap-cache residency Usama Arif
2026-11-05 18:39 ` [PATCH v9 26/32] mm/khugepaged: treat PMD swap entries as mapped THPs Usama Arif
2026-11-05 18:39 ` [PATCH v9 27/32] mm: handle PMD swap entries in MADV_WILLNEED Usama Arif
2026-11-06 18:43   ` sashiko-bot
2026-11-05 18:39 ` [PATCH v9 28/32] mm: handle PMD swap entries in UFFDIO_MOVE Usama Arif
2026-11-06 18:43   ` sashiko-bot
2026-11-05 18:39 ` [PATCH v9 29/32] mm: don't PTE-batch a swap-in over a hardware-poisoned subpage Usama Arif
2026-11-05 18:39 ` [PATCH v9 30/32] mm: handle PMD swap entry faults on swap-in Usama Arif
2026-11-06 18:43   ` sashiko-bot
2026-11-05 18:39 ` [PATCH v9 31/32] mm: install PMD swap entries on swap-out Usama Arif
2026-11-06 18:43   ` sashiko-bot [this message]
2026-11-05 18:39 ` [PATCH v9 32/32] selftests/mm: add PMD swap entry tests Usama Arif
2026-11-06 18:43   ` sashiko-bot

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=sashiko-outbox-202086@kernel.org \
    --to=sashiko-bot@kernel.org \
    --cc=akpm@linux-foundation.org \
    --cc=alex@ghiti.fr \
    --cc=baohua@kernel.org \
    --cc=baolin.wang@linux.alibaba.com \
    --cc=baoquan.he@linux.dev \
    --cc=chenhuacai@kernel.org \
    --cc=chrisl@kernel.org \
    --cc=david@kernel.org \
    --cc=dev.jain@arm.com \
    --cc=hannes@cmpxchg.org \
    --cc=kas@kernel.org \
    --cc=kasong@tencent.com \
    --cc=kernel-team@meta.com \
    --cc=lance.yang@linux.dev \
    --cc=liam@infradead.org \
    --cc=linux-mm@kvack.org \
    --cc=ljs@kernel.org \
    --cc=luizcap@redhat.com \
    --cc=nico.pache@linux.dev \
    --cc=nphamcs@gmail.com \
    --cc=qi.zheng@linux.dev \
    --cc=riel@surriel.com \
    --cc=ryan.roberts@arm.com \
    --cc=sashiko-reviews@lists.linux.dev \
    --cc=shakeel.butt@linux.dev \
    --cc=shikemeng@huaweicloud.com \
    --cc=usama.arif@linux.dev \
    --cc=vbabka@kernel.org \
    --cc=willy@infradead.org \
    --cc=ying.huang@linux.alibaba.com \
    --cc=yosry@kernel.org \
    --cc=youngjun.park@lge.com \
    --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