[RESEND v7 20/29] mm/madvise: skip PMD swap entries for MADV_COLD and MADV_PAGEOUT

From: Usama Arif

Date: Mon Sep 14 2026 - 09:18:05 EST


A swapped-out THP has no resident folio to deactivate or reclaim, which is
also the conclusion the PTE path reaches when it walks the swap entries and
skips every one.

madvise_cold_or_pageout_pte_range() gets there the long way round:
pmd_trans_huge() is false for a PMD swap entry, so it falls out of the huge
branch and tries to map a PTE table that a non-present PMD does not have.

The entry can also appear after that first, lockless test, so re-check
under the PMD lock; otherwise a PMD that turned into a swap entry while
pmd_trans_huge_lock() was being taken trips the VM_WARN_ON_ONCE() that
expects a migration or device-private entry.

Signed-off-by: Usama Arif <usama.arif@xxxxxxxxx>
---
mm/madvise.c | 12 ++++++++++++
1 file changed, 12 insertions(+)

diff --git a/mm/madvise.c b/mm/madvise.c
index 70acb39b8b3dd..d3b5ed7e3cffb 100644
--- a/mm/madvise.c
+++ b/mm/madvise.c
@@ -385,6 +385,15 @@ static int madvise_cold_or_pageout_pte_range(pmd_t *pmd,
!can_do_file_pageout(vma);

#ifdef CONFIG_TRANSPARENT_HUGEPAGE
+ /*
+ * Swapped-out THPs have no resident folio to deactivate or reclaim.
+ * Avoid descending into or splitting a PMD swap entry.
+ */
+ if (pmd_is_swap_entry(*pmd)) {
+ walk->action = ACTION_CONTINUE;
+ return 0;
+ }
+
if (pmd_trans_huge(*pmd)) {
pmd_t orig_pmd;
unsigned long next = pmd_addr_end(addr, end);
@@ -395,6 +404,9 @@ static int madvise_cold_or_pageout_pte_range(pmd_t *pmd,
return 0;

orig_pmd = *pmd;
+ if (pmd_is_swap_entry(orig_pmd))
+ goto huge_unlock;
+
if (unlikely(!pmd_present(orig_pmd))) {
VM_WARN_ON_ONCE(!pmd_is_migration_entry(orig_pmd) &&
!pmd_is_device_private_entry(orig_pmd));
--
2.53.0-Meta