* Re: [PATCH] mm/swapfile: drop a large unmapped swap-in folio in unuse_pte()
2026-10-30 19:11 [PATCH] mm/swapfile: drop a large unmapped swap-in folio in unuse_pte() Mani Tofigh via B4 Relay
@ 2026-10-30 22:15 ` David Hildenbrand (Arm)
2026-10-30 22:54 ` Andrew Morton
2026-10-31 5:54 ` Kairui Song
2 siblings, 0 replies; 6+ messages in thread
From: David Hildenbrand (Arm) @ 2026-10-30 22:15 UTC (permalink / raw)
To: mt4022, Andrew Morton, Chris Li, Kairui Song
Cc: Kemeng Shi, Nhat Pham, Baoquan He, Barry Song, Youngjun Park,
linux-mm, linux-kernel, tz2294, stable
On 10/30/26 20:11, Mani Tofigh via B4 Relay wrote:
> From: Mani Tofigh <mt4022@columbia.edu>
>
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@vger.kernel.org
> Signed-off-by: Mani Tofigh <mt4022@columbia.edu>
> ---
> 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@kernel.org/
>
> 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
^ permalink raw reply [flat|nested] 6+ messages in thread* Re: [PATCH] mm/swapfile: drop a large unmapped swap-in folio in unuse_pte()
2026-10-30 19:11 [PATCH] mm/swapfile: drop a large unmapped swap-in folio in unuse_pte() Mani Tofigh via B4 Relay
2026-10-30 22:15 ` David Hildenbrand (Arm)
@ 2026-10-30 22:54 ` Andrew Morton
2026-10-31 5:54 ` Kairui Song
2026-10-31 9:34 ` David Hildenbrand (Arm)
2026-10-31 5:54 ` Kairui Song
2 siblings, 2 replies; 6+ messages in thread
From: Andrew Morton @ 2026-10-30 22:54 UTC (permalink / raw)
To: Mani Tofigh
Cc: Chris Li, Kairui Song, Kemeng Shi, Nhat Pham, Baoquan He,
Barry Song, Youngjun Park, David Hildenbrand, linux-mm,
linux-kernel, tz2294, stable
On Fri, 30 Oct 2026 15:11:56 -0400 Mani Tofigh <mt4022@columbia.edu> wrote:
> 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():
>
> [ 6.424485] kernel BUG at mm/memory.c:5195!
>
> ...
>
> 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.
>
> 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.
>
> --- 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;
> + }
Let's not WARN here. If it hits and someone reports it, what do we
tell them? "uh, that's working as designed, no action needed".
Also, won't this change cause try_to_unuse() to keep spinning away until
someone kills it? Might be better to return -EBUSY and fail the
swapoff?
^ permalink raw reply [flat|nested] 6+ messages in thread* Re: [PATCH] mm/swapfile: drop a large unmapped swap-in folio in unuse_pte()
2026-10-30 22:54 ` Andrew Morton
@ 2026-10-31 5:54 ` Kairui Song
2026-10-31 9:34 ` David Hildenbrand (Arm)
1 sibling, 0 replies; 6+ messages in thread
From: Kairui Song @ 2026-10-31 5:54 UTC (permalink / raw)
To: Andrew Morton
Cc: Mani Tofigh, Chris Li, Kemeng Shi, Nhat Pham, Baoquan He,
Barry Song, Youngjun Park, David Hildenbrand, linux-mm,
linux-kernel, tz2294, stable
On Sat, Oct 31, 2026 at 6:54 AM Andrew Morton <akpm@linux-foundation.org> wrote:
>
> On Fri, 30 Oct 2026 15:11:56 -0400 Mani Tofigh <mt4022@columbia.edu> wrote:
>
> > 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():
> >
> > [ 6.424485] kernel BUG at mm/memory.c:5195!
> >
> > ...
> >
> > 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.
> >
> > 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.
> >
> > --- 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;
> > + }
>
> Let's not WARN here. If it hits and someone reports it, what do we
> tell them? "uh, that's working as designed, no action needed".
I think we better keep it, a dirty folio in the swap cache must not be
dropped directly or it can lose data: the copy on the swap device is stale,
so a later swapin of a shared entry reads that stale copy back. If it
triggers it means somewhere forgot to write back a dirty folio properly.
We may generalize this check into the swap cache helper though,
something like:
WARN_ON_ONCE(folio_swapped(folio) && folio_test_dirty(folio));
There are some special users so it's kept out side.
> Also, won't this change cause try_to_unuse() to keep spinning away until
> someone kills it? Might be better to return -EBUSY and fail the
> swapoff?
It seems fine? The folio is removed from the swap cache, so the next pass
will go on. But you're right about the dirty and WARN case, it spins, so
maybe:
if (unlikely(folio_test_large(folio) && !folio_test_anon(folio))) {
if (WARN_ON_ONCE(folio_test_dirty(folio)))
return -EBUSY;
swap_cache_del_folio(folio);
return 0;
}
We won't run into this unless there are some other bugs I think.
^ permalink raw reply [flat|nested] 6+ messages in thread* Re: [PATCH] mm/swapfile: drop a large unmapped swap-in folio in unuse_pte()
2026-10-30 22:54 ` Andrew Morton
2026-10-31 5:54 ` Kairui Song
@ 2026-10-31 9:34 ` David Hildenbrand (Arm)
1 sibling, 0 replies; 6+ messages in thread
From: David Hildenbrand (Arm) @ 2026-10-31 9:34 UTC (permalink / raw)
To: Andrew Morton, Mani Tofigh
Cc: Chris Li, Kairui Song, Kemeng Shi, Nhat Pham, Baoquan He,
Barry Song, Youngjun Park, linux-mm, linux-kernel, tz2294,
stable
On 10/30/26 23:54, Andrew Morton wrote:
> On Fri, 30 Oct 2026 15:11:56 -0400 Mani Tofigh <mt4022@columbia.edu> wrote:
>
>> 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():
>>
>> [ 6.424485] kernel BUG at mm/memory.c:5195!
>>
>> ...
>>
>> 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.
>>
>> 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.
>>
>> --- 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;
>> + }
>
> Let's not WARN here. If it hits and someone reports it, what do we
> tell them? "uh, that's working as designed, no action needed".
I thought harder about this, and it just shouldn't happen.
Something that was swapped in and never mapped (!anon) that would be dirty would
reveal a bug elsewhere.
I think we can just turn that into a VM_WARN_ON_ONCE() and still drop the folio.
(there is a reason we never saw this so far in do_swap_page())
--
Cheers,
David
^ permalink raw reply [flat|nested] 6+ messages in thread
* Re: [PATCH] mm/swapfile: drop a large unmapped swap-in folio in unuse_pte()
2026-10-30 19:11 [PATCH] mm/swapfile: drop a large unmapped swap-in folio in unuse_pte() Mani Tofigh via B4 Relay
2026-10-30 22:15 ` David Hildenbrand (Arm)
2026-10-30 22:54 ` Andrew Morton
@ 2026-10-31 5:54 ` Kairui Song
2 siblings, 0 replies; 6+ messages in thread
From: Kairui Song @ 2026-10-31 5:54 UTC (permalink / raw)
To: mt4022
Cc: Andrew Morton, Chris Li, Kemeng Shi, Nhat Pham, Baoquan He,
Barry Song, Youngjun Park, David Hildenbrand, linux-mm,
linux-kernel, tz2294, stable
On Sat, Oct 31, 2026 at 3:12 AM Mani Tofigh via B4 Relay
<devnull+mt4022.columbia.edu@kernel.org> wrote:
>
> From: Mani Tofigh <mt4022@columbia.edu>
>
> 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():
>
> [ 6.424485] kernel BUG at mm/memory.c:5195!
So what's being triggered here is this right?
BUG_ON(folio_test_anon(folio) && PageAnonExclusive(page));
That check is a bit overkill to be a BUG I think, and we have been trying
to remove unnecessary kernel panics. Before f1879e8a0c60 we can occasionally
see large folios got stuck in swap cache and partially mapped (e.g. failed
writeback, see activate_locked: in vmscan.c, much less likely but still
occurs from time to time), but the ones still referenced by swap PTEs don't
have PAE set so we are good.
Now the problem here is unuse_pte() calls folio_add_new_anon_rmap()
while only one sub PTE is mapped. So yeah, that's a problem.
Still it doesn't have to panic the kernel though.
> [ 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.
>
> 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.
Great working catching this!
>
> Fixes: f1879e8a0c60 ("mm, swap: never bypass the swap cache even for SWP_SYNCHRONOUS_IO")
> Cc: stable@vger.kernel.org
> Signed-off-by: Mani Tofigh <mt4022@columbia.edu>
> ---
> 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.
>
> [1] https://lore.kernel.org/all/20261029-fixes_core-v2-41-a64ce7f83abb@kernel.org/
>
> 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;
> + }
Force dropping a large folio for any swapoff doesn't seems very
reasonable.. We really should reuse the common large swapin code between
unuse_pte and the ordinary fault processing code, and there has been quite
some patches just for unuse_pte just because its out of sync with
do_swap_page.
But, having this fix first and backport for stable looks good to me, clean
up and improvement can come later and I'm trying to properly split the
folios when the PTE backing it is partially gone during swapin, that seems
much more reasonable.
Introducing a helper to avoid code duplication from this fix is also a
good idea.
^ permalink raw reply [flat|nested] 6+ messages in thread