Re: [PATCH] mm/swapfile: drop a large unmapped swap-in folio in unuse_pte()
From: David Hildenbrand (Arm)
Date: Fri Oct 30 2026 - 18:16:08 EST
On 10/30/26 20:11, Mani Tofigh via B4 Relay wrote:
> From: Mani Tofigh <mt4022@xxxxxxxxxxxx>
>
Thanks for the report!
> Since commit f1879e8a0c60 ("mm, swap: never bypass the swap cache even
> for SWP_SYNCHRONOUS_IO"), a fault can read a swapped-out mTHP range into
> one large folio in the swap cache. The folio only becomes anon once
> do_swap_page() maps all of it, so if the faulting PTE changes in the
> meantime the folio stays in the swap cache while not being mapped or
> anon:
>
> Task A (fault) Task B
> do_swap_page()
> /* swaps in large folio F */
> madvise(MADV_DONTNEED)
> /* zaps the faulting PTE */
> /* PTE changed (F not mapped) */
>
> swapoff then maps it, which marks all of its pages as mapped and
> exclusive, and the next fault on one of the other PTEs crashes in
> do_swap_page():
Ugh.
Why is this special to large folios? I guess because a single PTE keeps the
large folio alive, right?
The problem really is that unuse_pte() open-codes whet do_swap_page() would
usually do. And it doesn't handle large folios in this scenario correctly.
What a mess.
I guess we could just teach unuse_pte() to only map a single PTE.
folio_add_new_anon_rmap() assumes the folios is fully mapped. We could add
up to twp folio_remove_rmap_ptes() calls with everything except the page we want
to map.
But reading below ...
>
> [ 6.424485] kernel BUG at mm/memory.c:5195!
> [ 6.424503] Oops: invalid opcode: 0000 [#1] SMP KASAN NOPTI
> [ 6.427654] CPU: 0 UID: 0 PID: 67 Comm: swapmthp Tainted: G W 7.3.0-17087-gd865f2677c44 #1 PREEMPT
> [ 6.432665] RIP: 0010:do_swap_page+0x24eb/0x2fa0
> [ 6.451035] Call Trace:
> [ 6.451567] <TASK>
> [ 6.453841] __handle_mm_fault+0x13b5/0x3b40
> [ 6.458525] handle_mm_fault+0x16f/0x480
> [ 6.461143] do_user_addr_fault+0x312/0xa30
> [ 6.462044] exc_page_fault+0x66/0xc0
> [ 6.462846] asm_exc_page_fault+0x26/0x30
> [ 6.476708] </TASK>
>
> do_swap_page() already drops such a folio from the swap cache when it
> can't map all of it. Do the same in unuse_pte() so that swapoff swaps
> the entries in again. With the fix, nothing is reported.
We indeed have this
if (!folio_test_anon(folio) && folio_test_large(folio) &&
nr_pages != folio_nr_pages(folio)) {
...
}
check.
The nr_pages check makes sense as in your case below it's simply nr_pages == 1.
>
> This bug was identified and confirmed with the help of a deterministic
> hypervisor currently under development. The C reproducer below however
> hits it within a second.
Rings a bell :)
>
> Fixes: f1879e8a0c60 ("mm, swap: never bypass the swap cache even for SWP_SYNCHRONOUS_IO")
> Cc: stable@xxxxxxxxxxxxxxx
> Signed-off-by: Mani Tofigh <mt4022@xxxxxxxxxxxx>
> ---
> This conflicts in context with patch 41/47 of David's v2 series [1],
> which changes the line right below in unuse_pte() for KSM folios. I
> can rebase on top of it once it's applied if needed.
It will likely get applied on top of
git git://git.kernel.org/pub/scm/linux/kernel/git/mm/core.git for-next
Tomorrow and then quickly find its way into next. So if a resend is required,
best to send against that.
>
> [1] https://lore.kernel.org/all/20261029-fixes_core-v2-41-a64ce7f83abb@xxxxxxxxxx/
>
> Reproducer: https://github.com/manitofigh/files/blob/main/linux-repros/swapmthp.c
> ---
> mm/swapfile.c | 12 ++++++++++++
> 1 file changed, 12 insertions(+)
>
> diff --git a/mm/swapfile.c b/mm/swapfile.c
> index 189f04d8838f..b0b75ff16835 100644
> --- a/mm/swapfile.c
> +++ b/mm/swapfile.c
> @@ -2518,6 +2518,18 @@ static int unuse_pte(struct vm_area_struct *vma, pmd_t *pmd,
> if (!folio_matches_swap_entry(folio, entry))
> return 0;
>
> + /*
> + * A large folio that is not anon yet was swapped in by a page fault
> + * that did not map it (yet). It can only be mapped as a whole, by
> + * do_swap_page(). Drop it from the swap cache and swap in small
> + * folios instead.
> + */
> + if (unlikely(folio_test_large(folio) && !folio_test_anon(folio))) {
> + if (!WARN_ON_ONCE(folio_test_dirty(folio)))
> + swap_cache_del_folio(folio);
> + return 0;
> + }
I don't enjoy the additional code duplication, including the comment.
Can you pack the existing handling in do_swap_page() into a helper, pass
nr_pages, and reuse it here?
I guess finding a good name is the main challenge ...
static bool swapcache_folio_mappable(struct folio *folio,
unsigned int nr_pages)
{
if (!folio_test_anon(folio) || !folio_test_large(folio) ||
nr_pages == folio_nr_pages(folio))
return true;
if (!WARN_ON_ONCE(folio_test_dirty(folio)))
swap_cache_del_folio(folio);
return false;
}
Just a thought.
But it does look like the right fix.
--
Cheers,
David