AI Trajectory


Trajectory: (Expand/Collapse All)
Seq Timestamp Type Name Duration
0/0 2026/07/23 17:01 flow patch-triage
0m Args:
null

Results:
{
  "EnableConfigs": [
    "THP_SWAP",
    "TRANSPARENT_HUGEPAGE",
    "USERFAULTFD",
    "ZSWAP"
  ],
  "FocusSymbols": [
    "do_huge_pmd_swap_page",
    "set_pmd_swap_entry",
    "swap_pmd_cache_lookup",
    "unuse_pmd",
    "unuse_pmd_entry",
    "move_swap_pmd",
    "mincore_pmd_swap"
  ],
  "Reasoning": "The patch introduces PMD-level swap entries, modifying core memory management logic including page faults, swap out/in, swapoff, mincore, and userfaultfd. This is a complex functional change that should be heavily fuzzed to catch potential memory corruption or logic errors.",
  "WorthFuzzing": true
}

1/1 2026/07/23 17:01 action read-patch-diff
0m Args:
null

Results:
{
  "PatchDiff": "commit 80071256e0cafe01425f47ef6faa9dbec6553be5\nAuthor: syz-cluster \u003ctriage@syzkaller.com\u003e\nDate:   Thu Jul 23 17:01:43 2026 +0000\n\n    syz-cluster: applied patch under review\n\ndiff --git a/Documentation/admin-guide/mm/transhuge.rst b/Documentation/admin-guide/mm/transhuge.rst\nindex 16f37135ed80df..b421d7982db637 100644\n--- a/Documentation/admin-guide/mm/transhuge.rst\n+++ b/Documentation/admin-guide/mm/transhuge.rst\n@@ -630,6 +630,11 @@ thp_swpout\n \tis incremented every time a huge page is swapout in one\n \tpiece without splitting.\n \n+thp_swpout_pmd\n+\tis incremented every time a PMD mapping is replaced by a PMD-level\n+\tswap entry. A fork-shared THP can increment this counter once for each\n+\tPMD mapping that is swapped out.\n+\n thp_swpout_fallback\n \tis incremented if a huge page has to be split before swapout.\n \tUsually because failed to allocate some continuous swap space\ndiff --git a/arch/arm64/include/asm/pgtable.h b/arch/arm64/include/asm/pgtable.h\nindex a2681d75535841..860f95573d1fb5 100644\n--- a/arch/arm64/include/asm/pgtable.h\n+++ b/arch/arm64/include/asm/pgtable.h\n@@ -598,6 +598,12 @@ static inline int pmd_protnone(pmd_t pmd)\n #define pmd_swp_clear_uffd(pmd) \\\n \t\t\t\tpte_pmd(pte_swp_clear_uffd(pmd_pte(pmd)))\n #endif /* CONFIG_HAVE_ARCH_USERFAULTFD_WP */\n+#ifdef CONFIG_ARCH_HAS_PMD_SOFTLEAVES\n+#define pmd_swp_exclusive(pmd)\tpte_swp_exclusive(pmd_pte(pmd))\n+#define pmd_swp_mkexclusive(pmd)\tpte_pmd(pte_swp_mkexclusive(pmd_pte(pmd)))\n+#define pmd_swp_clear_exclusive(pmd) \\\n+\t\t\t\tpte_pmd(pte_swp_clear_exclusive(pmd_pte(pmd)))\n+#endif\n \n #define pmd_write(pmd)\t\tpte_write(pmd_pte(pmd))\n \ndiff --git a/arch/loongarch/include/asm/pgtable.h b/arch/loongarch/include/asm/pgtable.h\nindex 1952e34bc8ee00..aa8e1223d3973e 100644\n--- a/arch/loongarch/include/asm/pgtable.h\n+++ b/arch/loongarch/include/asm/pgtable.h\n@@ -357,6 +357,25 @@ static inline pte_t pte_swp_clear_exclusive(pte_t pte)\n \treturn pte;\n }\n \n+#ifdef CONFIG_ARCH_HAS_PMD_SOFTLEAVES\n+static inline pmd_t pmd_swp_mkexclusive(pmd_t pmd)\n+{\n+\tpmd_val(pmd) |= _PAGE_SWP_EXCLUSIVE;\n+\treturn pmd;\n+}\n+\n+static inline bool pmd_swp_exclusive(pmd_t pmd)\n+{\n+\treturn pmd_val(pmd) \u0026 _PAGE_SWP_EXCLUSIVE;\n+}\n+\n+static inline pmd_t pmd_swp_clear_exclusive(pmd_t pmd)\n+{\n+\tpmd_val(pmd) \u0026= ~_PAGE_SWP_EXCLUSIVE;\n+\treturn pmd;\n+}\n+#endif\n+\n #define pte_none(pte)\t\t(!(pte_val(pte) \u0026 ~_PAGE_GLOBAL))\n #define pte_present(pte)\t(pte_val(pte) \u0026 (_PAGE_PRESENT | _PAGE_PROTNONE))\n #define pte_no_exec(pte)\t(pte_val(pte) \u0026 _PAGE_NO_EXEC)\ndiff --git a/arch/powerpc/include/asm/book3s/64/pgtable.h b/arch/powerpc/include/asm/book3s/64/pgtable.h\nindex f4db7d7fbd5c62..6a899d0793b3b6 100644\n--- a/arch/powerpc/include/asm/book3s/64/pgtable.h\n+++ b/arch/powerpc/include/asm/book3s/64/pgtable.h\n@@ -699,6 +699,23 @@ static inline pte_t pte_swp_clear_exclusive(pte_t pte)\n \treturn __pte_raw(pte_raw(pte) \u0026 cpu_to_be64(~_PAGE_SWP_EXCLUSIVE));\n }\n \n+#ifdef CONFIG_ARCH_HAS_PMD_SOFTLEAVES\n+static inline pmd_t pmd_swp_mkexclusive(pmd_t pmd)\n+{\n+\treturn __pmd_raw(pmd_raw(pmd) | cpu_to_be64(_PAGE_SWP_EXCLUSIVE));\n+}\n+\n+static inline bool pmd_swp_exclusive(pmd_t pmd)\n+{\n+\treturn !!(pmd_raw(pmd) \u0026 cpu_to_be64(_PAGE_SWP_EXCLUSIVE));\n+}\n+\n+static inline pmd_t pmd_swp_clear_exclusive(pmd_t pmd)\n+{\n+\treturn __pmd_raw(pmd_raw(pmd) \u0026 cpu_to_be64(~_PAGE_SWP_EXCLUSIVE));\n+}\n+#endif\n+\n static inline bool check_pte_access(unsigned long access, unsigned long ptev)\n {\n \t/*\ndiff --git a/arch/riscv/include/asm/pgtable.h b/arch/riscv/include/asm/pgtable.h\nindex 7f21742b2b6a0c..5f53b158a95cde 100644\n--- a/arch/riscv/include/asm/pgtable.h\n+++ b/arch/riscv/include/asm/pgtable.h\n@@ -1206,6 +1206,21 @@ static inline pte_t pte_swp_clear_exclusive(pte_t pte)\n }\n \n #ifdef CONFIG_ARCH_HAS_PMD_SOFTLEAVES\n+static inline bool pmd_swp_exclusive(pmd_t pmd)\n+{\n+\treturn pte_swp_exclusive(pmd_pte(pmd));\n+}\n+\n+static inline pmd_t pmd_swp_mkexclusive(pmd_t pmd)\n+{\n+\treturn pte_pmd(pte_swp_mkexclusive(pmd_pte(pmd)));\n+}\n+\n+static inline pmd_t pmd_swp_clear_exclusive(pmd_t pmd)\n+{\n+\treturn pte_pmd(pte_swp_clear_exclusive(pmd_pte(pmd)));\n+}\n+\n #define __pmd_to_swp_entry(pmd) ((swp_entry_t) { pmd_val(pmd) })\n #define __swp_entry_to_pmd(swp) __pmd((swp).val)\n #endif /* CONFIG_ARCH_HAS_PMD_SOFTLEAVES */\ndiff --git a/arch/s390/include/asm/pgtable.h b/arch/s390/include/asm/pgtable.h\nindex e882663a58e776..490e4a3464b194 100644\n--- a/arch/s390/include/asm/pgtable.h\n+++ b/arch/s390/include/asm/pgtable.h\n@@ -870,6 +870,23 @@ static inline pte_t pte_swp_clear_exclusive(pte_t pte)\n \treturn clear_pte_bit(pte, __pgprot(_PAGE_SWP_EXCLUSIVE));\n }\n \n+#ifdef CONFIG_ARCH_HAS_PMD_SOFTLEAVES\n+static inline pmd_t pmd_swp_mkexclusive(pmd_t pmd)\n+{\n+\treturn set_pmd_bit(pmd, __pgprot(_PAGE_SWP_EXCLUSIVE));\n+}\n+\n+static inline bool pmd_swp_exclusive(pmd_t pmd)\n+{\n+\treturn pmd_val(pmd) \u0026 _PAGE_SWP_EXCLUSIVE;\n+}\n+\n+static inline pmd_t pmd_swp_clear_exclusive(pmd_t pmd)\n+{\n+\treturn clear_pmd_bit(pmd, __pgprot(_PAGE_SWP_EXCLUSIVE));\n+}\n+#endif\n+\n static inline int pte_soft_dirty(pte_t pte)\n {\n \treturn pte_val(pte) \u0026 _PAGE_SOFT_DIRTY;\ndiff --git a/arch/x86/include/asm/pgtable.h b/arch/x86/include/asm/pgtable.h\nindex 8e0018fadd14e6..b5da5447e83d09 100644\n--- a/arch/x86/include/asm/pgtable.h\n+++ b/arch/x86/include/asm/pgtable.h\n@@ -1525,6 +1525,23 @@ static inline pte_t pte_swp_clear_exclusive(pte_t pte)\n \treturn pte_clear_flags(pte, _PAGE_SWP_EXCLUSIVE);\n }\n \n+#ifdef CONFIG_ARCH_HAS_PMD_SOFTLEAVES\n+static inline pmd_t pmd_swp_mkexclusive(pmd_t pmd)\n+{\n+\treturn pmd_set_flags(pmd, _PAGE_SWP_EXCLUSIVE);\n+}\n+\n+static inline int pmd_swp_exclusive(pmd_t pmd)\n+{\n+\treturn pmd_flags(pmd) \u0026 _PAGE_SWP_EXCLUSIVE;\n+}\n+\n+static inline pmd_t pmd_swp_clear_exclusive(pmd_t pmd)\n+{\n+\treturn pmd_clear_flags(pmd, _PAGE_SWP_EXCLUSIVE);\n+}\n+#endif\n+\n #ifdef CONFIG_HAVE_ARCH_SOFT_DIRTY\n static inline pte_t pte_swp_mksoft_dirty(pte_t pte)\n {\ndiff --git a/fs/proc/task_mmu.c b/fs/proc/task_mmu.c\nindex 817e3e0f919430..77e591f0eec683 100644\n--- a/fs/proc/task_mmu.c\n+++ b/fs/proc/task_mmu.c\n@@ -1046,6 +1046,27 @@ static void smaps_pte_hole_lookup(unsigned long addr, struct mm_walk *walk)\n #endif\n }\n \n+static void smaps_account_swap(struct mem_size_stats *mss,\n+\t\t\t       softleaf_t entry, unsigned long size)\n+{\n+\tunsigned long nr_pages = size \u003e\u003e PAGE_SHIFT;\n+\n+\tmss-\u003eswap += size;\n+\tdo {\n+\t\tint mapcount = swp_swapcount(entry);\n+\n+\t\tif (mapcount \u003e= 2) {\n+\t\t\tu64 pss_delta = (u64)PAGE_SIZE \u003c\u003c PSS_SHIFT;\n+\n+\t\t\tdo_div(pss_delta, mapcount);\n+\t\t\tmss-\u003eswap_pss += pss_delta;\n+\t\t} else {\n+\t\t\tmss-\u003eswap_pss += (u64)PAGE_SIZE \u003c\u003c PSS_SHIFT;\n+\t\t}\n+\t\tentry.val++;\n+\t} while (--nr_pages);\n+}\n+\n static void smaps_pte_entry(pte_t *pte, unsigned long addr,\n \t\tstruct mm_walk *walk)\n {\n@@ -1067,18 +1088,7 @@ static void smaps_pte_entry(pte_t *pte, unsigned long addr,\n \t\tconst softleaf_t entry = softleaf_from_pte(ptent);\n \n \t\tif (softleaf_is_swap(entry)) {\n-\t\t\tint mapcount;\n-\n-\t\t\tmss-\u003eswap += PAGE_SIZE;\n-\t\t\tmapcount = swp_swapcount(entry);\n-\t\t\tif (mapcount \u003e= 2) {\n-\t\t\t\tu64 pss_delta = (u64)PAGE_SIZE \u003c\u003c PSS_SHIFT;\n-\n-\t\t\t\tdo_div(pss_delta, mapcount);\n-\t\t\t\tmss-\u003eswap_pss += pss_delta;\n-\t\t\t} else {\n-\t\t\t\tmss-\u003eswap_pss += (u64)PAGE_SIZE \u003c\u003c PSS_SHIFT;\n-\t\t\t}\n+\t\t\tsmaps_account_swap(mss, entry, PAGE_SIZE);\n \t\t} else if (softleaf_has_pfn(entry)) {\n \t\t\tif (softleaf_is_device_private(entry))\n \t\t\t\tpresent = true;\n@@ -1108,9 +1118,13 @@ static void smaps_pmd_entry(pmd_t *pmd, unsigned long addr,\n \tif (pmd_present(*pmd)) {\n \t\tpage = vm_normal_page_pmd(vma, addr, *pmd);\n \t\tpresent = true;\n-\t} else if (unlikely(thp_migration_supported())) {\n+\t} else {\n \t\tconst softleaf_t entry = softleaf_from_pmd(*pmd);\n \n+\t\tif (softleaf_is_swap(entry)) {\n+\t\t\tsmaps_account_swap(mss, entry, HPAGE_PMD_SIZE);\n+\t\t\treturn;\n+\t\t}\n \t\tif (softleaf_has_pfn(entry))\n \t\t\tpage = softleaf_to_page(entry);\n \t}\n@@ -1755,7 +1769,7 @@ static inline void clear_soft_dirty_pmd(struct vm_area_struct *vma,\n \t\tpmd = pmd_clear_soft_dirty(pmd);\n \n \t\tset_pmd_at(vma-\u003evm_mm, addr, pmdp, pmd);\n-\t} else if (pmd_is_migration_entry(pmd)) {\n+\t} else if (pmd_is_migration_entry(pmd) || pmd_is_swap_entry(pmd)) {\n \t\tpmd = pmd_swp_clear_soft_dirty(pmd);\n \t\tset_pmd_at(vma-\u003evm_mm, addr, pmdp, pmd);\n \t}\n@@ -2115,7 +2129,7 @@ static int pagemap_pmd_range_thp(pmd_t *pmdp, unsigned long addr,\n \t\t\tflags |= PM_UFFD_WP;\n \t\tif (pm-\u003eshow_pfn)\n \t\t\tframe = pmd_pfn(pmd) + idx;\n-\t} else if (thp_migration_supported()) {\n+\t} else if (pmd_is_valid_softleaf(pmd)) {\n \t\tconst softleaf_t entry = softleaf_from_pmd(pmd);\n \t\tunsigned long offset;\n \n@@ -2581,7 +2595,7 @@ static void make_uffd_wp_pmd(struct vm_area_struct *vma,\n \t\told = pmdp_invalidate_ad(vma, addr, pmdp);\n \t\tpmd = pmd_mkuffd(old);\n \t\tset_pmd_at(vma-\u003evm_mm, addr, pmdp, pmd);\n-\t} else if (pmd_is_migration_entry(pmd)) {\n+\t} else if (pmd_is_migration_entry(pmd) || pmd_is_swap_entry(pmd)) {\n \t\tpmd = pmd_swp_mkuffd(pmd);\n \t\tset_pmd_at(vma-\u003evm_mm, addr, pmdp, pmd);\n \t}\ndiff --git a/include/linux/huge_mm.h b/include/linux/huge_mm.h\nindex c745f7ad22987f..41cf643a3f55f7 100644\n--- a/include/linux/huge_mm.h\n+++ b/include/linux/huge_mm.h\n@@ -552,6 +552,17 @@ vm_fault_t do_huge_pmd_uffd_rwp(struct vm_fault *vmf);\n \n vm_fault_t do_huge_pmd_device_private(struct vm_fault *vmf);\n \n+#ifdef CONFIG_THP_SWAP\n+vm_fault_t do_huge_pmd_swap_page(struct vm_fault *vmf);\n+int set_pmd_swap_entry(struct page_vma_mapped_walk *pvmw,\n+\t\t       struct folio *folio);\n+#else\n+static inline vm_fault_t do_huge_pmd_swap_page(struct vm_fault *vmf)\n+{\n+\treturn 0;\n+}\n+#endif\n+\n extern struct folio *huge_zero_folio;\n extern unsigned long huge_zero_pfn;\n \n@@ -754,6 +765,11 @@ static inline vm_fault_t do_huge_pmd_device_private(struct vm_fault *vmf)\n \treturn 0;\n }\n \n+static inline vm_fault_t do_huge_pmd_swap_page(struct vm_fault *vmf)\n+{\n+\treturn 0;\n+}\n+\n static inline bool is_huge_zero_folio(const struct folio *folio)\n {\n \treturn false;\ndiff --git a/include/linux/leafops.h b/include/linux/leafops.h\nindex 4c1476ae323435..8397193ba3999f 100644\n--- a/include/linux/leafops.h\n+++ b/include/linux/leafops.h\n@@ -102,6 +102,8 @@ static inline softleaf_t softleaf_from_pmd(pmd_t pmd)\n \t\tpmd = pmd_swp_clear_soft_dirty(pmd);\n \tif (pmd_swp_uffd(pmd))\n \t\tpmd = pmd_swp_clear_uffd(pmd);\n+\tif (pmd_swp_exclusive(pmd))\n+\t\tpmd = pmd_swp_clear_exclusive(pmd);\n \tarch_entry = __pmd_to_swp_entry(pmd);\n \n \t/* Temporary until swp_entry_t eliminated. */\n@@ -634,18 +636,30 @@ static inline bool pmd_is_migration_entry(pmd_t pmd)\n  */\n static inline bool softleaf_is_valid_pmd_entry(softleaf_t entry)\n {\n-\t/* Only device private, migration entries valid for PMD. */\n+\t/* Device private, migration, and swap entries valid for PMD. */\n \treturn softleaf_is_device_private(entry) ||\n-\t\tsoftleaf_is_migration(entry);\n+\t\tsoftleaf_is_migration(entry) ||\n+\t\tsoftleaf_is_swap(entry);\n+}\n+\n+/**\n+ * pmd_is_swap_entry() - Does this PMD entry encode an actual swap entry?\n+ * @pmd: PMD entry.\n+ *\n+ * Returns: true if the PMD encodes a swap entry, otherwise false.\n+ */\n+static inline bool pmd_is_swap_entry(pmd_t pmd)\n+{\n+\treturn softleaf_is_swap(softleaf_from_pmd(pmd));\n }\n \n /**\n  * pmd_is_valid_softleaf() - Is this PMD entry a valid softleaf entry?\n  * @pmd: PMD entry.\n  *\n- * PMD leaf entries are valid only if they are device private or migration\n- * entries. This function asserts that a PMD leaf entry is valid in this\n- * respect.\n+ * PMD leaf entries are valid only if they are device private, migration,\n+ * or swap entries. This function asserts that a PMD leaf entry is valid\n+ * in this respect.\n  *\n  * Returns: true if the PMD entry is a valid leaf entry, otherwise false.\n  */\n@@ -673,6 +687,8 @@ static inline struct folio *pmd_to_softleaf_folio(pmd_t pmd)\n \t\tVM_WARN_ON_ONCE(true);\n \t\treturn NULL;\n \t}\n+\tif (!softleaf_has_pfn(entry))\n+\t\treturn NULL;\n \treturn softleaf_to_folio(entry);\n }\n \ndiff --git a/include/linux/pgtable.h b/include/linux/pgtable.h\nindex 8c093c119e5a82..e10a7e91e42606 100644\n--- a/include/linux/pgtable.h\n+++ b/include/linux/pgtable.h\n@@ -1917,6 +1917,23 @@ static inline pmd_t pmd_swp_clear_soft_dirty(pmd_t pmd)\n }\n #endif\n \n+#ifndef CONFIG_ARCH_HAS_PMD_SOFTLEAVES\n+static inline pmd_t pmd_swp_mkexclusive(pmd_t pmd)\n+{\n+\treturn pmd;\n+}\n+\n+static inline bool pmd_swp_exclusive(pmd_t pmd)\n+{\n+\treturn false;\n+}\n+\n+static inline pmd_t pmd_swp_clear_exclusive(pmd_t pmd)\n+{\n+\treturn pmd;\n+}\n+#endif\n+\n #ifndef __HAVE_PFNMAP_TRACKING\n /*\n  * Interfaces that can be used by architecture code to keep track of\ndiff --git a/include/linux/swap.h b/include/linux/swap.h\nindex 2cb1d29307c54b..a87fc6832f494a 100644\n--- a/include/linux/swap.h\n+++ b/include/linux/swap.h\n@@ -389,7 +389,7 @@ sector_t swap_folio_sector(struct folio *folio);\n  * All entries must be allocated by folio_alloc_swap(). And they must have\n  * a swap count \u003e 1. See comments of folio_*_swap helpers for more info.\n  */\n-int swap_dup_entry_direct(swp_entry_t entry);\n+int swap_dup_entries_direct(swp_entry_t entry, int nr);\n void swap_put_entries_direct(swp_entry_t entry, int nr);\n \n /*\n@@ -433,7 +433,7 @@ static inline void free_swap_cache(struct folio *folio)\n {\n }\n \n-static inline int swap_dup_entry_direct(swp_entry_t ent)\n+static inline int swap_dup_entries_direct(swp_entry_t ent, int nr)\n {\n \treturn 0;\n }\ndiff --git a/include/linux/vm_event_item.h b/include/linux/vm_event_item.h\nindex 2628ccda076a07..f8fd4e13698c3f 100644\n--- a/include/linux/vm_event_item.h\n+++ b/include/linux/vm_event_item.h\n@@ -108,6 +108,7 @@ enum vm_event_item { PGPGIN, PGPGOUT, PSWPIN, PSWPOUT,\n \t\tTHP_ZERO_PAGE_ALLOC_FAILED,\n \t\tTHP_SWPOUT,\n \t\tTHP_SWPOUT_FALLBACK,\n+\t\tTHP_SWPOUT_PMD,\n #endif\n #ifdef CONFIG_BALLOON\n \t\tBALLOON_INFLATE,\ndiff --git a/include/linux/zswap.h b/include/linux/zswap.h\nindex 30c193a1207e16..cd9efcf9dec942 100644\n--- a/include/linux/zswap.h\n+++ b/include/linux/zswap.h\n@@ -35,6 +35,7 @@ void zswap_lruvec_state_init(struct lruvec *lruvec);\n void zswap_folio_swapin(struct folio *folio);\n bool zswap_is_enabled(void);\n bool zswap_never_enabled(void);\n+bool zswap_is_present(swp_entry_t entry, unsigned int nr);\n #else\n \n struct zswap_lruvec_state {};\n@@ -69,6 +70,11 @@ static inline bool zswap_never_enabled(void)\n \treturn true;\n }\n \n+static inline bool zswap_is_present(swp_entry_t entry, unsigned int nr)\n+{\n+\treturn false;\n+}\n+\n #endif\n \n #endif /* _LINUX_ZSWAP_H */\ndiff --git a/mm/hmm.c b/mm/hmm.c\nindex 87952c1259c543..b96850a0cb49f8 100644\n--- a/mm/hmm.c\n+++ b/mm/hmm.c\n@@ -376,7 +376,8 @@ static int hmm_vma_handle_absent_pmd(struct mm_walk *walk, unsigned long start,\n \trequired_fault = hmm_range_need_fault(hmm_vma_walk, hmm_pfns,\n \t\t\t\t\t      npages, 0);\n \tif (required_fault) {\n-\t\tif (softleaf_is_device_private(entry))\n+\t\tif (softleaf_is_device_private(entry) ||\n+\t\t    softleaf_is_swap(entry))\n \t\t\treturn hmm_record_fault(addr, end, required_fault, walk);\n \t\telse\n \t\t\treturn -EFAULT;\ndiff --git a/mm/huge_memory.c b/mm/huge_memory.c\nindex 04e8a6b553435f..c014631e5a26c3 100644\n--- a/mm/huge_memory.c\n+++ b/mm/huge_memory.c\n@@ -41,6 +41,7 @@\n #include \u003clinux/pgalloc.h\u003e\n #include \u003clinux/pgalloc_tag.h\u003e\n #include \u003clinux/pagewalk.h\u003e\n+#include \u003clinux/zswap.h\u003e\n \n #include \u003casm/tlb.h\u003e\n #include \"internal.h\"\n@@ -1806,7 +1807,7 @@ bool touch_pmd(struct vm_area_struct *vma, unsigned long addr,\n \treturn false;\n }\n \n-static void copy_huge_non_present_pmd(\n+static int copy_huge_non_present_pmd(\n \t\tstruct mm_struct *dst_mm, struct mm_struct *src_mm,\n \t\tpmd_t *dst_pmd, pmd_t *src_pmd, unsigned long addr,\n \t\tstruct vm_area_struct *dst_vma, struct vm_area_struct *src_vma,\n@@ -1852,14 +1853,35 @@ static void copy_huge_non_present_pmd(\n \t\t */\n \t\tfolio_try_dup_anon_rmap_pmd(src_folio, \u0026src_folio-\u003epage,\n \t\t\t\t\t    dst_vma, src_vma);\n+\t} else if (softleaf_is_swap(entry)) {\n+\t\tint err;\n+\n+\t\t/*\n+\t\t * PMD swap entry: duplicate swap references and clear\n+\t\t * exclusive on source, matching copy_nonpresent_pte().\n+\t\t */\n+\t\terr = swap_dup_entries_direct(entry, HPAGE_PMD_NR);\n+\t\tif (err \u003c 0)\n+\t\t\treturn err;\n+\n+\t\tmm_prepare_for_swap_entries(dst_mm);\n+\n+\t\tif (pmd_swp_exclusive(pmd)) {\n+\t\t\tpmd = pmd_swp_clear_exclusive(pmd);\n+\t\t\tset_pmd_at(src_mm, addr, src_pmd, pmd);\n+\t\t}\n \t}\n \n-\tadd_mm_counter(dst_mm, MM_ANONPAGES, HPAGE_PMD_NR);\n+\tif (softleaf_is_swap(entry))\n+\t\tadd_mm_counter(dst_mm, MM_SWAPENTS, HPAGE_PMD_NR);\n+\telse\n+\t\tadd_mm_counter(dst_mm, MM_ANONPAGES, HPAGE_PMD_NR);\n \tmm_inc_nr_ptes(dst_mm);\n \tpgtable_trans_huge_deposit(dst_mm, dst_pmd, pgtable);\n \tif (!userfaultfd_protected(dst_vma))\n \t\tpmd = pmd_swp_clear_uffd(pmd);\n \tset_pmd_at(dst_mm, addr, dst_pmd, pmd);\n+\treturn 0;\n }\n \n int copy_huge_pmd(struct mm_struct *dst_mm, struct mm_struct *src_mm,\n@@ -1900,6 +1922,7 @@ int copy_huge_pmd(struct mm_struct *dst_mm, struct mm_struct *src_mm,\n \tif (unlikely(!pgtable))\n \t\tgoto out;\n \n+retry:\n \tdst_ptl = pmd_lock(dst_mm, dst_pmd);\n \tsrc_ptl = pmd_lockptr(src_mm, src_pmd);\n \tspin_lock_nested(src_ptl, SINGLE_DEPTH_NESTING);\n@@ -1907,11 +1930,29 @@ int copy_huge_pmd(struct mm_struct *dst_mm, struct mm_struct *src_mm,\n \tret = -EAGAIN;\n \tpmd = *src_pmd;\n \n-\tif (unlikely(thp_migration_supported() \u0026\u0026\n-\t\t     pmd_is_valid_softleaf(pmd))) {\n-\t\tcopy_huge_non_present_pmd(dst_mm, src_mm, dst_pmd, src_pmd, addr,\n-\t\t\t\t\t  dst_vma, src_vma, pmd, pgtable);\n-\t\tret = 0;\n+\tif (unlikely(pmd_is_valid_softleaf(pmd))) {\n+\t\tret = copy_huge_non_present_pmd(dst_mm, src_mm, dst_pmd, src_pmd,\n+\t\t\t\t\t\taddr, dst_vma, src_vma, pmd,\n+\t\t\t\t\t\tpgtable);\n+\t\tif (ret) {\n+\t\t\tspin_unlock(src_ptl);\n+\t\t\tspin_unlock(dst_ptl);\n+\t\t\t/*\n+\t\t\t * For PMD swap entries -ENOMEM means the per-cluster\n+\t\t\t * swap-extend table couldn't be GFP_ATOMIC-allocated.\n+\t\t\t * try the GFP_KERNEL fallback once before giving up.\n+\t\t\t */\n+\t\t\tif (ret == -ENOMEM) {\n+\t\t\t\tsoftleaf_t entry = softleaf_from_pmd(pmd);\n+\n+\t\t\t\tif (softleaf_is_swap(entry) \u0026\u0026\n+\t\t\t\t    !swap_retry_table_alloc(entry, HPAGE_PMD_NR,\n+\t\t\t\t\t\t\t    GFP_KERNEL))\n+\t\t\t\t\tgoto retry;\n+\t\t\t}\n+\t\t\tpte_free(dst_mm, pgtable);\n+\t\t\tgoto out;\n+\t\t}\n \t\tgoto out_unlock;\n \t}\n \n@@ -2308,6 +2349,240 @@ vm_fault_t do_huge_pmd_numa_page(struct vm_fault *vmf)\n \treturn 0;\n }\n \n+#ifdef CONFIG_THP_SWAP\n+/**\n+ * do_huge_pmd_swap_page() - Handle a fault on a PMD-level swap entry.\n+ * @vmf: Fault context. vmf-\u003eorig_pmd contains the swap PMD.\n+ *\n+ * A PMD swap entry is a compact encoding for HPAGE_PMD_NR consecutive swap\n+ * slots. If the swap cache still has one PMD-sized folio covering the range,\n+ * map it directly at PMD level. If the range has been split into per-page\n+ * cache state, or zswap may have per-page state for it, split the PMD swap\n+ * entry and retry at PTE granularity.\n+ *\n+ * Return: VM_FAULT_* flags.\n+ */\n+vm_fault_t do_huge_pmd_swap_page(struct vm_fault *vmf)\n+{\n+\tstruct vm_area_struct *vma = vmf-\u003evma;\n+\tstruct mm_struct *mm = vma-\u003evm_mm;\n+\tstruct folio *folio;\n+\tstruct page *page;\n+\tstruct swap_info_struct *si;\n+\tunsigned long haddr = vmf-\u003eaddress \u0026 HPAGE_PMD_MASK;\n+\tsoftleaf_t entry;\n+\tswp_entry_t swp_entry;\n+\tpmd_t pmd;\n+\tvm_fault_t ret = 0;\n+\tbool exclusive, rwp_restore = false;\n+\tbool write = vmf-\u003eflags \u0026 FAULT_FLAG_WRITE;\n+\trmap_t rmap_flags = RMAP_NONE;\n+\tenum swap_pmd_cache cache_state;\n+\n+\tentry = softleaf_from_pmd(vmf-\u003eorig_pmd);\n+\tif (unlikely(!softleaf_is_swap(entry)))\n+\t\treturn 0;\n+\n+\tswp_entry = entry;\n+\n+\t/* Prevent swapoff from happening to us. */\n+\tsi = get_swap_device(swp_entry);\n+\tif (unlikely(!si))\n+\t\treturn 0;\n+\n+\tcache_state = swap_pmd_cache_lookup(swp_entry, \u0026folio);\n+\tif (cache_state == SWAP_PMD_CACHE_SPLIT)\n+\t\tgoto split_fallback;\n+\tif (!folio) {\n+\t\t/*\n+\t\t * PMD swap entries encode ordinary per-page swap slots. If any\n+\t\t * slot is in zswap, split and let the PTE swap path load the\n+\t\t * range per page. Otherwise the range is all on disk and can be\n+\t\t * read back as one PMD-sized folio.\n+\t\t */\n+\t\tif (zswap_is_present(swp_entry, HPAGE_PMD_NR))\n+\t\t\tgoto split_fallback;\n+\n+\t\tfolio = swapin_sync(swp_entry, GFP_HIGHUSER_MOVABLE,\n+\t\t\t\t    BIT(HPAGE_PMD_ORDER), vmf, NULL, 0);\n+\t\tif (IS_ERR_OR_NULL(folio))\n+\t\t\tgoto split_fallback;\n+\n+\t\t/* Had to read from swap area: Major fault */\n+\t\tret = VM_FAULT_MAJOR;\n+\t\tcount_vm_event(PGMAJFAULT);\n+\t\tcount_memcg_event_mm(mm, PGMAJFAULT);\n+\t}\n+\n+\tret |= folio_lock_or_retry(folio, vmf);\n+\tif (ret \u0026 VM_FAULT_RETRY)\n+\t\tgoto out_release;\n+\n+\t/* Verify the folio is still in swap cache and matches our entry */\n+\tif (unlikely(!folio_matches_swap_entry(folio, swp_entry)))\n+\t\tgoto out_page;\n+\n+\t/*\n+\t * Folio should be PMD-sized; if not (e.g. split in swap cache),\n+\t * split the PMD swap entry and retry at PTE level.\n+\t */\n+\tif (folio_nr_pages(folio) != HPAGE_PMD_NR) {\n+\t\tfolio_unlock(folio);\n+\t\tfolio_put(folio);\n+\t\tgoto split_fallback;\n+\t}\n+\n+\tif (unlikely(!folio_test_uptodate(folio))) {\n+\t\tif (zswap_is_present(swp_entry, HPAGE_PMD_NR)) {\n+\t\t\tfolio_unlock(folio);\n+\t\t\tfolio_put(folio);\n+\t\t\tgoto split_fallback;\n+\t\t}\n+\t\tret = VM_FAULT_SIGBUS;\n+\t\tgoto out_page;\n+\t}\n+\n+\t/*\n+\t * If any subpage is hardware-poisoned, split the PMD swap entry and\n+\t * let the PTE swap-in path handle each page individually so\n+\t * do_swap_page() can return VM_FAULT_HWPOISON for the poisoned\n+\t * subpage rather than mapping the corrupted memory as one THP.\n+\t */\n+\tif (unlikely(folio_contain_hwpoisoned_page(folio))) {\n+\t\tfolio_unlock(folio);\n+\t\tfolio_put(folio);\n+\t\tgoto split_fallback;\n+\t}\n+\n+\tpage = folio_page(folio, 0);\n+\tarch_swap_restore(folio_swap(swp_entry, folio), folio);\n+\n+\tfolio_throttle_swaprate(folio, GFP_KERNEL);\n+\n+\t/* Lock the PMD and verify it hasn't changed */\n+\tvmf-\u003eptl = pmd_lock(mm, vmf-\u003epmd);\n+\tif (unlikely(!pmd_same(vmf-\u003eorig_pmd, pmdp_get(vmf-\u003epmd)))) {\n+\t\tspin_unlock(vmf-\u003eptl);\n+\t\tgoto out_page;\n+\t}\n+\n+\texclusive = pmd_swp_exclusive(vmf-\u003eorig_pmd);\n+\n+\t/*\n+\t * Some swap backends (e.g. zram) don't support concurrent page\n+\t * modifications while under writeback. If we map exclusive on such\n+\t * a backend while the folio is still under writeback, the writeback\n+\t * may see partial modifications and corrupt the swap slot. Drop the\n+\t * exclusive marker and only map R/O for that case; further GUP\n+\t * references can't appear once the page is fully unmapped, so this\n+\t * is safe.\n+\t */\n+\tif (exclusive \u0026\u0026 folio_test_writeback(folio) \u0026\u0026\n+\t    data_race(si-\u003eflags \u0026 SWP_STABLE_WRITES))\n+\t\texclusive = false;\n+\n+\t/*\n+\t * Set up the PMD mapping. Similar to do_swap_page() but at PMD level.\n+\t */\n+\tadd_mm_counter(mm, MM_ANONPAGES, HPAGE_PMD_NR);\n+\tadd_mm_counter(mm, MM_SWAPENTS, -HPAGE_PMD_NR);\n+\n+\tpmd = folio_mk_pmd(folio, vma-\u003evm_page_prot);\n+\tpmd = pmd_mkyoung(pmd);\n+\n+\tif (pmd_swp_soft_dirty(vmf-\u003eorig_pmd))\n+\t\tpmd = pmd_mksoft_dirty(pmd);\n+\tif (pmd_swp_uffd(vmf-\u003eorig_pmd))\n+\t\tpmd = pmd_mkuffd(pmd);\n+\tif (pmd_swp_uffd(vmf-\u003eorig_pmd) \u0026\u0026 userfaultfd_rwp(vma)) {\n+\t\tpmd = pmd_modify(pmd, PAGE_NONE);\n+\t\trwp_restore = true;\n+\t}\n+\n+\t/*\n+\t * Check exclusivity to determine if we can map writable.\n+\t */\n+\tif (exclusive) {\n+\t\tif (!rwp_restore \u0026\u0026 (vma-\u003evm_flags \u0026 VM_WRITE) \u0026\u0026\n+\t\t    !userfaultfd_huge_pmd_wp(vma, pmd) \u0026\u0026\n+\t\t    !pmd_needs_soft_dirty_wp(vma, pmd)) {\n+\t\t\tpmd = pmd_mkwrite(pmd, vma);\n+\t\t\tif (write)\n+\t\t\t\tpmd = pmd_mkdirty(pmd);\n+\t\t}\n+\t\trmap_flags |= RMAP_EXCLUSIVE;\n+\t}\n+\n+\tflush_icache_pages(vma, page, HPAGE_PMD_NR);\n+\n+\tif (!folio_test_anon(folio))\n+\t\tfolio_add_new_anon_rmap(folio, vma, haddr, rmap_flags);\n+\telse\n+\t\tfolio_add_anon_rmap_pmd(folio, page, vma, haddr, rmap_flags);\n+\n+\tfolio_put_swap(folio, NULL);\n+\n+\tset_pmd_at(mm, haddr, vmf-\u003epmd, pmd);\n+\tupdate_mmu_cache_pmd(vma, haddr, vmf-\u003epmd);\n+\n+\t/* Update orig_pmd for any follow-up wp_huge_pmd() below. */\n+\tvmf-\u003eorig_pmd = pmd;\n+\n+\t/*\n+\t * Conditionally try to free up the swap cache. Do it after mapping,\n+\t * so raced page faults will likely see the folio in swap cache and\n+\t * wait on the folio lock.\n+\t */\n+\tif (should_try_to_free_swap(si, folio, vma, exclusive, vmf-\u003eflags))\n+\t\tfolio_free_swap(folio);\n+\n+\tspin_unlock(vmf-\u003eptl);\n+\n+\tfolio_unlock(folio);\n+\tput_swap_device(si);\n+\n+\t/*\n+\t * If the write fault wasn't satisfied above (folio is shared without\n+\t * exclusivity), call wp_huge_pmd() to handle COW or\n+\t * userfaultfd-wp without forcing a second fault.\n+\t *\n+\t * wp_huge_pmd() may return VM_FAULT_FALLBACK if it had to split the\n+\t * PMD; that's a normal outcome, and the natural PTE-level refault will\n+\t * complete the COW. Mask it so callers (and the arch fault handler)\n+\t * don't see VM_FAULT_FALLBACK as a fatal VM_FAULT_ERROR.\n+\t */\n+\tif (write \u0026\u0026 !pmd_write(pmd) \u0026\u0026 !rwp_restore) {\n+\t\tvm_fault_t wp_ret = wp_huge_pmd(vmf);\n+\n+\t\twp_ret \u0026= ~VM_FAULT_FALLBACK;\n+\t\tret |= wp_ret;\n+\t\tif (ret \u0026 VM_FAULT_ERROR)\n+\t\t\tret \u0026= VM_FAULT_ERROR;\n+\t}\n+\n+\treturn ret;\n+\n+out_page:\n+\tfolio_unlock(folio);\n+out_release:\n+\tfolio_put(folio);\n+\tput_swap_device(si);\n+\treturn ret;\n+\n+split_fallback:\n+\t__split_huge_pmd(vma, vmf-\u003epmd, haddr, false);\n+\tput_swap_device(si);\n+\treturn 0;\n+}\n+#endif /* CONFIG_THP_SWAP */\n+static inline void zap_deposited_table(struct mm_struct *mm, pmd_t *pmd)\n+{\n+\tpgtable_t pgtable;\n+\n+\tpgtable = pgtable_trans_huge_withdraw(mm, pmd);\n+\tpte_free(mm, pgtable);\n+\tmm_dec_nr_ptes(mm);\n+}\n /*\n  * Return true if we do MADV_FREE successfully on entire pmd page.\n  * Otherwise, return false.\n@@ -2332,6 +2607,21 @@ bool madvise_free_huge_pmd(struct mmu_gather *tlb, struct vm_area_struct *vma,\n \t\tgoto out;\n \n \tif (unlikely(!pmd_present(orig_pmd))) {\n+\t\tif (pmd_is_swap_entry(orig_pmd)) {\n+\t\t\tif (next - addr != HPAGE_PMD_SIZE) {\n+\t\t\t\tspin_unlock(ptl);\n+\t\t\t\t__split_huge_pmd(vma, pmd, addr, false);\n+\t\t\t\tgoto out_unlocked;\n+\t\t\t}\n+\t\t\tsoftleaf_t sl = softleaf_from_pmd(orig_pmd);\n+\n+\t\t\tpmdp_huge_get_and_clear(mm, addr, pmd);\n+\t\t\tzap_deposited_table(mm, pmd);\n+\t\t\tspin_unlock(ptl);\n+\t\t\tswap_put_entries_direct(sl, HPAGE_PMD_NR);\n+\t\t\tadd_mm_counter(mm, MM_SWAPENTS, -HPAGE_PMD_NR);\n+\t\t\treturn true;\n+\t\t}\n \t\tVM_WARN_ON_ONCE(!pmd_is_migration_entry(orig_pmd) \u0026\u0026\n \t\t\t\t!pmd_is_device_private_entry(orig_pmd));\n \t\tgoto out;\n@@ -2382,15 +2672,6 @@ bool madvise_free_huge_pmd(struct mmu_gather *tlb, struct vm_area_struct *vma,\n \treturn ret;\n }\n \n-static inline void zap_deposited_table(struct mm_struct *mm, pmd_t *pmd)\n-{\n-\tpgtable_t pgtable;\n-\n-\tpgtable = pgtable_trans_huge_withdraw(mm, pmd);\n-\tpte_free(mm, pgtable);\n-\tmm_dec_nr_ptes(mm);\n-}\n-\n static void zap_huge_pmd_folio(struct mm_struct *mm, struct vm_area_struct *vma,\n \t\tpmd_t pmdval, struct folio *folio, bool is_present)\n {\n@@ -2483,6 +2764,16 @@ bool zap_huge_pmd(struct mmu_gather *tlb, struct vm_area_struct *vma,\n \tarch_check_zapped_pmd(vma, orig_pmd);\n \ttlb_remove_pmd_tlb_entry(tlb, pmd, addr);\n \n+\tif (pmd_is_swap_entry(orig_pmd)) {\n+\t\tsoftleaf_t sl = softleaf_from_pmd(orig_pmd);\n+\n+\t\tzap_deposited_table(mm, pmd);\n+\t\tspin_unlock(ptl);\n+\t\tswap_put_entries_direct(sl, HPAGE_PMD_NR);\n+\t\tadd_mm_counter(mm, MM_SWAPENTS, -HPAGE_PMD_NR);\n+\t\treturn true;\n+\t}\n+\n \tis_present = pmd_present(orig_pmd);\n \tfolio = normal_or_softleaf_folio_pmd(vma, addr, orig_pmd, is_present);\n \thas_deposit = has_deposited_pgtable(vma, orig_pmd, folio);\n@@ -2515,7 +2806,8 @@ static inline int pmd_move_must_withdraw(spinlock_t *new_pmd_ptl,\n static pmd_t move_soft_dirty_pmd(pmd_t pmd)\n {\n \tif (pgtable_supports_soft_dirty()) {\n-\t\tif (unlikely(pmd_is_migration_entry(pmd)))\n+\t\tif (unlikely(pmd_is_migration_entry(pmd) ||\n+\t\t\t     pmd_is_swap_entry(pmd)))\n \t\t\tpmd = pmd_swp_mksoft_dirty(pmd);\n \t\telse if (pmd_present(pmd))\n \t\t\tpmd = pmd_mksoft_dirty(pmd);\n@@ -2606,7 +2898,14 @@ static void change_non_present_huge_pmd(struct mm_struct *mm,\n \tpmd_t newpmd;\n \n \tVM_WARN_ON(!pmd_is_valid_softleaf(*pmd));\n-\tif (softleaf_is_migration_write(entry)) {\n+\n+\t/*\n+\t * PMD swap entries don't encode write permission in the entry type,\n+\t * so only uffd_wp flag changes apply. No folio lookup needed.\n+\t */\n+\tif (softleaf_is_swap(entry)) {\n+\t\tnewpmd = *pmd;\n+\t} else if (softleaf_is_migration_write(entry)) {\n \t\tconst struct folio *folio = softleaf_to_folio(entry);\n \n \t\t/*\n@@ -2666,7 +2965,7 @@ int change_huge_pmd(struct mmu_gather *tlb, struct vm_area_struct *vma,\n \tif (!ptl)\n \t\treturn 0;\n \n-\tif (thp_migration_supported() \u0026\u0026 pmd_is_valid_softleaf(*pmd)) {\n+\tif (pmd_is_valid_softleaf(*pmd)) {\n \t\tchange_non_present_huge_pmd(mm, addr, pmd, uffd_prot,\n \t\t\t\t\t    uffd_prot_resolve);\n \t\tgoto unlock;\n@@ -2793,6 +3092,74 @@ int change_huge_pud(struct mmu_gather *tlb, struct vm_area_struct *vma,\n #endif\n \n #ifdef CONFIG_USERFAULTFD\n+/*\n+ * Move a PMD-level swap entry from src_pmd to dst_pmd. Both PMD locks are\n+ * acquired here; src_folio (if present) must already be locked. The deposited\n+ * page table backing the source THP is moved across with the entry.\n+ */\n+static int move_swap_pmd(struct mm_struct *mm, struct vm_area_struct *dst_vma,\n+\t\t\t unsigned long dst_addr, unsigned long src_addr,\n+\t\t\t pmd_t *dst_pmd, pmd_t *src_pmd,\n+\t\t\t pmd_t orig_dst_pmd, pmd_t orig_src_pmd,\n+\t\t\t spinlock_t *dst_ptl, spinlock_t *src_ptl,\n+\t\t\t struct folio *src_folio, swp_entry_t entry)\n+{\n+\tpgtable_t src_pgtable;\n+\tpmd_t moved_pmd;\n+\n+\t/*\n+\t * The folio may have been freed and reused for a different swap entry\n+\t * while it was unlocked. Re-verify the association.\n+\t */\n+\tif (src_folio \u0026\u0026 unlikely(!folio_matches_swap_entry(src_folio, entry) ||\n+\t\t\t\t  folio_nr_pages(src_folio) != HPAGE_PMD_NR))\n+\t\treturn -EAGAIN;\n+\n+\tdouble_pt_lock(dst_ptl, src_ptl);\n+\n+\tif (!pmd_same(*src_pmd, orig_src_pmd) ||\n+\t    !pmd_same(*dst_pmd, orig_dst_pmd)) {\n+\t\tdouble_pt_unlock(dst_ptl, src_ptl);\n+\t\treturn -EAGAIN;\n+\t}\n+\n+\t/*\n+\t * If the folio is in the swap cache, re-anchor its anon rmap to the\n+\t * destination VMA so a future swap-in fault at dst_addr finds it.\n+\t * Otherwise, re-check the whole PMD swap range: a PMD swap entry is\n+\t * only a compact encoding for 512 swap slots, and any per-slot cached\n+\t * folio would need the PTE move path to update its rmap metadata.\n+\t */\n+\tif (src_folio) {\n+\t\tfolio_move_anon_rmap(src_folio, dst_vma);\n+\t\tsrc_folio-\u003eindex = linear_page_index(dst_vma, dst_addr);\n+\t} else {\n+\t\tunsigned int type = swp_type(entry);\n+\t\tpgoff_t offset = swp_offset(entry);\n+\t\tint i;\n+\n+\t\tfor (i = 0; i \u003c HPAGE_PMD_NR; i++) {\n+\t\t\tif (swap_cache_has_folio(swp_entry(type, offset + i))) {\n+\t\t\t\tdouble_pt_unlock(dst_ptl, src_ptl);\n+\t\t\t\treturn -EAGAIN;\n+\t\t\t}\n+\t\t}\n+\t}\n+\n+\tmoved_pmd = pmdp_huge_get_and_clear(mm, src_addr, src_pmd);\n+\tif (pgtable_supports_soft_dirty())\n+\t\tmoved_pmd = pmd_swp_mksoft_dirty(moved_pmd);\n+\tif (userfaultfd_rwp(dst_vma))\n+\t\tmoved_pmd = pmd_swp_mkuffd(moved_pmd);\n+\tset_pmd_at(mm, dst_addr, dst_pmd, moved_pmd);\n+\n+\tsrc_pgtable = pgtable_trans_huge_withdraw(mm, src_pmd);\n+\tpgtable_trans_huge_deposit(mm, dst_pmd, src_pgtable);\n+\n+\tdouble_pt_unlock(dst_ptl, src_ptl);\n+\treturn 0;\n+}\n+\n /*\n  * The PT lock for src_pmd and dst_vma/src_vma (for reading) are locked by\n  * the caller, but it must return after releasing the page_table_lock.\n@@ -2827,11 +3194,76 @@ int move_pages_huge_pmd(struct mm_struct *mm, pmd_t *dst_pmd, pmd_t *src_pmd, pm\n \t}\n \n \tif (!pmd_trans_huge(src_pmdval)) {\n-\t\tspin_unlock(src_ptl);\n \t\tif (pmd_is_migration_entry(src_pmdval)) {\n+\t\t\tspin_unlock(src_ptl);\n \t\t\tpmd_migration_entry_wait(mm, src_pmd);\n \t\t\treturn -EAGAIN;\n \t\t}\n+\t\tif (pmd_is_swap_entry(src_pmdval)) {\n+\t\t\tswp_entry_t entry;\n+\t\t\tstruct swap_info_struct *si;\n+\t\t\tenum swap_pmd_cache cache_state;\n+\n+\t\t\t/*\n+\t\t\t * UFFDIO_MOVE on anon mappings requires single-owner\n+\t\t\t * semantics; refuse to move a shared swap entry.\n+\t\t\t */\n+\t\t\tif (!pmd_swp_exclusive(src_pmdval)) {\n+\t\t\t\tspin_unlock(src_ptl);\n+\t\t\t\treturn -EBUSY;\n+\t\t\t}\n+\n+\t\t\tentry = softleaf_from_pmd(src_pmdval);\n+\t\t\tspin_unlock(src_ptl);\n+\n+\t\t\t/* Pin the swap device against a racing swapoff. */\n+\t\t\tsi = get_swap_device(entry);\n+\t\t\tif (unlikely(!si))\n+\t\t\t\treturn -EAGAIN;\n+\n+\t\t\tsrc_folio = NULL;\n+\t\t\tcache_state = swap_pmd_cache_lookup(entry, \u0026src_folio);\n+\t\t\tif (cache_state == SWAP_PMD_CACHE_SPLIT) {\n+\t\t\t\tput_swap_device(si);\n+\t\t\t\t__split_huge_pmd(src_vma, src_pmd, src_addr, false);\n+\t\t\t\treturn -EAGAIN;\n+\t\t\t}\n+\n+\t\t\tmmu_notifier_range_init(\u0026range, MMU_NOTIFY_CLEAR, 0,\n+\t\t\t\t\t\tmm, src_addr,\n+\t\t\t\t\t\tsrc_addr + HPAGE_PMD_SIZE);\n+\t\t\tmmu_notifier_invalidate_range_start(\u0026range);\n+\n+\t\t\tif (src_folio) {\n+\t\t\t\tfolio_lock(src_folio);\n+\t\t\t\tif (!folio_matches_swap_entry(src_folio, entry) ||\n+\t\t\t\t    folio_nr_pages(src_folio) != HPAGE_PMD_NR) {\n+\t\t\t\t\terr = -EAGAIN;\n+\t\t\t\t\tfolio_unlock(src_folio);\n+\t\t\t\t\tfolio_put(src_folio);\n+\t\t\t\t\tmmu_notifier_invalidate_range_end(\u0026range);\n+\t\t\t\t\tput_swap_device(si);\n+\t\t\t\t\t__split_huge_pmd(src_vma, src_pmd,\n+\t\t\t\t\t\t\t src_addr, false);\n+\t\t\t\t\treturn err;\n+\t\t\t\t}\n+\t\t\t}\n+\n+\t\t\tdst_ptl = pmd_lockptr(mm, dst_pmd);\n+\t\t\terr = move_swap_pmd(mm, dst_vma, dst_addr, src_addr,\n+\t\t\t\t\t    dst_pmd, src_pmd, dst_pmdval,\n+\t\t\t\t\t    src_pmdval, dst_ptl, src_ptl,\n+\t\t\t\t\t    src_folio, entry);\n+\n+\t\t\tmmu_notifier_invalidate_range_end(\u0026range);\n+\t\t\tif (src_folio) {\n+\t\t\t\tfolio_unlock(src_folio);\n+\t\t\t\tfolio_put(src_folio);\n+\t\t\t}\n+\t\t\tput_swap_device(si);\n+\t\t\treturn err;\n+\t\t}\n+\t\tspin_unlock(src_ptl);\n \t\treturn -ENOENT;\n \t}\n \n@@ -3210,6 +3642,14 @@ static void __split_huge_pmd_locked(struct vm_area_struct *vma, pmd_t *pmd,\n \t\t\tfolio_add_anon_rmap_ptes(folio, page, HPAGE_PMD_NR,\n \t\t\t\t\t\t vma, haddr, rmap_flags);\n \t\t}\n+\t} else if (pmd_is_swap_entry(*pmd)) {\n+\t\tVM_WARN_ON_ONCE(freeze);\n+\t\t/* Swap entries have no page for the migration freeze path. */\n+\t\tfreeze = false;\n+\t\told_pmd = *pmd;\n+\t\tsoft_dirty = pmd_swp_soft_dirty(old_pmd);\n+\t\tuffd_wp = pmd_swp_uffd(old_pmd);\n+\t\tanon_exclusive = pmd_swp_exclusive(old_pmd);\n \t} else {\n \t\t/*\n \t\t * Up to this point the pmd is present and huge and userland has\n@@ -3346,6 +3786,25 @@ static void __split_huge_pmd_locked(struct vm_area_struct *vma, pmd_t *pmd,\n \t\t\tVM_WARN_ON(!pte_none(ptep_get(pte + i)));\n \t\t\tset_pte_at(mm, addr, pte + i, entry);\n \t\t}\n+\t} else if (pmd_is_swap_entry(old_pmd)) {\n+\t\tsoftleaf_t sl_entry = softleaf_from_pmd(old_pmd);\n+\t\tpte_t swp_pte;\n+\t\tswp_entry_t sub_entry;\n+\n+\t\tfor (i = 0, addr = haddr; i \u003c HPAGE_PMD_NR;\n+\t\t     i++, addr += PAGE_SIZE) {\n+\t\t\tsub_entry = swp_entry(swp_type(sl_entry),\n+\t\t\t\t\t      swp_offset(sl_entry) + i);\n+\t\t\tswp_pte = swp_entry_to_pte(sub_entry);\n+\t\t\tif (soft_dirty)\n+\t\t\t\tswp_pte = pte_swp_mksoft_dirty(swp_pte);\n+\t\t\tif (uffd_wp)\n+\t\t\t\tswp_pte = pte_swp_mkuffd(swp_pte);\n+\t\t\tif (anon_exclusive)\n+\t\t\t\tswp_pte = pte_swp_mkexclusive(swp_pte);\n+\t\t\tVM_WARN_ON(!pte_none(ptep_get(pte + i)));\n+\t\t\tset_pte_at(mm, addr, pte + i, swp_pte);\n+\t\t}\n \t} else {\n \t\tpte_t entry;\n \n@@ -3373,7 +3832,7 @@ static void __split_huge_pmd_locked(struct vm_area_struct *vma, pmd_t *pmd,\n \t}\n \tpte_unmap(pte);\n \n-\tif (!pmd_is_migration_entry(*pmd))\n+\tif (!pmd_is_migration_entry(old_pmd) \u0026\u0026 !pmd_is_swap_entry(old_pmd))\n \t\tfolio_remove_rmap_pmd(folio, page, vma);\n \tif (freeze)\n \t\tput_page(page);\n@@ -5102,3 +5561,83 @@ void remove_migration_pmd(struct page_vma_mapped_walk *pvmw, struct page *new)\n \ttrace_remove_migration_pmd(address, pmd_val(pmde));\n }\n #endif\n+\n+#ifdef CONFIG_THP_SWAP\n+/**\n+ * set_pmd_swap_entry() - Replace a PMD mapping with a PMD-level swap entry.\n+ * @pvmw: Page vma mapped walk context, must have pvmw-\u003epmd set and\n+ *        pvmw-\u003epte NULL (i.e. PMD-mapped).\n+ * @folio: The folio being swapped out. Must be in the swap cache.\n+ *\n+ * This installs a PMD-level swap entry in place of a present PMD mapping,\n+ * avoiding the need to split the PMD into PTE-level swap entries.\n+ *\n+ * Return: 0 on success, negative error code on failure.\n+ */\n+int set_pmd_swap_entry(struct page_vma_mapped_walk *pvmw,\n+\t\t       struct folio *folio)\n+{\n+\tstruct vm_area_struct *vma = pvmw-\u003evma;\n+\tstruct mm_struct *mm = vma-\u003evm_mm;\n+\tunsigned long address = pvmw-\u003eaddress;\n+\tunsigned long haddr = address \u0026 HPAGE_PMD_MASK;\n+\tstruct page *page = folio_page(folio, 0);\n+\tbool anon_exclusive;\n+\tpmd_t pmdval;\n+\tswp_entry_t entry;\n+\tpmd_t pmdswp;\n+\n+\tif (!(pvmw-\u003epmd \u0026\u0026 !pvmw-\u003epte))\n+\t\treturn 0;\n+\n+\tVM_BUG_ON_FOLIO(!folio_test_swapcache(folio), folio);\n+\tVM_BUG_ON_FOLIO(!folio_test_anon(folio), folio);\n+\n+\tif (unlikely(folio_test_swapbacked(folio) !=\n+\t\t\tfolio_test_swapcache(folio))) {\n+\t\tWARN_ON_ONCE(1);\n+\t\treturn -EBUSY;\n+\t}\n+\n+\tflush_cache_range(vma, haddr, haddr + HPAGE_PMD_SIZE);\n+\n+\tpmdval = pmdp_invalidate(vma, haddr, pvmw-\u003epmd);\n+\n+\t/* Update high watermark before we lower rss */\n+\tupdate_hiwater_rss(mm);\n+\n+\tif (folio_dup_swap(folio, NULL) \u003c 0) {\n+\t\tset_pmd_at(mm, haddr, pvmw-\u003epmd, pmdval);\n+\t\treturn -ENOMEM;\n+\t}\n+\n+\t/* See folio_try_share_anon_rmap_pmd(): invalidate PMD first. */\n+\tanon_exclusive = PageAnonExclusive(page);\n+\tif (anon_exclusive \u0026\u0026 folio_try_share_anon_rmap_pmd(folio, page)) {\n+\t\tfolio_put_swap(folio, NULL);\n+\t\tset_pmd_at(mm, haddr, pvmw-\u003epmd, pmdval);\n+\t\treturn -EBUSY;\n+\t}\n+\n+\tmm_prepare_for_swap_entries(mm);\n+\n+\tif (pmd_dirty(pmdval))\n+\t\tfolio_mark_dirty(folio);\n+\n+\tentry = folio-\u003eswap;\n+\tpmdswp = softleaf_to_pmd(entry);\n+\tif (pmd_soft_dirty(pmdval))\n+\t\tpmdswp = pmd_swp_mksoft_dirty(pmdswp);\n+\tif (pmd_uffd(pmdval))\n+\t\tpmdswp = pmd_swp_mkuffd(pmdswp);\n+\tif (anon_exclusive)\n+\t\tpmdswp = pmd_swp_mkexclusive(pmdswp);\n+\tset_pmd_at(mm, haddr, pvmw-\u003epmd, pmdswp);\n+\n+\tfolio_remove_rmap_pmd(folio, page, vma);\n+\tfolio_put(folio);\n+\n+\tcount_vm_event(THP_SWPOUT_PMD);\n+\treturn 0;\n+}\n+#endif /* CONFIG_THP_SWAP */\ndiff --git a/mm/internal.h b/mm/internal.h\nindex 5758dcaf439235..7e5893602c8fb2 100644\n--- a/mm/internal.h\n+++ b/mm/internal.h\n@@ -581,6 +581,40 @@ static inline vm_fault_t vmf_anon_prepare(struct vm_fault *vmf)\n }\n \n vm_fault_t do_swap_page(struct vm_fault *vmf);\n+vm_fault_t wp_huge_pmd(struct vm_fault *vmf);\n+\n+/*\n+ * Check if we should call folio_free_swap to free the swap cache.\n+ * folio_free_swap only frees the swap cache to release the slot if swap\n+ * count is zero, so we don't need to check the swap count here.\n+ */\n+static inline bool should_try_to_free_swap(struct swap_info_struct *si,\n+\t\t\t\t\t   struct folio *folio,\n+\t\t\t\t\t   struct vm_area_struct *vma,\n+\t\t\t\t\t   bool exclusive,\n+\t\t\t\t\t   unsigned int fault_flags)\n+{\n+\tif (!folio_test_swapcache(folio))\n+\t\treturn false;\n+\t/*\n+\t * Always try to free swap cache for SWP_SYNCHRONOUS_IO devices. Swap\n+\t * cache can help save some IO or memory overhead, but these devices\n+\t * are fast, and meanwhile, swap cache pinning the slot deferring the\n+\t * release of metadata or fragmentation is a more critical issue.\n+\t */\n+\tif (data_race(si-\u003eflags \u0026 SWP_SYNCHRONOUS_IO))\n+\t\treturn true;\n+\tif (mem_cgroup_swap_full(folio) || (vma-\u003evm_flags \u0026 VM_LOCKED) ||\n+\t    folio_test_mlocked(folio))\n+\t\treturn true;\n+\n+\t/*\n+\t * Free the swapcache only if we are the exclusive user and\n+\t * this is a write fault.\n+\t */\n+\treturn (fault_flags \u0026 FAULT_FLAG_WRITE) \u0026\u0026 exclusive;\n+}\n+\n void folio_rotate_reclaimable(struct folio *folio);\n bool __folio_end_writeback(struct folio *folio);\n void deactivate_file_folio(struct folio *folio);\ndiff --git a/mm/khugepaged.c b/mm/khugepaged.c\nindex 27e8f3077e80fe..f9c00f5ef2e394 100644\n--- a/mm/khugepaged.c\n+++ b/mm/khugepaged.c\n@@ -1101,6 +1101,12 @@ static inline enum scan_result check_pmd_state(pmd_t *pmd)\n \t */\n \tif (pmd_is_migration_entry(pmde))\n \t\treturn SCAN_PMD_MAPPED;\n+\t/*\n+\t * A PMD-mapped THP that has been swapped out is still a THP from\n+\t * khugepaged's perspective; treat it like a present huge PMD.\n+\t */\n+\tif (pmd_is_swap_entry(pmde))\n+\t\treturn SCAN_PMD_MAPPED;\n \tif (!pmd_present(pmde))\n \t\treturn SCAN_NO_PTE_TABLE;\n \tif (pmd_trans_huge(pmde))\ndiff --git a/mm/madvise.c b/mm/madvise.c\nindex 07a21ca31bad4d..1d5154b10dfcca 100644\n--- a/mm/madvise.c\n+++ b/mm/madvise.c\n@@ -32,6 +32,7 @@\n #include \u003clinux/leafops.h\u003e\n #include \u003clinux/shmem_fs.h\u003e\n #include \u003clinux/mmu_notifier.h\u003e\n+#include \u003clinux/zswap.h\u003e\n \n #include \u003casm/tlb.h\u003e\n \n@@ -193,6 +194,91 @@ static int swapin_walk_pmd_entry(pmd_t *pmd, unsigned long start,\n \tspinlock_t *ptl;\n \tunsigned long addr;\n \n+\tptl = pmd_trans_huge_lock(pmd, vma);\n+\tif (ptl) {\n+\t\tpmd_t pmdval = *pmd;\n+\n+\t\tif (pmd_is_swap_entry(pmdval)) {\n+\t\t\tsoftleaf_t entry = softleaf_from_pmd(pmdval);\n+\t\t\tstruct vm_fault vmf = {\n+\t\t\t\t.vma = vma,\n+\t\t\t\t.address = start,\n+\t\t\t\t.real_address = start,\n+\t\t\t\t.pmd = pmd,\n+\t\t\t};\n+\t\t\tstruct swap_info_struct *si;\n+\t\t\tstruct folio *folio;\n+\t\t\tenum swap_pmd_cache cache_state;\n+\t\t\tbool split = false;\n+\n+\t\t\tcache_state = swap_pmd_cache_lookup(entry, \u0026folio);\n+\t\t\tif (cache_state == SWAP_PMD_CACHE_HUGE) {\n+\t\t\t\tfolio_put(folio);\n+\t\t\t\tspin_unlock(ptl);\n+\t\t\t\tgoto ret;\n+\t\t\t}\n+\t\t\tif (cache_state == SWAP_PMD_CACHE_SPLIT ||\n+\t\t\t    zswap_is_present(entry, HPAGE_PMD_NR)) {\n+\t\t\t\tspin_unlock(ptl);\n+\t\t\t\t__split_huge_pmd(vma, pmd, start, false);\n+\t\t\t\twalk-\u003eaction = ACTION_AGAIN;\n+\t\t\t\tgoto ret;\n+\t\t\t}\n+\n+\t\t\t/*\n+\t\t\t * Pin the swap device under the PMD lock so the\n+\t\t\t * PMD-swap-entry observation keeps the entry valid for\n+\t\t\t * swapin_sync().\n+\t\t\t */\n+\t\t\tsi = get_swap_device(entry);\n+\t\t\tspin_unlock(ptl);\n+\t\t\tif (!si)\n+\t\t\t\tgoto ret;\n+\n+\t\t\tfolio = swapin_sync(entry, GFP_HIGHUSER_MOVABLE,\n+\t\t\t\t\t    BIT(HPAGE_PMD_ORDER), \u0026vmf,\n+\t\t\t\t\t    NULL, 0);\n+\t\t\t/*\n+\t\t\t * The empty-cache observation was made under the PMD\n+\t\t\t * lock, but swap cache can change after dropping it. If\n+\t\t\t * PMD-order swapin lost a race to per-slot cache state,\n+\t\t\t * retry through the PTE path.\n+\t\t\t */\n+\t\t\tif (IS_ERR(folio)) {\n+\t\t\t\tif (PTR_ERR(folio) == -EBUSY)\n+\t\t\t\t\tsplit = true;\n+\t\t\t} else if (folio) {\n+\t\t\t\tif (folio_nr_pages(folio) != HPAGE_PMD_NR) {\n+\t\t\t\t\tsplit = true;\n+\t\t\t\t} else if (!folio_test_locked(folio) \u0026\u0026\n+\t\t\t\t\t !folio_test_uptodate(folio) \u0026\u0026\n+\t\t\t\t\t zswap_is_present(entry, HPAGE_PMD_NR)) {\n+\t\t\t\t\tfolio_lock(folio);\n+\t\t\t\t\t/*\n+\t\t\t\t\t * A failed PMD-order zswap load leaves the\n+\t\t\t\t\t * folio clean and not uptodate, but another\n+\t\t\t\t\t * thread can remove it from swap cache before\n+\t\t\t\t\t * we acquire the lock. Revalidate the\n+\t\t\t\t\t * association before deleting it so the PTE\n+\t\t\t\t\t * retry can load the per-page state.\n+\t\t\t\t\t */\n+\t\t\t\t\tif (folio_matches_swap_entry(folio, entry))\n+\t\t\t\t\t\tswap_cache_del_folio(folio);\n+\t\t\t\t\tfolio_unlock(folio);\n+\t\t\t\t\tsplit = true;\n+\t\t\t\t}\n+\t\t\t\tfolio_put(folio);\n+\t\t\t}\n+\t\t\tput_swap_device(si);\n+\t\t\tif (split) {\n+\t\t\t\t__split_huge_pmd(vma, pmd, start, false);\n+\t\t\t\twalk-\u003eaction = ACTION_AGAIN;\n+\t\t\t}\n+\t\t\tgoto ret;\n+\t\t}\n+\t\tspin_unlock(ptl);\n+\t}\n+\n \tfor (addr = start; addr \u003c end; addr += PAGE_SIZE) {\n \t\tpte_t pte;\n \t\tsoftleaf_t entry;\n@@ -221,6 +307,7 @@ static int swapin_walk_pmd_entry(pmd_t *pmd, unsigned long start,\n \tif (ptep)\n \t\tpte_unmap_unlock(ptep, ptl);\n \tswap_read_submit(\u0026ctx);\n+ret:\n \tcond_resched();\n \n \treturn 0;\n@@ -374,6 +461,15 @@ static int madvise_cold_or_pageout_pte_range(pmd_t *pmd,\n \t\t\t\t\t!can_do_file_pageout(vma);\n \n #ifdef CONFIG_TRANSPARENT_HUGEPAGE\n+\t/*\n+\t * Swapped-out THPs have no resident folio to deactivate or reclaim.\n+\t * Avoid descending into or splitting a PMD swap entry.\n+\t */\n+\tif (pmd_is_swap_entry(*pmd)) {\n+\t\twalk-\u003eaction = ACTION_CONTINUE;\n+\t\treturn 0;\n+\t}\n+\n \tif (pmd_trans_huge(*pmd)) {\n \t\tpmd_t orig_pmd;\n \t\tunsigned long next = pmd_addr_end(addr, end);\n@@ -384,6 +480,9 @@ static int madvise_cold_or_pageout_pte_range(pmd_t *pmd,\n \t\t\treturn 0;\n \n \t\torig_pmd = *pmd;\n+\t\tif (pmd_is_swap_entry(orig_pmd))\n+\t\t\tgoto huge_unlock;\n+\n \t\tif (is_huge_zero_pmd(orig_pmd))\n \t\t\tgoto huge_unlock;\n \n@@ -665,7 +764,7 @@ static int madvise_free_pte_range(pmd_t *pmd, unsigned long addr,\n \tint nr, max_nr;\n \n \tnext = pmd_addr_end(addr, end);\n-\tif (pmd_trans_huge(*pmd))\n+\tif (pmd_trans_huge(*pmd) || pmd_is_swap_entry(*pmd))\n \t\tif (madvise_free_huge_pmd(tlb, vma, pmd, addr, next))\n \t\t\treturn 0;\n \ndiff --git a/mm/memory.c b/mm/memory.c\nindex a620d425ec95e8..f4ab24a49d0d32 100644\n--- a/mm/memory.c\n+++ b/mm/memory.c\n@@ -1012,7 +1012,7 @@ copy_nonpresent_pte(struct mm_struct *dst_mm, struct mm_struct *src_mm,\n \tstruct page *page;\n \n \tif (likely(softleaf_is_swap(entry))) {\n-\t\tif (swap_dup_entry_direct(entry) \u003c 0)\n+\t\tif (swap_dup_entries_direct(entry, 1) \u003c 0)\n \t\t\treturn -EIO;\n \n \t\tmm_prepare_for_swap_entries(dst_mm);\n@@ -1427,7 +1427,7 @@ copy_pte_range(struct vm_area_struct *dst_vma, struct vm_area_struct *src_vma,\n \n \tif (ret == -EIO) {\n \t\tVM_WARN_ON_ONCE(!entry.val);\n-\t\tif (swap_retry_table_alloc(entry, GFP_KERNEL) \u003c 0) {\n+\t\tif (swap_retry_table_alloc(entry, 1, GFP_KERNEL) \u003c 0) {\n \t\t\tret = -ENOMEM;\n \t\t\tgoto out;\n \t\t}\n@@ -4636,38 +4636,6 @@ static vm_fault_t remove_device_exclusive_entry(struct vm_fault *vmf)\n \treturn 0;\n }\n \n-/*\n- * Check if we should call folio_free_swap to free the swap cache.\n- * folio_free_swap only frees the swap cache to release the slot if swap\n- * count is zero, so we don't need to check the swap count here.\n- */\n-static inline bool should_try_to_free_swap(struct swap_info_struct *si,\n-\t\t\t\t\t   struct folio *folio,\n-\t\t\t\t\t   struct vm_area_struct *vma,\n-\t\t\t\t\t   bool exclusive,\n-\t\t\t\t\t   unsigned int fault_flags)\n-{\n-\tif (!folio_test_swapcache(folio))\n-\t\treturn false;\n-\t/*\n-\t * Always try to free swap cache for SWP_SYNCHRONOUS_IO devices. Swap\n-\t * cache can help save some IO or memory overhead, but these devices\n-\t * are fast, and meanwhile, swap cache pinning the slot deferring the\n-\t * release of metadata or fragmentation is a more critical issue.\n-\t */\n-\tif (data_race(si-\u003eflags \u0026 SWP_SYNCHRONOUS_IO))\n-\t\treturn true;\n-\tif (mem_cgroup_swap_full(folio) || (vma-\u003evm_flags \u0026 VM_LOCKED) ||\n-\t    folio_test_mlocked(folio))\n-\t\treturn true;\n-\n-\t/*\n-\t * Free the swapcache only if we are the exclusive user and\n-\t * this is a write fault.\n-\t */\n-\treturn (fault_flags \u0026 FAULT_FLAG_WRITE) \u0026\u0026 exclusive;\n-}\n-\n static vm_fault_t pte_marker_clear(struct vm_fault *vmf)\n {\n \tvmf-\u003epte = pte_offset_map_lock(vmf-\u003evma-\u003evm_mm, vmf-\u003epmd,\n@@ -5047,7 +5015,8 @@ vm_fault_t do_swap_page(struct vm_fault *vmf)\n \tpage_idx = 0;\n \taddress = vmf-\u003eaddress;\n \tptep = vmf-\u003epte;\n-\tif (folio_test_large(folio) \u0026\u0026 folio_test_swapcache(folio)) {\n+\tif (folio_test_large(folio) \u0026\u0026 folio_test_swapcache(folio) \u0026\u0026\n+\t    !folio_contain_hwpoisoned_page(folio)) {\n \t\tint nr = folio_nr_pages(folio);\n \t\tunsigned long idx = folio_page_idx(folio, page);\n \t\tunsigned long folio_start = address - idx * PAGE_SIZE;\n@@ -6382,8 +6351,7 @@ static inline vm_fault_t create_huge_pmd(struct vm_fault *vmf)\n \treturn VM_FAULT_FALLBACK;\n }\n \n-/* `inline' is required to avoid gcc 4.1.2 build error */\n-static inline vm_fault_t wp_huge_pmd(struct vm_fault *vmf)\n+vm_fault_t wp_huge_pmd(struct vm_fault *vmf)\n {\n \tstruct vm_area_struct *vma = vmf-\u003evma;\n \tconst bool unshare = vmf-\u003eflags \u0026 FAULT_FLAG_UNSHARE;\n@@ -6676,6 +6644,9 @@ static vm_fault_t __handle_mm_fault(struct vm_area_struct *vma,\n \n \t\tif (pmd_is_migration_entry(vmf.orig_pmd))\n \t\t\tpmd_migration_entry_wait(mm, vmf.pmd);\n+\t\telse if (IS_ENABLED(CONFIG_THP_SWAP) \u0026\u0026\n+\t\t\t pmd_is_swap_entry(vmf.orig_pmd))\n+\t\t\treturn do_huge_pmd_swap_page(\u0026vmf);\n \t\treturn 0;\n \t}\n \tif (pmd_trans_huge(vmf.orig_pmd)) {\ndiff --git a/mm/mincore.c b/mm/mincore.c\nindex ff4ac828176837..3f0fba964c8a33 100644\n--- a/mm/mincore.c\n+++ b/mm/mincore.c\n@@ -85,6 +85,41 @@ static unsigned char mincore_swap(swp_entry_t entry, bool shmem)\n \treturn present;\n }\n \n+#ifdef CONFIG_THP_SWAP\n+static void mincore_pmd_swap(swp_entry_t entry, unsigned long addr,\n+\t\t\t     unsigned long end, unsigned char *vec)\n+{\n+\tunsigned long haddr = addr \u0026 HPAGE_PMD_MASK;\n+\tunsigned long start = (addr - haddr) \u003e\u003e PAGE_SHIFT;\n+\tunsigned long nr = (end - addr) \u003e\u003e PAGE_SHIFT;\n+\tstruct folio *folio;\n+\tenum swap_pmd_cache state;\n+\tint i;\n+\n+\tstate = swap_pmd_cache_lookup(entry, \u0026folio);\n+\tif (state == SWAP_PMD_CACHE_HUGE) {\n+\t\tmemset(vec, folio_test_uptodate(folio), nr);\n+\t\tfolio_put(folio);\n+\t\treturn;\n+\t}\n+\n+\tif (state == SWAP_PMD_CACHE_EMPTY) {\n+\t\tmemset(vec, 0, nr);\n+\t\treturn;\n+\t}\n+\n+\t/*\n+\t * The PMD swap entry is only a compact encoding for consecutive swap\n+\t * slots. If the PMD-sized swapcache folio was split, report residency\n+\t * from the individual slots covered by this mincore() range.\n+\t */\n+\tfor (i = 0; i \u003c nr; i++)\n+\t\tvec[i] = mincore_swap(swp_entry(swp_type(entry),\n+\t\t\t\t\t\tswp_offset(entry) + start + i),\n+\t\t\t\t      false);\n+}\n+#endif\n+\n /*\n  * Later we can get more picky about what \"in core\" means precisely.\n  * For now, simply check to see if the page is in the page cache,\n@@ -171,7 +206,15 @@ static int mincore_pte_range(pmd_t *pmd, unsigned long addr, unsigned long end,\n \n \tptl = pmd_trans_huge_lock(pmd, vma);\n \tif (ptl) {\n-\t\tmemset(vec, 1, nr);\n+\t\tif (pmd_is_swap_entry(*pmd)) {\n+#ifdef CONFIG_THP_SWAP\n+\t\t\tmincore_pmd_swap(softleaf_from_pmd(*pmd), addr, end, vec);\n+#else\n+\t\t\tmemset(vec, 0, nr);\n+#endif\n+\t\t} else {\n+\t\t\tmemset(vec, 1, nr);\n+\t\t}\n \t\tspin_unlock(ptl);\n \t\tgoto out;\n \t}\ndiff --git a/mm/rmap.c b/mm/rmap.c\nindex b7ead3e9f06412..4574f7b969b60c 100644\n--- a/mm/rmap.c\n+++ b/mm/rmap.c\n@@ -2282,6 +2282,25 @@ static bool try_to_unmap_one(struct folio *folio, struct vm_area_struct *vma,\n \t\t\t\tgoto walk_abort;\n \t\t\t}\n \n+#ifdef CONFIG_THP_SWAP\n+\t\t\t/*\n+\t\t\t * If the folio is in the swap cache and we're not\n+\t\t\t * asked to split, install a PMD-level swap entry.\n+\t\t\t */\n+\t\t\tif (!(flags \u0026 TTU_SPLIT_HUGE_PMD) \u0026\u0026\n+\t\t\t    folio_test_anon(folio) \u0026\u0026\n+\t\t\t    folio_test_swapcache(folio)) {\n+\t\t\t\tif (set_pmd_swap_entry(\u0026pvmw, folio))\n+\t\t\t\t\tgoto walk_abort;\n+\n+\t\t\t\tadd_mm_counter(mm, MM_ANONPAGES,\n+\t\t\t\t\t       -HPAGE_PMD_NR);\n+\t\t\t\tadd_mm_counter(mm, MM_SWAPENTS,\n+\t\t\t\t\t       HPAGE_PMD_NR);\n+\t\t\t\tgoto walk_done;\n+\t\t\t}\n+#endif\n+\n \t\t\tif (flags \u0026 TTU_SPLIT_HUGE_PMD) {\n \t\t\t\t/*\n \t\t\t\t * We temporarily have to drop the PTL and\ndiff --git a/mm/swap.h b/mm/swap.h\nindex 0260f9b4c7eee2..520f9d5a60a4c1 100644\n--- a/mm/swap.h\n+++ b/mm/swap.h\n@@ -249,7 +249,7 @@ static inline void swap_cluster_unlock_irq(struct swap_cluster_info *ci)\n \tspin_unlock_irq(\u0026ci-\u003elock);\n }\n \n-extern int swap_retry_table_alloc(swp_entry_t entry, gfp_t gfp);\n+int swap_retry_table_alloc(swp_entry_t entry, unsigned int nr, gfp_t gfp);\n \n /*\n  * Below are the core routines for doing swap for a folio.\n@@ -338,6 +338,23 @@ static inline bool folio_matches_swap_entry(const struct folio *folio,\n bool swap_cache_has_folio(swp_entry_t entry);\n struct folio *swap_cache_get_folio(swp_entry_t entry);\n void *swap_cache_get_shadow(swp_entry_t entry);\n+enum swap_pmd_cache {\n+\tSWAP_PMD_CACHE_EMPTY,\n+\tSWAP_PMD_CACHE_HUGE,\n+\tSWAP_PMD_CACHE_SPLIT,\n+};\n+\n+#ifdef CONFIG_THP_SWAP\n+enum swap_pmd_cache swap_pmd_cache_lookup(swp_entry_t entry,\n+\t\t\t\t\t  struct folio **foliop);\n+#else\n+static inline enum swap_pmd_cache swap_pmd_cache_lookup(swp_entry_t entry,\n+\t\t\t\t\t\t\tstruct folio **foliop)\n+{\n+\t*foliop = NULL;\n+\treturn SWAP_PMD_CACHE_EMPTY;\n+}\n+#endif\n void swap_cache_del_folio(struct folio *folio);\n struct folio *swap_cache_alloc_folio(swp_entry_t target_entry, gfp_t gfp_mask,\n \t\t\t\t     unsigned long orders, struct vm_fault *vmf,\n@@ -455,7 +472,8 @@ static inline int swap_writeout(struct swap_io_ctx *ctx, struct folio *folio)\n \treturn 0;\n }\n \n-static inline int swap_retry_table_alloc(swp_entry_t entry, gfp_t gfp)\n+static inline int swap_retry_table_alloc(swp_entry_t entry, unsigned int nr,\n+\t\t\t\t\t gfp_t gfp)\n {\n \treturn -EINVAL;\n }\ndiff --git a/mm/swap_state.c b/mm/swap_state.c\nindex 5be825911e645e..1f08fb5220361b 100644\n--- a/mm/swap_state.c\n+++ b/mm/swap_state.c\n@@ -123,6 +123,50 @@ bool swap_cache_has_folio(swp_entry_t entry)\n \treturn swp_tb_is_folio(swp_tb);\n }\n \n+#ifdef CONFIG_THP_SWAP\n+/**\n+ * swap_pmd_cache_lookup - classify the swap cache behind a PMD swap entry\n+ * @entry: first swap slot encoded by the PMD swap entry\n+ * @foliop: returned PMD-sized folio, with a reference, if present\n+ *\n+ * A PMD swap entry is a compact page-table encoding for HPAGE_PMD_NR\n+ * consecutive swap slots. The swap cache behind those slots can be empty,\n+ * one PMD-sized folio, or per-slot folios after the original folio was split.\n+ *\n+ * Context: Caller must keep @entry valid using the usual swap cache rules.\n+ * Return: SWAP_PMD_CACHE_EMPTY if no slot in the PMD range has a cached folio,\n+ * SWAP_PMD_CACHE_HUGE if one PMD-sized folio covers the range, or\n+ * SWAP_PMD_CACHE_SPLIT if the range needs per-page handling.\n+ */\n+enum swap_pmd_cache swap_pmd_cache_lookup(swp_entry_t entry,\n+\t\t\t\t\t  struct folio **foliop)\n+{\n+\tunsigned int type = swp_type(entry);\n+\tpgoff_t offset = swp_offset(entry);\n+\tstruct folio *folio;\n+\tint i;\n+\n+\t*foliop = NULL;\n+\n+\tfolio = swap_cache_get_folio(entry);\n+\tif (folio) {\n+\t\tif (folio_nr_pages(folio) == HPAGE_PMD_NR) {\n+\t\t\t*foliop = folio;\n+\t\t\treturn SWAP_PMD_CACHE_HUGE;\n+\t\t}\n+\t\tfolio_put(folio);\n+\t\treturn SWAP_PMD_CACHE_SPLIT;\n+\t}\n+\n+\tfor (i = 1; i \u003c HPAGE_PMD_NR; i++) {\n+\t\tif (swap_cache_has_folio(swp_entry(type, offset + i)))\n+\t\t\treturn SWAP_PMD_CACHE_SPLIT;\n+\t}\n+\n+\treturn SWAP_PMD_CACHE_EMPTY;\n+}\n+#endif\n+\n /**\n  * swap_cache_get_shadow - Looks up a shadow in the swap cache.\n  * @entry: swap entry used for the lookup.\ndiff --git a/mm/swapfile.c b/mm/swapfile.c\nindex 5d15913dcf8631..46f4f4c07bf6fb 100644\n--- a/mm/swapfile.c\n+++ b/mm/swapfile.c\n@@ -42,6 +42,7 @@\n #include \u003clinux/suspend.h\u003e\n #include \u003clinux/zswap.h\u003e\n #include \u003clinux/plist.h\u003e\n+#include \u003clinux/huge_mm.h\u003e\n \n #include \u003casm/tlbflush.h\u003e\n #include \u003clinux/leafops.h\u003e\n@@ -1462,9 +1463,11 @@ static bool swap_sync_discard(void)\n \n static int swap_extend_table_alloc(struct swap_info_struct *si,\n \t\t\t\t   struct swap_cluster_info *ci,\n-\t\t\t\t   unsigned int ci_off, gfp_t gfp)\n+\t\t\t\t   unsigned int ci_off, unsigned int nr,\n+\t\t\t\t   gfp_t gfp)\n {\n \tint count;\n+\tunsigned int i;\n \tvoid *table;\n \n \ttable = kzalloc(sizeof(ci-\u003eextend_table[0]) * SWAPFILE_CLUSTER, gfp);\n@@ -1480,15 +1483,21 @@ static int swap_extend_table_alloc(struct swap_info_struct *si,\n \t */\n \tif (!cluster_table_is_alloced(ci))\n \t\tgoto out_free;\n-\tcount = swp_tb_get_count(__swap_table_get(ci, ci_off));\n-\tif (count \u003c (SWP_TB_COUNT_MAX - 1))\n-\t\tgoto out_free;\n \tif (ci-\u003eextend_table)\n \t\tgoto out_free;\n-\n-\tci-\u003eextend_table = table;\n-\tspin_unlock(\u0026ci-\u003elock);\n-\treturn 0;\n+\t/*\n+\t * The caller may not know which slot in [ci_off, ci_off + nr) hit\n+\t * SWP_TB_COUNT_MAX - 1. Confirm at least one slot in the range still\n+\t * needs the extend table before committing the allocation.\n+\t */\n+\tfor (i = 0; i \u003c nr; i++) {\n+\t\tcount = swp_tb_get_count(__swap_table_get(ci, ci_off + i));\n+\t\tif (count \u003e= (SWP_TB_COUNT_MAX - 1)) {\n+\t\t\tci-\u003eextend_table = table;\n+\t\t\tspin_unlock(\u0026ci-\u003elock);\n+\t\t\treturn 0;\n+\t\t}\n+\t}\n \n out_free:\n \tspin_unlock(\u0026ci-\u003elock);\n@@ -1496,7 +1505,7 @@ static int swap_extend_table_alloc(struct swap_info_struct *si,\n \treturn 0;\n }\n \n-int swap_retry_table_alloc(swp_entry_t entry, gfp_t gfp)\n+int swap_retry_table_alloc(swp_entry_t entry, unsigned int nr, gfp_t gfp)\n {\n \tint ret;\n \tstruct swap_info_struct *si;\n@@ -1508,7 +1517,8 @@ int swap_retry_table_alloc(swp_entry_t entry, gfp_t gfp)\n \t\treturn 0;\n \n \tci = __swap_offset_to_cluster(si, offset);\n-\tret = swap_extend_table_alloc(si, ci, swp_cluster_offset(entry), gfp);\n+\tret = swap_extend_table_alloc(si, ci, swp_cluster_offset(entry), nr,\n+\t\t\t\t      gfp);\n \n \tput_swap_device(si);\n \treturn ret;\n@@ -1709,7 +1719,8 @@ static int swap_dup_entries_cluster(struct swap_info_struct *si,\n \t\tif (unlikely(err)) {\n \t\t\tif (err == -ENOMEM) {\n \t\t\t\tspin_unlock(\u0026ci-\u003elock);\n-\t\t\t\terr = swap_extend_table_alloc(si, ci, ci_off, GFP_ATOMIC);\n+\t\t\t\terr = swap_extend_table_alloc(si, ci, ci_off, 1,\n+\t\t\t\t\t\t\t      GFP_ATOMIC);\n \t\t\t\tspin_lock(\u0026ci-\u003elock);\n \t\t\t\tif (!err)\n \t\t\t\t\tgoto restart;\n@@ -1720,6 +1731,7 @@ static int swap_dup_entries_cluster(struct swap_info_struct *si,\n \tswap_cluster_unlock(ci);\n \treturn 0;\n failed:\n+\t/* The caller's page-table or swap-cache reference pins every slot. */\n \twhile (ci_off-- \u003e ci_start)\n \t\t__swap_cluster_put_entry(ci, ci_off);\n \tswap_extend_table_try_free(ci);\n@@ -2652,6 +2664,153 @@ static int unuse_pte_range(struct vm_area_struct *vma, pmd_t *pmd,\n \treturn 0;\n }\n \n+#ifdef CONFIG_THP_SWAP\n+/*\n+ * unuse_pmd - Map a locked folio at PMD granularity during swapoff.\n+ *\n+ * The caller provides a locked, swapped-in folio.  Returns 0 on success\n+ * (PMD was mapped).  Returns -EAGAIN if the swap cache folio no longer\n+ * matches the entry or the PMD changed under the lock (try_to_unuse will\n+ * rescan). Returns -EIO if the folio is not uptodate or contains a poisoned\n+ * subpage; in that case the PMD is split so unuse_pte_range() can handle\n+ * individual pages.\n+ */\n+static int unuse_pmd(struct vm_area_struct *vma, pmd_t *pmd,\n+\t\t     unsigned long addr, softleaf_t entry,\n+\t\t     struct folio *folio)\n+{\n+\tstruct mm_struct *mm = vma-\u003evm_mm;\n+\tstruct page *page;\n+\tpmd_t new_pmd, old_pmd;\n+\tspinlock_t *ptl;\n+\trmap_t rmap_flags = RMAP_NONE;\n+\tbool exclusive;\n+\n+\tif (unlikely(!folio_matches_swap_entry(folio, entry)))\n+\t\treturn -EAGAIN;\n+\n+\tif (unlikely(!folio_test_uptodate(folio) ||\n+\t\t     folio_contain_hwpoisoned_page(folio))) {\n+\t\t__split_huge_pmd(vma, pmd, addr, false);\n+\t\treturn -EIO;\n+\t}\n+\n+\tpage = folio_page(folio, 0);\n+\n+\tptl = pmd_lock(mm, pmd);\n+\told_pmd = pmdp_get(pmd);\n+\n+\tif (!pmd_is_swap_entry(old_pmd) ||\n+\t    softleaf_from_pmd(old_pmd).val != entry.val) {\n+\t\tspin_unlock(ptl);\n+\t\treturn -EAGAIN;\n+\t}\n+\n+\texclusive = pmd_swp_exclusive(old_pmd);\n+\n+\t/*\n+\t * Some architectures may have to restore extra metadata to the folio\n+\t * when reading from swap. This metadata may be indexed by swap entry\n+\t * so this must be called before folio_put_swap().\n+\t */\n+\tarch_swap_restore(folio_swap(entry, folio), folio);\n+\n+\tadd_mm_counter(mm, MM_ANONPAGES, HPAGE_PMD_NR);\n+\tadd_mm_counter(mm, MM_SWAPENTS, -HPAGE_PMD_NR);\n+\n+\tnew_pmd = folio_mk_pmd(folio, vma-\u003evm_page_prot);\n+\tnew_pmd = pmd_mkold(new_pmd);\n+\tif (pmd_swp_soft_dirty(old_pmd))\n+\t\tnew_pmd = pmd_mksoft_dirty(new_pmd);\n+\tif (pmd_swp_uffd(old_pmd))\n+\t\tnew_pmd = pmd_mkuffd(new_pmd);\n+\tif (pmd_swp_uffd(old_pmd) \u0026\u0026 userfaultfd_rwp(vma))\n+\t\tnew_pmd = pmd_modify(new_pmd, PAGE_NONE);\n+\n+\tif (exclusive)\n+\t\trmap_flags |= RMAP_EXCLUSIVE;\n+\n+\tfolio_get(folio);\n+\tif (!folio_test_anon(folio))\n+\t\tfolio_add_new_anon_rmap(folio, vma, addr, rmap_flags);\n+\telse\n+\t\tfolio_add_anon_rmap_pmd(folio, page, vma, addr, rmap_flags);\n+\n+\tset_pmd_at(mm, addr, pmd, new_pmd);\n+\tfolio_put_swap(folio, NULL);\n+\n+\tspin_unlock(ptl);\n+\n+\tfolio_free_swap(folio);\n+\treturn 0;\n+}\n+\n+/*\n+ * Try to swap in a PMD swap entry as a whole THP. Returns 0 on success.\n+ * If the swap cache no longer has one PMD-sized folio, zswap may require\n+ * per-page loading, or a PMD-order allocation/read fails, split the PMD so\n+ * the caller can fall back to unuse_pte_range(). Otherwise propagates the\n+ * error from unuse_pmd().\n+ */\n+static int unuse_pmd_entry(struct vm_area_struct *vma, pmd_t *pmd,\n+\t\t\t   unsigned long addr, softleaf_t entry)\n+{\n+\tstruct folio *folio;\n+\tenum swap_pmd_cache cache_state;\n+\tint ret;\n+\n+\tcache_state = swap_pmd_cache_lookup(entry, \u0026folio);\n+\tif (cache_state == SWAP_PMD_CACHE_SPLIT) {\n+\t\tret = -EAGAIN;\n+\t\tgoto split_fallback;\n+\t}\n+\tif (!folio) {\n+\t\tstruct vm_fault vmf = {\n+\t\t\t.vma = vma,\n+\t\t\t.address = addr,\n+\t\t\t.real_address = addr,\n+\t\t\t.pmd = pmd,\n+\t\t};\n+\n+\t\tif (zswap_is_present(entry, HPAGE_PMD_NR)) {\n+\t\t\tret = -EAGAIN;\n+\t\t\tgoto split_fallback;\n+\t\t}\n+\n+\t\tfolio = swapin_sync(entry, GFP_HIGHUSER_MOVABLE,\n+\t\t\t\t    BIT(HPAGE_PMD_ORDER), \u0026vmf, NULL, 0);\n+\t\tif (IS_ERR_OR_NULL(folio)) {\n+\t\t\tret = folio ? PTR_ERR(folio) : -ENOMEM;\n+\t\t\tgoto split_fallback;\n+\t\t}\n+\t}\n+\n+\tfolio_lock(folio);\n+\tfolio_wait_writeback(folio);\n+\t/*\n+\t * If the cached folio is no longer PMD-sized (e.g. split in the\n+\t * swap cache by deferred_split_scan() or memory_failure() while\n+\t * the PMD swap entry was installed), the PMD swap entry no longer\n+\t * maps a single contiguous folio.  Split the PMD swap entry so\n+\t * unuse_pte_range() can swap the per-slot folios in individually.\n+\t */\n+\tif (folio_nr_pages(folio) != HPAGE_PMD_NR) {\n+\t\tfolio_unlock(folio);\n+\t\tfolio_put(folio);\n+\t\tret = -EAGAIN;\n+\t\tgoto split_fallback;\n+\t}\n+\tret = unuse_pmd(vma, pmd, addr, entry, folio);\n+\tfolio_unlock(folio);\n+\tfolio_put(folio);\n+\treturn ret;\n+\n+split_fallback:\n+\t__split_huge_pmd(vma, pmd, addr, false);\n+\treturn ret;\n+}\n+#endif\n+\n static inline int unuse_pmd_range(struct vm_area_struct *vma, pud_t *pud,\n \t\t\t\tunsigned long addr, unsigned long end,\n \t\t\t\tunsigned int type)\n@@ -2664,6 +2823,20 @@ static inline int unuse_pmd_range(struct vm_area_struct *vma, pud_t *pud,\n \tdo {\n \t\tcond_resched();\n \t\tnext = pmd_addr_end(addr, end);\n+\n+#ifdef CONFIG_THP_SWAP\n+\t\tpmd_t pmdval = pmdp_get(pmd);\n+\n+\t\tif (pmd_is_swap_entry(pmdval)) {\n+\t\t\tsoftleaf_t sl = softleaf_from_pmd(pmdval);\n+\n+\t\t\tif (swp_type(sl) == type) {\n+\t\t\t\tif (!unuse_pmd_entry(vma, pmd, addr, sl))\n+\t\t\t\t\tcontinue;\n+\t\t\t}\n+\t\t}\n+#endif\n+\n \t\tret = unuse_pte_range(vma, pmd, addr, next, type);\n \t\tif (ret)\n \t\t\treturn ret;\n@@ -3911,8 +4084,9 @@ void si_swapinfo(struct sysinfo *val)\n }\n \n /*\n- * swap_dup_entry_direct() - Increase reference count of a swap entry by one.\n+ * swap_dup_entries_direct() - Increase reference count of swap entries by one.\n  * @entry: first swap entry from which we want to increase the refcount.\n+ * @nr: number of contiguous swap entries to duplicate.\n  *\n  * Returns 0 for success, or -ENOMEM if the extend table is required\n  * but could not be atomically allocated.  Returns -EINVAL if the swap\n@@ -3924,7 +4098,7 @@ void si_swapinfo(struct sysinfo *val)\n  * Also the swap entry must have a count \u003e= 1. Otherwise folio_dup_swap should\n  * be used.\n  */\n-int swap_dup_entry_direct(swp_entry_t entry)\n+int swap_dup_entries_direct(swp_entry_t entry, int nr)\n {\n \tstruct swap_info_struct *si;\n \n@@ -3941,7 +4115,7 @@ int swap_dup_entry_direct(swp_entry_t entry)\n \t */\n \tVM_WARN_ON_ONCE(!swap_entry_swapped(si, entry));\n \n-\treturn swap_dup_entries_cluster(si, swp_offset(entry), 1);\n+\treturn swap_dup_entries_cluster(si, swp_offset(entry), nr);\n }\n \n #if defined(CONFIG_MEMCG) \u0026\u0026 defined(CONFIG_BLK_CGROUP)\ndiff --git a/mm/vmscan.c b/mm/vmscan.c\nindex 54e79824554a7a..a70c773da8d140 100644\n--- a/mm/vmscan.c\n+++ b/mm/vmscan.c\n@@ -1328,7 +1328,14 @@ static unsigned int shrink_folio_list(struct list_head *folio_list,\n \t\t\tenum ttu_flags flags = TTU_BATCH_FLUSH;\n \t\t\tbool was_swapbacked = folio_test_swapbacked(folio);\n \n-\t\t\tif (folio_test_pmd_mappable(folio))\n+\t\t\t/*\n+\t\t\t * With THP_SWAP, PMD-mappable folios already in the\n+\t\t\t * swap cache can be unmapped with a PMD-level swap\n+\t\t\t * entry, avoiding the cost of splitting the PMD.\n+\t\t\t */\n+\t\t\tif (folio_test_pmd_mappable(folio) \u0026\u0026\n+\t\t\t    !(IS_ENABLED(CONFIG_THP_SWAP) \u0026\u0026\n+\t\t\t      folio_test_swapcache(folio)))\n \t\t\t\tflags |= TTU_SPLIT_HUGE_PMD;\n \t\t\t/*\n \t\t\t * Without TTU_SYNC, try_to_unmap will only begin to\ndiff --git a/mm/vmstat.c b/mm/vmstat.c\nindex 4e26e5fd666626..68e6efc0dabd16 100644\n--- a/mm/vmstat.c\n+++ b/mm/vmstat.c\n@@ -1422,6 +1422,7 @@ const char * const vmstat_text[] = {\n \t[I(THP_ZERO_PAGE_ALLOC_FAILED)]\t\t= \"thp_zero_page_alloc_failed\",\n \t[I(THP_SWPOUT)]\t\t\t\t= \"thp_swpout\",\n \t[I(THP_SWPOUT_FALLBACK)]\t\t= \"thp_swpout_fallback\",\n+\t[I(THP_SWPOUT_PMD)]\t\t\t= \"thp_swpout_pmd\",\n #endif\n #ifdef CONFIG_BALLOON\n \t[I(BALLOON_INFLATE)]\t\t\t= \"balloon_inflate\",\ndiff --git a/mm/zswap.c b/mm/zswap.c\nindex 4e76a4a87cdcc0..384492f1f6964e 100644\n--- a/mm/zswap.c\n+++ b/mm/zswap.c\n@@ -1561,6 +1561,23 @@ bool zswap_store(struct folio *folio)\n \treturn ret;\n }\n \n+/**\n+ * zswap_is_present() - is any slot in [entry, entry + nr) in zswap?\n+ * @entry: base swap entry of the range\n+ * @nr: number of contiguous slots to check (pass 1 for a single-slot query)\n+ */\n+bool zswap_is_present(swp_entry_t entry, unsigned int nr)\n+{\n+\tpgoff_t offset = swp_offset(entry);\n+\tstruct xarray *tree = swap_zswap_tree(entry);\n+\tunsigned long index = offset;\n+\n+\tif (!nr || zswap_never_enabled())\n+\t\treturn false;\n+\n+\treturn xa_find(tree, \u0026index, offset + nr - 1, XA_PRESENT);\n+}\n+\n /**\n  * zswap_load() - load a folio from zswap\n  * @folio: folio to load\n@@ -1573,10 +1590,9 @@ bool zswap_store(struct folio *folio)\n  *  NOT marked up-to-date, so that an IO error is emitted (e.g. do_swap_page()\n  *  will SIGBUS).\n  *\n- *  -EINVAL: if the swapped out content was in zswap, but the page belongs\n- *  to a large folio, which is not supported by zswap. The folio is unlocked,\n- *  but NOT marked up-to-date, so that an IO error is emitted (e.g.\n- *  do_swap_page() will SIGBUS).\n+ *  -EIO: if a slot in a large-folio range is unexpectedly still in zswap.\n+ *  The folio is unlocked, but NOT marked up-to-date, so that an IO\n+ *  error is emitted (e.g. do_swap_page() will SIGBUS).\n  *\n  *  -ENOENT: if the swapped out content was not in zswap. The folio remains\n  *  locked on return.\n@@ -1595,13 +1611,19 @@ int zswap_load(struct folio *folio)\n \t\treturn -ENOENT;\n \n \t/*\n-\t * Large folios should not be swapped in while zswap is being used, as\n-\t * they are not properly handled. Zswap does not properly load large\n-\t * folios, and a large folio may only be partially in zswap.\n+\t * A large folio reaches zswap_load() only when its whole range is\n+\t * expected to be on disk: PMD swap-entry consumers split before\n+\t * calling into PMD-order swapin whenever any slot is still in zswap.\n+\t * Confirm the range is entirely absent from zswap and return -ENOENT\n+\t * so the caller reads it from disk; if a slot is unexpectedly still in\n+\t * zswap, fail the read rather than return partially-initialized data.\n \t */\n-\tif (WARN_ON_ONCE(folio_test_large(folio))) {\n-\t\tfolio_unlock(folio);\n-\t\treturn -EINVAL;\n+\tif (folio_test_large(folio)) {\n+\t\tif (zswap_is_present(swp, folio_nr_pages(folio))) {\n+\t\t\tfolio_unlock(folio);\n+\t\t\treturn -EIO;\n+\t\t}\n+\t\treturn -ENOENT;\n \t}\n \n \tentry = xa_load(tree, offset);\ndiff --git a/tools/testing/selftests/mm/Makefile b/tools/testing/selftests/mm/Makefile\nindex eae504bd94c83a..25af4d7b633e05 100644\n--- a/tools/testing/selftests/mm/Makefile\n+++ b/tools/testing/selftests/mm/Makefile\n@@ -104,6 +104,7 @@ TEST_GEN_FILES += guard-regions\n TEST_GEN_FILES += merge\n TEST_GEN_FILES += rmap\n TEST_GEN_FILES += folio_split_race_test\n+TEST_GEN_FILES += pmd_swap\n \n ifneq ($(ARCH),arm64)\n TEST_GEN_FILES += soft-dirty\n@@ -165,6 +166,7 @@ TEST_PROGS += ksft_mremap.sh\n TEST_PROGS += ksft_pagemap.sh\n TEST_PROGS += ksft_pfnmap.sh\n TEST_PROGS += ksft_pkey.sh\n+TEST_PROGS += ksft_pmd_swap.sh\n TEST_PROGS += ksft_process_madv.sh\n TEST_PROGS += ksft_process_mrelease.sh\n TEST_PROGS += ksft_rmap.sh\ndiff --git a/tools/testing/selftests/mm/ksft_pmd_swap.sh b/tools/testing/selftests/mm/ksft_pmd_swap.sh\nnew file mode 100755\nindex 00000000000000..0f070b4729a893\n--- /dev/null\n+++ b/tools/testing/selftests/mm/ksft_pmd_swap.sh\n@@ -0,0 +1,4 @@\n+#!/bin/sh -e\n+# SPDX-License-Identifier: GPL-2.0\n+\n+./run_vmtests.sh -t pmd_swap\ndiff --git a/tools/testing/selftests/mm/pmd_swap.c b/tools/testing/selftests/mm/pmd_swap.c\nnew file mode 100644\nindex 00000000000000..7cf9ac074e9643\n--- /dev/null\n+++ b/tools/testing/selftests/mm/pmd_swap.c\n@@ -0,0 +1,874 @@\n+// SPDX-License-Identifier: GPL-2.0\n+/*\n+ * Test PMD-level swap entries.\n+ *\n+ * Verifies that when a PMD-mapped THP is swapped out the kernel installs\n+ * a single PMD-level swap entry (instead of splitting into 512 PTE-level\n+ * entries), and that operations on the swapped region behave correctly:\n+ *   basic         - swap out + swap in preserves data\n+ *   fork          - parent and child both see the data\n+ *   fork_cow      - COW after fork keeps parent's data isolated\n+ *   cycles        - repeated swap out/in does not corrupt data\n+ *   write         - faulting in via a write restores a PMD-mapped THP\n+ *   rwp_swapin    - userfaultfd RWP survives PMD-order swap-in\n+ *   munmap        - munmap on a PMD swap entry frees swap slots cleanly\n+ *   mprotect      - mprotect on a PMD swap entry preserves data\n+ *   mremap        - mremap on a PMD swap entry preserves data\n+ *   pagemap        - pagemap reports the entries as swapped\n+ *   mincore        - mincore walks a PMD swap entry without faulting it in\n+ *   madvise_free   - MADV_FREE on a PMD swap entry does not crash\n+ *   madvise_willneed - MADV_WILLNEED handles a PMD swap entry\n+ *   uffdio_move    - UFFDIO_MOVE moves a PMD swap entry\n+ *   swapoff        - swapoff handles PMD swap entries (needs PMD_SWAP_DEVICE)\n+ */\n+#define _GNU_SOURCE\n+#include \u003cstdio.h\u003e\n+#include \u003cstdlib.h\u003e\n+#include \u003cstring.h\u003e\n+#include \u003cunistd.h\u003e\n+#include \u003csys/mman.h\u003e\n+#include \u003csys/wait.h\u003e\n+#include \u003cfcntl.h\u003e\n+#include \u003cerrno.h\u003e\n+#include \u003cstdint.h\u003e\n+#include \u003csys/random.h\u003e\n+#include \u003csys/swap.h\u003e\n+#include \u003csys/syscall.h\u003e\n+#include \u003csys/ioctl.h\u003e\n+#include \u003cpoll.h\u003e\n+#include \u003cpthread.h\u003e\n+#include \u003clinux/userfaultfd.h\u003e\n+#include \u003ctime.h\u003e\n+\n+#include \"kselftest_harness.h\"\n+#include \"vm_util.h\"\n+\n+#define ZSWAP_ENABLED_PATH \"/sys/module/zswap/parameters/enabled\"\n+\n+static bool check_swapped(int pagemap_fd, char *addr, unsigned long size)\n+{\n+\tunsigned long off;\n+\n+\tfor (off = 0; off \u003c size; off += getpagesize())\n+\t\tif (!pagemap_is_swapped(pagemap_fd, addr + off))\n+\t\t\treturn false;\n+\treturn true;\n+}\n+\n+static bool zswap_enabled(void)\n+{\n+\tchar enabled = 0;\n+\tFILE *f;\n+\n+\tf = fopen(ZSWAP_ENABLED_PATH, \"r\");\n+\tif (!f)\n+\t\treturn false;\n+\n+\tif (fscanf(f, \" %c\", \u0026enabled) != 1)\n+\t\tenabled = 0;\n+\tfclose(f);\n+\n+\treturn enabled == 'Y' || enabled == 'y' || enabled == '1';\n+}\n+\n+static bool swap_available(int pagemap_fd)\n+{\n+\tchar *p;\n+\tbool ret;\n+\n+\tp = mmap(NULL, getpagesize(), PROT_READ | PROT_WRITE,\n+\t\t MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);\n+\tif (p == MAP_FAILED)\n+\t\treturn false;\n+\n+\tmemset(p, 0xab, getpagesize());\n+\tmadvise(p, getpagesize(), MADV_PAGEOUT);\n+\tret = pagemap_is_swapped(pagemap_fd, p);\n+\tmunmap(p, getpagesize());\n+\treturn ret;\n+}\n+\n+static unsigned long read_vm_event(const char *name)\n+{\n+\tchar line[256];\n+\tsize_t name_len = strlen(name);\n+\tunsigned long val = 0;\n+\tFILE *f;\n+\n+\tf = fopen(\"/proc/vmstat\", \"r\");\n+\tif (!f)\n+\t\treturn 0;\n+\twhile (fgets(line, sizeof(line), f)) {\n+\t\tif (!strncmp(line, name, name_len) \u0026\u0026 line[name_len] == ' ') {\n+\t\t\tval = strtoul(line + name_len + 1, NULL, 10);\n+\t\t\tbreak;\n+\t\t}\n+\t}\n+\tfclose(f);\n+\treturn val;\n+}\n+\n+static unsigned int random_seed(void)\n+{\n+\tunsigned int seed;\n+\n+\tif (getrandom(\u0026seed, sizeof(seed), 0) != sizeof(seed))\n+\t\tseed = (unsigned int)time(NULL);\n+\treturn seed;\n+}\n+\n+static unsigned char pattern_byte(unsigned int seed, unsigned long off)\n+{\n+\treturn (unsigned char)(seed + off);\n+}\n+\n+static void fill_pattern(char *buf, unsigned long size, unsigned int seed)\n+{\n+\tunsigned long i;\n+\n+\tfor (i = 0; i \u003c size; i++)\n+\t\tbuf[i] = (char)pattern_byte(seed, i);\n+}\n+\n+static bool verify_pattern(char *buf, unsigned long size, unsigned int seed)\n+{\n+\tunsigned long i;\n+\n+\tfor (i = 0; i \u003c size; i++)\n+\t\tif ((unsigned char)buf[i] != pattern_byte(seed, i))\n+\t\t\treturn false;\n+\treturn true;\n+}\n+\n+/*\n+ * mmap an anonymous PMD-aligned region of pmd_size bytes. Over-allocates\n+ * by one PMD and trims the unaligned head/tail so the returned address is\n+ * PMD-aligned (required for whole-PMD UFFDIO_MOVE).\n+ */\n+static char *mmap_pmd_aligned(unsigned long pmd_size)\n+{\n+\tunsigned long pad = pmd_size;\n+\tchar *raw, *aligned;\n+\n+\traw = mmap(NULL, pmd_size + pad, PROT_READ | PROT_WRITE,\n+\t\t   MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);\n+\tif (raw == MAP_FAILED)\n+\t\treturn MAP_FAILED;\n+\n+\taligned = (char *)(((uintptr_t)raw + pmd_size - 1) \u0026 ~(pmd_size - 1));\n+\tif (aligned != raw)\n+\t\tmunmap(raw, aligned - raw);\n+\tif (aligned + pmd_size != raw + pmd_size + pad)\n+\t\tmunmap(aligned + pmd_size,\n+\t\t       (raw + pmd_size + pad) - (aligned + pmd_size));\n+\treturn aligned;\n+}\n+\n+/*\n+ * mmap a PMD-aligned PMD-sized region, request THP, fill with a pattern,\n+ * and swap it out. Verifies via the thp_swpout_pmd vmstat counter that\n+ * the swap-out installed a PMD swap entry rather than splitting to PTEs.\n+ */\n+static char *alloc_fill_swap_thp(unsigned long pmd_size, int pagemap_fd,\n+\t\t\t\t unsigned int seed)\n+{\n+\tunsigned long pmd_before, pmd_after;\n+\tchar *mem;\n+\n+\tmem = mmap_pmd_aligned(pmd_size);\n+\tif (mem == MAP_FAILED)\n+\t\treturn MAP_FAILED;\n+\n+\tmadvise(mem, pmd_size, MADV_HUGEPAGE);\n+\tfill_pattern(mem, pmd_size, seed);\n+\n+\tpmd_before = read_vm_event(\"thp_swpout_pmd\");\n+\n+\tif (madvise(mem, pmd_size, MADV_PAGEOUT) ||\n+\t    !check_swapped(pagemap_fd, mem, pmd_size)) {\n+\t\tmunmap(mem, pmd_size);\n+\t\treturn MAP_FAILED;\n+\t}\n+\n+\tpmd_after = read_vm_event(\"thp_swpout_pmd\");\n+\tprintf(\"# thp_swpout_pmd: %lu -\u003e %lu\\n\", pmd_before, pmd_after);\n+\tif (pmd_after - pmd_before \u003c 1) {\n+\t\tmunmap(mem, pmd_size);\n+\t\treturn MAP_FAILED;\n+\t}\n+\treturn mem;\n+}\n+\n+struct rwp_access_args {\n+\tunsigned char *addr;\n+\tunsigned char expected;\n+\tbool write;\n+\tbool ok;\n+};\n+\n+static void *rwp_access_thread(void *data)\n+{\n+\tstruct rwp_access_args *args = data;\n+\n+\tif (args-\u003ewrite)\n+\t\t*args-\u003eaddr = args-\u003eexpected;\n+\targs-\u003eok = *args-\u003eaddr == args-\u003eexpected;\n+\treturn NULL;\n+}\n+\n+static int register_rwp(char *addr, unsigned long size, bool protect)\n+{\n+\tstruct uffdio_register reg = {};\n+\tstruct uffdio_rwprotect rwp = {};\n+\tstruct uffdio_api api = {};\n+\tint uffd;\n+\n+\tuffd = syscall(__NR_userfaultfd, O_CLOEXEC | O_NONBLOCK);\n+\tif (uffd \u003c 0)\n+\t\treturn -1;\n+\n+\tapi.api = UFFD_API;\n+\tapi.features = UFFD_FEATURE_RWP;\n+\tif (ioctl(uffd, UFFDIO_API, \u0026api) ||\n+\t    !(api.features \u0026 UFFD_FEATURE_RWP))\n+\t\tgoto error;\n+\n+\treg.range.start = (unsigned long)addr;\n+\treg.range.len = size;\n+\treg.mode = UFFDIO_REGISTER_MODE_RWP;\n+\tif (ioctl(uffd, UFFDIO_REGISTER, \u0026reg))\n+\t\tgoto error;\n+\n+\tif (!protect)\n+\t\treturn uffd;\n+\n+\trwp.range.start = (unsigned long)addr;\n+\trwp.range.len = size;\n+\trwp.mode = UFFDIO_RWPROTECT_MODE_RWP;\n+\tif (!ioctl(uffd, UFFDIO_RWPROTECT, \u0026rwp))\n+\t\treturn uffd;\n+\n+error:\n+\tclose(uffd);\n+\treturn -1;\n+}\n+\n+static bool expect_rwp_fault(int uffd, char *addr, unsigned long size,\n+\t\t\t     unsigned char expected, bool write)\n+{\n+\tstruct rwp_access_args args = {\n+\t\t.addr = (unsigned char *)addr,\n+\t\t.expected = expected,\n+\t\t.write = write,\n+\t};\n+\tstruct uffdio_rwprotect rwp = {\n+\t\t.range = {\n+\t\t\t.start = (unsigned long)addr,\n+\t\t\t.len = size,\n+\t\t},\n+\t};\n+\tstruct pollfd pollfd = {\n+\t\t.fd = uffd,\n+\t\t.events = POLLIN,\n+\t};\n+\tstruct uffd_msg msg = {};\n+\tpthread_t thread;\n+\tbool saw_rwp = false;\n+\tint ret;\n+\n+\tif (pthread_create(\u0026thread, NULL, rwp_access_thread, \u0026args))\n+\t\treturn false;\n+\n+\tret = poll(\u0026pollfd, 1, 5000);\n+\tif (ret == 1 \u0026\u0026 (pollfd.revents \u0026 POLLIN) \u0026\u0026\n+\t    read(uffd, \u0026msg, sizeof(msg)) == (ssize_t)sizeof(msg)) {\n+\t\tsaw_rwp = msg.event == UFFD_EVENT_PAGEFAULT \u0026\u0026\n+\t\t\t  (msg.arg.pagefault.flags \u0026 UFFD_PAGEFAULT_FLAG_RWP);\n+\t}\n+\n+\t/* Resolve the access even on failure so the worker cannot remain blocked. */\n+\tioctl(uffd, UFFDIO_RWPROTECT, \u0026rwp);\n+\tif (pthread_join(thread, NULL))\n+\t\treturn false;\n+\treturn saw_rwp \u0026\u0026 args.ok;\n+}\n+\n+FIXTURE(pmd_swap)\n+{\n+\tunsigned long pmd_size;\n+\tint pagemap_fd;\n+\tunsigned int seed;\n+\tbool zswap_enabled;\n+};\n+\n+FIXTURE_SETUP(pmd_swap)\n+{\n+\tself-\u003epagemap_fd = -1;\n+\n+\tself-\u003epmd_size = read_pmd_pagesize();\n+\tif (!self-\u003epmd_size)\n+\t\tSKIP(return, \"Cannot determine PMD size\\n\");\n+\n+\tself-\u003epagemap_fd = open(\"/proc/self/pagemap\", O_RDONLY);\n+\tif (self-\u003epagemap_fd \u003c 0)\n+\t\tSKIP(return, \"Cannot open /proc/self/pagemap\\n\");\n+\n+\tif (!swap_available(self-\u003epagemap_fd))\n+\t\tSKIP(return, \"Swap not available or not working\\n\");\n+\n+\tself-\u003eseed = random_seed();\n+\tself-\u003ezswap_enabled = zswap_enabled();\n+}\n+\n+FIXTURE_TEARDOWN(pmd_swap)\n+{\n+\tif (self-\u003epagemap_fd \u003e= 0)\n+\t\tclose(self-\u003epagemap_fd);\n+}\n+\n+/*\n+ * Allocate a PMD-sized THP, write a pattern, swap it out, read it back,\n+ * verify the pattern.\n+ */\n+TEST_F(pmd_swap, basic)\n+{\n+\tchar *mem;\n+\n+\tmem = alloc_fill_swap_thp(self-\u003epmd_size, self-\u003epagemap_fd, self-\u003eseed);\n+\tif (mem == MAP_FAILED)\n+\t\tSKIP(return, \"Could not create swapped THP\\n\");\n+\n+\tASSERT_TRUE(verify_pattern(mem, self-\u003epmd_size, self-\u003eseed));\n+\n+\tmunmap(mem, self-\u003epmd_size);\n+}\n+\n+/*\n+ * Allocate a THP, swap it out, fork, verify both parent and child see\n+ * the correct data.\n+ */\n+TEST_F(pmd_swap, fork)\n+{\n+\tchar *mem;\n+\tpid_t pid;\n+\tint status;\n+\n+\tmem = alloc_fill_swap_thp(self-\u003epmd_size, self-\u003epagemap_fd, self-\u003eseed);\n+\tif (mem == MAP_FAILED)\n+\t\tSKIP(return, \"Could not create swapped THP\\n\");\n+\n+\tpid = fork();\n+\tASSERT_GE(pid, 0);\n+\n+\tif (pid == 0)\n+\t\t_exit(verify_pattern(mem, self-\u003epmd_size, self-\u003eseed) ? 0 : 1);\n+\n+\tASSERT_TRUE(verify_pattern(mem, self-\u003epmd_size, self-\u003eseed));\n+\n+\tASSERT_EQ(waitpid(pid, \u0026status, 0), pid);\n+\tASSERT_TRUE(WIFEXITED(status));\n+\tASSERT_EQ(WEXITSTATUS(status), 0);\n+\n+\tmunmap(mem, self-\u003epmd_size);\n+}\n+\n+/*\n+ * Swap out, fork, then have parent and child write different patterns.\n+ * Exercises COW on shared PMD swap entries: writes after fork must\n+ * trigger copy-on-write so the parent's data stays isolated from the\n+ * child's.  Both processes write and then verify their own pattern so\n+ * the parent-side COW path is exercised too (a parent-only-read variant\n+ * only proves the swap entry survived fork).\n+ */\n+TEST_F(pmd_swap, fork_cow)\n+{\n+\tunsigned int parent_seed = self-\u003eseed;\n+\tunsigned int child_seed = ~self-\u003eseed;\n+\tunsigned int post_seed = self-\u003eseed ^ 0xa5a5a5a5;\n+\tchar *mem;\n+\tpid_t pid;\n+\tint status;\n+\n+\tmem = alloc_fill_swap_thp(self-\u003epmd_size, self-\u003epagemap_fd, parent_seed);\n+\tif (mem == MAP_FAILED)\n+\t\tSKIP(return, \"Could not create swapped THP\\n\");\n+\n+\tpid = fork();\n+\tASSERT_GE(pid, 0);\n+\n+\tif (pid == 0) {\n+\t\tfill_pattern(mem, self-\u003epmd_size, child_seed);\n+\t\t_exit(verify_pattern(mem, self-\u003epmd_size, child_seed) ? 0 : 1);\n+\t}\n+\n+\tASSERT_EQ(waitpid(pid, \u0026status, 0), pid);\n+\n+\t/* Child's writes must not leak back into the parent. */\n+\tASSERT_TRUE(verify_pattern(mem, self-\u003epmd_size, parent_seed));\n+\tASSERT_TRUE(WIFEXITED(status));\n+\tASSERT_EQ(WEXITSTATUS(status), 0);\n+\n+\t/* Now trigger the parent-side COW and confirm the write sticks. */\n+\tfill_pattern(mem, self-\u003epmd_size, post_seed);\n+\tASSERT_TRUE(verify_pattern(mem, self-\u003epmd_size, post_seed));\n+\n+\tmunmap(mem, self-\u003epmd_size);\n+}\n+\n+/*\n+ * Swap a THP out and in repeatedly without data corruption.\n+ */\n+TEST_F(pmd_swap, cycles)\n+{\n+\tconst int num_cycles = 5;\n+\tchar *mem;\n+\tint cycle;\n+\n+\tfor (cycle = 0; cycle \u003c num_cycles; cycle++) {\n+\t\tunsigned int seed = self-\u003eseed + cycle;\n+\n+\t\tmem = alloc_fill_swap_thp(self-\u003epmd_size, self-\u003epagemap_fd, seed);\n+\t\tif (mem == MAP_FAILED)\n+\t\t\tSKIP(return, \"Could not create swapped THP at cycle %d\\n\",\n+\t\t\t     cycle);\n+\n+\t\tASSERT_TRUE(verify_pattern(mem, self-\u003epmd_size, seed));\n+\n+\t\tmunmap(mem, self-\u003epmd_size);\n+\t}\n+}\n+\n+/*\n+ * Swap out, fault in via a write to the first page, verify the write\n+ * reinstates a THP mapping and the rest of the THP is preserved.\n+ */\n+TEST_F(pmd_swap, write)\n+{\n+\tunsigned int seed = self-\u003eseed;\n+\tchar *mem;\n+\tunsigned long i;\n+\n+\tmem = alloc_fill_swap_thp(self-\u003epmd_size, self-\u003epagemap_fd, seed);\n+\tif (mem == MAP_FAILED)\n+\t\tSKIP(return, \"Could not create swapped THP\\n\");\n+\n+\tmem[0] = 0xbb;\n+\tASSERT_EQ(mem[0], (char)0xbb);\n+\n+\tif (self-\u003ezswap_enabled) {\n+\t\tTH_LOG(\"zswap is enabled, so PMD mapping is not checked\");\n+\t} else {\n+\t\tASSERT_TRUE(check_huge_anon(mem, 1, self-\u003epmd_size));\n+\t}\n+\n+\tfor (i = 1; i \u003c self-\u003epmd_size; i++)\n+\t\tASSERT_EQ((unsigned char)mem[i], pattern_byte(seed, i));\n+\n+\tmunmap(mem, self-\u003epmd_size);\n+}\n+\n+/* RWP protection on a swap PMD must survive PMD-order swap-in. */\n+TEST_F(pmd_swap, rwp_swapin)\n+{\n+\tunsigned int seed = self-\u003eseed;\n+\tchar *mem;\n+\tint uffd;\n+\n+\tmem = alloc_fill_swap_thp(self-\u003epmd_size, self-\u003epagemap_fd, seed);\n+\tif (mem == MAP_FAILED)\n+\t\tSKIP(return, \"Could not create swapped THP\\n\");\n+\n+\tuffd = register_rwp(mem, self-\u003epmd_size, true);\n+\tif (uffd \u003c 0) {\n+\t\tmunmap(mem, self-\u003epmd_size);\n+\t\tSKIP(return, \"Userfaultfd RWP unsupported\\n\");\n+\t}\n+\n+\tASSERT_TRUE(expect_rwp_fault(uffd, mem, self-\u003epmd_size,\n+\t\t\t\t     pattern_byte(seed, 0), false)) {\n+\t\tclose(uffd);\n+\t\tmunmap(mem, self-\u003epmd_size);\n+\t}\n+\tASSERT_TRUE(verify_pattern(mem, self-\u003epmd_size, seed));\n+\n+\tclose(uffd);\n+\tmunmap(mem, self-\u003epmd_size);\n+}\n+\n+/*\n+ * munmap while the folio is swapped out. Exercises zap_huge_pmd() on a\n+ * PMD swap entry — must free the swap slots without trying to look up\n+ * a folio.\n+ */\n+TEST_F(pmd_swap, munmap)\n+{\n+\tchar *mem;\n+\n+\tmem = alloc_fill_swap_thp(self-\u003epmd_size, self-\u003epagemap_fd, self-\u003eseed);\n+\tif (mem == MAP_FAILED)\n+\t\tSKIP(return, \"Could not create swapped THP\\n\");\n+\n+\tmunmap(mem, self-\u003epmd_size);\n+}\n+\n+/*\n+ * Change protection on a swapped PMD entry, then fault back in and\n+ * verify data. Exercises change_non_present_huge_pmd().\n+ */\n+TEST_F(pmd_swap, mprotect)\n+{\n+\tunsigned int seed = self-\u003eseed;\n+\tchar *mem;\n+\n+\tmem = alloc_fill_swap_thp(self-\u003epmd_size, self-\u003epagemap_fd, seed);\n+\tif (mem == MAP_FAILED)\n+\t\tSKIP(return, \"Could not create swapped THP\\n\");\n+\n+\tASSERT_EQ(mprotect(mem, self-\u003epmd_size, PROT_READ), 0);\n+\tASSERT_EQ(mprotect(mem, self-\u003epmd_size, PROT_READ | PROT_WRITE), 0);\n+\n+\tASSERT_TRUE(verify_pattern(mem, self-\u003epmd_size, seed));\n+\n+\tmunmap(mem, self-\u003epmd_size);\n+}\n+\n+/*\n+ * UFFDIO_MOVE a PMD swap entry from src to a registered dst. Exercises\n+ * move_pages_huge_pmd() handling of pmd_is_swap_entry: the whole PMD swap\n+ * entry must move to dst without splitting, and the destination must\n+ * read back the original pattern after a swap-in fault.\n+ */\n+TEST_F(pmd_swap, uffdio_move)\n+{\n+\tunsigned int seed = self-\u003eseed;\n+\tstruct uffdio_register reg = {};\n+\tstruct uffdio_move move = {};\n+\tstruct uffdio_api api = {};\n+\tchar *src, *dst;\n+\tbool rwp;\n+\tint uffd;\n+\n+\tdst = mmap_pmd_aligned(self-\u003epmd_size);\n+\tif (dst == MAP_FAILED)\n+\t\tSKIP(return, \"Could not mmap aligned dst\\n\");\n+\n+\tsrc = alloc_fill_swap_thp(self-\u003epmd_size, self-\u003epagemap_fd, seed);\n+\tif (src == MAP_FAILED) {\n+\t\tmunmap(dst, self-\u003epmd_size);\n+\t\tSKIP(return, \"Could not create swapped THP\\n\");\n+\t}\n+\tif ((uintptr_t)src \u0026 (self-\u003epmd_size - 1)) {\n+\t\tmunmap(src, self-\u003epmd_size);\n+\t\tmunmap(dst, self-\u003epmd_size);\n+\t\tSKIP(return, \"src not PMD-aligned\\n\");\n+\t}\n+\n+\tuffd = syscall(__NR_userfaultfd, O_CLOEXEC | O_NONBLOCK);\n+\tif (uffd \u003c 0) {\n+\t\tmunmap(src, self-\u003epmd_size);\n+\t\tmunmap(dst, self-\u003epmd_size);\n+\t\tSKIP(return, \"userfaultfd unavailable\\n\");\n+\t}\n+\n+\tapi.api = UFFD_API;\n+\tapi.features = UFFD_FEATURE_MOVE | UFFD_FEATURE_RWP;\n+\tif (ioctl(uffd, UFFDIO_API, \u0026api) ||\n+\t    !(api.features \u0026 UFFD_FEATURE_MOVE)) {\n+\t\tclose(uffd);\n+\t\tmunmap(src, self-\u003epmd_size);\n+\t\tmunmap(dst, self-\u003epmd_size);\n+\t\tSKIP(return, \"UFFD_FEATURE_MOVE unsupported\\n\");\n+\t}\n+\trwp = api.features \u0026 UFFD_FEATURE_RWP;\n+\n+\treg.range.start = (unsigned long)dst;\n+\treg.range.len = self-\u003epmd_size;\n+\treg.mode = UFFDIO_REGISTER_MODE_MISSING |\n+\t\t   (rwp ? UFFDIO_REGISTER_MODE_RWP : 0);\n+\tif (ioctl(uffd, UFFDIO_REGISTER, \u0026reg)) {\n+\t\tclose(uffd);\n+\t\tmunmap(src, self-\u003epmd_size);\n+\t\tmunmap(dst, self-\u003epmd_size);\n+\t\tSKIP(return, \"UFFDIO_REGISTER failed\\n\");\n+\t}\n+\n+\tmove.dst = (unsigned long)dst;\n+\tmove.src = (unsigned long)src;\n+\tmove.len = self-\u003epmd_size;\n+\tif (ioctl(uffd, UFFDIO_MOVE, \u0026move)) {\n+\t\tint saved_errno = errno;\n+\n+\t\tclose(uffd);\n+\t\tmunmap(src, self-\u003epmd_size);\n+\t\tmunmap(dst, self-\u003epmd_size);\n+\t\tASSERT_EQ(saved_errno, 0);\n+\t}\n+\tASSERT_EQ(move.move, self-\u003epmd_size);\n+\n+\t/* dst inherits the PMD swap entry; reading it must restore the data. */\n+\tASSERT_TRUE(check_swapped(self-\u003epagemap_fd, dst, self-\u003epmd_size));\n+\tif (rwp) {\n+\t\tASSERT_TRUE(expect_rwp_fault(uffd, dst, self-\u003epmd_size,\n+\t\t\t\t\t     pattern_byte(seed, 0), false)) {\n+\t\t\tclose(uffd);\n+\t\t\tmunmap(src, self-\u003epmd_size);\n+\t\t\tmunmap(dst, self-\u003epmd_size);\n+\t\t}\n+\t}\n+\tASSERT_TRUE(verify_pattern(dst, self-\u003epmd_size, seed));\n+\tif (self-\u003ezswap_enabled) {\n+\t\tTH_LOG(\"zswap is enabled, so PMD mapping is not checked\");\n+\t} else {\n+\t\t/* The whole-PMD path must reinstate a THP, not 512 PTE folios. */\n+\t\tASSERT_TRUE(check_huge_anon(dst, 1, self-\u003epmd_size));\n+\t}\n+\n+\tclose(uffd);\n+\tmunmap(src, self-\u003epmd_size);\n+\tmunmap(dst, self-\u003epmd_size);\n+}\n+\n+/*\n+ * Move a swapped PMD entry to a new address, fault in, verify data.\n+ * Exercises move_huge_pmd() and move_soft_dirty_pmd().\n+ */\n+TEST_F(pmd_swap, mremap)\n+{\n+\tunsigned int seed = self-\u003eseed;\n+\tchar *mem, *new_mem;\n+\n+\tmem = alloc_fill_swap_thp(self-\u003epmd_size, self-\u003epagemap_fd, seed);\n+\tif (mem == MAP_FAILED)\n+\t\tSKIP(return, \"Could not create swapped THP\\n\");\n+\n+\tnew_mem = mremap(mem, self-\u003epmd_size, self-\u003epmd_size, MREMAP_MAYMOVE);\n+\tif (new_mem == MAP_FAILED) {\n+\t\tmunmap(mem, self-\u003epmd_size);\n+\t\tASSERT_NE(new_mem, MAP_FAILED);\n+\t}\n+\n+\tASSERT_TRUE(verify_pattern(new_mem, self-\u003epmd_size, seed));\n+\n+\tmunmap(new_mem, self-\u003epmd_size);\n+}\n+\n+/*\n+ * Read /proc/self/pagemap on a PMD swap entry. Exercises the pagemap\n+ * PMD walker which must handle PMD swap entries without trying to\n+ * convert them to a page via softleaf_to_page().\n+ */\n+TEST_F(pmd_swap, pagemap)\n+{\n+\tchar *mem;\n+\tuint64_t entry;\n+\tunsigned long off;\n+\n+\tmem = alloc_fill_swap_thp(self-\u003epmd_size, self-\u003epagemap_fd, self-\u003eseed);\n+\tif (mem == MAP_FAILED)\n+\t\tSKIP(return, \"Could not create swapped THP\\n\");\n+\n+\tfor (off = 0; off \u003c self-\u003epmd_size; off += getpagesize()) {\n+\t\tentry = pagemap_get_entry(self-\u003epagemap_fd, mem + off);\n+\t\t/* Bit 62 = swapped */\n+\t\tASSERT_TRUE(entry \u0026 (1ULL \u003c\u003c 62));\n+\t}\n+\n+\tmunmap(mem, self-\u003epmd_size);\n+}\n+\n+/*\n+ * mincore() on a swapped-out PMD-mapped THP must handle the non-present PMD\n+ * entry in place. The call must not fault the PMD back in or split the entry.\n+ */\n+TEST_F(pmd_swap, mincore)\n+{\n+\tunsigned long pages = self-\u003epmd_size / getpagesize();\n+\tunsigned char *vec;\n+\tchar *mem;\n+\n+\tmem = alloc_fill_swap_thp(self-\u003epmd_size, self-\u003epagemap_fd, self-\u003eseed);\n+\tif (mem == MAP_FAILED)\n+\t\tSKIP(return, \"Could not create swapped THP\\n\");\n+\n+\tvec = calloc(pages, sizeof(*vec));\n+\tASSERT_NE(vec, NULL) {\n+\t\tmunmap(mem, self-\u003epmd_size);\n+\t}\n+\n+\tASSERT_EQ(mincore(mem, self-\u003epmd_size, vec), 0) {\n+\t\tfree(vec);\n+\t\tmunmap(mem, self-\u003epmd_size);\n+\t}\n+\tASSERT_TRUE(check_swapped(self-\u003epagemap_fd, mem, self-\u003epmd_size)) {\n+\t\tfree(vec);\n+\t\tmunmap(mem, self-\u003epmd_size);\n+\t}\n+\n+\tfree(vec);\n+\tmunmap(mem, self-\u003epmd_size);\n+}\n+\n+/*\n+ * MADV_FREE on a swapped-out PMD must free the swap slots and clear the\n+ * entry. After the call, pagemap must no longer report the pages as\n+ * swapped, and accessing the region must yield zero pages.\n+ */\n+TEST_F(pmd_swap, madvise_free)\n+{\n+\tchar *mem;\n+\tunsigned long i;\n+\n+\tmem = alloc_fill_swap_thp(self-\u003epmd_size, self-\u003epagemap_fd, self-\u003eseed);\n+\tif (mem == MAP_FAILED)\n+\t\tSKIP(return, \"Could not create swapped THP\\n\");\n+\n+\tASSERT_TRUE(check_swapped(self-\u003epagemap_fd, mem, self-\u003epmd_size));\n+\tASSERT_EQ(madvise(mem, self-\u003epmd_size, MADV_FREE), 0);\n+\tASSERT_FALSE(check_swapped(self-\u003epagemap_fd, mem, self-\u003epmd_size));\n+\n+\tfor (i = 0; i \u003c self-\u003epmd_size; i += getpagesize())\n+\t\tASSERT_EQ(mem[i], 0);\n+\n+\tmunmap(mem, self-\u003epmd_size);\n+}\n+\n+/*\n+ * MADV_WILLNEED on a swapped-out PMD-mapped THP may schedule PMD-order\n+ * swapin I/O, find the PMD-sized folio already resident in the swap cache,\n+ * or split to the PTE path when zswap has per-page state for the range.\n+ */\n+TEST_F(pmd_swap, madvise_willneed)\n+{\n+\tchar *mem;\n+\n+\tmem = alloc_fill_swap_thp(self-\u003epmd_size, self-\u003epagemap_fd, self-\u003eseed);\n+\tif (mem == MAP_FAILED)\n+\t\tSKIP(return, \"Could not create swapped THP\\n\");\n+\n+\tASSERT_EQ(madvise(mem, self-\u003epmd_size, MADV_WILLNEED), 0);\n+\tASSERT_TRUE(check_swapped(self-\u003epagemap_fd, mem, self-\u003epmd_size));\n+\n+\t/* First touch faults the data back in. */\n+\tASSERT_TRUE(verify_pattern(mem, self-\u003epmd_size, self-\u003eseed));\n+\n+\tif (self-\u003ezswap_enabled)\n+\t\tTH_LOG(\"zswap is enabled, so PMD mapping is not checked\");\n+\telse\n+\t\tASSERT_TRUE(check_huge_anon(mem, 1, self-\u003epmd_size));\n+\n+\tmunmap(mem, self-\u003epmd_size);\n+}\n+\n+/*\n+ * swapoff requires a dedicated swap device path. Use a separate fixture\n+ * that picks the device up from the PMD_SWAP_DEVICE environment variable\n+ * and skips when unset.\n+ */\n+FIXTURE(pmd_swap_swapoff)\n+{\n+\tunsigned long pmd_size;\n+\tint pagemap_fd;\n+\tconst char *swap_dev;\n+\tunsigned int seed;\n+\tbool zswap_enabled;\n+};\n+\n+FIXTURE_SETUP(pmd_swap_swapoff)\n+{\n+\tself-\u003epagemap_fd = -1;\n+\tself-\u003eswap_dev = getenv(\"PMD_SWAP_DEVICE\");\n+\tif (!self-\u003eswap_dev)\n+\t\tSKIP(return, \"PMD_SWAP_DEVICE env var not set\\n\");\n+\n+\tself-\u003epmd_size = read_pmd_pagesize();\n+\tif (!self-\u003epmd_size)\n+\t\tSKIP(return, \"Cannot determine PMD size\\n\");\n+\n+\tself-\u003epagemap_fd = open(\"/proc/self/pagemap\", O_RDONLY);\n+\tif (self-\u003epagemap_fd \u003c 0)\n+\t\tSKIP(return, \"Cannot open /proc/self/pagemap\\n\");\n+\n+\tif (!swap_available(self-\u003epagemap_fd))\n+\t\tSKIP(return, \"Swap not available or not working\\n\");\n+\n+\tself-\u003eseed = random_seed();\n+\tself-\u003ezswap_enabled = zswap_enabled();\n+}\n+\n+FIXTURE_TEARDOWN(pmd_swap_swapoff)\n+{\n+\tif (self-\u003epagemap_fd \u003e= 0)\n+\t\tclose(self-\u003epagemap_fd);\n+}\n+\n+/*\n+ * Swap out a THP, then turn off swap. Verify data is intact. When zswap is\n+ * not active, the PMD-order swapoff path should preserve the huge mapping.\n+ */\n+TEST_F(pmd_swap_swapoff, basic)\n+{\n+\tunsigned int seed = self-\u003eseed;\n+\tchar *mem;\n+\tint uffd, ret, err;\n+\n+\tmem = alloc_fill_swap_thp(self-\u003epmd_size, self-\u003epagemap_fd, seed);\n+\tif (mem == MAP_FAILED)\n+\t\tSKIP(return, \"Could not create swapped THP\\n\");\n+\tuffd = register_rwp(mem, self-\u003epmd_size, true);\n+\n+\tret = swapoff(self-\u003eswap_dev);\n+\terr = errno;\n+\tASSERT_EQ(ret, 0) {\n+\t\tTH_LOG(\"swapoff(%s) failed: %s\", self-\u003eswap_dev, strerror(err));\n+\t\tif (uffd \u003e= 0)\n+\t\t\tclose(uffd);\n+\t\tmunmap(mem, self-\u003epmd_size);\n+\t}\n+\n+\t/*\n+\t * Check the PMD residency before touching the memory.  If we read\n+\t * first, a bug that left a PMD swap entry in place after swapoff\n+\t * would silently trigger do_huge_pmd_swap_page() and reinstall a\n+\t * PMD mapping, masking the regression.\n+\t */\n+\tif (self-\u003ezswap_enabled) {\n+\t\tTH_LOG(\"zswap is enabled, so PMD mapping is not checked\");\n+\t} else {\n+\t\tASSERT_TRUE(check_huge_anon(mem, 1, self-\u003epmd_size)) {\n+\t\t\tswapon(self-\u003eswap_dev, 0);\n+\t\t\tif (uffd \u003e= 0)\n+\t\t\t\tclose(uffd);\n+\t\t\tmunmap(mem, self-\u003epmd_size);\n+\t\t}\n+\t}\n+\tif (uffd \u003e= 0) {\n+\t\tASSERT_TRUE(expect_rwp_fault(uffd, mem, self-\u003epmd_size,\n+\t\t\t\t\t     pattern_byte(seed, 0), false)) {\n+\t\t\tswapon(self-\u003eswap_dev, 0);\n+\t\t\tclose(uffd);\n+\t\t\tmunmap(mem, self-\u003epmd_size);\n+\t\t}\n+\t}\n+\n+\tASSERT_TRUE(verify_pattern(mem, self-\u003epmd_size, seed)) {\n+\t\tswapon(self-\u003eswap_dev, 0);\n+\t\tif (uffd \u003e= 0)\n+\t\t\tclose(uffd);\n+\t\tmunmap(mem, self-\u003epmd_size);\n+\t}\n+\n+\tret = swapon(self-\u003eswap_dev, 0);\n+\terr = errno;\n+\tASSERT_EQ(ret, 0) {\n+\t\tTH_LOG(\"swapon(%s) failed: %s\", self-\u003eswap_dev, strerror(err));\n+\t\tif (uffd \u003e= 0)\n+\t\t\tclose(uffd);\n+\t\tmunmap(mem, self-\u003epmd_size);\n+\t}\n+\n+\tif (uffd \u003e= 0)\n+\t\tclose(uffd);\n+\tmunmap(mem, self-\u003epmd_size);\n+}\n+\n+TEST_HARNESS_MAIN\ndiff --git a/tools/testing/selftests/mm/run_vmtests.sh b/tools/testing/selftests/mm/run_vmtests.sh\nindex 687d115e3bd807..0a860c4852f2be 100755\n--- a/tools/testing/selftests/mm/run_vmtests.sh\n+++ b/tools/testing/selftests/mm/run_vmtests.sh\n@@ -69,6 +69,8 @@ separated by spaces:\n \ttest pagemap_scan IOCTL\n - pfnmap\n \ttests for VM_PFNMAP handling\n+- pmd_swap\n+\ttests for PMD-level swap entries\n - process_madv\n \ttest for process_madv\n - cow\n@@ -399,6 +401,8 @@ CATEGORY=\"pagemap\" run_test ./pagemap_ioctl\n \n CATEGORY=\"pfnmap\" run_test ./pfnmap\n \n+CATEGORY=\"pmd_swap\" run_test ./pmd_swap\n+\n # COW tests\n CATEGORY=\"cow\" run_test ./cow\n \n"
}

2/1 2026/07/23 17:01 agent patch-evaluator
0m Model:
best-expensive

Args:
null

Results:
{
  "EnableConfigs": [
    "THP_SWAP",
    "TRANSPARENT_HUGEPAGE",
    "USERFAULTFD",
    "ZSWAP"
  ],
  "FocusSymbols": [
    "do_huge_pmd_swap_page",
    "set_pmd_swap_entry",
    "swap_pmd_cache_lookup",
    "unuse_pmd",
    "unuse_pmd_entry",
    "move_swap_pmd",
    "mincore_pmd_swap"
  ],
  "Reasoning": "The patch introduces PMD-level swap entries, modifying core memory management logic including page faults, swap out/in, swapoff, mincore, and userfaultfd. This is a complex functional change that should be heavily fuzzed to catch potential memory corruption or logic errors.",
  "WorthFuzzing": true
}

Instruction:
You are an expert Linux kernel maintainer.
Your job is to review a provided patch series and determine
if it makes functional changes to the kernel that should be fuzzed.

IMPORTANT: The changes have ALREADY been applied and committed as the HEAD commit in
your workspace. Do NOT rely on your internal knowledge of the kernel. You must actively
use your code access tools to examine the actual source code and confirm any assumptions.

Return WorthFuzzing=false if the patch only contains:
- Modifications to Documentation/, Kconfig files, or code comments.
- Purely decorative changes, such as logging (e.g., pr_err, printk) or tracepoints.
- Changes to numeric constants or macros that do not functionally alter execution flow.
- Code paths that are impossible to reach in virtualized environments like GCE or QEMU,
even when utilizing software-emulated hardware (e.g., usb gadget, mac80211_hwsim).

If it modifies reachable core kernel logic, drivers, or architectures, use your code search
tools to verify the code can be executed, then return WorthFuzzing=true.

When returning WorthFuzzing=true, you MUST ALSO:
1. Extract any specific kernel functions that should be heavily fuzzed into FocusSymbols.
   Avoid listing generic hot-path functions to prevent skewed test distributions.
2. Identify any specific CONFIG_ options required to properly test this new/modified feature.
   Go and look into the Kconfig files and check for ifdefs around the code, do not make assumptions.
   Do not list too generic configs (we already have them enabled). Only list those that
   specifically cover the modified code. List them in the EnableConfigs output array,
   and DO NOT add a 'CONFIG_' prefix (e.g., return "NET_IPV4" instead of "CONFIG_NET_IPV4").
Prefer calling several tools at the same time to save round-trips.


Use set-results tool to provide results of the analysis.
It must be called exactly once before the final reply.
Ignore results of this tool.

Prompt:
For your convenience, here is the diff of the changes:
commit 80071256e0cafe01425f47ef6faa9dbec6553be5
Author: syz-cluster <triage@syzkaller.com>
Date:   Thu Jul 23 17:01:43 2026 +0000

    syz-cluster: applied patch under review

diff --git a/Documentation/admin-guide/mm/transhuge.rst b/Documentation/admin-guide/mm/transhuge.rst
index 16f37135ed80df..b421d7982db637 100644
--- a/Documentation/admin-guide/mm/transhuge.rst
+++ b/Documentation/admin-guide/mm/transhuge.rst
@@ -630,6 +630,11 @@ thp_swpout
 	is incremented every time a huge page is swapout in one
 	piece without splitting.
 
+thp_swpout_pmd
+	is incremented every time a PMD mapping is replaced by a PMD-level
+	swap entry. A fork-shared THP can increment this counter once for each
+	PMD mapping that is swapped out.
+
 thp_swpout_fallback
 	is incremented if a huge page has to be split before swapout.
 	Usually because failed to allocate some continuous swap space
diff --git a/arch/arm64/include/asm/pgtable.h b/arch/arm64/include/asm/pgtable.h
index a2681d75535841..860f95573d1fb5 100644
--- a/arch/arm64/include/asm/pgtable.h
+++ b/arch/arm64/include/asm/pgtable.h
@@ -598,6 +598,12 @@ static inline int pmd_protnone(pmd_t pmd)
 #define pmd_swp_clear_uffd(pmd) \
 				pte_pmd(pte_swp_clear_uffd(pmd_pte(pmd)))
 #endif /* CONFIG_HAVE_ARCH_USERFAULTFD_WP */
+#ifdef CONFIG_ARCH_HAS_PMD_SOFTLEAVES
+#define pmd_swp_exclusive(pmd)	pte_swp_exclusive(pmd_pte(pmd))
+#define pmd_swp_mkexclusive(pmd)	pte_pmd(pte_swp_mkexclusive(pmd_pte(pmd)))
+#define pmd_swp_clear_exclusive(pmd) \
+				pte_pmd(pte_swp_clear_exclusive(pmd_pte(pmd)))
+#endif
 
 #define pmd_write(pmd)		pte_write(pmd_pte(pmd))
 
diff --git a/arch/loongarch/include/asm/pgtable.h b/arch/loongarch/include/asm/pgtable.h
index 1952e34bc8ee00..aa8e1223d3973e 100644
--- a/arch/loongarch/include/asm/pgtable.h
+++ b/arch/loongarch/include/asm/pgtable.h
@@ -357,6 +357,25 @@ static inline pte_t pte_swp_clear_exclusive(pte_t pte)
 	return pte;
 }
 
+#ifdef CONFIG_ARCH_HAS_PMD_SOFTLEAVES
+static inline pmd_t pmd_swp_mkexclusive(pmd_t pmd)
+{
+	pmd_val(pmd) |= _PAGE_SWP_EXCLUSIVE;
+	return pmd;
+}
+
+static inline bool pmd_swp_exclusive(pmd_t pmd)
+{
+	return pmd_val(pmd) & _PAGE_SWP_EXCLUSIVE;
+}
+
+static inline pmd_t pmd_swp_clear_exclusive(pmd_t pmd)
+{
+	pmd_val(pmd) &= ~_PAGE_SWP_EXCLUSIVE;
+	return pmd;
+}
+#endif
+
 #define pte_none(pte)		(!(pte_val(pte) & ~_PAGE_GLOBAL))
 #define pte_present(pte)	(pte_val(pte) & (_PAGE_PRESENT | _PAGE_PROTNONE))
 #define pte_no_exec(pte)	(pte_val(pte) & _PAGE_NO_EXEC)
diff --git a/arch/powerpc/include/asm/book3s/64/pgtable.h b/arch/powerpc/include/asm/book3s/64/pgtable.h
index f4db7d7fbd5c62..6a899d0793b3b6 100644
--- a/arch/powerpc/include/asm/book3s/64/pgtable.h
+++ b/arch/powerpc/include/asm/book3s/64/pgtable.h
@@ -699,6 +699,23 @@ static inline pte_t pte_swp_clear_exclusive(pte_t pte)
 	return __pte_raw(pte_raw(pte) & cpu_to_be64(~_PAGE_SWP_EXCLUSIVE));
 }
 
+#ifdef CONFIG_ARCH_HAS_PMD_SOFTLEAVES
+static inline pmd_t pmd_swp_mkexclusive(pmd_t pmd)
+{
+	return __pmd_raw(pmd_raw(pmd) | cpu_to_be64(_PAGE_SWP_EXCLUSIVE));
+}
+
+static inline bool pmd_swp_exclusive(pmd_t pmd)
+{
+	return !!(pmd_raw(pmd) & cpu_to_be64(_PAGE_SWP_EXCLUSIVE));
+}
+
+static inline pmd_t pmd_swp_clear_exclusive(pmd_t pmd)
+{
+	return __pmd_raw(pmd_raw(pmd) & cpu_to_be64(~_PAGE_SWP_EXCLUSIVE));
+}
+#endif
+
 static inline bool check_pte_access(unsigned long access, unsigned long ptev)
 {
 	/*
diff --git a/arch/riscv/include/asm/pgtable.h b/arch/riscv/include/asm/pgtable.h
index 7f21742b2b6a0c..5f53b158a95cde 100644
--- a/arch/riscv/include/asm/pgtable.h
+++ b/arch/riscv/include/asm/pgtable.h
@@ -1206,6 +1206,21 @@ static inline pte_t pte_swp_clear_exclusive(pte_t pte)
 }
 
 #ifdef CONFIG_ARCH_HAS_PMD_SOFTLEAVES
+static inline bool pmd_swp_exclusive(pmd_t pmd)
+{
+	return pte_swp_exclusive(pmd_pte(pmd));
+}
+
+static inline pmd_t pmd_swp_mkexclusive(pmd_t pmd)
+{
+	return pte_pmd(pte_swp_mkexclusive(pmd_pte(pmd)));
+}
+
+static inline pmd_t pmd_swp_clear_exclusive(pmd_t pmd)
+{
+	return pte_pmd(pte_swp_clear_exclusive(pmd_pte(pmd)));
+}
+
 #define __pmd_to_swp_entry(pmd) ((swp_entry_t) { pmd_val(pmd) })
 #define __swp_entry_to_pmd(swp) __pmd((swp).val)
 #endif /* CONFIG_ARCH_HAS_PMD_SOFTLEAVES */
diff --git a/arch/s390/include/asm/pgtable.h b/arch/s390/include/asm/pgtable.h
index e882663a58e776..490e4a3464b194 100644
--- a/arch/s390/include/asm/pgtable.h
+++ b/arch/s390/include/asm/pgtable.h
@@ -870,6 +870,23 @@ static inline pte_t pte_swp_clear_exclusive(pte_t pte)
 	return clear_pte_bit(pte, __pgprot(_PAGE_SWP_EXCLUSIVE));
 }
 
+#ifdef CONFIG_ARCH_HAS_PMD_SOFTLEAVES
+static inline pmd_t pmd_swp_mkexclusive(pmd_t pmd)
+{
+	return set_pmd_bit(pmd, __pgprot(_PAGE_SWP_EXCLUSIVE));
+}
+
+static inline bool pmd_swp_exclusive(pmd_t pmd)
+{
+	return pmd_val(pmd) & _PAGE_SWP_EXCLUSIVE;
+}
+
+static inline pmd_t pmd_swp_clear_exclusive(pmd_t pmd)
+{
+	return clear_pmd_bit(pmd, __pgprot(_PAGE_SWP_EXCLUSIVE));
+}
+#endif
+
 static inline int pte_soft_dirty(pte_t pte)
 {
 	return pte_val(pte) & _PAGE_SOFT_DIRTY;
diff --git a/arch/x86/include/asm/pgtable.h b/arch/x86/include/asm/pgtable.h
index 8e0018fadd14e6..b5da5447e83d09 100644
--- a/arch/x86/include/asm/pgtable.h
+++ b/arch/x86/include/asm/pgtable.h
@@ -1525,6 +1525,23 @@ static inline pte_t pte_swp_clear_exclusive(pte_t pte)
 	return pte_clear_flags(pte, _PAGE_SWP_EXCLUSIVE);
 }
 
+#ifdef CONFIG_ARCH_HAS_PMD_SOFTLEAVES
+static inline pmd_t pmd_swp_mkexclusive(pmd_t pmd)
+{
+	return pmd_set_flags(pmd, _PAGE_SWP_EXCLUSIVE);
+}
+
+static inline int pmd_swp_exclusive(pmd_t pmd)
+{
+	return pmd_flags(pmd) & _PAGE_SWP_EXCLUSIVE;
+}
+
+static inline pmd_t pmd_swp_clear_exclusive(pmd_t pmd)
+{
+	return pmd_clear_flags(pmd, _PAGE_SWP_EXCLUSIVE);
+}
+#endif
+
 #ifdef CONFIG_HAVE_ARCH_SOFT_DIRTY
 static inline pte_t pte_swp_mksoft_dirty(pte_t pte)
 {
diff --git a/fs/proc/task_mmu.c b/fs/proc/task_mmu.c
index 817e3e0f919430..77e591f0eec683 100644
--- a/fs/proc/task_mmu.c
+++ b/fs/proc/task_mmu.c
@@ -1046,6 +1046,27 @@ static void smaps_pte_hole_lookup(unsigned long addr, struct mm_walk *walk)
 #endif
 }
 
+static void smaps_account_swap(struct mem_size_stats *mss,
+			       softleaf_t entry, unsigned long size)
+{
+	unsigned long nr_pages = size >> PAGE_SHIFT;
+
+	mss->swap += size;
+	do {
+		int mapcount = swp_swapcount(entry);
+
+		if (mapcount >= 2) {
+			u64 pss_delta = (u64)PAGE_SIZE << PSS_SHIFT;
+
+			do_div(pss_delta, mapcount);
+			mss->swap_pss += pss_delta;
+		} else {
+			mss->swap_pss += (u64)PAGE_SIZE << PSS_SHIFT;
+		}
+		entry.val++;
+	} while (--nr_pages);
+}
+
 static void smaps_pte_entry(pte_t *pte, unsigned long addr,
 		struct mm_walk *walk)
 {
@@ -1067,18 +1088,7 @@ static void smaps_pte_entry(pte_t *pte, unsigned long addr,
 		const softleaf_t entry = softleaf_from_pte(ptent);
 
 		if (softleaf_is_swap(entry)) {
-			int mapcount;
-
-			mss->swap += PAGE_SIZE;
-			mapcount = swp_swapcount(entry);
-			if (mapcount >= 2) {
-				u64 pss_delta = (u64)PAGE_SIZE << PSS_SHIFT;
-
-				do_div(pss_delta, mapcount);
-				mss->swap_pss += pss_delta;
-			} else {
-				mss->swap_pss += (u64)PAGE_SIZE << PSS_SHIFT;
-			}
+			smaps_account_swap(mss, entry, PAGE_SIZE);
 		} else if (softleaf_has_pfn(entry)) {
 			if (softleaf_is_device_private(entry))
 				present = true;
@@ -1108,9 +1118,13 @@ static void smaps_pmd_entry(pmd_t *pmd, unsigned long addr,
 	if (pmd_present(*pmd)) {
 		page = vm_normal_page_pmd(vma, addr, *pmd);
 		present = true;
-	} else if (unlikely(thp_migration_supported())) {
+	} else {
 		const softleaf_t entry = softleaf_from_pmd(*pmd);
 
+		if (softleaf_is_swap(entry)) {
+			smaps_account_swap(mss, entry, HPAGE_PMD_SIZE);
+			return;
+		}
 		if (softleaf_has_pfn(entry))
 			page = softleaf_to_page(entry);
 	}
@@ -1755,7 +1769,7 @@ static inline void clear_soft_dirty_pmd(struct vm_area_struct *vma,
 		pmd = pmd_clear_soft_dirty(pmd);
 
 		set_pmd_at(vma->vm_mm, addr, pmdp, pmd);
-	} else if (pmd_is_migration_entry(pmd)) {
+	} else if (pmd_is_migration_entry(pmd) || pmd_is_swap_entry(pmd)) {
 		pmd = pmd_swp_clear_soft_dirty(pmd);
 		set_pmd_at(vma->vm_mm, addr, pmdp, pmd);
 	}
@@ -2115,7 +2129,7 @@ static int pagemap_pmd_range_thp(pmd_t *pmdp, unsigned long addr,
 			flags |= PM_UFFD_WP;
 		if (pm->show_pfn)
 			frame = pmd_pfn(pmd) + idx;
-	} else if (thp_migration_supported()) {
+	} else if (pmd_is_valid_softleaf(pmd)) {
 		const softleaf_t entry = softleaf_from_pmd(pmd);
 		unsigned long offset;
 
@@ -2581,7 +2595,7 @@ static void make_uffd_wp_pmd(struct vm_area_struct *vma,
 		old = pmdp_invalidate_ad(vma, addr, pmdp);
 		pmd = pmd_mkuffd(old);
 		set_pmd_at(vma->vm_mm, addr, pmdp, pmd);
-	} else if (pmd_is_migration_entry(pmd)) {
+	} else if (pmd_is_migration_entry(pmd) || pmd_is_swap_entry(pmd)) {
 		pmd = pmd_swp_mkuffd(pmd);
 		set_pmd_at(vma->vm_mm, addr, pmdp, pmd);
 	}
diff --git a/include/linux/huge_mm.h b/include/linux/huge_mm.h
index c745f7ad22987f..41cf643a3f55f7 100644
--- a/include/linux/huge_mm.h
+++ b/include/linux/huge_mm.h
@@ -552,6 +552,17 @@ vm_fault_t do_huge_pmd_uffd_rwp(struct vm_fault *vmf);
 
 vm_fault_t do_huge_pmd_device_private(struct vm_fault *vmf);
 
+#ifdef CONFIG_THP_SWAP
+vm_fault_t do_huge_pmd_swap_page(struct vm_fault *vmf);
+int set_pmd_swap_entry(struct page_vma_mapped_walk *pvmw,
+		       struct folio *folio);
+#else
+static inline vm_fault_t do_huge_pmd_swap_page(struct vm_fault *vmf)
+{
+	return 0;
+}
+#endif
+
 extern struct folio *huge_zero_folio;
 extern unsigned long huge_zero_pfn;
 
@@ -754,6 +765,11 @@ static inline vm_fault_t do_huge_pmd_device_private(struct vm_fault *vmf)
 	return 0;
 }
 
+static inline vm_fault_t do_huge_pmd_swap_page(struct vm_fault *vmf)
+{
+	return 0;
+}
+
 static inline bool is_huge_zero_folio(const struct folio *folio)
 {
 	return false;
diff --git a/include/linux/leafops.h b/include/linux/leafops.h
index 4c1476ae323435..8397193ba3999f 100644
--- a/include/linux/leafops.h
+++ b/include/linux/leafops.h
@@ -102,6 +102,8 @@ static inline softleaf_t softleaf_from_pmd(pmd_t pmd)
 		pmd = pmd_swp_clear_soft_dirty(pmd);
 	if (pmd_swp_uffd(pmd))
 		pmd = pmd_swp_clear_uffd(pmd);
+	if (pmd_swp_exclusive(pmd))
+		pmd = pmd_swp_clear_exclusive(pmd);
 	arch_entry = __pmd_to_swp_entry(pmd);
 
 	/* Temporary until swp_entry_t eliminated. */
@@ -634,18 +636,30 @@ static inline bool pmd_is_migration_entry(pmd_t pmd)
  */
 static inline bool softleaf_is_valid_pmd_entry(softleaf_t entry)
 {
-	/* Only device private, migration entries valid for PMD. */
+	/* Device private, migration, and swap entries valid for PMD. */
 	return softleaf_is_device_private(entry) ||
-		softleaf_is_migration(entry);
+		softleaf_is_migration(entry) ||
+		softleaf_is_swap(entry);
+}
+
+/**
+ * pmd_is_swap_entry() - Does this PMD entry encode an actual swap entry?
+ * @pmd: PMD entry.
+ *
+ * Returns: true if the PMD encodes a swap entry, otherwise false.
+ */
+static inline bool pmd_is_swap_entry(pmd_t pmd)
+{
+	return softleaf_is_swap(softleaf_from_pmd(pmd));
 }
 
 /**
  * pmd_is_valid_softleaf() - Is this PMD entry a valid softleaf entry?
  * @pmd: PMD entry.
  *
- * PMD leaf entries are valid only if they are device private or migration
- * entries. This function asserts that a PMD leaf entry is valid in this
- * respect.
+ * PMD leaf entries are valid only if they are device private, migration,
+ * or swap entries. This function asserts that a PMD leaf entry is valid
+ * in this respect.
  *
  * Returns: true if the PMD entry is a valid leaf entry, otherwise false.
  */
@@ -673,6 +687,8 @@ static inline struct folio *pmd_to_softleaf_folio(pmd_t pmd)
 		VM_WARN_ON_ONCE(true);
 		return NULL;
 	}
+	if (!softleaf_has_pfn(entry))
+		return NULL;
 	return softleaf_to_folio(entry);
 }
 
diff --git a/include/linux/pgtable.h b/include/linux/pgtable.h
index 8c093c119e5a82..e10a7e91e42606 100644
--- a/include/linux/pgtable.h
+++ b/include/linux/pgtable.h
@@ -1917,6 +1917,23 @@ static inline pmd_t pmd_swp_clear_soft_dirty(pmd_t pmd)
 }
 #endif
 
+#ifndef CONFIG_ARCH_HAS_PMD_SOFTLEAVES
+static inline pmd_t pmd_swp_mkexclusive(pmd_t pmd)
+{
+	return pmd;
+}
+
+static inline bool pmd_swp_exclusive(pmd_t pmd)
+{
+	return false;
+}
+
+static inline pmd_t pmd_swp_clear_exclusive(pmd_t pmd)
+{
+	return pmd;
+}
+#endif
+
 #ifndef __HAVE_PFNMAP_TRACKING
 /*
  * Interfaces that can be used by architecture code to keep track of
diff --git a/include/linux/swap.h b/include/linux/swap.h
index 2cb1d29307c54b..a87fc6832f494a 100644
--- a/include/linux/swap.h
+++ b/include/linux/swap.h
@@ -389,7 +389,7 @@ sector_t swap_folio_sector(struct folio *folio);
  * All entries must be allocated by folio_alloc_swap(). And they must have
  * a swap count > 1. See comments of folio_*_swap helpers for more info.
  */
-int swap_dup_entry_direct(swp_entry_t entry);
+int swap_dup_entries_direct(swp_entry_t entry, int nr);
 void swap_put_entries_direct(swp_entry_t entry, int nr);
 
 /*
@@ -433,7 +433,7 @@ static inline void free_swap_cache(struct folio *folio)
 {
 }
 
-static inline int swap_dup_entry_direct(swp_entry_t ent)
+static inline int swap_dup_entries_direct(swp_entry_t ent, int nr)
 {
 	return 0;
 }
diff --git a/include/linux/vm_event_item.h b/include/linux/vm_event_item.h
index 2628ccda076a07..f8fd4e13698c3f 100644
--- a/include/linux/vm_event_item.h
+++ b/include/linux/vm_event_item.h
@@ -108,6 +108,7 @@ enum vm_event_item { PGPGIN, PGPGOUT, PSWPIN, PSWPOUT,
 		THP_ZERO_PAGE_ALLOC_FAILED,
 		THP_SWPOUT,
 		THP_SWPOUT_FALLBACK,
+		THP_SWPOUT_PMD,
 #endif
 #ifdef CONFIG_BALLOON
 		BALLOON_INFLATE,
diff --git a/include/linux/zswap.h b/include/linux/zswap.h
index 30c193a1207e16..cd9efcf9dec942 100644
--- a/include/linux/zswap.h
+++ b/include/linux/zswap.h
@@ -35,6 +35,7 @@ void zswap_lruvec_state_init(struct lruvec *lruvec);
 void zswap_folio_swapin(struct folio *folio);
 bool zswap_is_enabled(void);
 bool zswap_never_enabled(void);
+bool zswap_is_present(swp_entry_t entry, unsigned int nr);
 #else
 
 struct zswap_lruvec_state {};
@@ -69,6 +70,11 @@ static inline bool zswap_never_enabled(void)
 	return true;
 }
 
+static inline bool zswap_is_present(swp_entry_t entry, unsigned int nr)
+{
+	return false;
+}
+
 #endif
 
 #endif /* _LINUX_ZSWAP_H */
diff --git a/mm/hmm.c b/mm/hmm.c
index 87952c1259c543..b96850a0cb49f8 100644
--- a/mm/hmm.c
+++ b/mm/hmm.c
@@ -376,7 +376,8 @@ static int hmm_vma_handle_absent_pmd(struct mm_walk *walk, unsigned long start,
 	required_fault = hmm_range_need_fault(hmm_vma_walk, hmm_pfns,
 					      npages, 0);
 	if (required_fault) {
-		if (softleaf_is_device_private(entry))
+		if (softleaf_is_device_private(entry) ||
+		    softleaf_is_swap(entry))
 			return hmm_record_fault(addr, end, required_fault, walk);
 		else
 			return -EFAULT;
diff --git a/mm/huge_memory.c b/mm/huge_memory.c
index 04e8a6b553435f..c014631e5a26c3 100644
--- a/mm/huge_memory.c
+++ b/mm/huge_memory.c
@@ -41,6 +41,7 @@
 #include <linux/pgalloc.h>
 #include <linux/pgalloc_tag.h>
 #include <linux/pagewalk.h>
+#include <linux/zswap.h>
 
 #include <asm/tlb.h>
 #include "internal.h"
@@ -1806,7 +1807,7 @@ bool touch_pmd(struct vm_area_struct *vma, unsigned long addr,
 	return false;
 }
 
-static void copy_huge_non_present_pmd(
+static int copy_huge_non_present_pmd(
 		struct mm_struct *dst_mm, struct mm_struct *src_mm,
 		pmd_t *dst_pmd, pmd_t *src_pmd, unsigned long addr,
 		struct vm_area_struct *dst_vma, struct vm_area_struct *src_vma,
@@ -1852,14 +1853,35 @@ static void copy_huge_non_present_pmd(
 		 */
 		folio_try_dup_anon_rmap_pmd(src_folio, &src_folio->page,
 					    dst_vma, src_vma);
+	} else if (softleaf_is_swap(entry)) {
+		int err;
+
+		/*
+		 * PMD swap entry: duplicate swap references and clear
+		 * exclusive on source, matching copy_nonpresent_pte().
+		 */
+		err = swap_dup_entries_direct(entry, HPAGE_PMD_NR);
+		if (err < 0)
+			return err;
+
+		mm_prepare_for_swap_entries(dst_mm);
+
+		if (pmd_swp_exclusive(pmd)) {
+			pmd = pmd_swp_clear_exclusive(pmd);
+			set_pmd_at(src_mm, addr, src_pmd, pmd);
+		}
 	}
 
-	add_mm_counter(dst_mm, MM_ANONPAGES, HPAGE_PMD_NR);
+	if (softleaf_is_swap(entry))
+		add_mm_counter(dst_mm, MM_SWAPENTS, HPAGE_PMD_NR);
+	else
+		add_mm_counter(dst_mm, MM_ANONPAGES, HPAGE_PMD_NR);
 	mm_inc_nr_ptes(dst_mm);
 	pgtable_trans_huge_deposit(dst_mm, dst_pmd, pgtable);
 	if (!userfaultfd_protected(dst_vma))
 		pmd = pmd_swp_clear_uffd(pmd);
 	set_pmd_at(dst_mm, addr, dst_pmd, pmd);
+	return 0;
 }
 
 int copy_huge_pmd(struct mm_struct *dst_mm, struct mm_struct *src_mm,
@@ -1900,6 +1922,7 @@ int copy_huge_pmd(struct mm_struct *dst_mm, struct mm_struct *src_mm,
 	if (unlikely(!pgtable))
 		goto out;
 
+retry:
 	dst_ptl = pmd_lock(dst_mm, dst_pmd);
 	src_ptl = pmd_lockptr(src_mm, src_pmd);
 	spin_lock_nested(src_ptl, SINGLE_DEPTH_NESTING);
@@ -1907,11 +1930,29 @@ int copy_huge_pmd(struct mm_struct *dst_mm, struct mm_struct *src_mm,
 	ret = -EAGAIN;
 	pmd = *src_pmd;
 
-	if (unlikely(thp_migration_supported() &&
-		     pmd_is_valid_softleaf(pmd))) {
-		copy_huge_non_present_pmd(dst_mm, src_mm, dst_pmd, src_pmd, addr,
-					  dst_vma, src_vma, pmd, pgtable);
-		ret = 0;
+	if (unlikely(pmd_is_valid_softleaf(pmd))) {
+		ret = copy_huge_non_present_pmd(dst_mm, src_mm, dst_pmd, src_pmd,
+						addr, dst_vma, src_vma, pmd,
+						pgtable);
+		if (ret) {
+			spin_unlock(src_ptl);
+			spin_unlock(dst_ptl);
+			/*
+			 * For PMD swap entries -ENOMEM means the per-cluster
+			 * swap-extend table couldn't be GFP_ATOMIC-allocated.
+			 * try the GFP_KERNEL fallback once before giving up.
+			 */
+			if (ret == -ENOMEM) {
+				softleaf_t entry = softleaf_from_pmd(pmd);
+
+				if (softleaf_is_swap(entry) &&
+				    !swap_retry_table_alloc(entry, HPAGE_PMD_NR,
+							    GFP_KERNEL))
+					goto retry;
+			}
+			pte_free(dst_mm, pgtable);
+			goto out;
+		}
 		goto out_unlock;
 	}
 
@@ -2308,6 +2349,240 @@ vm_fault_t do_huge_pmd_numa_page(struct vm_fault *vmf)
 	return 0;
 }
 
+#ifdef CONFIG_THP_SWAP
+/**
+ * do_huge_pmd_swap_page() - Handle a fault on a PMD-level swap entry.
+ * @vmf: Fault context. vmf->orig_pmd contains the swap PMD.
+ *
+ * A PMD swap entry is a compact encoding for HPAGE_PMD_NR consecutive swap
+ * slots. If the swap cache still has one PMD-sized folio covering the range,
+ * map it directly at PMD level. If the range has been split into per-page
+ * cache state, or zswap may have per-page state for it, split the PMD swap
+ * entry and retry at PTE granularity.
+ *
+ * Return: VM_FAULT_* flags.
+ */
+vm_fault_t do_huge_pmd_swap_page(struct vm_fault *vmf)
+{
+	struct vm_area_struct *vma = vmf->vma;
+	struct mm_struct *mm = vma->vm_mm;
+	struct folio *folio;
+	struct page *page;
+	struct swap_info_struct *si;
+	unsigned long haddr = vmf->address & HPAGE_PMD_MASK;
+	softleaf_t entry;
+	swp_entry_t swp_entry;
+	pmd_t pmd;
+	vm_fault_t ret = 0;
+	bool exclusive, rwp_restore = false;
+	bool write = vmf->flags & FAULT_FLAG_WRITE;
+	rmap_t rmap_flags = RMAP_NONE;
+	enum swap_pmd_cache cache_state;
+
+	entry = softleaf_from_pmd(vmf->orig_pmd);
+	if (unlikely(!softleaf_is_swap(entry)))
+		return 0;
+
+	swp_entry = entry;
+
+	/* Prevent swapoff from happening to us. */
+	si = get_swap_device(swp_entry);
+	if (unlikely(!si))
+		return 0;
+
+	cache_state = swap_pmd_cache_lookup(swp_entry, &folio);
+	if (cache_state == SWAP_PMD_CACHE_SPLIT)
+		goto split_fallback;
+	if (!folio) {
+		/*
+		 * PMD swap entries encode ordinary per-page swap slots. If any
+		 * slot is in zswap, split and let the PTE swap path load the
+		 * range per page. Otherwise the range is all on disk and can be
+		 * read back as one PMD-sized folio.
+		 */
+		if (zswap_is_present(swp_entry, HPAGE_PMD_NR))
+			goto split_fallback;
+
+		folio = swapin_sync(swp_entry, GFP_HIGHUSER_MOVABLE,
+				    BIT(HPAGE_PMD_ORDER), vmf, NULL, 0);
+		if (IS_ERR_OR_NULL(folio))
+			goto split_fallback;
+
+		/* Had to read from swap area: Major fault */
+		ret = VM_FAULT_MAJOR;
+		count_vm_event(PGMAJFAULT);
+		count_memcg_event_mm(mm, PGMAJFAULT);
+	}
+
+	ret |= folio_lock_or_retry(folio, vmf);
+	if (ret & VM_FAULT_RETRY)
+		goto out_release;
+
+	/* Verify the folio is still in swap cache and matches our entry */
+	if (unlikely(!folio_matches_swap_entry(folio, swp_entry)))
+		goto out_page;
+
+	/*
+	 * Folio should be PMD-sized; if not (e.g. split in swap cache),
+	 * split the PMD swap entry and retry at PTE level.
+	 */
+	if (folio_nr_pages(folio) != HPAGE_PMD_NR) {
+		folio_unlock(folio);
+		folio_put(folio);
+		goto split_fallback;
+	}
+
+	if (unlikely(!folio_test_uptodate(folio))) {
+		if (zswap_is_present(swp_entry, HPAGE_PMD_NR)) {
+			folio_unlock(folio);
+			folio_put(folio);
+			goto split_fallback;
+		}
+		ret = VM_FAULT_SIGBUS;
+		goto out_page;
+	}
+
+	/*
+	 * If any subpage is hardware-poisoned, split the PMD swap entry and
+	 * let the PTE swap-in path handle each page individually so
+	 * do_swap_page() can return VM_FAULT_HWPOISON for the poisoned
+	 * subpage rather than mapping the corrupted memory as one THP.
+	 */
+	if (unlikely(folio_contain_hwpoisoned_page(folio))) {
+		folio_unlock(folio);
+		folio_put(folio);
+		goto split_fallback;
+	}
+
+	page = folio_page(folio, 0);
+	arch_swap_restore(folio_swap(swp_entry, folio), folio);
+
+	folio_throttle_swaprate(folio, GFP_KERNEL);
+
+	/* Lock the PMD and verify it hasn't changed */
+	vmf->ptl = pmd_lock(mm, vmf->pmd);
+	if (unlikely(!pmd_same(vmf->orig_pmd, pmdp_get(vmf->pmd)))) {
+		spin_unlock(vmf->ptl);
+		goto out_page;
+	}
+
+	exclusive = pmd_swp_exclusive(vmf->orig_pmd);
+
+	/*
+	 * Some swap backends (e.g. zram) don't support concurrent page
+	 * modifications while under writeback. If we map exclusive on such
+	 * a backend while the folio is still under writeback, the writeback
+	 * may see partial modifications and corrupt the swap slot. Drop the
+	 * exclusive marker and only map R/O for that case; further GUP
+	 * references can't appear once the page is fully unmapped, so this
+	 * is safe.
+	 */
+	if (exclusive && folio_test_writeback(folio) &&
+	    data_race(si->flags & SWP_STABLE_WRITES))
+		exclusive = false;
+
+	/*
+	 * Set up the PMD mapping. Similar to do_swap_page() but at PMD level.
+	 */
+	add_mm_counter(mm, MM_ANONPAGES, HPAGE_PMD_NR);
+	add_mm_counter(mm, MM_SWAPENTS, -HPAGE_PMD_NR);
+
+	pmd = folio_mk_pmd(folio, vma->vm_page_prot);
+	pmd = pmd_mkyoung(pmd);
+
+	if (pmd_swp_soft_dirty(vmf->orig_pmd))
+		pmd = pmd_mksoft_dirty(pmd);
+	if (pmd_swp_uffd(vmf->orig_pmd))
+		pmd = pmd_mkuffd(pmd);
+	if (pmd_swp_uffd(vmf->orig_pmd) && userfaultfd_rwp(vma)) {
+		pmd = pmd_modify(pmd, PAGE_NONE);
+		rwp_restore = true;
+	}
+
+	/*
+	 * Check exclusivity to determine if we can map writable.
+	 */
+	if (exclusive) {
+		if (!rwp_restore && (vma->vm_flags & VM_WRITE) &&
+		    !userfaultfd_huge_pmd_wp(vma, pmd) &&
+		    !pmd_needs_soft_dirty_wp(vma, pmd)) {
+			pmd = pmd_mkwrite(pmd, vma);
+			if (write)
+				pmd = pmd_mkdirty(pmd);
+		}
+		rmap_flags |= RMAP_EXCLUSIVE;
+	}
+
+	flush_icache_pages(vma, page, HPAGE_PMD_NR);
+
+	if (!folio_test_anon(folio))
+		folio_add_new_anon_rmap(folio, vma, haddr, rmap_flags);
+	else
+		folio_add_anon_rmap_pmd(folio, page, vma, haddr, rmap_flags);
+
+	folio_put_swap(folio, NULL);
+
+	set_pmd_at(mm, haddr, vmf->pmd, pmd);
+	update_mmu_cache_pmd(vma, haddr, vmf->pmd);
+
+	/* Update orig_pmd for any follow-up wp_huge_pmd() below. */
+	vmf->orig_pmd = pmd;
+
+	/*
+	 * Conditionally try to free up the swap cache. Do it after mapping,
+	 * so raced page faults will likely see the folio in swap cache and
+	 * wait on the folio lock.
+	 */
+	if (should_try_to_free_swap(si, folio, vma, exclusive, vmf->flags))
+		folio_free_swap(folio);
+
+	spin_unlock(vmf->ptl);
+
+	folio_unlock(folio);
+	put_swap_device(si);
+
+	/*
+	 * If the write fault wasn't satisfied above (folio is shared without
+	 * exclusivity), call wp_huge_pmd() to handle COW or
+	 * userfaultfd-wp without forcing a second fault.
+	 *
+	 * wp_huge_pmd() may return VM_FAULT_FALLBACK if it had to split the
+	 * PMD; that's a normal outcome, and the natural PTE-level refault will
+	 * complete the COW. Mask it so callers (and the arch fault handler)
+	 * don't see VM_FAULT_FALLBACK as a fatal VM_FAULT_ERROR.
+	 */
+	if (write && !pmd_write(pmd) && !rwp_restore) {
+		vm_fault_t wp_ret = wp_huge_pmd(vmf);
+
+		wp_ret &= ~VM_FAULT_FALLBACK;
+		ret |= wp_ret;
+		if (ret & VM_FAULT_ERROR)
+			ret &= VM_FAULT_ERROR;
+	}
+
+	return ret;
+
+out_page:
+	folio_unlock(folio);
+out_release:
+	folio_put(folio);
+	put_swap_device(si);
+	return ret;
+
+split_fallback:
+	__split_huge_pmd(vma, vmf->pmd, haddr, false);
+	put_swap_device(si);
+	return 0;
+}
+#endif /* CONFIG_THP_SWAP */
+static inline void zap_deposited_table(struct mm_struct *mm, pmd_t *pmd)
+{
+	pgtable_t pgtable;
+
+	pgtable = pgtable_trans_huge_withdraw(mm, pmd);
+	pte_free(mm, pgtable);
+	mm_dec_nr_ptes(mm);
+}
 /*
  * Return true if we do MADV_FREE successfully on entire pmd page.
  * Otherwise, return false.
@@ -2332,6 +2607,21 @@ bool madvise_free_huge_pmd(struct mmu_gather *tlb, struct vm_area_struct *vma,
 		goto out;
 
 	if (unlikely(!pmd_present(orig_pmd))) {
+		if (pmd_is_swap_entry(orig_pmd)) {
+			if (next - addr != HPAGE_PMD_SIZE) {
+				spin_unlock(ptl);
+				__split_huge_pmd(vma, pmd, addr, false);
+				goto out_unlocked;
+			}
+			softleaf_t sl = softleaf_from_pmd(orig_pmd);
+
+			pmdp_huge_get_and_clear(mm, addr, pmd);
+			zap_deposited_table(mm, pmd);
+			spin_unlock(ptl);
+			swap_put_entries_direct(sl, HPAGE_PMD_NR);
+			add_mm_counter(mm, MM_SWAPENTS, -HPAGE_PMD_NR);
+			return true;
+		}
 		VM_WARN_ON_ONCE(!pmd_is_migration_entry(orig_pmd) &&
 				!pmd_is_device_private_entry(orig_pmd));
 		goto out;
@@ -2382,15 +2672,6 @@ bool madvise_free_huge_pmd(struct mmu_gather *tlb, struct vm_area_struct *vma,
 	return ret;
 }
 
-static inline void zap_deposited_table(struct mm_struct *mm, pmd_t *pmd)
-{
-	pgtable_t pgtable;
-
-	pgtable = pgtable_trans_huge_withdraw(mm, pmd);
-	pte_free(mm, pgtable);
-	mm_dec_nr_ptes(mm);
-}
-
 static void zap_huge_pmd_folio(struct mm_struct *mm, struct vm_area_struct *vma,
 		pmd_t pmdval, struct folio *folio, bool is_present)
 {
@@ -2483,6 +2764,16 @@ bool zap_huge_pmd(struct mmu_gather *tlb, struct vm_area_struct *vma,
 	arch_check_zapped_pmd(vma, orig_pmd);
 	tlb_remove_pmd_tlb_entry(tlb, pmd, addr);
 
+	if (pmd_is_swap_entry(orig_pmd)) {
+		softleaf_t sl = softleaf_from_pmd(orig_pmd);
+
+		zap_deposited_table(mm, pmd);
+		spin_unlock(ptl);
+		swap_put_entries_direct(sl, HPAGE_PMD_NR);
+		add_mm_counter(mm, MM_SWAPENTS, -HPAGE_PMD_NR);
+		return true;
+	}
+
 	is_present = pmd_present(orig_pmd);
 	folio = normal_or_softleaf_folio_pmd(vma, addr, orig_pmd, is_present);
 	has_deposit = has_deposited_pgtable(vma, orig_pmd, folio);
@@ -2515,7 +2806,8 @@ static inline int pmd_move_must_withdraw(spinlock_t *new_pmd_ptl,
 static pmd_t move_soft_dirty_pmd(pmd_t pmd)
 {
 	if (pgtable_supports_soft_dirty()) {
-		if (unlikely(pmd_is_migration_entry(pmd)))
+		if (unlikely(pmd_is_migration_entry(pmd) ||
+			     pmd_is_swap_entry(pmd)))
 			pmd = pmd_swp_mksoft_dirty(pmd);
 		else if (pmd_present(pmd))
 			pmd = pmd_mksoft_dirty(pmd);
@@ -2606,7 +2898,14 @@ static void change_non_present_huge_pmd(struct mm_struct *mm,
 	pmd_t newpmd;
 
 	VM_WARN_ON(!pmd_is_valid_softleaf(*pmd));
-	if (softleaf_is_migration_write(entry)) {
+
+	/*
+	 * PMD swap entries don't encode write permission in the entry type,
+	 * so only uffd_wp flag changes apply. No folio lookup needed.
+	 */
+	if (softleaf_is_swap(entry)) {
+		newpmd = *pmd;
+	} else if (softleaf_is_migration_write(entry)) {
 		const struct folio *folio = softleaf_to_folio(entry);
 
 		/*
@@ -2666,7 +2965,7 @@ int change_huge_pmd(struct mmu_gather *tlb, struct vm_area_struct *vma,
 	if (!ptl)
 		return 0;
 
-	if (thp_migration_supported() && pmd_is_valid_softleaf(*pmd)) {
+	if (pmd_is_valid_softleaf(*pmd)) {
 		change_non_present_huge_pmd(mm, addr, pmd, uffd_prot,
 					    uffd_prot_resolve);
 		goto unlock;
@@ -2793,6 +3092,74 @@ int change_huge_pud(struct mmu_gather *tlb, struct vm_area_struct *vma,
 #endif
 
 #ifdef CONFIG_USERFAULTFD
+/*
+ * Move a PMD-level swap entry from src_pmd to dst_pmd. Both PMD locks are
+ * acquired here; src_folio (if present) must already be locked. The deposited
+ * page table backing the source THP is moved across with the entry.
+ */
+static int move_swap_pmd(struct mm_struct *mm, struct vm_area_struct *dst_vma,
+			 unsigned long dst_addr, unsigned long src_addr,
+			 pmd_t *dst_pmd, pmd_t *src_pmd,
+			 pmd_t orig_dst_pmd, pmd_t orig_src_pmd,
+			 spinlock_t *dst_ptl, spinlock_t *src_ptl,
+			 struct folio *src_folio, swp_entry_t entry)
+{
+	pgtable_t src_pgtable;
+	pmd_t moved_pmd;
+
+	/*
+	 * The folio may have been freed and reused for a different swap entry
+	 * while it was unlocked. Re-verify the association.
+	 */
+	if (src_folio && unlikely(!folio_matches_swap_entry(src_folio, entry) ||
+				  folio_nr_pages(src_folio) != HPAGE_PMD_NR))
+		return -EAGAIN;
+
+	double_pt_lock(dst_ptl, src_ptl);
+
+	if (!pmd_same(*src_pmd, orig_src_pmd) ||
+	    !pmd_same(*dst_pmd, orig_dst_pmd)) {
+		double_pt_unlock(dst_ptl, src_ptl);
+		return -EAGAIN;
+	}
+
+	/*
+	 * If the folio is in the swap cache, re-anchor its anon rmap to the
+	 * destination VMA so a future swap-in fault at dst_addr finds it.
+	 * Otherwise, re-check the whole PMD swap range: a PMD swap entry is
+	 * only a compact encoding for 512 swap slots, and any per-slot cached
+	 * folio would need the PTE move path to update its rmap metadata.
+	 */
+	if (src_folio) {
+		folio_move_anon_rmap(src_folio, dst_vma);
+		src_folio->index = linear_page_index(dst_vma, dst_addr);
+	} else {
+		unsigned int type = swp_type(entry);
+		pgoff_t offset = swp_offset(entry);
+		int i;
+
+		for (i = 0; i < HPAGE_PMD_NR; i++) {
+			if (swap_cache_has_folio(swp_entry(type, offset + i))) {
+				double_pt_unlock(dst_ptl, src_ptl);
+				return -EAGAIN;
+			}
+		}
+	}
+
+	moved_pmd = pmdp_huge_get_and_clear(mm, src_addr, src_pmd);
+	if (pgtable_supports_soft_dirty())
+		moved_pmd = pmd_swp_mksoft_dirty(moved_pmd);
+	if (userfaultfd_rwp(dst_vma))
+		moved_pmd = pmd_swp_mkuffd(moved_pmd);
+	set_pmd_at(mm, dst_addr, dst_pmd, moved_pmd);
+
+	src_pgtable = pgtable_trans_huge_withdraw(mm, src_pmd);
+	pgtable_trans_huge_deposit(mm, dst_pmd, src_pgtable);
+
+	double_pt_unlock(dst_ptl, src_ptl);
+	return 0;
+}
+
 /*
  * The PT lock for src_pmd and dst_vma/src_vma (for reading) are locked by
  * the caller, but it must return after releasing the page_table_lock.
@@ -2827,11 +3194,76 @@ int move_pages_huge_pmd(struct mm_struct *mm, pmd_t *dst_pmd, pmd_t *src_pmd, pm
 	}
 
 	if (!pmd_trans_huge(src_pmdval)) {
-		spin_unlock(src_ptl);
 		if (pmd_is_migration_entry(src_pmdval)) {
+			spin_unlock(src_ptl);
 			pmd_migration_entry_wait(mm, src_pmd);
 			return -EAGAIN;
 		}
+		if (pmd_is_swap_entry(src_pmdval)) {
+			swp_entry_t entry;
+			struct swap_info_struct *si;
+			enum swap_pmd_cache cache_state;
+
+			/*
+			 * UFFDIO_MOVE on anon mappings requires single-owner
+			 * semantics; refuse to move a shared swap entry.
+			 */
+			if (!pmd_swp_exclusive(src_pmdval)) {
+				spin_unlock(src_ptl);
+				return -EBUSY;
+			}
+
+			entry = softleaf_from_pmd(src_pmdval);
+			spin_unlock(src_ptl);
+
+			/* Pin the swap device against a racing swapoff. */
+			si = get_swap_device(entry);
+			if (unlikely(!si))
+				return -EAGAIN;
+
+			src_folio = NULL;
+			cache_state = swap_pmd_cache_lookup(entry, &src_folio);
+			if (cache_state == SWAP_PMD_CACHE_SPLIT) {
+				put_swap_device(si);
+				__split_huge_pmd(src_vma, src_pmd, src_addr, false);
+				return -EAGAIN;
+			}
+
+			mmu_notifier_range_init(&range, MMU_NOTIFY_CLEAR, 0,
+						mm, src_addr,
+						src_addr + HPAGE_PMD_SIZE);
+			mmu_notifier_invalidate_range_start(&range);
+
+			if (src_folio) {
+				folio_lock(src_folio);
+				if (!folio_matches_swap_entry(src_folio, entry) ||
+				    folio_nr_pages(src_folio) != HPAGE_PMD_NR) {
+					err = -EAGAIN;
+					folio_unlock(src_folio);
+					folio_put(src_folio);
+					mmu_notifier_invalidate_range_end(&range);
+					put_swap_device(si);
+					__split_huge_pmd(src_vma, src_pmd,
+							 src_addr, false);
+					return err;
+				}
+			}
+
+			dst_ptl = pmd_lockptr(mm, dst_pmd);
+			err = move_swap_pmd(mm, dst_vma, dst_addr, src_addr,
+					    dst_pmd, src_pmd, dst_pmdval,
+					    src_pmdval, dst_ptl, src_ptl,
+					    src_folio, entry);
+
+			mmu_notifier_invalidate_range_end(&range);
+			if (src_folio) {
+				folio_unlock(src_folio);
+				folio_put(src_folio);
+			}
+			put_swap_device(si);
+			return err;
+		}
+		spin_unlock(src_ptl);
 		return -ENOENT;
 	}
 
@@ -3210,6 +3642,14 @@ static void __split_huge_pmd_locked(struct vm_area_struct *vma, pmd_t *pmd,
 			folio_add_anon_rmap_ptes(folio, page, HPAGE_PMD_NR,
 						 vma, haddr, rmap_flags);
 		}
+	} else if (pmd_is_swap_entry(*pmd)) {
+		VM_WARN_ON_ONCE(freeze);
+		/* Swap entries have no page for the migration freeze path. */
+		freeze = false;
+		old_pmd = *pmd;
+		soft_dirty = pmd_swp_soft_dirty(old_pmd);
+		uffd_wp = pmd_swp_uffd(old_pmd);
+		anon_exclusive = pmd_swp_exclusive(old_pmd);
 	} else {
 		/*
 		 * Up to this point the pmd is present and huge and userland has
@@ -3346,6 +3786,25 @@ static void __split_huge_pmd_locked(struct vm_area_struct *vma, pmd_t *pmd,
 			VM_WARN_ON(!pte_none(ptep_get(pte + i)));
 			set_pte_at(mm, addr, pte + i, entry);
 		}
+	} else if (pmd_is_swap_entry(old_pmd)) {
+		softleaf_t sl_entry = softleaf_from_pmd(old_pmd);
+		pte_t swp_pte;
+		swp_entry_t sub_entry;
+
+		for (i = 0, addr = haddr; i < HPAGE_PMD_NR;
+		     i++, addr += PAGE_SIZE) {
+			sub_entry = swp_entry(swp_type(sl_entry),
+					      swp_offset(sl_entry) + i);
+			swp_pte = swp_entry_to_pte(sub_entry);
+			if (soft_dirty)
+				swp_pte = pte_swp_mksoft_dirty(swp_pte);
+			if (uffd_wp)
+				swp_pte = pte_swp_mkuffd(swp_pte);
+			if (anon_exclusive)
+				swp_pte = pte_swp_mkexclusive(swp_pte);
+			VM_WARN_ON(!pte_none(ptep_get(pte + i)));
+			set_pte_at(mm, addr, pte + i, swp_pte);
+		}
 	} else {
 		pte_t entry;
 
@@ -3373,7 +3832,7 @@ static void __split_huge_pmd_locked(struct vm_area_struct *vma, pmd_t *pmd,
 	}
 	pte_unmap(pte);
 
-	if (!pmd_is_migration_entry(*pmd))
+	if (!pmd_is_migration_entry(old_pmd) && !pmd_is_swap_entry(old_pmd))
 		folio_remove_rmap_pmd(folio, page, vma);
 	if (freeze)
 		put_page(page);
@@ -5102,3 +5561,83 @@ void remove_migration_pmd(struct page_vma_mapped_walk *pvmw, struct page *new)
 	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)
+{
+	struct vm_area_struct *vma = pvmw->vma;
+	struct mm_struct *mm = vma->vm_mm;
+	unsigned long address = pvmw->address;
+	unsigned long haddr = address & HPAGE_PMD_MASK;
+	struct page *page = folio_page(folio, 0);
+	bool anon_exclusive;
+	pmd_t pmdval;
+	swp_entry_t entry;
+	pmd_t pmdswp;
+
+	if (!(pvmw->pmd && !pvmw->pte))
+		return 0;
+
+	VM_BUG_ON_FOLIO(!folio_test_swapcache(folio), folio);
+	VM_BUG_ON_FOLIO(!folio_test_anon(folio), folio);
+
+	if (unlikely(folio_test_swapbacked(folio) !=
+			folio_test_swapcache(folio))) {
+		WARN_ON_ONCE(1);
+		return -EBUSY;
+	}
+
+	flush_cache_range(vma, haddr, haddr + HPAGE_PMD_SIZE);
+
+	pmdval = pmdp_invalidate(vma, haddr, pvmw->pmd);
+
+	/* Update high watermark before we lower rss */
+	update_hiwater_rss(mm);
+
+	if (folio_dup_swap(folio, NULL) < 0) {
+		set_pmd_at(mm, haddr, pvmw->pmd, pmdval);
+		return -ENOMEM;
+	}
+
+	/* See folio_try_share_anon_rmap_pmd(): invalidate PMD first. */
+	anon_exclusive = PageAnonExclusive(page);
+	if (anon_exclusive && folio_try_share_anon_rmap_pmd(folio, page)) {
+		folio_put_swap(folio, NULL);
+		set_pmd_at(mm, haddr, pvmw->pmd, pmdval);
+		return -EBUSY;
+	}
+
+	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);
+
+	folio_remove_rmap_pmd(folio, page, vma);
+	folio_put(folio);
+
+	count_vm_event(THP_SWPOUT_PMD);
+	return 0;
+}
+#endif /* CONFIG_THP_SWAP */
diff --git a/mm/internal.h b/mm/internal.h
index 5758dcaf439235..7e5893602c8fb2 100644
--- a/mm/internal.h
+++ b/mm/internal.h
@@ -581,6 +581,40 @@ static inline vm_fault_t vmf_anon_prepare(struct vm_fault *vmf)
 }
 
 vm_fault_t do_swap_page(struct vm_fault *vmf);
+vm_fault_t wp_huge_pmd(struct vm_fault *vmf);
+
+/*
+ * Check if we should call folio_free_swap to free the swap cache.
+ * folio_free_swap only frees the swap cache to release the slot if swap
+ * count is zero, so we don't need to check the swap count here.
+ */
+static inline bool should_try_to_free_swap(struct swap_info_struct *si,
+					   struct folio *folio,
+					   struct vm_area_struct *vma,
+					   bool exclusive,
+					   unsigned int fault_flags)
+{
+	if (!folio_test_swapcache(folio))
+		return false;
+	/*
+	 * Always try to free swap cache for SWP_SYNCHRONOUS_IO devices. Swap
+	 * cache can help save some IO or memory overhead, but these devices
+	 * are fast, and meanwhile, swap cache pinning the slot deferring the
+	 * release of metadata or fragmentation is a more critical issue.
+	 */
+	if (data_race(si->flags & SWP_SYNCHRONOUS_IO))
+		return true;
+	if (mem_cgroup_swap_full(folio) || (vma->vm_flags & VM_LOCKED) ||
+	    folio_test_mlocked(folio))
+		return true;
+
+	/*
+	 * Free the swapcache only if we are the exclusive user and
+	 * this is a write fault.
+	 */
+	return (fault_flags & FAULT_FLAG_WRITE) && exclusive;
+}
+
 void folio_rotate_reclaimable(struct folio *folio);
 bool __folio_end_writeback(struct folio *folio);
 void deactivate_file_folio(struct folio *folio);
diff --git a/mm/khugepaged.c b/mm/khugepaged.c
index 27e8f3077e80fe..f9c00f5ef2e394 100644
--- a/mm/khugepaged.c
+++ b/mm/khugepaged.c
@@ -1101,6 +1101,12 @@ static inline enum scan_result check_pmd_state(pmd_t *pmd)
 	 */
 	if (pmd_is_migration_entry(pmde))
 		return SCAN_PMD_MAPPED;
+	/*
+	 * A PMD-mapped THP that has been swapped out is still a THP from
+	 * khugepaged's perspective; treat it like a present huge PMD.
+	 */
+	if (pmd_is_swap_entry(pmde))
+		return SCAN_PMD_MAPPED;
 	if (!pmd_present(pmde))
 		return SCAN_NO_PTE_TABLE;
 	if (pmd_trans_huge(pmde))
diff --git a/mm/madvise.c b/mm/madvise.c
index 07a21ca31bad4d..1d5154b10dfcca 100644
--- a/mm/madvise.c
+++ b/mm/madvise.c
@@ -32,6 +32,7 @@
 #include <linux/leafops.h>
 #include <linux/shmem_fs.h>
 #include <linux/mmu_notifier.h>
+#include <linux/zswap.h>
 
 #include <asm/tlb.h>
 
@@ -193,6 +194,91 @@ static int swapin_walk_pmd_entry(pmd_t *pmd, unsigned long start,
 	spinlock_t *ptl;
 	unsigned long addr;
 
+	ptl = pmd_trans_huge_lock(pmd, vma);
+	if (ptl) {
+		pmd_t pmdval = *pmd;
+
+		if (pmd_is_swap_entry(pmdval)) {
+			softleaf_t entry = softleaf_from_pmd(pmdval);
+			struct vm_fault vmf = {
+				.vma = vma,
+				.address = start,
+				.real_address = start,
+				.pmd = pmd,
+			};
+			struct swap_info_struct *si;
+			struct folio *folio;
+			enum swap_pmd_cache cache_state;
+			bool split = false;
+
+			cache_state = swap_pmd_cache_lookup(entry, &folio);
+			if (cache_state == SWAP_PMD_CACHE_HUGE) {
+				folio_put(folio);
+				spin_unlock(ptl);
+				goto ret;
+			}
+			if (cache_state == SWAP_PMD_CACHE_SPLIT ||
+			    zswap_is_present(entry, HPAGE_PMD_NR)) {
+				spin_unlock(ptl);
+				__split_huge_pmd(vma, pmd, start, false);
+				walk->action = ACTION_AGAIN;
+				goto ret;
+			}
+
+			/*
+			 * Pin the swap device under the PMD lock so the
+			 * PMD-swap-entry observation keeps the entry valid for
+			 * swapin_sync().
+			 */
+			si = get_swap_device(entry);
+			spin_unlock(ptl);
+			if (!si)
+				goto ret;
+
+			folio = swapin_sync(entry, GFP_HIGHUSER_MOVABLE,
+					    BIT(HPAGE_PMD_ORDER), &vmf,
+					    NULL, 0);
+			/*
+			 * The empty-cache observation was made under the PMD
+			 * lock, but swap cache can change after dropping it. If
+			 * PMD-order swapin lost a race to per-slot cache state,
+			 * retry through the PTE path.
+			 */
+			if (IS_ERR(folio)) {
+				if (PTR_ERR(folio) == -EBUSY)
+					split = true;
+			} else if (folio) {
+				if (folio_nr_pages(folio) != HPAGE_PMD_NR) {
+					split = true;
+				} else if (!folio_test_locked(folio) &&
+					 !folio_test_uptodate(folio) &&
+					 zswap_is_present(entry, HPAGE_PMD_NR)) {
+					folio_lock(folio);
+					/*
+					 * A failed PMD-order zswap load leaves the
+					 * folio clean and not uptodate, but another
+					 * thread can remove it from swap cache before
+					 * we acquire the lock. Revalidate the
+					 * association before deleting it so the PTE
+					 * retry can load the per-page state.
+					 */
+					if (folio_matches_swap_entry(folio, entry))
+						swap_cache_del_folio(folio);
+					folio_unlock(folio);
+					split = true;
+				}
+				folio_put(folio);
+			}
+			put_swap_device(si);
+			if (split) {
+				__split_huge_pmd(vma, pmd, start, false);
+				walk->action = ACTION_AGAIN;
+			}
+			goto ret;
+		}
+		spin_unlock(ptl);
+	}
+
 	for (addr = start; addr < end; addr += PAGE_SIZE) {
 		pte_t pte;
 		softleaf_t entry;
@@ -221,6 +307,7 @@ static int swapin_walk_pmd_entry(pmd_t *pmd, unsigned long start,
 	if (ptep)
 		pte_unmap_unlock(ptep, ptl);
 	swap_read_submit(&ctx);
+ret:
 	cond_resched();
 
 	return 0;
@@ -374,6 +461,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);
@@ -384,6 +480,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 (is_huge_zero_pmd(orig_pmd))
 			goto huge_unlock;
 
@@ -665,7 +764,7 @@ static int madvise_free_pte_range(pmd_t *pmd, unsigned long addr,
 	int nr, max_nr;
 
 	next = pmd_addr_end(addr, end);
-	if (pmd_trans_huge(*pmd))
+	if (pmd_trans_huge(*pmd) || pmd_is_swap_entry(*pmd))
 		if (madvise_free_huge_pmd(tlb, vma, pmd, addr, next))
 			return 0;
 
diff --git a/mm/memory.c b/mm/memory.c
index a620d425ec95e8..f4ab24a49d0d32 100644
--- a/mm/memory.c
+++ b/mm/memory.c
@@ -1012,7 +1012,7 @@ copy_nonpresent_pte(struct mm_struct *dst_mm, struct mm_struct *src_mm,
 	struct page *page;
 
 	if (likely(softleaf_is_swap(entry))) {
-		if (swap_dup_entry_direct(entry) < 0)
+		if (swap_dup_entries_direct(entry, 1) < 0)
 			return -EIO;
 
 		mm_prepare_for_swap_entries(dst_mm);
@@ -1427,7 +1427,7 @@ copy_pte_range(struct vm_area_struct *dst_vma, struct vm_area_struct *src_vma,
 
 	if (ret == -EIO) {
 		VM_WARN_ON_ONCE(!entry.val);
-		if (swap_retry_table_alloc(entry, GFP_KERNEL) < 0) {
+		if (swap_retry_table_alloc(entry, 1, GFP_KERNEL) < 0) {
 			ret = -ENOMEM;
 			goto out;
 		}
@@ -4636,38 +4636,6 @@ static vm_fault_t remove_device_exclusive_entry(struct vm_fault *vmf)
 	return 0;
 }
 
-/*
- * Check if we should call folio_free_swap to free the swap cache.
- * folio_free_swap only frees the swap cache to release the slot if swap
- * count is zero, so we don't need to check the swap count here.
- */
-static inline bool should_try_to_free_swap(struct swap_info_struct *si,
-					   struct folio *folio,
-					   struct vm_area_struct *vma,
-					   bool exclusive,
-					   unsigned int fault_flags)
-{
-	if (!folio_test_swapcache(folio))
-		return false;
-	/*
-	 * Always try to free swap cache for SWP_SYNCHRONOUS_IO devices. Swap
-	 * cache can help save some IO or memory overhead, but these devices
-	 * are fast, and meanwhile, swap cache pinning the slot deferring the
-	 * release of metadata or fragmentation is a more critical issue.
-	 */
-	if (data_race(si->flags & SWP_SYNCHRONOUS_IO))
-		return true;
-	if (mem_cgroup_swap_full(folio) || (vma->vm_flags & VM_LOCKED) ||
-	    folio_test_mlocked(folio))
-		return true;
-
-	/*
-	 * Free the swapcache only if we are the exclusive user and
-	 * this is a write fault.
-	 */
-	return (fault_flags & FAULT_FLAG_WRITE) && exclusive;
-}
-
 static vm_fault_t pte_marker_clear(struct vm_fault *vmf)
 {
 	vmf->pte = pte_offset_map_lock(vmf->vma->vm_mm, vmf->pmd,
@@ -5047,7 +5015,8 @@ vm_fault_t do_swap_page(struct vm_fault *vmf)
 	page_idx = 0;
 	address = vmf->address;
 	ptep = vmf->pte;
-	if (folio_test_large(folio) && folio_test_swapcache(folio)) {
+	if (folio_test_large(folio) && folio_test_swapcache(folio) &&
+	    !folio_contain_hwpoisoned_page(folio)) {
 		int nr = folio_nr_pages(folio);
 		unsigned long idx = folio_page_idx(folio, page);
 		unsigned long folio_start = address - idx * PAGE_SIZE;
@@ -6382,8 +6351,7 @@ static inline vm_fault_t create_huge_pmd(struct vm_fault *vmf)
 	return VM_FAULT_FALLBACK;
 }
 
-/* `inline' is required to avoid gcc 4.1.2 build error */
-static inline vm_fault_t wp_huge_pmd(struct vm_fault *vmf)
+vm_fault_t wp_huge_pmd(struct vm_fault *vmf)
 {
 	struct vm_area_struct *vma = vmf->vma;
 	const bool unshare = vmf->flags & FAULT_FLAG_UNSHARE;
@@ -6676,6 +6644,9 @@ static vm_fault_t __handle_mm_fault(struct vm_area_struct *vma,
 
 		if (pmd_is_migration_entry(vmf.orig_pmd))
 			pmd_migration_entry_wait(mm, vmf.pmd);
+		else if (IS_ENABLED(CONFIG_THP_SWAP) &&
+			 pmd_is_swap_entry(vmf.orig_pmd))
+			return do_huge_pmd_swap_page(&vmf);
 		return 0;
 	}
 	if (pmd_trans_huge(vmf.orig_pmd)) {
diff --git a/mm/mincore.c b/mm/mincore.c
index ff4ac828176837..3f0fba964c8a33 100644
--- a/mm/mincore.c
+++ b/mm/mincore.c
@@ -85,6 +85,41 @@ static unsigned char mincore_swap(swp_entry_t entry, bool shmem)
 	return present;
 }
 
+#ifdef CONFIG_THP_SWAP
+static void mincore_pmd_swap(swp_entry_t entry, unsigned long addr,
+			     unsigned long end, unsigned char *vec)
+{
+	unsigned long haddr = addr & HPAGE_PMD_MASK;
+	unsigned long start = (addr - haddr) >> PAGE_SHIFT;
+	unsigned long nr = (end - addr) >> PAGE_SHIFT;
+	struct folio *folio;
+	enum swap_pmd_cache state;
+	int i;
+
+	state = swap_pmd_cache_lookup(entry, &folio);
+	if (state == SWAP_PMD_CACHE_HUGE) {
+		memset(vec, folio_test_uptodate(folio), nr);
+		folio_put(folio);
+		return;
+	}
+
+	if (state == SWAP_PMD_CACHE_EMPTY) {
+		memset(vec, 0, nr);
+		return;
+	}
+
+	/*
+	 * The PMD swap entry is only a compact encoding for consecutive swap
+	 * slots. If the PMD-sized swapcache folio was split, report residency
+	 * from the individual slots covered by this mincore() range.
+	 */
+	for (i = 0; i < nr; i++)
+		vec[i] = mincore_swap(swp_entry(swp_type(entry),
+						swp_offset(entry) + start + i),
+				      false);
+}
+#endif
+
 /*
  * Later we can get more picky about what "in core" means precisely.
  * For now, simply check to see if the page is in the page cache,
@@ -171,7 +206,15 @@ static int mincore_pte_range(pmd_t *pmd, unsigned long addr, unsigned long end,
 
 	ptl = pmd_trans_huge_lock(pmd, vma);
 	if (ptl) {
-		memset(vec, 1, nr);
+		if (pmd_is_swap_entry(*pmd)) {
+#ifdef CONFIG_THP_SWAP
+			mincore_pmd_swap(softleaf_from_pmd(*pmd), addr, end, vec);
+#else
+			memset(vec, 0, nr);
+#endif
+		} else {
+			memset(vec, 1, nr);
+		}
 		spin_unlock(ptl);
 		goto out;
 	}
diff --git a/mm/rmap.c b/mm/rmap.c
index b7ead3e9f06412..4574f7b969b60c 100644
--- a/mm/rmap.c
+++ b/mm/rmap.c
@@ -2282,6 +2282,25 @@ static bool try_to_unmap_one(struct folio *folio, struct vm_area_struct *vma,
 				goto walk_abort;
 			}
 
+#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;
+
+				add_mm_counter(mm, MM_ANONPAGES,
+					       -HPAGE_PMD_NR);
+				add_mm_counter(mm, MM_SWAPENTS,
+					       HPAGE_PMD_NR);
+				goto walk_done;
+			}
+#endif
+
 			if (flags & TTU_SPLIT_HUGE_PMD) {
 				/*
 				 * We temporarily have to drop the PTL and
diff --git a/mm/swap.h b/mm/swap.h
index 0260f9b4c7eee2..520f9d5a60a4c1 100644
--- a/mm/swap.h
+++ b/mm/swap.h
@@ -249,7 +249,7 @@ static inline void swap_cluster_unlock_irq(struct swap_cluster_info *ci)
 	spin_unlock_irq(&ci->lock);
 }
 
-extern int swap_retry_table_alloc(swp_entry_t entry, gfp_t gfp);
+int swap_retry_table_alloc(swp_entry_t entry, unsigned int nr, gfp_t gfp);
 
 /*
  * Below are the core routines for doing swap for a folio.
@@ -338,6 +338,23 @@ static inline bool folio_matches_swap_entry(const struct folio *folio,
 bool swap_cache_has_folio(swp_entry_t entry);
 struct folio *swap_cache_get_folio(swp_entry_t entry);
 void *swap_cache_get_shadow(swp_entry_t entry);
+enum swap_pmd_cache {
+	SWAP_PMD_CACHE_EMPTY,
+	SWAP_PMD_CACHE_HUGE,
+	SWAP_PMD_CACHE_SPLIT,
+};
+
+#ifdef CONFIG_THP_SWAP
+enum swap_pmd_cache swap_pmd_cache_lookup(swp_entry_t entry,
+					  struct folio **foliop);
+#else
+static inline enum swap_pmd_cache swap_pmd_cache_lookup(swp_entry_t entry,
+							struct folio **foliop)
+{
+	*foliop = NULL;
+	return SWAP_PMD_CACHE_EMPTY;
+}
+#endif
 void swap_cache_del_folio(struct folio *folio);
 struct folio *swap_cache_alloc_folio(swp_entry_t target_entry, gfp_t gfp_mask,
 				     unsigned long orders, struct vm_fault *vmf,
@@ -455,7 +472,8 @@ static inline int swap_writeout(struct swap_io_ctx *ctx, struct folio *folio)
 	return 0;
 }
 
-static inline int swap_retry_table_alloc(swp_entry_t entry, gfp_t gfp)
+static inline int swap_retry_table_alloc(swp_entry_t entry, unsigned int nr,
+					 gfp_t gfp)
 {
 	return -EINVAL;
 }
diff --git a/mm/swap_state.c b/mm/swap_state.c
index 5be825911e645e..1f08fb5220361b 100644
--- a/mm/swap_state.c
+++ b/mm/swap_state.c
@@ -123,6 +123,50 @@ bool swap_cache_has_folio(swp_entry_t entry)
 	return swp_tb_is_folio(swp_tb);
 }
 
+#ifdef CONFIG_THP_SWAP
+/**
+ * swap_pmd_cache_lookup - classify the swap cache behind a PMD swap entry
+ * @entry: first swap slot encoded by the PMD swap entry
+ * @foliop: returned PMD-sized folio, with a reference, if present
+ *
+ * A PMD swap entry is a compact page-table encoding for HPAGE_PMD_NR
+ * consecutive swap slots. The swap cache behind those slots can be empty,
+ * one PMD-sized folio, or per-slot folios after the original folio was split.
+ *
+ * Context: Caller must keep @entry valid using the usual swap cache rules.
+ * Return: SWAP_PMD_CACHE_EMPTY if no slot in the PMD range has a cached folio,
+ * SWAP_PMD_CACHE_HUGE if one PMD-sized folio covers the range, or
+ * SWAP_PMD_CACHE_SPLIT if the range needs per-page handling.
+ */
+enum swap_pmd_cache swap_pmd_cache_lookup(swp_entry_t entry,
+					  struct folio **foliop)
+{
+	unsigned int type = swp_type(entry);
+	pgoff_t offset = swp_offset(entry);
+	struct folio *folio;
+	int i;
+
+	*foliop = NULL;
+
+	folio = swap_cache_get_folio(entry);
+	if (folio) {
+		if (folio_nr_pages(folio) == HPAGE_PMD_NR) {
+			*foliop = folio;
+			return SWAP_PMD_CACHE_HUGE;
+		}
+		folio_put(folio);
+		return SWAP_PMD_CACHE_SPLIT;
+	}
+
+	for (i = 1; i < HPAGE_PMD_NR; i++) {
+		if (swap_cache_has_folio(swp_entry(type, offset + i)))
+			return SWAP_PMD_CACHE_SPLIT;
+	}
+
+	return SWAP_PMD_CACHE_EMPTY;
+}
+#endif
+
 /**
  * swap_cache_get_shadow - Looks up a shadow in the swap cache.
  * @entry: swap entry used for the lookup.
diff --git a/mm/swapfile.c b/mm/swapfile.c
index 5d15913dcf8631..46f4f4c07bf6fb 100644
--- a/mm/swapfile.c
+++ b/mm/swapfile.c
@@ -42,6 +42,7 @@
 #include <linux/suspend.h>
 #include <linux/zswap.h>
 #include <linux/plist.h>
+#include <linux/huge_mm.h>
 
 #include <asm/tlbflush.h>
 #include <linux/leafops.h>
@@ -1462,9 +1463,11 @@ static bool swap_sync_discard(void)
 
 static int swap_extend_table_alloc(struct swap_info_struct *si,
 				   struct swap_cluster_info *ci,
-				   unsigned int ci_off, gfp_t gfp)
+				   unsigned int ci_off, unsigned int nr,
+				   gfp_t gfp)
 {
 	int count;
+	unsigned int i;
 	void *table;
 
 	table = kzalloc(sizeof(ci->extend_table[0]) * SWAPFILE_CLUSTER, gfp);
@@ -1480,15 +1483,21 @@ static int swap_extend_table_alloc(struct swap_info_struct *si,
 	 */
 	if (!cluster_table_is_alloced(ci))
 		goto out_free;
-	count = swp_tb_get_count(__swap_table_get(ci, ci_off));
-	if (count < (SWP_TB_COUNT_MAX - 1))
-		goto out_free;
 	if (ci->extend_table)
 		goto out_free;
-
-	ci->extend_table = table;
-	spin_unlock(&ci->lock);
-	return 0;
+	/*
+	 * The caller may not know which slot in [ci_off, ci_off + nr) hit
+	 * SWP_TB_COUNT_MAX - 1. Confirm at least one slot in the range still
+	 * needs the extend table before committing the allocation.
+	 */
+	for (i = 0; i < nr; i++) {
+		count = swp_tb_get_count(__swap_table_get(ci, ci_off + i));
+		if (count >= (SWP_TB_COUNT_MAX - 1)) {
+			ci->extend_table = table;
+			spin_unlock(&ci->lock);
+			return 0;
+		}
+	}
 
 out_free:
 	spin_unlock(&ci->lock);
@@ -1496,7 +1505,7 @@ static int swap_extend_table_alloc(struct swap_info_struct *si,
 	return 0;
 }
 
-int swap_retry_table_alloc(swp_entry_t entry, gfp_t gfp)
+int swap_retry_table_alloc(swp_entry_t entry, unsigned int nr, gfp_t gfp)
 {
 	int ret;
 	struct swap_info_struct *si;
@@ -1508,7 +1517,8 @@ int swap_retry_table_alloc(swp_entry_t entry, gfp_t gfp)
 		return 0;
 
 	ci = __swap_offset_to_cluster(si, offset);
-	ret = swap_extend_table_alloc(si, ci, swp_cluster_offset(entry), gfp);
+	ret = swap_extend_table_alloc(si, ci, swp_cluster_offset(entry), nr,
+				      gfp);
 
 	put_swap_device(si);
 	return ret;
@@ -1709,7 +1719,8 @@ static int swap_dup_entries_cluster(struct swap_info_struct *si,
 		if (unlikely(err)) {
 			if (err == -ENOMEM) {
 				spin_unlock(&ci->lock);
-				err = swap_extend_table_alloc(si, ci, ci_off, GFP_ATOMIC);
+				err = swap_extend_table_alloc(si, ci, ci_off, 1,
+							      GFP_ATOMIC);
 				spin_lock(&ci->lock);
 				if (!err)
 					goto restart;
@@ -1720,6 +1731,7 @@ static int swap_dup_entries_cluster(struct swap_info_struct *si,
 	swap_cluster_unlock(ci);
 	return 0;
 failed:
+	/* The caller's page-table or swap-cache reference pins every slot. */
 	while (ci_off-- > ci_start)
 		__swap_cluster_put_entry(ci, ci_off);
 	swap_extend_table_try_free(ci);
@@ -2652,6 +2664,153 @@ static int unuse_pte_range(struct vm_area_struct *vma, pmd_t *pmd,
 	return 0;
 }
 
+#ifdef CONFIG_THP_SWAP
+/*
+ * unuse_pmd - Map a locked folio at PMD granularity during swapoff.
+ *
+ * The caller provides a locked, swapped-in folio.  Returns 0 on success
+ * (PMD was mapped).  Returns -EAGAIN if the swap cache folio no longer
+ * matches the entry or the PMD changed under the lock (try_to_unuse will
+ * rescan). Returns -EIO if the folio is not uptodate or contains a poisoned
+ * subpage; in that case the PMD is split so unuse_pte_range() can handle
+ * individual pages.
+ */
+static int unuse_pmd(struct vm_area_struct *vma, pmd_t *pmd,
+		     unsigned long addr, softleaf_t entry,
+		     struct folio *folio)
+{
+	struct mm_struct *mm = vma->vm_mm;
+	struct page *page;
+	pmd_t new_pmd, old_pmd;
+	spinlock_t *ptl;
+	rmap_t rmap_flags = RMAP_NONE;
+	bool exclusive;
+
+	if (unlikely(!folio_matches_swap_entry(folio, entry)))
+		return -EAGAIN;
+
+	if (unlikely(!folio_test_uptodate(folio) ||
+		     folio_contain_hwpoisoned_page(folio))) {
+		__split_huge_pmd(vma, pmd, addr, false);
+		return -EIO;
+	}
+
+	page = folio_page(folio, 0);
+
+	ptl = pmd_lock(mm, pmd);
+	old_pmd = pmdp_get(pmd);
+
+	if (!pmd_is_swap_entry(old_pmd) ||
+	    softleaf_from_pmd(old_pmd).val != entry.val) {
+		spin_unlock(ptl);
+		return -EAGAIN;
+	}
+
+	exclusive = pmd_swp_exclusive(old_pmd);
+
+	/*
+	 * Some architectures may have to restore extra metadata to the folio
+	 * when reading from swap. This metadata may be indexed by swap entry
+	 * so this must be called before folio_put_swap().
+	 */
+	arch_swap_restore(folio_swap(entry, folio), folio);
+
+	add_mm_counter(mm, MM_ANONPAGES, HPAGE_PMD_NR);
+	add_mm_counter(mm, MM_SWAPENTS, -HPAGE_PMD_NR);
+
+	new_pmd = folio_mk_pmd(folio, vma->vm_page_prot);
+	new_pmd = pmd_mkold(new_pmd);
+	if (pmd_swp_soft_dirty(old_pmd))
+		new_pmd = pmd_mksoft_dirty(new_pmd);
+	if (pmd_swp_uffd(old_pmd))
+		new_pmd = pmd_mkuffd(new_pmd);
+	if (pmd_swp_uffd(old_pmd) && userfaultfd_rwp(vma))
+		new_pmd = pmd_modify(new_pmd, PAGE_NONE);
+
+	if (exclusive)
+		rmap_flags |= RMAP_EXCLUSIVE;
+
+	folio_get(folio);
+	if (!folio_test_anon(folio))
+		folio_add_new_anon_rmap(folio, vma, addr, rmap_flags);
+	else
+		folio_add_anon_rmap_pmd(folio, page, vma, addr, rmap_flags);
+
+	set_pmd_at(mm, addr, pmd, new_pmd);
+	folio_put_swap(folio, NULL);
+
+	spin_unlock(ptl);
+
+	folio_free_swap(folio);
+	return 0;
+}
+
+/*
+ * Try to swap in a PMD swap entry as a whole THP. Returns 0 on success.
+ * If the swap cache no longer has one PMD-sized folio, zswap may require
+ * per-page loading, or a PMD-order allocation/read fails, split the PMD so
+ * the caller can fall back to unuse_pte_range(). Otherwise propagates the
+ * error from unuse_pmd().
+ */
+static int unuse_pmd_entry(struct vm_area_struct *vma, pmd_t *pmd,
+			   unsigned long addr, softleaf_t entry)
+{
+	struct folio *folio;
+	enum swap_pmd_cache cache_state;
+	int ret;
+
+	cache_state = swap_pmd_cache_lookup(entry, &folio);
+	if (cache_state == SWAP_PMD_CACHE_SPLIT) {
+		ret = -EAGAIN;
+		goto split_fallback;
+	}
+	if (!folio) {
+		struct vm_fault vmf = {
+			.vma = vma,
+			.address = addr,
+			.real_address = addr,
+			.pmd = pmd,
+		};
+
+		if (zswap_is_present(entry, HPAGE_PMD_NR)) {
+			ret = -EAGAIN;
+			goto split_fallback;
+		}
+
+		folio = swapin_sync(entry, GFP_HIGHUSER_MOVABLE,
+				    BIT(HPAGE_PMD_ORDER), &vmf, NULL, 0);
+		if (IS_ERR_OR_NULL(folio)) {
+			ret = folio ? PTR_ERR(folio) : -ENOMEM;
+			goto split_fallback;
+		}
+	}
+
+	folio_lock(folio);
+	folio_wait_writeback(folio);
+	/*
+	 * If the cached folio is no longer PMD-sized (e.g. split in the
+	 * swap cache by deferred_split_scan() or memory_failure() while
+	 * the PMD swap entry was installed), the PMD swap entry no longer
+	 * maps a single contiguous folio.  Split the PMD swap entry so
+	 * unuse_pte_range() can swap the per-slot folios in individually.
+	 */
+	if (folio_nr_pages(folio) != HPAGE_PMD_NR) {
+		folio_unlock(folio);
+		folio_put(folio);
+		ret = -EAGAIN;
+		goto split_fallback;
+	}
+	ret = unuse_pmd(vma, pmd, addr, entry, folio);
+	folio_unlock(folio);
+	folio_put(folio);
+	return ret;
+
+split_fallback:
+	__split_huge_pmd(vma, pmd, addr, false);
+	return ret;
+}
+#endif
+
 static inline int unuse_pmd_range(struct vm_area_struct *vma, pud_t *pud,
 				unsigned long addr, unsigned long end,
 				unsigned int type)
@@ -2664,6 +2823,20 @@ static inline int unuse_pmd_range(struct vm_area_struct *vma, pud_t *pud,
 	do {
 		cond_resched();
 		next = pmd_addr_end(addr, end);
+
+#ifdef CONFIG_THP_SWAP
+		pmd_t pmdval = pmdp_get(pmd);
+
+		if (pmd_is_swap_entry(pmdval)) {
+			softleaf_t sl = softleaf_from_pmd(pmdval);
+
+			if (swp_type(sl) == type) {
+				if (!unuse_pmd_entry(vma, pmd, addr, sl))
+					continue;
+			}
+		}
+#endif
+
 		ret = unuse_pte_range(vma, pmd, addr, next, type);
 		if (ret)
 			return ret;
@@ -3911,8 +4084,9 @@ void si_swapinfo(struct sysinfo *val)
 }
 
 /*
- * swap_dup_entry_direct() - Increase reference count of a swap entry by one.
+ * swap_dup_entries_direct() - Increase reference count of swap entries by one.
  * @entry: first swap entry from which we want to increase the refcount.
+ * @nr: number of contiguous swap entries to duplicate.
  *
  * Returns 0 for success, or -ENOMEM if the extend table is required
  * but could not be atomically allocated.  Returns -EINVAL if the swap
@@ -3924,7 +4098,7 @@ void si_swapinfo(struct sysinfo *val)
  * Also the swap entry must have a count >= 1. Otherwise folio_dup_swap should
  * be used.
  */
-int swap_dup_entry_direct(swp_entry_t entry)
+int swap_dup_entries_direct(swp_entry_t entry, int nr)
 {
 	struct swap_info_struct *si;
 
@@ -3941,7 +4115,7 @@ int swap_dup_entry_direct(swp_entry_t entry)
 	 */
 	VM_WARN_ON_ONCE(!swap_entry_swapped(si, entry));
 
-	return swap_dup_entries_cluster(si, swp_offset(entry), 1);
+	return swap_dup_entries_cluster(si, swp_offset(entry), nr);
 }
 
 #if defined(CONFIG_MEMCG) && defined(CONFIG_BLK_CGROUP)
diff --git a/mm/vmscan.c b/mm/vmscan.c
index 54e79824554a7a..a70c773da8d140 100644
--- a/mm/vmscan.c
+++ b/mm/vmscan.c
@@ -1328,7 +1328,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;
 			/*
 			 * Without TTU_SYNC, try_to_unmap will only begin to
diff --git a/mm/vmstat.c b/mm/vmstat.c
index 4e26e5fd666626..68e6efc0dabd16 100644
--- a/mm/vmstat.c
+++ b/mm/vmstat.c
@@ -1422,6 +1422,7 @@ const char * const vmstat_text[] = {
 	[I(THP_ZERO_PAGE_ALLOC_FAILED)]		= "thp_zero_page_alloc_failed",
 	[I(THP_SWPOUT)]				= "thp_swpout",
 	[I(THP_SWPOUT_FALLBACK)]		= "thp_swpout_fallback",
+	[I(THP_SWPOUT_PMD)]			= "thp_swpout_pmd",
 #endif
 #ifdef CONFIG_BALLOON
 	[I(BALLOON_INFLATE)]			= "balloon_inflate",
diff --git a/mm/zswap.c b/mm/zswap.c
index 4e76a4a87cdcc0..384492f1f6964e 100644
--- a/mm/zswap.c
+++ b/mm/zswap.c
@@ -1561,6 +1561,23 @@ bool zswap_store(struct folio *folio)
 	return ret;
 }
 
+/**
+ * zswap_is_present() - is any slot in [entry, entry + nr) in zswap?
+ * @entry: base swap entry of the range
+ * @nr: number of contiguous slots to check (pass 1 for a single-slot query)
+ */
+bool zswap_is_present(swp_entry_t entry, unsigned int nr)
+{
+	pgoff_t offset = swp_offset(entry);
+	struct xarray *tree = swap_zswap_tree(entry);
+	unsigned long index = offset;
+
+	if (!nr || zswap_never_enabled())
+		return false;
+
+	return xa_find(tree, &index, offset + nr - 1, XA_PRESENT);
+}
+
 /**
  * zswap_load() - load a folio from zswap
  * @folio: folio to load
@@ -1573,10 +1590,9 @@ bool zswap_store(struct folio *folio)
  *  NOT marked up-to-date, so that an IO error is emitted (e.g. do_swap_page()
  *  will SIGBUS).
  *
- *  -EINVAL: if the swapped out content was in zswap, but the page belongs
- *  to a large folio, which is not supported by zswap. The folio is unlocked,
- *  but NOT marked up-to-date, so that an IO error is emitted (e.g.
- *  do_swap_page() will SIGBUS).
+ *  -EIO: if a slot in a large-folio range is unexpectedly still in zswap.
+ *  The folio is unlocked, but NOT marked up-to-date, so that an IO
+ *  error is emitted (e.g. do_swap_page() will SIGBUS).
  *
  *  -ENOENT: if the swapped out content was not in zswap. The folio remains
  *  locked on return.
@@ -1595,13 +1611,19 @@ int zswap_load(struct folio *folio)
 		return -ENOENT;
 
 	/*
-	 * Large folios should not be swapped in while zswap is being used, as
-	 * they are not properly handled. Zswap does not properly load large
-	 * folios, and a large folio may only be partially in zswap.
+	 * A large folio reaches zswap_load() only when its whole range is
+	 * expected to be on disk: PMD swap-entry consumers split before
+	 * calling into PMD-order swapin whenever any slot is still in zswap.
+	 * Confirm the range is entirely absent from zswap and return -ENOENT
+	 * so the caller reads it from disk; if a slot is unexpectedly still in
+	 * zswap, fail the read rather than return partially-initialized data.
 	 */
-	if (WARN_ON_ONCE(folio_test_large(folio))) {
-		folio_unlock(folio);
-		return -EINVAL;
+	if (folio_test_large(folio)) {
+		if (zswap_is_present(swp, folio_nr_pages(folio))) {
+			folio_unlock(folio);
+			return -EIO;
+		}
+		return -ENOENT;
 	}
 
 	entry = xa_load(tree, offset);
diff --git a/tools/testing/selftests/mm/Makefile b/tools/testing/selftests/mm/Makefile
index eae504bd94c83a..25af4d7b633e05 100644
--- a/tools/testing/selftests/mm/Makefile
+++ b/tools/testing/selftests/mm/Makefile
@@ -104,6 +104,7 @@ TEST_GEN_FILES += guard-regions
 TEST_GEN_FILES += merge
 TEST_GEN_FILES += rmap
 TEST_GEN_FILES += folio_split_race_test
+TEST_GEN_FILES += pmd_swap
 
 ifneq ($(ARCH),arm64)
 TEST_GEN_FILES += soft-dirty
@@ -165,6 +166,7 @@ TEST_PROGS += ksft_mremap.sh
 TEST_PROGS += ksft_pagemap.sh
 TEST_PROGS += ksft_pfnmap.sh
 TEST_PROGS += ksft_pkey.sh
+TEST_PROGS += ksft_pmd_swap.sh
 TEST_PROGS += ksft_process_madv.sh
 TEST_PROGS += ksft_process_mrelease.sh
 TEST_PROGS += ksft_rmap.sh
diff --git a/tools/testing/selftests/mm/ksft_pmd_swap.sh b/tools/testing/selftests/mm/ksft_pmd_swap.sh
new file mode 100755
index 00000000000000..0f070b4729a893
--- /dev/null
+++ b/tools/testing/selftests/mm/ksft_pmd_swap.sh
@@ -0,0 +1,4 @@
+#!/bin/sh -e
+# SPDX-License-Identifier: GPL-2.0
+
+./run_vmtests.sh -t pmd_swap
diff --git a/tools/testing/selftests/mm/pmd_swap.c b/tools/testing/selftests/mm/pmd_swap.c
new file mode 100644
index 00000000000000..7cf9ac074e9643
--- /dev/null
+++ b/tools/testing/selftests/mm/pmd_swap.c
@@ -0,0 +1,874 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * Test PMD-level swap entries.
+ *
+ * Verifies that when a PMD-mapped THP is swapped out the kernel installs
+ * a single PMD-level swap entry (instead of splitting into 512 PTE-level
+ * entries), and that operations on the swapped region behave correctly:
+ *   basic         - swap out + swap in preserves data
+ *   fork          - parent and child both see the data
+ *   fork_cow      - COW after fork keeps parent's data isolated
+ *   cycles        - repeated swap out/in does not corrupt data
+ *   write         - faulting in via a write restores a PMD-mapped THP
+ *   rwp_swapin    - userfaultfd RWP survives PMD-order swap-in
+ *   munmap        - munmap on a PMD swap entry frees swap slots cleanly
+ *   mprotect      - mprotect on a PMD swap entry preserves data
+ *   mremap        - mremap on a PMD swap entry preserves data
+ *   pagemap        - pagemap reports the entries as swapped
+ *   mincore        - mincore walks a PMD swap entry without faulting it in
+ *   madvise_free   - MADV_FREE on a PMD swap entry does not crash
+ *   madvise_willneed - MADV_WILLNEED handles a PMD swap entry
+ *   uffdio_move    - UFFDIO_MOVE moves a PMD swap entry
+ *   swapoff        - swapoff handles PMD swap entries (needs PMD_SWAP_DEVICE)
+ */
+#define _GNU_SOURCE
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+#include <sys/mman.h>
+#include <sys/wait.h>
+#include <fcntl.h>
+#include <errno.h>
+#include <stdint.h>
+#include <sys/random.h>
+#include <sys/swap.h>
+#include <sys/syscall.h>
+#include <sys/ioctl.h>
+#include <poll.h>
+#include <pthread.h>
+#include <linux/userfaultfd.h>
+#include <time.h>
+
+#include "kselftest_harness.h"
+#include "vm_util.h"
+
+#define ZSWAP_ENABLED_PATH "/sys/module/zswap/parameters/enabled"
+
+static bool check_swapped(int pagemap_fd, char *addr, unsigned long size)
+{
+	unsigned long off;
+
+	for (off = 0; off < size; off += getpagesize())
+		if (!pagemap_is_swapped(pagemap_fd, addr + off))
+			return false;
+	return true;
+}
+
+static bool zswap_enabled(void)
+{
+	char enabled = 0;
+	FILE *f;
+
+	f = fopen(ZSWAP_ENABLED_PATH, "r");
+	if (!f)
+		return false;
+
+	if (fscanf(f, " %c", &enabled) != 1)
+		enabled = 0;
+	fclose(f);
+
+	return enabled == 'Y' || enabled == 'y' || enabled == '1';
+}
+
+static bool swap_available(int pagemap_fd)
+{
+	char *p;
+	bool ret;
+
+	p = mmap(NULL, getpagesize(), PROT_READ | PROT_WRITE,
+		 MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
+	if (p == MAP_FAILED)
+		return false;
+
+	memset(p, 0xab, getpagesize());
+	madvise(p, getpagesize(), MADV_PAGEOUT);
+	ret = pagemap_is_swapped(pagemap_fd, p);
+	munmap(p, getpagesize());
+	return ret;
+}
+
+static unsigned long read_vm_event(const char *name)
+{
+	char line[256];
+	size_t name_len = strlen(name);
+	unsigned long val = 0;
+	FILE *f;
+
+	f = fopen("/proc/vmstat", "r");
+	if (!f)
+		return 0;
+	while (fgets(line, sizeof(line), f)) {
+		if (!strncmp(line, name, name_len) && line[name_len] == ' ') {
+			val = strtoul(line + name_len + 1, NULL, 10);
+			break;
+		}
+	}
+	fclose(f);
+	return val;
+}
+
+static unsigned int random_seed(void)
+{
+	unsigned int seed;
+
+	if (getrandom(&seed, sizeof(seed), 0) != sizeof(seed))
+		seed = (unsigned int)time(NULL);
+	return seed;
+}
+
+static unsigned char pattern_byte(unsigned int seed, unsigned long off)
+{
+	return (unsigned char)(seed + off);
+}
+
+static void fill_pattern(char *buf, unsigned long size, unsigned int seed)
+{
+	unsigned long i;
+
+	for (i = 0; i < size; i++)
+		buf[i] = (char)pattern_byte(seed, i);
+}
+
+static bool verify_pattern(char *buf, unsigned long size, unsigned int seed)
+{
+	unsigned long i;
+
+	for (i = 0; i < size; i++)
+		if ((unsigned char)buf[i] != pattern_byte(seed, i))
+			return false;
+	return true;
+}
+
+/*
+ * mmap an anonymous PMD-aligned region of pmd_size bytes. Over-allocates
+ * by one PMD and trims the unaligned head/tail so the returned address is
+ * PMD-aligned (required for whole-PMD UFFDIO_MOVE).
+ */
+static char *mmap_pmd_aligned(unsigned long pmd_size)
+{
+	unsigned long pad = pmd_size;
+	char *raw, *aligned;
+
+	raw = mmap(NULL, pmd_size + pad, PROT_READ | PROT_WRITE,
+		   MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
+	if (raw == MAP_FAILED)
+		return MAP_FAILED;
+
+	aligned = (char *)(((uintptr_t)raw + pmd_size - 1) & ~(pmd_size - 1));
+	if (aligned != raw)
+		munmap(raw, aligned - raw);
+	if (aligned + pmd_size != raw + pmd_size + pad)
+		munmap(aligned + pmd_size,
+		       (raw + pmd_size + pad) - (aligned + pmd_size));
+	return aligned;
+}
+
+/*
+ * mmap a PMD-aligned PMD-sized region, request THP, fill with a pattern,
+ * and swap it out. Verifies via the thp_swpout_pmd vmstat counter that
+ * the swap-out installed a PMD swap entry rather than splitting to PTEs.
+ */
+static char *alloc_fill_swap_thp(unsigned long pmd_size, int pagemap_fd,
+				 unsigned int seed)
+{
+	unsigned long pmd_before, pmd_after;
+	char *mem;
+
+	mem = mmap_pmd_aligned(pmd_size);
+	if (mem == MAP_FAILED)
+		return MAP_FAILED;
+
+	madvise(mem, pmd_size, MADV_HUGEPAGE);
+	fill_pattern(mem, pmd_size, seed);
+
+	pmd_before = read_vm_event("thp_swpout_pmd");
+
+	if (madvise(mem, pmd_size, MADV_PAGEOUT) ||
+	    !check_swapped(pagemap_fd, mem, pmd_size)) {
+		munmap(mem, pmd_size);
+		return MAP_FAILED;
+	}
+
+	pmd_after = read_vm_event("thp_swpout_pmd");
+	printf("# thp_swpout_pmd: %lu -> %lu\n", pmd_before, pmd_after);
+	if (pmd_after - pmd_before < 1) {
+		munmap(mem, pmd_size);
+		return MAP_FAILED;
+	}
+	return mem;
+}
+
+struct rwp_access_args {
+	unsigned char *addr;
+	unsigned char expected;
+	bool write;
+	bool ok;
+};
+
+static void *rwp_access_thread(void *data)
+{
+	struct rwp_access_args *args = data;
+
+	if (args->write)
+		*args->addr = args->expected;
+	args->ok = *args->addr == args->expected;
+	return NULL;
+}
+
+static int register_rwp(char *addr, unsigned long size, bool protect)
+{
+	struct uffdio_register reg = {};
+	struct uffdio_rwprotect rwp = {};
+	struct uffdio_api api = {};
+	int uffd;
+
+	uffd = syscall(__NR_userfaultfd, O_CLOEXEC | O_NONBLOCK);
+	if (uffd < 0)
+		return -1;
+
+	api.api = UFFD_API;
+	api.features = UFFD_FEATURE_RWP;
+	if (ioctl(uffd, UFFDIO_API, &api) ||
+	    !(api.features & UFFD_FEATURE_RWP))
+		goto error;
+
+	reg.range.start = (unsigned long)addr;
+	reg.range.len = size;
+	reg.mode = UFFDIO_REGISTER_MODE_RWP;
+	if (ioctl(uffd, UFFDIO_REGISTER, &reg))
+		goto error;
+
+	if (!protect)
+		return uffd;
+
+	rwp.range.start = (unsigned long)addr;
+	rwp.range.len = size;
+	rwp.mode = UFFDIO_RWPROTECT_MODE_RWP;
+	if (!ioctl(uffd, UFFDIO_RWPROTECT, &rwp))
+		return uffd;
+
+error:
+	close(uffd);
+	return -1;
+}
+
+static bool expect_rwp_fault(int uffd, char *addr, unsigned long size,
+			     unsigned char expected, bool write)
+{
+	struct rwp_access_args args = {
+		.addr = (unsigned char *)addr,
+		.expected = expected,
+		.write = write,
+	};
+	struct uffdio_rwprotect rwp = {
+		.range = {
+			.start = (unsigned long)addr,
+			.len = size,
+		},
+	};
+	struct pollfd pollfd = {
+		.fd = uffd,
+		.events = POLLIN,
+	};
+	struct uffd_msg msg = {};
+	pthread_t thread;
+	bool saw_rwp = false;
+	int ret;
+
+	if (pthread_create(&thread, NULL, rwp_access_thread, &args))
+		return false;
+
+	ret = poll(&pollfd, 1, 5000);
+	if (ret == 1 && (pollfd.revents & POLLIN) &&
+	    read(uffd, &msg, sizeof(msg)) == (ssize_t)sizeof(msg)) {
+		saw_rwp = msg.event == UFFD_EVENT_PAGEFAULT &&
+			  (msg.arg.pagefault.flags & UFFD_PAGEFAULT_FLAG_RWP);
+	}
+
+	/* Resolve the access even on failure so the worker cannot remain blocked. */
+	ioctl(uffd, UFFDIO_RWPROTECT, &rwp);
+	if (pthread_join(thread, NULL))
+		return false;
+	return saw_rwp && args.ok;
+}
+
+FIXTURE(pmd_swap)
+{
+	unsigned long pmd_size;
+	int pagemap_fd;
+	unsigned int seed;
+	bool zswap_enabled;
+};
+
+FIXTURE_SETUP(pmd_swap)
+{
+	self->pagemap_fd = -1;
+
+	self->pmd_size = read_pmd_pagesize();
+	if (!self->pmd_size)
+		SKIP(return, "Cannot determine PMD size\n");
+
+	self->pagemap_fd = open("/proc/self/pagemap", O_RDONLY);
+	if (self->pagemap_fd < 0)
+		SKIP(return, "Cannot open /proc/self/pagemap\n");
+
+	if (!swap_available(self->pagemap_fd))
+		SKIP(return, "Swap not available or not working\n");
+
+	self->seed = random_seed();
+	self->zswap_enabled = zswap_enabled();
+}
+
+FIXTURE_TEARDOWN(pmd_swap)
+{
+	if (self->pagemap_fd >= 0)
+		close(self->pagemap_fd);
+}
+
+/*
+ * Allocate a PMD-sized THP, write a pattern, swap it out, read it back,
+ * verify the pattern.
+ */
+TEST_F(pmd_swap, basic)
+{
+	char *mem;
+
+	mem = alloc_fill_swap_thp(self->pmd_size, self->pagemap_fd, self->seed);
+	if (mem == MAP_FAILED)
+		SKIP(return, "Could not create swapped THP\n");
+
+	ASSERT_TRUE(verify_pattern(mem, self->pmd_size, self->seed));
+
+	munmap(mem, self->pmd_size);
+}
+
+/*
+ * Allocate a THP, swap it out, fork, verify both parent and child see
+ * the correct data.
+ */
+TEST_F(pmd_swap, fork)
+{
+	char *mem;
+	pid_t pid;
+	int status;
+
+	mem = alloc_fill_swap_thp(self->pmd_size, self->pagemap_fd, self->seed);
+	if (mem == MAP_FAILED)
+		SKIP(return, "Could not create swapped THP\n");
+
+	pid = fork();
+	ASSERT_GE(pid, 0);
+
+	if (pid == 0)
+		_exit(verify_pattern(mem, self->pmd_size, self->seed) ? 0 : 1);
+
+	ASSERT_TRUE(verify_pattern(mem, self->pmd_size, self->seed));
+
+	ASSERT_EQ(waitpid(pid, &status, 0), pid);
+	ASSERT_TRUE(WIFEXITED(status));
+	ASSERT_EQ(WEXITSTATUS(status), 0);
+
+	munmap(mem, self->pmd_size);
+}
+
+/*
+ * Swap out, fork, then have parent and child write different patterns.
+ * Exercises COW on shared PMD swap entries: writes after fork must
+ * trigger copy-on-write so the parent's data stays isolated from the
+ * child's.  Both processes write and then verify their own pattern so
+ * the parent-side COW path is exercised too (a parent-only-read variant
+ * only proves the swap entry survived fork).
+ */
+TEST_F(pmd_swap, fork_cow)
+{
+	unsigned int parent_seed = self->seed;
+	unsigned int child_seed = ~self->seed;
+	unsigned int post_seed = self->seed ^ 0xa5a5a5a5;
+	char *mem;
+	pid_t pid;
+	int status;
+
+	mem = alloc_fill_swap_thp(self->pmd_size, self->pagemap_fd, parent_seed);
+	if (mem == MAP_FAILED)
+		SKIP(return, "Could not create swapped THP\n");
+
+	pid = fork();
+	ASSERT_GE(pid, 0);
+
+	if (pid == 0) {
+		fill_pattern(mem, self->pmd_size, child_seed);
+		_exit(verify_pattern(mem, self->pmd_size, child_seed) ? 0 : 1);
+	}
+
+	ASSERT_EQ(waitpid(pid, &status, 0), pid);
+
+	/* Child's writes must not leak back into the parent. */
+	ASSERT_TRUE(verify_pattern(mem, self->pmd_size, parent_seed));
+	ASSERT_TRUE(WIFEXITED(status));
+	ASSERT_EQ(WEXITSTATUS(status), 0);
+
+	/* Now trigger the parent-side COW and confirm the write sticks. */
+	fill_pattern(mem, self->pmd_size, post_seed);
+	ASSERT_TRUE(verify_pattern(mem, self->pmd_size, post_seed));
+
+	munmap(mem, self->pmd_size);
+}
+
+/*
+ * Swap a THP out and in repeatedly without data corruption.
+ */
+TEST_F(pmd_swap, cycles)
+{
+	const int num_cycles = 5;
+	char *mem;
+	int cycle;
+
+	for (cycle = 0; cycle < num_cycles; cycle++) {
+		unsigned int seed = self->seed + cycle;
+
+		mem = alloc_fill_swap_thp(self->pmd_size, self->pagemap_fd, seed);
+		if (mem == MAP_FAILED)
+			SKIP(return, "Could not create swapped THP at cycle %d\n",
+			     cycle);
+
+		ASSERT_TRUE(verify_pattern(mem, self->pmd_size, seed));
+
+		munmap(mem, self->pmd_size);
+	}
+}
+
+/*
+ * Swap out, fault in via a write to the first page, verify the write
+ * reinstates a THP mapping and the rest of the THP is preserved.
+ */
+TEST_F(pmd_swap, write)
+{
+	unsigned int seed = self->seed;
+	char *mem;
+	unsigned long i;
+
+	mem = alloc_fill_swap_thp(self->pmd_size, self->pagemap_fd, seed);
+	if (mem == MAP_FAILED)
+		SKIP(return, "Could not create swapped THP\n");
+
+	mem[0] = 0xbb;
+	ASSERT_EQ(mem[0], (char)0xbb);
+
+	if (self->zswap_enabled) {
+		TH_LOG("zswap is enabled, so PMD mapping is not checked");
+	} else {
+		ASSERT_TRUE(check_huge_anon(mem, 1, self->pmd_size));
+	}
+
+	for (i = 1; i < self->pmd_size; i++)
+		ASSERT_EQ((unsigned char)mem[i], pattern_byte(seed, i));
+
+	munmap(mem, self->pmd_size);
+}
+
+/* RWP protection on a swap PMD must survive PMD-order swap-in. */
+TEST_F(pmd_swap, rwp_swapin)
+{
+	unsigned int seed = self->seed;
+	char *mem;
+	int uffd;
+
+	mem = alloc_fill_swap_thp(self->pmd_size, self->pagemap_fd, seed);
+	if (mem == MAP_FAILED)
+		SKIP(return, "Could not create swapped THP\n");
+
+	uffd = register_rwp(mem, self->pmd_size, true);
+	if (uffd < 0) {
+		munmap(mem, self->pmd_size);
+		SKIP(return, "Userfaultfd RWP unsupported\n");
+	}
+
+	ASSERT_TRUE(expect_rwp_fault(uffd, mem, self->pmd_size,
+				     pattern_byte(seed, 0), false)) {
+		close(uffd);
+		munmap(mem, self->pmd_size);
+	}
+	ASSERT_TRUE(verify_pattern(mem, self->pmd_size, seed));
+
+	close(uffd);
+	munmap(mem, self->pmd_size);
+}
+
+/*
+ * munmap while the folio is swapped out. Exercises zap_huge_pmd() on a
+ * PMD swap entry — must free the swap slots without trying to look up
+ * a folio.
+ */
+TEST_F(pmd_swap, munmap)
+{
+	char *mem;
+
+	mem = alloc_fill_swap_thp(self->pmd_size, self->pagemap_fd, self->seed);
+	if (mem == MAP_FAILED)
+		SKIP(return, "Could not create swapped THP\n");
+
+	munmap(mem, self->pmd_size);
+}
+
+/*
+ * Change protection on a swapped PMD entry, then fault back in and
+ * verify data. Exercises change_non_present_huge_pmd().
+ */
+TEST_F(pmd_swap, mprotect)
+{
+	unsigned int seed = self->seed;
+	char *mem;
+
+	mem = alloc_fill_swap_thp(self->pmd_size, self->pagemap_fd, seed);
+	if (mem == MAP_FAILED)
+		SKIP(return, "Could not create swapped THP\n");
+
+	ASSERT_EQ(mprotect(mem, self->pmd_size, PROT_READ), 0);
+	ASSERT_EQ(mprotect(mem, self->pmd_size, PROT_READ | PROT_WRITE), 0);
+
+	ASSERT_TRUE(verify_pattern(mem, self->pmd_size, seed));
+
+	munmap(mem, self->pmd_size);
+}
+
+/*
+ * UFFDIO_MOVE a PMD swap entry from src to a registered dst. Exercises
+ * move_pages_huge_pmd() handling of pmd_is_swap_entry: the whole PMD swap
+ * entry must move to dst without splitting, and the destination must
+ * read back the original pattern after a swap-in fault.
+ */
+TEST_F(pmd_swap, uffdio_move)
+{
+	unsigned int seed = self->seed;
+	struct uffdio_register reg = {};
+	struct uffdio_move move = {};
+	struct uffdio_api api = {};
+	char *src, *dst;
+	bool rwp;
+	int uffd;
+
+	dst = mmap_pmd_aligned(self->pmd_size);
+	if (dst == MAP_FAILED)
+		SKIP(return, "Could not mmap aligned dst\n");
+
+	src = alloc_fill_swap_thp(self->pmd_size, self->pagemap_fd, seed);
+	if (src == MAP_FAILED) {
+		munmap(dst, self->pmd_size);
+		SKIP(return, "Could not create swapped THP\n");
+	}
+	if ((uintptr_t)src & (self->pmd_size - 1)) {
+		munmap(src, self->pmd_size);
+		munmap(dst, self->pmd_size);
+		SKIP(return, "src not PMD-aligned\n");
+	}
+
+	uffd = syscall(__NR_userfaultfd, O_CLOEXEC | O_NONBLOCK);
+	if (uffd < 0) {
+		munmap(src, self->pmd_size);
+		munmap(dst, self->pmd_size);
+		SKIP(return, "userfaultfd unavailable\n");
+	}
+
+	api.api = UFFD_API;
+	api.features = UFFD_FEATURE_MOVE | UFFD_FEATURE_RWP;
+	if (ioctl(uffd, UFFDIO_API, &api) ||
+	    !(api.features & UFFD_FEATURE_MOVE)) {
+		close(uffd);
+		munmap(src, self->pmd_size);
+		munmap(dst, self->pmd_size);
+		SKIP(return, "UFFD_FEATURE_MOVE unsupported\n");
+	}
+	rwp = api.features & UFFD_FEATURE_RWP;
+
+	reg.range.start = (unsigned long)dst;
+	reg.range.len = self->pmd_size;
+	reg.mode = UFFDIO_REGISTER_MODE_MISSING |
+		   (rwp ? UFFDIO_REGISTER_MODE_RWP : 0);
+	if (ioctl(uffd, UFFDIO_REGISTER, &reg)) {
+		close(uffd);
+		munmap(src, self->pmd_size);
+		munmap(dst, self->pmd_size);
+		SKIP(return, "UFFDIO_REGISTER failed\n");
+	}
+
+	move.dst = (unsigned long)dst;
+	move.src = (unsigned long)src;
+	move.len = self->pmd_size;
+	if (ioctl(uffd, UFFDIO_MOVE, &move)) {
+		int saved_errno = errno;
+
+		close(uffd);
+		munmap(src, self->pmd_size);
+		munmap(dst, self->pmd_size);
+		ASSERT_EQ(saved_errno, 0);
+	}
+	ASSERT_EQ(move.move, self->pmd_size);
+
+	/* dst inherits the PMD swap entry; reading it must restore the data. */
+	ASSERT_TRUE(check_swapped(self->pagemap_fd, dst, self->pmd_size));
+	if (rwp) {
+		ASSERT_TRUE(expect_rwp_fault(uffd, dst, self->pmd_size,
+					     pattern_byte(seed, 0), false)) {
+			close(uffd);
+			munmap(src, self->pmd_size);
+			munmap(dst, self->pmd_size);
+		}
+	}
+	ASSERT_TRUE(verify_pattern(dst, self->pmd_size, seed));
+	if (self->zswap_enabled) {
+		TH_LOG("zswap is enabled, so PMD mapping is not checked");
+	} else {
+		/* The whole-PMD path must reinstate a THP, not 512 PTE folios. */
+		ASSERT_TRUE(check_huge_anon(dst, 1, self->pmd_size));
+	}
+
+	close(uffd);
+	munmap(src, self->pmd_size);
+	munmap(dst, self->pmd_size);
+}
+
+/*
+ * Move a swapped PMD entry to a new address, fault in, verify data.
+ * Exercises move_huge_pmd() and move_soft_dirty_pmd().
+ */
+TEST_F(pmd_swap, mremap)
+{
+	unsigned int seed = self->seed;
+	char *mem, *new_mem;
+
+	mem = alloc_fill_swap_thp(self->pmd_size, self->pagemap_fd, seed);
+	if (mem == MAP_FAILED)
+		SKIP(return, "Could not create swapped THP\n");
+
+	new_mem = mremap(mem, self->pmd_size, self->pmd_size, MREMAP_MAYMOVE);
+	if (new_mem == MAP_FAILED) {
+		munmap(mem, self->pmd_size);
+		ASSERT_NE(new_mem, MAP_FAILED);
+	}
+
+	ASSERT_TRUE(verify_pattern(new_mem, self->pmd_size, seed));
+
+	munmap(new_mem, self->pmd_size);
+}
+
+/*
+ * Read /proc/self/pagemap on a PMD swap entry. Exercises the pagemap
+ * PMD walker which must handle PMD swap entries without trying to
+ * convert them to a page via softleaf_to_page().
+ */
+TEST_F(pmd_swap, pagemap)
+{
+	char *mem;
+	uint64_t entry;
+	unsigned long off;
+
+	mem = alloc_fill_swap_thp(self->pmd_size, self->pagemap_fd, self->seed);
+	if (mem == MAP_FAILED)
+		SKIP(return, "Could not create swapped THP\n");
+
+	for (off = 0; off < self->pmd_size; off += getpagesize()) {
+		entry = pagemap_get_entry(self->pagemap_fd, mem + off);
+		/* Bit 62 = swapped */
+		ASSERT_TRUE(entry & (1ULL << 62));
+	}
+
+	munmap(mem, self->pmd_size);
+}
+
+/*
+ * mincore() on a swapped-out PMD-mapped THP must handle the non-present PMD
+ * entry in place. The call must not fault the PMD back in or split the entry.
+ */
+TEST_F(pmd_swap, mincore)
+{
+	unsigned long pages = self->pmd_size / getpagesize();
+	unsigned char *vec;
+	char *mem;
+
+	mem = alloc_fill_swap_thp(self->pmd_size, self->pagemap_fd, self->seed);
+	if (mem == MAP_FAILED)
+		SKIP(return, "Could not create swapped THP\n");
+
+	vec = calloc(pages, sizeof(*vec));
+	ASSERT_NE(vec, NULL) {
+		munmap(mem, self->pmd_size);
+	}
+
+	ASSERT_EQ(mincore(mem, self->pmd_size, vec), 0) {
+		free(vec);
+		munmap(mem, self->pmd_size);
+	}
+	ASSERT_TRUE(check_swapped(self->pagemap_fd, mem, self->pmd_size)) {
+		free(vec);
+		munmap(mem, self->pmd_size);
+	}
+
+	free(vec);
+	munmap(mem, self->pmd_size);
+}
+
+/*
+ * MADV_FREE on a swapped-out PMD must free the swap slots and clear the
+ * entry. After the call, pagemap must no longer report the pages as
+ * swapped, and accessing the region must yield zero pages.
+ */
+TEST_F(pmd_swap, madvise_free)
+{
+	char *mem;
+	unsigned long i;
+
+	mem = alloc_fill_swap_thp(self->pmd_size, self->pagemap_fd, self->seed);
+	if (mem == MAP_FAILED)
+		SKIP(return, "Could not create swapped THP\n");
+
+	ASSERT_TRUE(check_swapped(self->pagemap_fd, mem, self->pmd_size));
+	ASSERT_EQ(madvise(mem, self->pmd_size, MADV_FREE), 0);
+	ASSERT_FALSE(check_swapped(self->pagemap_fd, mem, self->pmd_size));
+
+	for (i = 0; i < self->pmd_size; i += getpagesize())
+		ASSERT_EQ(mem[i], 0);
+
+	munmap(mem, self->pmd_size);
+}
+
+/*
+ * MADV_WILLNEED on a swapped-out PMD-mapped THP may schedule PMD-order
+ * swapin I/O, find the PMD-sized folio already resident in the swap cache,
+ * or split to the PTE path when zswap has per-page state for the range.
+ */
+TEST_F(pmd_swap, madvise_willneed)
+{
+	char *mem;
+
+	mem = alloc_fill_swap_thp(self->pmd_size, self->pagemap_fd, self->seed);
+	if (mem == MAP_FAILED)
+		SKIP(return, "Could not create swapped THP\n");
+
+	ASSERT_EQ(madvise(mem, self->pmd_size, MADV_WILLNEED), 0);
+	ASSERT_TRUE(check_swapped(self->pagemap_fd, mem, self->pmd_size));
+
+	/* First touch faults the data back in. */
+	ASSERT_TRUE(verify_pattern(mem, self->pmd_size, self->seed));
+
+	if (self->zswap_enabled)
+		TH_LOG("zswap is enabled, so PMD mapping is not checked");
+	else
+		ASSERT_TRUE(check_huge_anon(mem, 1, self->pmd_size));
+
+	munmap(mem, self->pmd_size);
+}
+
+/*
+ * swapoff requires a dedicated swap device path. Use a separate fixture
+ * that picks the device up from the PMD_SWAP_DEVICE environment variable
+ * and skips when unset.
+ */
+FIXTURE(pmd_swap_swapoff)
+{
+	unsigned long pmd_size;
+	int pagemap_fd;
+	const char *swap_dev;
+	unsigned int seed;
+	bool zswap_enabled;
+};
+
+FIXTURE_SETUP(pmd_swap_swapoff)
+{
+	self->pagemap_fd = -1;
+	self->swap_dev = getenv("PMD_SWAP_DEVICE");
+	if (!self->swap_dev)
+		SKIP(return, "PMD_SWAP_DEVICE env var not set\n");
+
+	self->pmd_size = read_pmd_pagesize();
+	if (!self->pmd_size)
+		SKIP(return, "Cannot determine PMD size\n");
+
+	self->pagemap_fd = open("/proc/self/pagemap", O_RDONLY);
+	if (self->pagemap_fd < 0)
+		SKIP(return, "Cannot open /proc/self/pagemap\n");
+
+	if (!swap_available(self->pagemap_fd))
+		SKIP(return, "Swap not available or not working\n");
+
+	self->seed = random_seed();
+	self->zswap_enabled = zswap_enabled();
+}
+
+FIXTURE_TEARDOWN(pmd_swap_swapoff)
+{
+	if (self->pagemap_fd >= 0)
+		close(self->pagemap_fd);
+}
+
+/*
+ * Swap out a THP, then turn off swap. Verify data is intact. When zswap is
+ * not active, the PMD-order swapoff path should preserve the huge mapping.
+ */
+TEST_F(pmd_swap_swapoff, basic)
+{
+	unsigned int seed = self->seed;
+	char *mem;
+	int uffd, ret, err;
+
+	mem = alloc_fill_swap_thp(self->pmd_size, self->pagemap_fd, seed);
+	if (mem == MAP_FAILED)
+		SKIP(return, "Could not create swapped THP\n");
+	uffd = register_rwp(mem, self->pmd_size, true);
+
+	ret = swapoff(self->swap_dev);
+	err = errno;
+	ASSERT_EQ(ret, 0) {
+		TH_LOG("swapoff(%s) failed: %s", self->swap_dev, strerror(err));
+		if (uffd >= 0)
+			close(uffd);
+		munmap(mem, self->pmd_size);
+	}
+
+	/*
+	 * Check the PMD residency before touching the memory.  If we read
+	 * first, a bug that left a PMD swap entry in place after swapoff
+	 * would silently trigger do_huge_pmd_swap_page() and reinstall a
+	 * PMD mapping, masking the regression.
+	 */
+	if (self->zswap_enabled) {
+		TH_LOG("zswap is enabled, so PMD mapping is not checked");
+	} else {
+		ASSERT_TRUE(check_huge_anon(mem, 1, self->pmd_size)) {
+			swapon(self->swap_dev, 0);
+			if (uffd >= 0)
+				close(uffd);
+			munmap(mem, self->pmd_size);
+		}
+	}
+	if (uffd >= 0) {
+		ASSERT_TRUE(expect_rwp_fault(uffd, mem, self->pmd_size,
+					     pattern_byte(seed, 0), false)) {
+			swapon(self->swap_dev, 0);
+			close(uffd);
+			munmap(mem, self->pmd_size);
+		}
+	}
+
+	ASSERT_TRUE(verify_pattern(mem, self->pmd_size, seed)) {
+		swapon(self->swap_dev, 0);
+		if (uffd >= 0)
+			close(uffd);
+		munmap(mem, self->pmd_size);
+	}
+
+	ret = swapon(self->swap_dev, 0);
+	err = errno;
+	ASSERT_EQ(ret, 0) {
+		TH_LOG("swapon(%s) failed: %s", self->swap_dev, strerror(err));
+		if (uffd >= 0)
+			close(uffd);
+		munmap(mem, self->pmd_size);
+	}
+
+	if (uffd >= 0)
+		close(uffd);
+	munmap(mem, self->pmd_size);
+}
+
+TEST_HARNESS_MAIN
diff --git a/tools/testing/selftests/mm/run_vmtests.sh b/tools/testing/selftests/mm/run_vmtests.sh
index 687d115e3bd807..0a860c4852f2be 100755
--- a/tools/testing/selftests/mm/run_vmtests.sh
+++ b/tools/testing/selftests/mm/run_vmtests.sh
@@ -69,6 +69,8 @@ separated by spaces:
 	test pagemap_scan IOCTL
 - pfnmap
 	tests for VM_PFNMAP handling
+- pmd_swap
+	tests for PMD-level swap entries
 - process_madv
 	test for process_madv
 - cow
@@ -399,6 +401,8 @@ CATEGORY="pagemap" run_test ./pagemap_ioctl
 
 CATEGORY="pfnmap" run_test ./pfnmap
 
+CATEGORY="pmd_swap" run_test ./pmd_swap
+
 # COW tests
 CATEGORY="cow" run_test ./cow
 

3/2 2026/07/23 17:01 llm patch-evaluator
0m Model:
gemini-3.1-pro-preview

Args:
null

Results:
null

Tokens:
							input: 39995
							output: 20
							thoughts: 431
						

Thoughts:
**Patch Analysis: PMD-Level Swap Entries**

Okay, let's dive into this patch series. My initial assessment is that we need to determine if this introduces functional changes significant enough to warrant fuzzing. The key here is the introduction of PMD-level swap entries, or THP_SWAP. Looking at the file modifications – `mm/huge_memory.c`, `mm/swapfile.c`, `mm/madvise.c`, `mm/rmap.c`, `mm/memory.c`, and `mm/zswap.c` – it's clear this touches core memory management subsystems, specifically transparent hugepages and swap mechanisms. This is a big deal; this is definitely a functional change and absolutely needs fuzzing.

Now, let's identify the specific functions to target. Based on the changes, functions like `do_huge_pmd_swap_page`, `set_pmd_swap_entry`, `swap_pmd_cache_lookup`, `unuse_pmd`, `unuse_pmd_entry`, `swap_extend_table_alloc`, `swap_dup_entries_direct`, `move_swap_pmd`, and `mincore_pmd_swap` are immediately flagged as potential fuzzing targets.

Alright, let's confirm these functions are present in the patch. *Checks patch* Yep, they're all there. Good.

Next, we need the required configuration options. The patch uses `#ifdef CONFIG_THP_SWAP`, so `CONFIG_THP_SWAP` is obviously required. It also references `CONFIG_ARCH_HAS_PMD_SOFTLEAVES`, `CONFIG_USERFAULTFD`, and `CONFIG_ZSWAP`.

Finally, just to be thorough, I'll use `default_api:grepper` to double-check the `Kconfig` files and verify the presence and purpose of `THP_SWAP`. We need to be absolutely certain we're clear on any dependencies or potential configurations that might influence fuzzing effectiveness.



4/2 2026/07/23 17:01 tool grepper
0m Args:
{
  "Expression": "config THP_SWAP"
}

Results:
{
  "Output": "mm/Kconfig=966=endchoice\nmm/Kconfig-967-\nmm/Kconfig:968:config THP_SWAP\nmm/Kconfig-969-\tdef_bool y\n"
}

5/2 2026/07/23 17:02 llm patch-evaluator
0m Model:
gemini-3.1-pro-preview

Args:
null

Results:
null

Tokens:
							input: 40948
							output: 32
							thoughts: 36
						

Thoughts:
Here's my summary, as though I'm thinking through this myself:

**Investigating the Dependency of THP_SWAP**

Okay, so `THP_SWAP` is defined as a boolean, `def_bool y`, which means it's enabled by default, but there's obviously a dependency at play here. I need to understand what's controlling that enablement. Right, let's dive into the `mm/Kconfig` file. I'll jump to around line 968 and see what the surrounding configuration options reveal. Hopefully, the dependency should be clear from that section of the Kconfig file. I'm expecting to find a conditional statement or some other configuration option that must be met for `THP_SWAP` to be truly active. This is a crucial first step in understanding the overall memory management strategy, and understanding dependencies is foundational. Let's get to the bottom of this.



6/2 2026/07/23 17:02 tool read-file
0m Args:
{
  "File": "mm/Kconfig",
  "FirstLine": 960,
  "LineCount": 20
}

Results:
{
  "Contents": " 960:\t\t\tbool \"advise\"\n 961:\t\thelp\n 962:\t\t  Enable hugepage allocation for the tmpfs mount exclusively when\n 963:\t\t  applications supply the madvise(MADV_HUGEPAGE) hint.\n 964:\t\t  This ensures that hugepages are used only in response to explicit\n 965:\t\t  requests from applications.\n 966:\tendchoice\n 967:\t\n 968:\tconfig THP_SWAP\n 969:\t\tdef_bool y\n 970:\t\tdepends on ARCH_WANTS_THP_SWAP \u0026\u0026 SWAP \u0026\u0026 64BIT\n 971:\t\thelp\n 972:\t\t  Swap transparent huge pages in one piece, without splitting.\n 973:\t\t  XXX: For now, swap cluster backing transparent huge page\n 974:\t\t  will be split after swapout.\n 975:\t\n 976:\t\t  For selection by architectures with reasonable THP sizes.\n 977:\t\n 978:\tconfig NO_PAGE_MAPCOUNT\n 979:\t\tbool \"No per-page mapcount (EXPERIMENTAL)\"\n"
}

7/2 2026/07/23 17:02 llm patch-evaluator
0m Model:
gemini-3.1-pro-preview

Args:
null

Results:
null

Tokens:
							input: 41512
							output: 25
							thoughts: 111
						

Thoughts:
**Considering Transparent Hugepages and Swap Configuration**

Okay, so I'm diving into this and it really hinges on a specific set of conditions: `ARCH_WANTS_THP_SWAP && SWAP && 64BIT`. That's the baseline. Thinking about how we can manage this most effectively, it seems like we could just directly specify `THP_SWAP` within `EnableConfigs`. That streamlines things.

Now, clearly, `TRANSPARENT_HUGEPAGE` is essential for any of this to be relevant. It goes without saying. Also, I'm thinking about the implications of `USERFAULTFD`. The presence of `move_swap_pmd` being conditionally compiled under `#ifdef CONFIG_USERFAULTFD` is interesting. I'll need to remember that.

And, of course, `ZSWAP` is in play here. That’s another piece of the puzzle.

Finally, I should probably double-check the implications of `CONFIG_ARCH_HAS_PMD_SOFTLEAVES`. Gotta see how that factors in, too.



8/2 2026/07/23 17:02 tool grepper
0m Args:
{
  "Expression": "config ARCH_HAS_PMD_SOFTLEAVES"
}

Results:
{
  "Output": "mm/Kconfig=689=config ARCH_ENABLE_HUGEPAGE_MIGRATION\n--\nmm/Kconfig-691-\nmm/Kconfig:692:config ARCH_HAS_PMD_SOFTLEAVES\nmm/Kconfig-693-\tbool\n"
}

9/2 2026/07/23 17:02 llm patch-evaluator
0m Model:
gemini-3.1-pro-preview

Args:
null

Results:
null

Tokens:
							input: 41957
							output: 189
							thoughts: 437
						

Thoughts:
**Analyzing the PMD Swap Entry Implementation**

Okay, so this patch introduces a boolean selected by various architectures, which immediately tells me this is likely a significant architectural change related to how we handle swapped page tables at the PMD level.

My primary focus needs to be on the core functions. Let's see... I need to concentrate on `do_huge_pmd_swap_page`, `set_pmd_swap_entry`, `swap_pmd_cache_lookup`, `unuse_pmd`, `unuse_pmd_entry`, `move_swap_pmd`, `mincore_pmd_swap`, and `swap_dup_entries_direct`, and `swap_extend_table_alloc` which are all the important parts to examine.

Now, let's just make sure these functions aren't being called on the hot path, to assess the impact. So, `do_huge_pmd_swap_page` looks like it's page fault related, triggered when there's a PMD swap entry. `set_pmd_swap_entry` is hit during a swap out of PMD. `swap_pmd_cache_lookup` is involved in swap operations, that's important.  `unuse_pmd` and `unuse_pmd_entry` are called during swapoff, which is crucial for resource management. Then, `move_swap_pmd` is interesting because it's called during UFFDIO_MOVE and `mincore_pmd_swap` during mincore. Alright, these look like the right symbols to prioritize.

Based on the nature of these functions, I think it's time to create a fuzzing plan.

The fuzzing plan is as follows:
WorthFuzzing: true
FocusSymbols:
- do_huge_pmd_swap_page
- set_pmd_swap_entry
- swap_pmd_cache_lookup
- unuse_pmd
- unuse_pmd_entry
- move_swap_pmd
- mincore_pmd_swap

We must keep in mind these enabling configurations:
EnableConfigs:
- THP_SWAP
- TRANSPARENT_HUGEPAGE
- USERFAULTFD
- ZSWAP

Ultimately, this patch is introducing PMD-level swap entries, which significantly alters core memory management, including how we handle page faults, swap-in/out, swapoff, mincore, and userfaultfd. This is a complex functional change ripe for memory corruption or logic errors. Fuzzing is essential to make sure we don't break anything.



10/2 2026/07/23 17:02 tool set-results
0m Args:
{
  "EnableConfigs": [
    "THP_SWAP",
    "TRANSPARENT_HUGEPAGE",
    "USERFAULTFD",
    "ZSWAP"
  ],
  "FocusSymbols": [
    "do_huge_pmd_swap_page",
    "set_pmd_swap_entry",
    "swap_pmd_cache_lookup",
    "unuse_pmd",
    "unuse_pmd_entry",
    "move_swap_pmd",
    "mincore_pmd_swap"
  ],
  "Reasoning": "The patch introduces PMD-level swap entries, modifying core memory management logic including page faults, swap out/in, swapoff, mincore, and userfaultfd. This is a complex functional change that should be heavily fuzzed to catch potential memory corruption or logic errors.",
  "WorthFuzzing": true
}

Results:
{
  "EnableConfigs": [
    "THP_SWAP",
    "TRANSPARENT_HUGEPAGE",
    "USERFAULTFD",
    "ZSWAP"
  ],
  "FocusSymbols": [
    "do_huge_pmd_swap_page",
    "set_pmd_swap_entry",
    "swap_pmd_cache_lookup",
    "unuse_pmd",
    "unuse_pmd_entry",
    "move_swap_pmd",
    "mincore_pmd_swap"
  ],
  "Reasoning": "The patch introduces PMD-level swap entries, modifying core memory management logic including page faults, swap out/in, swapoff, mincore, and userfaultfd. This is a complex functional change that should be heavily fuzzed to catch potential memory corruption or logic errors.",
  "WorthFuzzing": true
}

LLM Calls Summary:
Total Calls Total Tokens Avg Tokens Total Duration (Seconds) Avg Duration (Seconds)
Tool Calls Summary:
Total Calls Total Duration (Seconds) Avg Duration (Seconds)