From: sashiko-bot@kernel.org
To: "Usama Arif" <usama.arif@linux.dev>,
"Andrew Morton" <akpm@linux-foundation.org>,
david@kernel.org, chrisl@kernel.org, kasong@tencent.com,
ljs@kernel.org, ziy@nvidia.com, linux-mm@kvack.org
Cc: chenhuacai@kernel.org, ying.huang@linux.alibaba.com,
"Baoquan He" <baoquan.he@linux.dev>,
willy@infradead.org, youngjun.park@lge.com, hannes@cmpxchg.org,
riel@surriel.com, shakeel.butt@linux.dev, alex@ghiti.fr,
kas@kernel.org, baohua@kernel.org, dev.jain@arm.com,
baolin.wang@linux.alibaba.com,
"Nico Pache" <nico.pache@linux.dev>,
"Liam R. Howlett" <liam@infradead.org>,
ryan.roberts@arm.com, "Vlastimil Babka" <vbabka@kernel.org>,
lance.yang@linux.dev, nphamcs@gmail.com,
shikemeng@huaweicloud.com, yosry@kernel.org, qi.zheng@linux.dev,
luizcap@redhat.com, kernel-team@meta.com
Subject: Re: [PATCH v9 30/32] mm: handle PMD swap entry faults on swap-in
Date: Fri, 06 Nov 2026 18:43:11 +0000 [thread overview]
Message-ID: <sashiko-outbox-202085@kernel.org> (raw)
In-Reply-To: <20261105183940.2236717-31-usama.arif@linux.dev>
Thank you for your contribution! Sashiko AI review found 3 potential
issues to consider:
- [Critical] mm: use-after-free in do_huge_pmd_swap_page() from
unlocking PTL first
- [Critical] mm: unmapped PMD swap-cache folio in
do_huge_pmd_swap_page() corrupts rmap
- [Medium] mm: missing PTE fault fallback after PMD split in
do_huge_pmd_swap_page()
--
commit 651cc7048660407a18f57e406abe979d3a9ccbfe
Author: Usama Arif <usama.arif@linux.dev>
mm: handle PMD swap entry faults on swap-in
This commit adds do_huge_pmd_swap_page() and hooks it into
__handle_mm_fault() to fault in PMD swap entries at PMD granularity when a
PMD-sized folio is available, or split the PMD swap entry and fall back to
PTE swap-in otherwise.
> diff --git a/mm/huge_memory.c b/mm/huge_memory.c
> index e02a885ea1d0b..b8d61cdf8cbfb 100644
> --- a/mm/huge_memory.c
> +++ b/mm/huge_memory.c
[ ... ]
> @@ -2425,6 +2426,266 @@ 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)
> +{
[ ... ]
> + if (!thp_vma_allowable_order(vma, vma->vm_flags, TVA_PAGEFAULT,
> + HPAGE_PMD_ORDER)) {
> + __split_huge_pmd(vma, vmf->pmd, haddr);
> + return 0;
> + }
> +
> + swp_entry = entry;
> +
> + /* Prevent swapoff from happening to us. */
> + si = get_swap_device(swp_entry);
> + if (IS_ERR_OR_NULL(si)) {
> + if (IS_ERR(si))
> + return VM_FAULT_SIGBUS;
> + 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;
[ ... ]
> + /* 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;
> + }
[ ... ]
> + 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);
[Severity: Critical]
Can releasing vmf->ptl before calling folio_unlock(folio) in
do_huge_pmd_swap_page() lead to a use-after-free on folio?
do_huge_pmd_swap_page() holds a single reference to folio from
swap_pmd_cache_lookup() or swapin_sync(), and set_pmd_at() transfers that
reference to the PMD mapping. When should_try_to_free_swap() is true (such
as on SWP_SYNCHRONOUS_IO swap devices, exclusive write faults, or VM_LOCKED
VMAs), folio_free_swap(folio) drops the swap-cache references as well.
At that point the faulting thread no longer holds its own reference on
folio; only vmf->ptl prevents the PMD mapping reference from going away. In
mm/memory.c:do_swap_page(), folio_unlock(folio) is called before
pte_unmap_unlock(vmf->pte, vmf->ptl):
if (should_try_to_free_swap(si, folio, vma, exclusive, vmf->flags))
folio_free_swap(folio);
folio_unlock(folio);
...
pte_unmap_unlock(vmf->pte, vmf->ptl);
With spin_unlock(vmf->ptl) happening first here, a racing MADV_DONTNEED or
munmap() in zap_huge_pmd() (or move_folios_to_lru() dropping a transient LRU
isolation reference) can unmap and free the folio while PG_locked is still
set before folio_unlock(folio) runs on this CPU. Should folio_unlock(folio)
be called before spin_unlock(vmf->ptl)?
> +
> + /*
> + * 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;
> +
> +unlock_split_fallback:
> + /*
> + * PTE fallback cannot add a single-page rmap to a PMD-sized folio that
> + * has never been mapped: do_swap_page() would hand the whole folio to
> + * folio_add_new_anon_rmap() while installing one PTE. Nor can it do
> + * anything useful with a folio that failed to read. Remove either from
> + * the swap cache so each slot is read back into its own order-0 folio.
> + * An uptodate anon swap-cache folio can be mapped one PTE at a time and
> + * must stay cached, so that any poisoned subpage stays visible to
> + * do_swap_page(). This mirrors unuse_pmd_entry().
> + */
> + if (folio_matches_swap_entry(folio, swp_entry) &&
> + (!folio_test_uptodate(folio) || !folio_test_anon(folio)))
> + swap_cache_del_folio(folio);
> + folio_unlock(folio);
> + folio_put(folio);
[Severity: Critical]
What happens if a newly allocated PMD-sized swap-cache folio
(!folio_test_anon(folio) or !folio_test_uptodate(folio)) exits via out_page
or out_release instead of unlock_split_fallback?
There appear to be a few paths where an unmapped PMD-sized folio remains in
the swap cache after the PMD swap entry is split or zapped:
1. If the PMD changed while swapin_sync() or folio_lock_or_retry() was
in progress (for example, a racing thread split the PMD at
split_fallback, or mprotect/madvise split the PMD), the !pmd_same()
check jumps to out_page without deleting the !folio_test_anon(folio)
(or !folio_test_uptodate(folio)) folio from the swap cache.
2. If the PMD changed because a racing zap_huge_pmd() or
madvise_free_huge_pmd() zapped the swap entry while this thread held the
folio lock, swap_put_entries_direct() -> __try_to_reclaim_swap() fails
folio_trylock(folio). Unlike mm/memory.c:do_swap_page(), which calls
folio_free_swap(folio) under out_page before unlocking the folio,
out_page here does not call folio_free_swap(folio), leaving the folio
and swap slots stranded in the swap cache.
3. On asynchronous swap devices, swapin_sync() inserts a locked
!folio_test_anon(folio) PMD-sized folio into the swap cache, and
folio_lock_or_retry() drops the fault lock and jumps to out_release with
VM_FAULT_RETRY (and MADV_WILLNEED's swapin_pmd_swap_entry() similarly
populates an unmapped PMD-sized folio). If the PMD is subsequently split
before the folio is mapped at PMD level (including via the
!thp_vma_allowable_order() check at the top of do_huge_pmd_swap_page(),
which splits the PMD without checking the swap cache), the unmapped
PMD-sized folio stays in the swap cache.
When a subsequent PTE fault enters mm/memory.c:do_swap_page() on the
HPAGE_PMD_NR contiguous split PTE swap entries, the large-folio PTE batching
logic batches all nr_pages = HPAGE_PMD_NR PTEs. Because nr_pages equals
folio_nr_pages(folio), the check in do_swap_page():
if (!folio_test_anon(folio) && folio_test_large(folio) &&
nr_pages != folio_nr_pages(folio)) {
if (!WARN_ON_ONCE(folio_test_dirty(folio)))
swap_cache_del_folio(folio);
goto out_nomap;
}
does not trigger, and do_swap_page() passes the PMD-mappable folio to
folio_add_new_anon_rmap() while installing PTEs. Since
folio_add_new_anon_rmap() initializes any folio_test_pmd_mappable(folio)
for a PMD mapping (_entire_mapcount = 0, _large_mapcount = 1,
_nr_pages_mapped = ENTIRELY_MAPPED, and PageAnonExclusive only on subpage
0), wouldn't PTE-mapping it corrupt the rmap/mapcounts and PageAnonExclusive
state across subpages?
> diff --git a/mm/memory.c b/mm/memory.c
> index 47bba4695d855..e71c590113e07 100644
> --- a/mm/memory.c
> +++ b/mm/memory.c
[ ... ]
> @@ -6778,6 +6747,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)) {
[Severity: Medium]
When do_huge_pmd_swap_page() splits the PMD (either at the
!thp_vma_allowable_order() check, at split_fallback, or inside wp_huge_pmd()
when a non-exclusive swapped-in THP must be split into PTEs for COW), should
__handle_mm_fault() fall through to the fallback label and call
handle_pte_fault(&vmf) rather than returning 0 (or masking out
VM_FAULT_FALLBACK)?
Other PMD handlers in __handle_mm_fault() such as create_huge_pmd() and
wp_huge_pmd() return VM_FAULT_FALLBACK so __handle_mm_fault() jumps to
fallback: and resolves the PTE fault in the same handle_mm_fault() call.
By returning 0 after splitting the PMD swap entry (or clearing
VM_FAULT_FALLBACK when wp_huge_pmd() splits the read-only PMD into read-only
PTEs without copying), callers of handle_mm_fault() that do not loop on 0 -
such as fixup_user_fault(), fault_in_safe_writeable(), and
user_event_mm_fault_in() - see a successful return while the target PTE is
still a non-present swap entry or read-only PTE, causing subsequent
pagefault-disabled user accesses to fail with -EFAULT.
--
Sashiko AI review · https://sashiko.dev/#/patchset/20261105183940.2236717-1-usama.arif@linux.dev?part=30
next prev parent reply other threads:[~2026-11-06 18:43 UTC|newest]
Thread overview: 43+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-11-05 18:38 [PATCH v9 00/32] mm: PMD-level swap entries for anonymous THPs Usama Arif
2026-11-05 18:38 ` [PATCH v9 01/32] mm: rename pmd_to_softleaf_folio() to pmd_softleaf_to_folio() Usama Arif
2026-11-05 18:38 ` [PATCH v9 02/32] arm64: mm: add PMD swap-exclusive helpers Usama Arif
2026-11-05 18:38 ` [PATCH v9 03/32] loongarch: " Usama Arif
2026-11-05 18:38 ` [PATCH v9 04/32] powerpc: " Usama Arif
2026-11-05 18:38 ` [PATCH v9 05/32] riscv: " Usama Arif
2026-11-05 18:38 ` [PATCH v9 06/32] s390: " Usama Arif
2026-11-05 18:38 ` [PATCH v9 07/32] x86: " Usama Arif
2026-11-05 18:38 ` [PATCH v9 08/32] mm: recognize PMD swap entries in the softleaf layer Usama Arif
2026-11-05 18:38 ` [PATCH v9 09/32] mm/debug_vm_pgtable: test PMD swap-exclusive helpers Usama Arif
2026-11-05 18:38 ` [PATCH v9 10/32] mm/migrate_device: avoid leaking folio references during THP splitting Usama Arif
2026-11-05 18:38 ` [PATCH v9 11/32] mm: make PMD migration-entry splitting explicit Usama Arif
2026-11-05 18:38 ` [PATCH v9 12/32] mm: split PMD swap entries into PTE swap entries Usama Arif
2026-11-05 18:38 ` [PATCH v9 13/32] mm/swap: allow duplicating a range of " Usama Arif
2026-11-05 18:38 ` [PATCH v9 14/32] mm: handle PMD swap entries in fork path Usama Arif
2026-11-05 18:38 ` [PATCH v9 15/32] mm: swap: share swap-cache allocation error cleanup Usama Arif
2026-11-06 21:10 ` Yosry Ahmed
2026-11-06 21:39 ` Nhat Pham
2026-11-05 18:39 ` [PATCH v9 16/32] mm: zswap: reject high-order swap cache allocations backed by zswap Usama Arif
2026-11-06 16:26 ` Nhat Pham
2026-11-06 21:16 ` Yosry Ahmed
2026-11-05 18:39 ` [PATCH v9 17/32] mm: swap in PMD swap entries as whole THPs during swapoff Usama Arif
2026-11-06 18:43 ` sashiko-bot
2026-11-05 18:39 ` [PATCH v9 18/32] fs/proc: account PMD swap entries in smaps Usama Arif
2026-11-05 18:39 ` [PATCH v9 19/32] mm: handle soft-dirty and uffd-wp on PMD swap entries Usama Arif
2026-11-05 18:39 ` [PATCH v9 20/32] mm/hmm: fault PMD swap entries on demand Usama Arif
2026-11-05 18:39 ` [PATCH v9 21/32] mm: free PMD swap entries in zap_huge_pmd() Usama Arif
2026-11-05 18:39 ` [PATCH v9 22/32] mm/madvise: free PMD swap entries with MADV_FREE Usama Arif
2026-11-05 18:39 ` [PATCH v9 23/32] mm/madvise: skip PMD swap entries for MADV_COLD and MADV_PAGEOUT Usama Arif
2026-11-05 18:39 ` [PATCH v9 24/32] mm/madvise: keep PMD swap entries whole for MADV_GUARD_INSTALL/REMOVE Usama Arif
2026-11-05 18:39 ` [PATCH v9 25/32] mm/mincore: report PMD swap-cache residency Usama Arif
2026-11-05 18:39 ` [PATCH v9 26/32] mm/khugepaged: treat PMD swap entries as mapped THPs Usama Arif
2026-11-05 18:39 ` [PATCH v9 27/32] mm: handle PMD swap entries in MADV_WILLNEED Usama Arif
2026-11-06 18:43 ` sashiko-bot
2026-11-05 18:39 ` [PATCH v9 28/32] mm: handle PMD swap entries in UFFDIO_MOVE Usama Arif
2026-11-06 18:43 ` sashiko-bot
2026-11-05 18:39 ` [PATCH v9 29/32] mm: don't PTE-batch a swap-in over a hardware-poisoned subpage Usama Arif
2026-11-05 18:39 ` [PATCH v9 30/32] mm: handle PMD swap entry faults on swap-in Usama Arif
2026-11-06 18:43 ` sashiko-bot [this message]
2026-11-05 18:39 ` [PATCH v9 31/32] mm: install PMD swap entries on swap-out Usama Arif
2026-11-06 18:43 ` sashiko-bot
2026-11-05 18:39 ` [PATCH v9 32/32] selftests/mm: add PMD swap entry tests Usama Arif
2026-11-06 18:43 ` sashiko-bot
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=sashiko-outbox-202085@kernel.org \
--to=sashiko-bot@kernel.org \
--cc=akpm@linux-foundation.org \
--cc=alex@ghiti.fr \
--cc=baohua@kernel.org \
--cc=baolin.wang@linux.alibaba.com \
--cc=baoquan.he@linux.dev \
--cc=chenhuacai@kernel.org \
--cc=chrisl@kernel.org \
--cc=david@kernel.org \
--cc=dev.jain@arm.com \
--cc=hannes@cmpxchg.org \
--cc=kas@kernel.org \
--cc=kasong@tencent.com \
--cc=kernel-team@meta.com \
--cc=lance.yang@linux.dev \
--cc=liam@infradead.org \
--cc=linux-mm@kvack.org \
--cc=ljs@kernel.org \
--cc=luizcap@redhat.com \
--cc=nico.pache@linux.dev \
--cc=nphamcs@gmail.com \
--cc=qi.zheng@linux.dev \
--cc=riel@surriel.com \
--cc=ryan.roberts@arm.com \
--cc=sashiko-reviews@lists.linux.dev \
--cc=shakeel.butt@linux.dev \
--cc=shikemeng@huaweicloud.com \
--cc=usama.arif@linux.dev \
--cc=vbabka@kernel.org \
--cc=willy@infradead.org \
--cc=ying.huang@linux.alibaba.com \
--cc=yosry@kernel.org \
--cc=youngjun.park@lge.com \
--cc=ziy@nvidia.com \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox