All of 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: 46+ 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   ` Usama Arif
2026-11-05 18:38 ` [PATCH v9 06/32] s390: " Usama Arif
2026-11-06 18:43   ` sashiko-bot
2026-11-10 17:17   ` Gerald Schaefer
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 an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.