Skip to contentSkip to conversation

WARNING in deferred_split_folio

syzbot syzbot 4 participants 17 messages Apr 1

Subsystems (3)
lore
Share & tools
lore

syzbot

author syzbot
#0 lore raw

Hello,

syzbot found the following issue on:

HEAD commit:    cf7c3c02fdd0 Add linux-next specific files for 20260330
git tree:       linux-next
console output: https://syzkaller.appspot.com/x/log.txt?x=154ee46a580000
kernel config:  https://syzkaller.appspot.com/x/.config?x=3944d875fa9bfb67
dashboard link: https://syzkaller.appspot.com/bug?extid=a7067a757858ac8eb085
compiler:       Debian clang version 21.1.8 (++20251221033036+2078da43e25a-1~exp1~20251221153213.50), Debian LLD 21.1.8
syz repro:      https://syzkaller.appspot.com/x/repro.syz?x=12c846ba580000

Downloadable assets:
disk image: https://storage.googleapis.com/syzbot-assets/053d3b49a360/disk-cf7c3c02.raw.xz
vmlinux: https://storage.googleapis.com/syzbot-assets/faabb37d41d0/vmlinux-cf7c3c02.xz
kernel image: https://storage.googleapis.com/syzbot-assets/8d47fe92aaa8/bzImage-cf7c3c02.xz

IMPORTANT: if you fix the issue, please add the following tag to the commit:

Reported-by [email protected]
 free_pages_and_swap_cache+0x2b9/0x490 mm/swap_state.c:401
 __tlb_batch_free_encoded_pages mm/mmu_gather.c:138 [inline]
 tlb_batch_pages_flush mm/mmu_gather.c:151 [inline]
 tlb_flush_mmu_free mm/mmu_gather.c:417 [inline]
 tlb_flush_mmu+0x6d3/0xa30 mm/mmu_gather.c:424
 tlb_finish_mmu+0xf9/0x230 mm/mmu_gather.c:549
 exit_mmap+0x498/0x9e0 mm/mmap.c:1313
 __mmput+0x118/0x430 kernel/fork.c:1177
 exit_mm+0x18e/0x250 kernel/exit.c:581
 do_exit+0x6a2/0x22c0 kernel/exit.c:962
 do_group_exit+0x21b/0x2d0 kernel/exit.c:1116
 __do_sys_exit_group kernel/exit.c:1127 [inline]
 __se_sys_exit_group kernel/exit.c:1125 [inline]
 __x64_sys_exit_group+0x3f/0x40 kernel/exit.c:1125
 x64_sys_call+0x221a/0x2240 arch/x86/include/generated/asm/syscalls_64.h:232
 do_syscall_x64 arch/x86/entry/syscall_64.c:63 [inline]
 do_syscall_64+0x15f/0xf80 arch/x86/entry/syscall_64.c:94
 entry_SYSCALL_64_after_hwframe+0x77/0x7f
------------[ cut here ]------------
1
WARNING: mm/huge_memory.c:4371 at deferred_split_folio+0x974/0xaa0 mm/huge_memory.c:4371, CPU#1: syz.3.1110/10500
Modules linked in:
CPU: 1 UID: 0 PID: 10500 Comm: syz.3.1110 Not tainted syzkaller #0 PREEMPT(full) 
Hardware name: Google Google Compute Engine/Google Compute Engine, BIOS Google 02/12/2026
RIP: 0010:deferred_split_folio+0x974/0xaa0 mm/huge_memory.c:4371
Code: 31 48 8d 65 d8 5b 41 5c 41 5d 41 5e 41 5f 5d e9 c2 67 8d 09 cc e8 8c 73 93 ff 48 89 df 48 c7 c6 20 5b fc 8b e8 dd 2b f5 fe 90 <0f> 0b 90 e9 d4 fe ff ff e8 9f 7a 8a 09 e8 6a 73 93 ff 48 89 df 48
RSP: 0018:ffffc900047ef540 EFLAGS: 00010046
RAX: 1c05fb65cfaab100 RBX: ffffea0001840000 RCX: 0000000080000001
RDX: 0000000000000002 RSI: ffffffff8e4da1c7 RDI: ffff88807d6f9e80
RBP: ffffc900047ef610 R08: ffff8880b87247d3 R09: 1ffff110170e48fa
R10: dffffc0000000000 R11: ffffed10170e48fb R12: ffffea0001840040
R13: 0000000000000000 R14: 0000000000010000 R15: 1ffff920008fdeb0
FS:  00007f32e32a76c0(0000) GS:ffff8881250e8000(0000) knlGS:0000000000000000
CS:  0010 DS: 0000 ES: 0000 CR0: 0000000080050033
CR2: 00007f5825757930 CR3: 0000000034ad8000 CR4: 00000000003526f0
Call Trace:
 <TASK>
 migrate_folio_move mm/migrate.c:1411 [inline]
 migrate_folios_move mm/migrate.c:1740 [inline]
 migrate_pages_batch+0x319f/0x4c40 mm/migrate.c:1996
 migrate_pages_sync mm/migrate.c:2026 [inline]
 migrate_pages+0x1c74/0x2a10 mm/migrate.c:2135
 do_mbind mm/mempolicy.c:1614 [inline]
 kernel_mbind mm/mempolicy.c:1757 [inline]
 __do_sys_mbind mm/mempolicy.c:1831 [inline]
 __se_sys_mbind+0xe89/0x10f0 mm/mempolicy.c:1827
 do_syscall_x64 arch/x86/entry/syscall_64.c:63 [inline]
 do_syscall_64+0x15f/0xf80 arch/x86/entry/syscall_64.c:94
 entry_SYSCALL_64_after_hwframe+0x77/0x7f
RIP: 0033:0x7f32e239c819
Code: ff c3 66 2e 0f 1f 84 00 00 00 00 00 0f 1f 44 00 00 48 89 f8 48 89 f7 48 89 d6 48 89 ca 4d 89 c2 4d 89 c8 4c 8b 4c 24 08 0f 05 <48> 3d 01 f0 ff ff 73 01 c3 48 c7 c1 e8 ff ff ff f7 d8 64 89 01 48
RSP: 002b:00007f32e32a7028 EFLAGS: 00000246 ORIG_RAX: 00000000000000ed
RAX: ffffffffffffffda RBX: 00007f32e2616090 RCX: 00007f32e239c819
RDX: 0000000000000000 RSI: 0000000000800000 RDI: 0000200000001000
RBP: 00007f32e2432c91 R08: 0000000000000000 R09: 0000000000000002
R10: 0000000000000000 R11: 0000000000000246 R12: 0000000000000000
R13: 00007f32e2616128 R14: 00007f32e2616090 R15: 00007ffe30c61cc8
 </TASK>

---
This report is generated by a bot. It may contain errors. See https://goo.gl/tpsmEJ for more information about syzbot. syzbot engineers can be reached at [email protected].

syzbot will keep track of this issue. See:
https://goo.gl/tpsmEJ#status for how to communicate with syzbot.

If the report is already addressed, let syzbot know by replying with:
#syz fix: exact-commit-title

If you want syzbot to run the reproducer, reply with:
#syz test: git://repo/address.git branch-or-commit-hash
If you attach or paste a git patch, syzbot will apply it before testing.

If you want to overwrite report's subsystems, reply with:
#syz set subsystems: new-subsystem
(See the list of subsystem names on the web dashboard)

If the report is a duplicate of another one, reply with:
#syz dup: exact-subject-of-another-report

If you want to undo deduplication, reply with:
#syz undup

author syzbot
#2 lore raw

Forwarded: [PATCH] mm/migrate: fix stale partially_mapped arg to deferred_split_folio()

In reply to syzbot
For archival purposes, forwarding an incoming command email to [email protected], [email protected].

For archival purposes, forwarding an incoming command email to [email protected], [email protected].

***

Subject: [PATCH] mm/migrate: fix stale partially_mapped arg to deferred_split_folio()
Author: [email protected]

#syz test: git://git.kernel.org/pub/scm/linux/kernel/git/next/linux-next.git master

In migrate_folio_move(), src_partially_mapped is sampled from the source folio before move_to_new_folio() is called:

  if (folio_order(src) > 1 &&
      !data_race(list_empty(&src->_deferred_list))) {
          src_deferred_split = true;
          src_partially_mapped = folio_test_partially_mapped(src);
  }

A concurrent thread can unmap pages from the source folio between this read and the actual migration, making the sampled value stale.

After move_to_new_folio() succeeds, __folio_migrate_mapping() has already copied all folio flags including PG_partially_mapped from src to dst. Passing the stale src_partially_mapped=false to deferred_split_folio(dst) while dst already has PG_partially_mapped=true triggers the invariant check in deferred_split_folio():

VM_WARN_ON_FOLIO(folio_test_partially_mapped(folio), folio)

at mm/huge_memory.c:4371, because the argument contradicts the flag already set on the folio.

Fix this by removing the src_partially_mapped variable entirely and reading PG_partially_mapped directly from dst after move_to_new_folio() completes, where it is authoritative and race-free.

---
 mm/migrate.c | 10 +++++-----
 1 file changed, 5 insertions(+), 5 deletions(-)
mm/migrate.c +5 -5
index 05cb408846f2..11236779e910 100644
@@ -1361,7 +1361,6 @@ static int migrate_folio_move(free_folio_t put_new_folio, unsigned long private,
 	int old_page_state = 0;
 	struct anon_vma *anon_vma = NULL;
 	bool src_deferred_split = false;
-	bool src_partially_mapped = false;
 	struct list_head *prev;
 
 	__migrate_folio_extract(dst, &old_page_state, &anon_vma);
@@ -1378,7 +1377,6 @@ static int migrate_folio_move(free_folio_t put_new_folio, unsigned long private,
 	if (folio_order(src) > 1 &&
 	    !data_race(list_empty(&src->_deferred_list))) {
 		src_deferred_split = true;
-		src_partially_mapped = folio_test_partially_mapped(src);
 	}
 
 	rc = move_to_new_folio(dst, src, mode);
@@ -1404,11 +1402,13 @@ static int migrate_folio_move(free_folio_t put_new_folio, unsigned long private,
 	/*
 	 * Requeue the destination folio on the deferred split queue if
 	 * the source was on the queue.  The source is unqueued in
-	 * __folio_migrate_mapping(), so we recorded the state from
-	 * before move_to_new_folio().
+	 * __folio_migrate_mapping(). Read PG_partially_mapped directly from
+	 * dst: move_to_new_folio() copies all flags from src to dst, so dst
+	 * now holds the correct authoritative state. The pre-migration value
+	 * sampled from src is racy and must not be used.
 	 */
 	if (src_deferred_split)
-		deferred_split_folio(dst, src_partially_mapped);
+		deferred_split_folio(dst, folio_test_partially_mapped(dst));
 
 out_unlock_both:
 	folio_unlock(dst);
Signature · 1 line
2.43.0
reviewer
#3 lore raw
In reply to syzbot

+Cc Usama

Quoted 50 lines · syzbot

Hello,

syzbot found the following issue on:

HEAD commit:    cf7c3c02fdd0 Add linux-next specific files for 20260330
git tree:       linux-next
console output: https://syzkaller.appspot.com/x/log.txt?x=154ee46a580000
kernel config:  https://syzkaller.appspot.com/x/.config?x=3944d875fa9bfb67
dashboard link: https://syzkaller.appspot.com/bug?extid=a7067a757858ac8eb085
compiler:       Debian clang version 21.1.8 (++20251221033036+2078da43e25a-1~exp1~20251221153213.50), Debian LLD 21.1.8
syz repro:      https://syzkaller.appspot.com/x/repro.syz?x=12c846ba580000

Downloadable assets:
disk image: https://storage.googleapis.com/syzbot-assets/053d3b49a360/disk-cf7c3c02.raw.xz
vmlinux: https://storage.googleapis.com/syzbot-assets/faabb37d41d0/vmlinux-cf7c3c02.xz
kernel image: https://storage.googleapis.com/syzbot-assets/8d47fe92aaa8/bzImage-cf7c3c02.xz

IMPORTANT: if you fix the issue, please add the following tag to the commit:

Reported-by [email protected]
free_pages_and_swap_cache+0x2b9/0x490 mm/swap_state.c:401
__tlb_batch_free_encoded_pages mm/mmu_gather.c:138 [inline]
tlb_batch_pages_flush mm/mmu_gather.c:151 [inline]
tlb_flush_mmu_free mm/mmu_gather.c:417 [inline]
tlb_flush_mmu+0x6d3/0xa30 mm/mmu_gather.c:424
tlb_finish_mmu+0xf9/0x230 mm/mmu_gather.c:549
exit_mmap+0x498/0x9e0 mm/mmap.c:1313
__mmput+0x118/0x430 kernel/fork.c:1177
exit_mm+0x18e/0x250 kernel/exit.c:581
do_exit+0x6a2/0x22c0 kernel/exit.c:962
do_group_exit+0x21b/0x2d0 kernel/exit.c:1116
__do_sys_exit_group kernel/exit.c:1127 [inline]
__se_sys_exit_group kernel/exit.c:1125 [inline]
__x64_sys_exit_group+0x3f/0x40 kernel/exit.c:1125
x64_sys_call+0x221a/0x2240 arch/x86/include/generated/asm/syscalls_64.h:232
do_syscall_x64 arch/x86/entry/syscall_64.c:63 [inline]
do_syscall_64+0x15f/0xf80 arch/x86/entry/syscall_64.c:94
entry_SYSCALL_64_after_hwframe+0x77/0x7f
------------[ cut here ]------------
1
WARNING: mm/huge_memory.c:4371 at deferred_split_folio+0x974/0xaa0 mm/huge_memory.c:4371, CPU#1: syz.3.1110/10500
Modules linked in:
CPU: 1 UID: 0 PID: 10500 Comm: syz.3.1110 Not tainted syzkaller #0 PREEMPT(full) 
Hardware name: Google Google Compute Engine/Google Compute Engine, BIOS Google 02/12/2026
RIP: 0010:deferred_split_folio+0x974/0xaa0 mm/huge_memory.c:4371
Code: 31 48 8d 65 d8 5b 41 5c 41 5d 41 5e 41 5f 5d e9 c2 67 8d 09 cc e8 8c 73 93 ff 48 89 df 48 c7 c6 20 5b fc 8b e8 dd 2b f5 fe 90 <0f> 0b 90 e9 d4 fe ff ff e8 9f 7a 8a 09 e8 6a 73 93 ff 48 89 df 48
RSP: 0018:ffffc900047ef540 EFLAGS: 00010046
RAX: 1c05fb65cfaab100 RBX: ffffea0001840000 RCX: 0000000080000001
RDX: 0000000000000002 RSI: ffffffff8e4da1c7 RDI: ffff88807d6f9e80
RBP: ffffc900047ef610 R08: ffff8880b87247d3 R09: 1ffff110170e48fa
Quoted 8 lines
R10: dffffc0000000000 R11: ffffed10170e48fb R12: ffffea0001840040
R13: 0000000000000000 R14: 0000000000010000 R15: 1ffff920008fdeb0
FS:  00007f32e32a76c0(0000) GS:ffff8881250e8000(0000) knlGS:0000000000000000
CS:  0010 DS: 0000 ES: 0000 CR0: 0000000080050033
CR2: 00007f5825757930 CR3: 0000000034ad8000 CR4: 00000000003526f0
Call Trace:
<TASK>
migrate_folio_move mm/migrate.c:1411 [inline]

Looks like a race introduced by commit[1] ("mm: migrate: requeue destination folio on deferred split queue").

Between folio migration (mbind) and rmap removal (exit_mmap), I guess :)

migrate_folio_move() snapshots src_partially_mapped from src before migration:

	if (folio_order(src) > 1 &&
	    !data_race(list_empty(&src->_deferred_list))) {
		src_deferred_split = true;
		src_partially_mapped = folio_test_partially_mapped(src);
	}

Then move_to_new_folio() eventually unqueues src in __folio_migrate_mapping():

	folio_unqueue_deferred_split(src);

After that, migration restores mappings to dst:

	if (old_page_state & PAGE_WAS_MAPPED)
		remove_migration_ptes(src, dst, 0);

At that point, dst is already visible again. A concurrent unmap path from another sharer can then remove some of those mappings and reach deferred_split_folio(dst, true), which sets PG_partially_mapped on dst.

Migration then resumes and does:

	if (src_deferred_split)
		deferred_split_folio(dst, src_partially_mapped);

If the earlier snapshot from src was false, this becomes deferred_split_folio(dst, false), but dst may already have been marked partially mapped by the concurrent rmap-removal path, so the WARN in deferred_split_folio() fires:

	if (partially_mapped) {
		...
	} else {
		/* partially mapped folios cannot become non-partially mapped */
		VM_WARN_ON_FOLIO(folio_test_partially_mapped(folio), folio);
	}

[1] https://lore.kernel.org/all/[email protected]/

Quoted 48 lines
migrate_folios_move mm/migrate.c:1740 [inline]
migrate_pages_batch+0x319f/0x4c40 mm/migrate.c:1996
migrate_pages_sync mm/migrate.c:2026 [inline]
migrate_pages+0x1c74/0x2a10 mm/migrate.c:2135
do_mbind mm/mempolicy.c:1614 [inline]
kernel_mbind mm/mempolicy.c:1757 [inline]
__do_sys_mbind mm/mempolicy.c:1831 [inline]
__se_sys_mbind+0xe89/0x10f0 mm/mempolicy.c:1827
do_syscall_x64 arch/x86/entry/syscall_64.c:63 [inline]
do_syscall_64+0x15f/0xf80 arch/x86/entry/syscall_64.c:94
entry_SYSCALL_64_after_hwframe+0x77/0x7f
RIP: 0033:0x7f32e239c819
Code: ff c3 66 2e 0f 1f 84 00 00 00 00 00 0f 1f 44 00 00 48 89 f8 48 89 f7 48 89 d6 48 89 ca 4d 89 c2 4d 89 c8 4c 8b 4c 24 08 0f 05 <48> 3d 01 f0 ff ff 73 01 c3 48 c7 c1 e8 ff ff ff f7 d8 64 89 01 48
RSP: 002b:00007f32e32a7028 EFLAGS: 00000246 ORIG_RAX: 00000000000000ed
RAX: ffffffffffffffda RBX: 00007f32e2616090 RCX: 00007f32e239c819
RDX: 0000000000000000 RSI: 0000000000800000 RDI: 0000200000001000
RBP: 00007f32e2432c91 R08: 0000000000000000 R09: 0000000000000002
R10: 0000000000000000 R11: 0000000000000246 R12: 0000000000000000
R13: 00007f32e2616128 R14: 00007f32e2616090 R15: 00007ffe30c61cc8
</TASK>

---
This report is generated by a bot. It may contain errors. See https://goo.gl/tpsmEJ for more information about syzbot. syzbot engineers can be reached at [email protected].

syzbot will keep track of this issue. See:
https://goo.gl/tpsmEJ#status for how to communicate with syzbot.

If the report is already addressed, let syzbot know by replying with:
#syz fix: exact-commit-title

If you want syzbot to run the reproducer, reply with:
#syz test: git://repo/address.git branch-or-commit-hash
If you attach or paste a git patch, syzbot will apply it before testing.

If you want to overwrite report's subsystems, reply with:
#syz set subsystems: new-subsystem
(See the list of subsystem names on the web dashboard)

If the report is a duplicate of another one, reply with:
#syz dup: exact-subject-of-another-report

If you want to undo deduplication, reply with:
#syz undup

reviewer
#4 lore raw
Quoted 102 lines · Lance Yang

+Cc Usama

Quoted 50 lines · syzbot

Hello,

syzbot found the following issue on:

HEAD commit:    cf7c3c02fdd0 Add linux-next specific files for 20260330
git tree:       linux-next
console output: https://syzkaller.appspot.com/x/log.txt?x=154ee46a580000
kernel config:  https://syzkaller.appspot.com/x/.config?x=3944d875fa9bfb67
dashboard link: https://syzkaller.appspot.com/bug?extid=a7067a757858ac8eb085
compiler:       Debian clang version 21.1.8 (++20251221033036+2078da43e25a-1~exp1~20251221153213.50), Debian LLD 21.1.8
syz repro:      https://syzkaller.appspot.com/x/repro.syz?x=12c846ba580000

Downloadable assets:
disk image: https://storage.googleapis.com/syzbot-assets/053d3b49a360/disk-cf7c3c02.raw.xz
vmlinux: https://storage.googleapis.com/syzbot-assets/faabb37d41d0/vmlinux-cf7c3c02.xz
kernel image: https://storage.googleapis.com/syzbot-assets/8d47fe92aaa8/bzImage-cf7c3c02.xz

IMPORTANT: if you fix the issue, please add the following tag to the commit:

Reported-by [email protected]
free_pages_and_swap_cache+0x2b9/0x490 mm/swap_state.c:401
__tlb_batch_free_encoded_pages mm/mmu_gather.c:138 [inline]
tlb_batch_pages_flush mm/mmu_gather.c:151 [inline]
tlb_flush_mmu_free mm/mmu_gather.c:417 [inline]
tlb_flush_mmu+0x6d3/0xa30 mm/mmu_gather.c:424
tlb_finish_mmu+0xf9/0x230 mm/mmu_gather.c:549
exit_mmap+0x498/0x9e0 mm/mmap.c:1313
__mmput+0x118/0x430 kernel/fork.c:1177
exit_mm+0x18e/0x250 kernel/exit.c:581
do_exit+0x6a2/0x22c0 kernel/exit.c:962
do_group_exit+0x21b/0x2d0 kernel/exit.c:1116
__do_sys_exit_group kernel/exit.c:1127 [inline]
__se_sys_exit_group kernel/exit.c:1125 [inline]
__x64_sys_exit_group+0x3f/0x40 kernel/exit.c:1125
x64_sys_call+0x221a/0x2240 arch/x86/include/generated/asm/syscalls_64.h:232
do_syscall_x64 arch/x86/entry/syscall_64.c:63 [inline]
do_syscall_64+0x15f/0xf80 arch/x86/entry/syscall_64.c:94
entry_SYSCALL_64_after_hwframe+0x77/0x7f
------------[ cut here ]------------
1
WARNING: mm/huge_memory.c:4371 at deferred_split_folio+0x974/0xaa0 mm/huge_memory.c:4371, CPU#1: syz.3.1110/10500
Modules linked in:
CPU: 1 UID: 0 PID: 10500 Comm: syz.3.1110 Not tainted syzkaller #0 PREEMPT(full) 
Hardware name: Google Google Compute Engine/Google Compute Engine, BIOS Google 02/12/2026
RIP: 0010:deferred_split_folio+0x974/0xaa0 mm/huge_memory.c:4371
Code: 31 48 8d 65 d8 5b 41 5c 41 5d 41 5e 41 5f 5d e9 c2 67 8d 09 cc e8 8c 73 93 ff 48 89 df 48 c7 c6 20 5b fc 8b e8 dd 2b f5 fe 90 <0f> 0b 90 e9 d4 fe ff ff e8 9f 7a 8a 09 e8 6a 73 93 ff 48 89 df 48
RSP: 0018:ffffc900047ef540 EFLAGS: 00010046
RAX: 1c05fb65cfaab100 RBX: ffffea0001840000 RCX: 0000000080000001
RDX: 0000000000000002 RSI: ffffffff8e4da1c7 RDI: ffff88807d6f9e80
RBP: ffffc900047ef610 R08: ffff8880b87247d3 R09: 1ffff110170e48fa
Quoted 8 lines
R10: dffffc0000000000 R11: ffffed10170e48fb R12: ffffea0001840040
R13: 0000000000000000 R14: 0000000000010000 R15: 1ffff920008fdeb0
FS:  00007f32e32a76c0(0000) GS:ffff8881250e8000(0000) knlGS:0000000000000000
CS:  0010 DS: 0000 ES: 0000 CR0: 0000000080050033
CR2: 00007f5825757930 CR3: 0000000034ad8000 CR4: 00000000003526f0
Call Trace:
<TASK>
migrate_folio_move mm/migrate.c:1411 [inline]

Looks like a race introduced by commit[1] ("mm: migrate: requeue destination folio on deferred split queue").

Between folio migration (mbind) and rmap removal (exit_mmap), I guess :)

migrate_folio_move() snapshots src_partially_mapped from src before migration:

	if (folio_order(src) > 1 &&
	    !data_race(list_empty(&src->_deferred_list))) {
		src_deferred_split = true;
		src_partially_mapped = folio_test_partially_mapped(src);
	}

Then move_to_new_folio() eventually unqueues src in __folio_migrate_mapping():

	folio_unqueue_deferred_split(src);

After that, migration restores mappings to dst:

	if (old_page_state & PAGE_WAS_MAPPED)
		remove_migration_ptes(src, dst, 0);

At that point, dst is already visible again. A concurrent unmap path from another sharer can then remove some of those mappings and reach deferred_split_folio(dst, true), which sets PG_partially_mapped on dst.

Migration then resumes and does:

	if (src_deferred_split)
		deferred_split_folio(dst, src_partially_mapped);

If the earlier snapshot from src was false, this becomes deferred_split_folio(dst, false), but dst may already have been marked partially mapped by the concurrent rmap-removal path, so the WARN in deferred_split_folio() fires:

Quoted 9 lines
	if (partially_mapped) {
		...
	} else {
		/* partially mapped folios cannot become non-partially mapped */
		VM_WARN_ON_FOLIO(folio_test_partially_mapped(folio), folio);
	}

[1] https://lore.kernel.org/all/[email protected]/

Perhaps the WARN is simply too strict there :)

Migration already holds the folio lock on dst, while the competing rmap-removal path runs under the page-table lock. So once remove_migration_ptes(src, dst, 0) makes dst visible again, this race looks hard to avoid.

So maybe the simplest fix is just to drop the WARN in the !partially_mapped path:

---8<---
Subject: [PATCH 1/1] mm/thp: avoid false warning in deferred_split_folio()

From: Lance Yang <[email protected]>

migrate_folio_move() snapshots src_partially_mapped from src before migration and later requeues dst after remove_migration_ptes(src, dst, 0).

Once dst is visible again, a competing rmap-removal path can legally set PG_partially_mapped before the migration path reaches deferred_split_folio(dst, src_partially_mapped).

Migration already holds the folio lock on dst, while the competing rmap-removal path runs under the page-table lock. So once remove_migration_ptes(src, dst, 0) makes dst visible again, this race looks hard to avoid.

So just drop the WARN in the !partially_mapped path and preserve an already-set PG_partially_mapped bit.

Fixes 8a8ca142a488 ("mm: migrate: requeue destination folio on deferred split queue")
Reported-by [email protected]
---
 mm/huge_memory.c | 3 ---
 1 file changed, 3 deletions(-)
mm/huge_memory.c +0 -3
index 745eb3d0d4a7..8ea8e293dc7c 100644
@@ -4433,9 +4433,6 @@ void deferred_split_folio(struct folio *folio, bool partially_mapped)
 			mod_mthp_stat(folio_order(folio), MTHP_STAT_NR_ANON_PARTIALLY_MAPPED, 1);
 
 		}
-	} else {
-		/* partially mapped folios cannot become non-partially mapped */
-		VM_WARN_ON_FOLIO(folio_test_partially_mapped(folio), folio);
 	}
 	if (list_empty(&folio->_deferred_list)) {
 		struct mem_cgroup *memcg;

---

Thanks,
Lance

#5 lore raw
Quoted 141 lines · Lance Yang
Quoted 102 lines · Lance Yang

+Cc Usama

Quoted 50 lines · syzbot

Hello,

syzbot found the following issue on:

HEAD commit:    cf7c3c02fdd0 Add linux-next specific files for 20260330
git tree:       linux-next
console output: https://syzkaller.appspot.com/x/log.txt?x=154ee46a580000
kernel config:  https://syzkaller.appspot.com/x/.config?x=3944d875fa9bfb67
dashboard link: https://syzkaller.appspot.com/bug?extid=a7067a757858ac8eb085
compiler:       Debian clang version 21.1.8 (++20251221033036+2078da43e25a-1~exp1~20251221153213.50), Debian LLD 21.1.8
syz repro:      https://syzkaller.appspot.com/x/repro.syz?x=12c846ba580000

Downloadable assets:
disk image: https://storage.googleapis.com/syzbot-assets/053d3b49a360/disk-cf7c3c02.raw.xz
vmlinux: https://storage.googleapis.com/syzbot-assets/faabb37d41d0/vmlinux-cf7c3c02.xz
kernel image: https://storage.googleapis.com/syzbot-assets/8d47fe92aaa8/bzImage-cf7c3c02.xz

IMPORTANT: if you fix the issue, please add the following tag to the commit:

Reported-by [email protected]
free_pages_and_swap_cache+0x2b9/0x490 mm/swap_state.c:401
__tlb_batch_free_encoded_pages mm/mmu_gather.c:138 [inline]
tlb_batch_pages_flush mm/mmu_gather.c:151 [inline]
tlb_flush_mmu_free mm/mmu_gather.c:417 [inline]
tlb_flush_mmu+0x6d3/0xa30 mm/mmu_gather.c:424
tlb_finish_mmu+0xf9/0x230 mm/mmu_gather.c:549
exit_mmap+0x498/0x9e0 mm/mmap.c:1313
__mmput+0x118/0x430 kernel/fork.c:1177
exit_mm+0x18e/0x250 kernel/exit.c:581
do_exit+0x6a2/0x22c0 kernel/exit.c:962
do_group_exit+0x21b/0x2d0 kernel/exit.c:1116
__do_sys_exit_group kernel/exit.c:1127 [inline]
__se_sys_exit_group kernel/exit.c:1125 [inline]
__x64_sys_exit_group+0x3f/0x40 kernel/exit.c:1125
x64_sys_call+0x221a/0x2240 arch/x86/include/generated/asm/syscalls_64.h:232
do_syscall_x64 arch/x86/entry/syscall_64.c:63 [inline]
do_syscall_64+0x15f/0xf80 arch/x86/entry/syscall_64.c:94
entry_SYSCALL_64_after_hwframe+0x77/0x7f
------------[ cut here ]------------
1
WARNING: mm/huge_memory.c:4371 at deferred_split_folio+0x974/0xaa0 mm/huge_memory.c:4371, CPU#1: syz.3.1110/10500
Modules linked in:
CPU: 1 UID: 0 PID: 10500 Comm: syz.3.1110 Not tainted syzkaller #0 PREEMPT(full) 
Hardware name: Google Google Compute Engine/Google Compute Engine, BIOS Google 02/12/2026
RIP: 0010:deferred_split_folio+0x974/0xaa0 mm/huge_memory.c:4371
Code: 31 48 8d 65 d8 5b 41 5c 41 5d 41 5e 41 5f 5d e9 c2 67 8d 09 cc e8 8c 73 93 ff 48 89 df 48 c7 c6 20 5b fc 8b e8 dd 2b f5 fe 90 <0f> 0b 90 e9 d4 fe ff ff e8 9f 7a 8a 09 e8 6a 73 93 ff 48 89 df 48
RSP: 0018:ffffc900047ef540 EFLAGS: 00010046
RAX: 1c05fb65cfaab100 RBX: ffffea0001840000 RCX: 0000000080000001
RDX: 0000000000000002 RSI: ffffffff8e4da1c7 RDI: ffff88807d6f9e80
RBP: ffffc900047ef610 R08: ffff8880b87247d3 R09: 1ffff110170e48fa
Quoted 8 lines
R10: dffffc0000000000 R11: ffffed10170e48fb R12: ffffea0001840040
R13: 0000000000000000 R14: 0000000000010000 R15: 1ffff920008fdeb0
FS:  00007f32e32a76c0(0000) GS:ffff8881250e8000(0000) knlGS:0000000000000000
CS:  0010 DS: 0000 ES: 0000 CR0: 0000000080050033
CR2: 00007f5825757930 CR3: 0000000034ad8000 CR4: 00000000003526f0
Call Trace:
<TASK>
migrate_folio_move mm/migrate.c:1411 [inline]

Looks like a race introduced by commit[1] ("mm: migrate: requeue destination folio on deferred split queue").

Between folio migration (mbind) and rmap removal (exit_mmap), I guess :)

migrate_folio_move() snapshots src_partially_mapped from src before migration:

	if (folio_order(src) > 1 &&
	    !data_race(list_empty(&src->_deferred_list))) {
		src_deferred_split = true;
		src_partially_mapped = folio_test_partially_mapped(src);
	}

Then move_to_new_folio() eventually unqueues src in __folio_migrate_mapping():

	folio_unqueue_deferred_split(src);

After that, migration restores mappings to dst:

	if (old_page_state & PAGE_WAS_MAPPED)
		remove_migration_ptes(src, dst, 0);

At that point, dst is already visible again. A concurrent unmap path from another sharer can then remove some of those mappings and reach deferred_split_folio(dst, true), which sets PG_partially_mapped on dst.

Migration then resumes and does:

	if (src_deferred_split)
		deferred_split_folio(dst, src_partially_mapped);

If the earlier snapshot from src was false, this becomes deferred_split_folio(dst, false), but dst may already have been marked partially mapped by the concurrent rmap-removal path, so the WARN in deferred_split_folio() fires:

Quoted 9 lines
	if (partially_mapped) {
		...
	} else {
		/* partially mapped folios cannot become non-partially mapped */
		VM_WARN_ON_FOLIO(folio_test_partially_mapped(folio), folio);
	}

[1] https://lore.kernel.org/all/[email protected]/

Perhaps the WARN is simply too strict there :)

Migration already holds the folio lock on dst, while the competing rmap-removal path runs under the page-table lock. So once remove_migration_ptes(src, dst, 0) makes dst visible again, this race looks hard to avoid.

So maybe the simplest fix is just to drop the WARN in the !partially_mapped path:

---8<---
Subject: [PATCH 1/1] mm/thp: avoid false warning in deferred_split_folio()

From: Lance Yang <[email protected]>

migrate_folio_move() snapshots src_partially_mapped from src before migration and later requeues dst after remove_migration_ptes(src, dst, 0).

Once dst is visible again, a competing rmap-removal path can legally set PG_partially_mapped before the migration path reaches deferred_split_folio(dst, src_partially_mapped).

Migration already holds the folio lock on dst, while the competing rmap-removal path runs under the page-table lock. So once remove_migration_ptes(src, dst, 0) makes dst visible again, this race looks hard to avoid.

Quoted 7 lines

So just drop the WARN in the !partially_mapped path and preserve an already-set PG_partially_mapped bit.

Fixes 8a8ca142a488 ("mm: migrate: requeue destination folio on deferred split queue")
Reported-by [email protected]

A fix just appeared:

https://lore.kernel.org/r/[email protected]

Have to think about this :)

Signature · 3 lines
Cheers,

David
#6 lore raw
In reply to Lance Yang
Quoted 157 lines · Lance Yang
Quoted 102 lines · Lance Yang

+Cc Usama

Quoted 50 lines · syzbot

Hello,

syzbot found the following issue on:

HEAD commit:    cf7c3c02fdd0 Add linux-next specific files for 20260330
git tree:       linux-next
console output: https://syzkaller.appspot.com/x/log.txt?x=154ee46a580000
kernel config:  https://syzkaller.appspot.com/x/.config?x=3944d875fa9bfb67
dashboard link: https://syzkaller.appspot.com/bug?extid=a7067a757858ac8eb085
compiler:       Debian clang version 21.1.8 (++20251221033036+2078da43e25a-1~exp1~20251221153213.50), Debian LLD 21.1.8
syz repro:      https://syzkaller.appspot.com/x/repro.syz?x=12c846ba580000

Downloadable assets:
disk image: https://storage.googleapis.com/syzbot-assets/053d3b49a360/disk-cf7c3c02.raw.xz
vmlinux: https://storage.googleapis.com/syzbot-assets/faabb37d41d0/vmlinux-cf7c3c02.xz
kernel image: https://storage.googleapis.com/syzbot-assets/8d47fe92aaa8/bzImage-cf7c3c02.xz

IMPORTANT: if you fix the issue, please add the following tag to the commit:

Reported-by [email protected]
free_pages_and_swap_cache+0x2b9/0x490 mm/swap_state.c:401
__tlb_batch_free_encoded_pages mm/mmu_gather.c:138 [inline]
tlb_batch_pages_flush mm/mmu_gather.c:151 [inline]
tlb_flush_mmu_free mm/mmu_gather.c:417 [inline]
tlb_flush_mmu+0x6d3/0xa30 mm/mmu_gather.c:424
tlb_finish_mmu+0xf9/0x230 mm/mmu_gather.c:549
exit_mmap+0x498/0x9e0 mm/mmap.c:1313
__mmput+0x118/0x430 kernel/fork.c:1177
exit_mm+0x18e/0x250 kernel/exit.c:581
do_exit+0x6a2/0x22c0 kernel/exit.c:962
do_group_exit+0x21b/0x2d0 kernel/exit.c:1116
__do_sys_exit_group kernel/exit.c:1127 [inline]
__se_sys_exit_group kernel/exit.c:1125 [inline]
__x64_sys_exit_group+0x3f/0x40 kernel/exit.c:1125
x64_sys_call+0x221a/0x2240 arch/x86/include/generated/asm/syscalls_64.h:232
do_syscall_x64 arch/x86/entry/syscall_64.c:63 [inline]
do_syscall_64+0x15f/0xf80 arch/x86/entry/syscall_64.c:94
entry_SYSCALL_64_after_hwframe+0x77/0x7f
------------[ cut here ]------------
1
WARNING: mm/huge_memory.c:4371 at deferred_split_folio+0x974/0xaa0 mm/huge_memory.c:4371, CPU#1: syz.3.1110/10500
Modules linked in:
CPU: 1 UID: 0 PID: 10500 Comm: syz.3.1110 Not tainted syzkaller #0 PREEMPT(full) 
Hardware name: Google Google Compute Engine/Google Compute Engine, BIOS Google 02/12/2026
RIP: 0010:deferred_split_folio+0x974/0xaa0 mm/huge_memory.c:4371
Code: 31 48 8d 65 d8 5b 41 5c 41 5d 41 5e 41 5f 5d e9 c2 67 8d 09 cc e8 8c 73 93 ff 48 89 df 48 c7 c6 20 5b fc 8b e8 dd 2b f5 fe 90 <0f> 0b 90 e9 d4 fe ff ff e8 9f 7a 8a 09 e8 6a 73 93 ff 48 89 df 48
RSP: 0018:ffffc900047ef540 EFLAGS: 00010046
RAX: 1c05fb65cfaab100 RBX: ffffea0001840000 RCX: 0000000080000001
RDX: 0000000000000002 RSI: ffffffff8e4da1c7 RDI: ffff88807d6f9e80
RBP: ffffc900047ef610 R08: ffff8880b87247d3 R09: 1ffff110170e48fa
Quoted 8 lines
R10: dffffc0000000000 R11: ffffed10170e48fb R12: ffffea0001840040
R13: 0000000000000000 R14: 0000000000010000 R15: 1ffff920008fdeb0
FS:  00007f32e32a76c0(0000) GS:ffff8881250e8000(0000) knlGS:0000000000000000
CS:  0010 DS: 0000 ES: 0000 CR0: 0000000080050033
CR2: 00007f5825757930 CR3: 0000000034ad8000 CR4: 00000000003526f0
Call Trace:
<TASK>
migrate_folio_move mm/migrate.c:1411 [inline]

Looks like a race introduced by commit[1] ("mm: migrate: requeue destination folio on deferred split queue").

Between folio migration (mbind) and rmap removal (exit_mmap), I guess :)

migrate_folio_move() snapshots src_partially_mapped from src before migration:

	if (folio_order(src) > 1 &&
	    !data_race(list_empty(&src->_deferred_list))) {
		src_deferred_split = true;
		src_partially_mapped = folio_test_partially_mapped(src);
	}

Then move_to_new_folio() eventually unqueues src in __folio_migrate_mapping():

	folio_unqueue_deferred_split(src);

After that, migration restores mappings to dst:

	if (old_page_state & PAGE_WAS_MAPPED)
		remove_migration_ptes(src, dst, 0);

At that point, dst is already visible again. A concurrent unmap path from another sharer can then remove some of those mappings and reach deferred_split_folio(dst, true), which sets PG_partially_mapped on dst.

Migration then resumes and does:

	if (src_deferred_split)
		deferred_split_folio(dst, src_partially_mapped);

If the earlier snapshot from src was false, this becomes deferred_split_folio(dst, false), but dst may already have been marked partially mapped by the concurrent rmap-removal path, so the WARN in deferred_split_folio() fires:

Quoted 9 lines
	if (partially_mapped) {
		...
	} else {
		/* partially mapped folios cannot become non-partially mapped */
		VM_WARN_ON_FOLIO(folio_test_partially_mapped(folio), folio);
	}

[1] https://lore.kernel.org/all/[email protected]/

Perhaps the WARN is simply too strict there :)

Migration already holds the folio lock on dst, while the competing rmap-removal path runs under the page-table lock. So once remove_migration_ptes(src, dst, 0) makes dst visible again, this race looks hard to avoid.

So maybe the simplest fix is just to drop the WARN in the !partially_mapped path:

---8<---
Subject: [PATCH 1/1] mm/thp: avoid false warning in deferred_split_folio()

From: Lance Yang <[email protected]>

migrate_folio_move() snapshots src_partially_mapped from src before migration and later requeues dst after remove_migration_ptes(src, dst, 0).

Once dst is visible again, a competing rmap-removal path can legally set PG_partially_mapped before the migration path reaches deferred_split_folio(dst, src_partially_mapped).

Migration already holds the folio lock on dst, while the competing rmap-removal path runs under the page-table lock. So once remove_migration_ptes(src, dst, 0) makes dst visible again, this race looks hard to avoid.

So just drop the WARN in the !partially_mapped path and preserve an already-set PG_partially_mapped bit.

Fixes 8a8ca142a488 ("mm: migrate: requeue destination folio on deferred split queue")
Reported-by [email protected]
---
 mm/huge_memory.c | 3 ---
 1 file changed, 3 deletions(-)
mm/huge_memory.c +0 -0
index 745eb3d0d4a7..8ea8e293dc7c 100644

mm/huge_memory.c void deferred_split_folio(struct folio *folio, bool partially_mapped) · line 4436

Quoted 7 lines
mm/huge_memory.c +0 -3
@@ -4433,9 +4433,6 @@ void deferred_split_folio(struct folio *folio, bool partially_mapped)
 			mod_mthp_stat(folio_order(folio), MTHP_STAT_NR_ANON_PARTIALLY_MAPPED, 1);
 
 		}
-	} else {
-		/* partially mapped folios cannot become non-partially mapped */
-		VM_WARN_ON_FOLIO(folio_test_partially_mapped(folio), folio);
 	}

Can't we simply move the setting before restoring migration ptes?

mm/migrate.c +9 -10
index 05cb408846f2..5f222cb0ca90 100644
@@ -1385,6 +1385,15 @@ static int migrate_folio_move(free_folio_t put_new_folio, unsigned long private,
        if (rc)
                goto out;
 
+       /*
+        * Requeue the destination folio on the deferred split queue if
+        * the source was on the queue.  The source is unqueued in
+        * __folio_migrate_mapping(), so we recorded the state from
+        * before move_to_new_folio().
+        */
+       if (src_deferred_split)
+               deferred_split_folio(dst, src_partially_mapped);
+
        /*
         * When successful, push dst to LRU immediately: so that if it
         * turns out to be an mlocked page, remove_migration_ptes() will
@@ -1400,16 +1409,6 @@ static int migrate_folio_move(free_folio_t put_new_folio, unsigned long private,
 
        if (old_page_state & PAGE_WAS_MAPPED)
                remove_migration_ptes(src, dst, 0);
-
-       /*
-        * Requeue the destination folio on the deferred split queue if
-        * the source was on the queue.  The source is unqueued in
-        * __folio_migrate_mapping(), so we recorded the state from
-        * before move_to_new_folio().
-        */
-       if (src_deferred_split)
-               deferred_split_folio(dst, src_partially_mapped);
-
 out_unlock_both:
        folio_unlock(dst);
        folio_set_owner_migrate_reason(dst, reason);
Signature · 3 lines
Cheers,

David
reviewer
#7 lore raw

+Cc Deepanshu

Quoted 160 lines · David Hildenbrand (Arm)
Quoted 159 lines · Lance Yang
Quoted 102 lines · Lance Yang

+Cc Usama

Quoted 50 lines · syzbot

Hello,

syzbot found the following issue on:

HEAD commit:    cf7c3c02fdd0 Add linux-next specific files for 20260330
git tree:       linux-next
console output: https://syzkaller.appspot.com/x/log.txt?x=154ee46a580000
kernel config:  https://syzkaller.appspot.com/x/.config?x=3944d875fa9bfb67
dashboard link: https://syzkaller.appspot.com/bug?extid=a7067a757858ac8eb085
compiler:       Debian clang version 21.1.8 (++20251221033036+2078da43e25a-1~exp1~20251221153213.50), Debian LLD 21.1.8
syz repro:      https://syzkaller.appspot.com/x/repro.syz?x=12c846ba580000

Downloadable assets:
disk image: https://storage.googleapis.com/syzbot-assets/053d3b49a360/disk-cf7c3c02.raw.xz
vmlinux: https://storage.googleapis.com/syzbot-assets/faabb37d41d0/vmlinux-cf7c3c02.xz
kernel image: https://storage.googleapis.com/syzbot-assets/8d47fe92aaa8/bzImage-cf7c3c02.xz

IMPORTANT: if you fix the issue, please add the following tag to the commit:

Reported-by [email protected]
free_pages_and_swap_cache+0x2b9/0x490 mm/swap_state.c:401
__tlb_batch_free_encoded_pages mm/mmu_gather.c:138 [inline]
tlb_batch_pages_flush mm/mmu_gather.c:151 [inline]
tlb_flush_mmu_free mm/mmu_gather.c:417 [inline]
tlb_flush_mmu+0x6d3/0xa30 mm/mmu_gather.c:424
tlb_finish_mmu+0xf9/0x230 mm/mmu_gather.c:549
exit_mmap+0x498/0x9e0 mm/mmap.c:1313
__mmput+0x118/0x430 kernel/fork.c:1177
exit_mm+0x18e/0x250 kernel/exit.c:581
do_exit+0x6a2/0x22c0 kernel/exit.c:962
do_group_exit+0x21b/0x2d0 kernel/exit.c:1116
__do_sys_exit_group kernel/exit.c:1127 [inline]
__se_sys_exit_group kernel/exit.c:1125 [inline]
__x64_sys_exit_group+0x3f/0x40 kernel/exit.c:1125
x64_sys_call+0x221a/0x2240 arch/x86/include/generated/asm/syscalls_64.h:232
do_syscall_x64 arch/x86/entry/syscall_64.c:63 [inline]
do_syscall_64+0x15f/0xf80 arch/x86/entry/syscall_64.c:94
entry_SYSCALL_64_after_hwframe+0x77/0x7f
------------[ cut here ]------------
1
WARNING: mm/huge_memory.c:4371 at deferred_split_folio+0x974/0xaa0 mm/huge_memory.c:4371, CPU#1: syz.3.1110/10500
Modules linked in:
CPU: 1 UID: 0 PID: 10500 Comm: syz.3.1110 Not tainted syzkaller #0 PREEMPT(full) 
Hardware name: Google Google Compute Engine/Google Compute Engine, BIOS Google 02/12/2026
RIP: 0010:deferred_split_folio+0x974/0xaa0 mm/huge_memory.c:4371
Code: 31 48 8d 65 d8 5b 41 5c 41 5d 41 5e 41 5f 5d e9 c2 67 8d 09 cc e8 8c 73 93 ff 48 89 df 48 c7 c6 20 5b fc 8b e8 dd 2b f5 fe 90 <0f> 0b 90 e9 d4 fe ff ff e8 9f 7a 8a 09 e8 6a 73 93 ff 48 89 df 48
RSP: 0018:ffffc900047ef540 EFLAGS: 00010046
RAX: 1c05fb65cfaab100 RBX: ffffea0001840000 RCX: 0000000080000001
RDX: 0000000000000002 RSI: ffffffff8e4da1c7 RDI: ffff88807d6f9e80
RBP: ffffc900047ef610 R08: ffff8880b87247d3 R09: 1ffff110170e48fa
Quoted 8 lines
R10: dffffc0000000000 R11: ffffed10170e48fb R12: ffffea0001840040
R13: 0000000000000000 R14: 0000000000010000 R15: 1ffff920008fdeb0
FS:  00007f32e32a76c0(0000) GS:ffff8881250e8000(0000) knlGS:0000000000000000
CS:  0010 DS: 0000 ES: 0000 CR0: 0000000080050033
CR2: 00007f5825757930 CR3: 0000000034ad8000 CR4: 00000000003526f0
Call Trace:
<TASK>
migrate_folio_move mm/migrate.c:1411 [inline]

Looks like a race introduced by commit[1] ("mm: migrate: requeue destination folio on deferred split queue").

Between folio migration (mbind) and rmap removal (exit_mmap), I guess :)

migrate_folio_move() snapshots src_partially_mapped from src before migration:

	if (folio_order(src) > 1 &&
	    !data_race(list_empty(&src->_deferred_list))) {
		src_deferred_split = true;
		src_partially_mapped = folio_test_partially_mapped(src);
	}

Then move_to_new_folio() eventually unqueues src in __folio_migrate_mapping():

	folio_unqueue_deferred_split(src);

After that, migration restores mappings to dst:

	if (old_page_state & PAGE_WAS_MAPPED)
		remove_migration_ptes(src, dst, 0);

At that point, dst is already visible again. A concurrent unmap path from another sharer can then remove some of those mappings and reach deferred_split_folio(dst, true), which sets PG_partially_mapped on dst.

Migration then resumes and does:

	if (src_deferred_split)
		deferred_split_folio(dst, src_partially_mapped);

If the earlier snapshot from src was false, this becomes deferred_split_folio(dst, false), but dst may already have been marked partially mapped by the concurrent rmap-removal path, so the WARN in deferred_split_folio() fires:

Quoted 9 lines
	if (partially_mapped) {
		...
	} else {
		/* partially mapped folios cannot become non-partially mapped */
		VM_WARN_ON_FOLIO(folio_test_partially_mapped(folio), folio);
	}

[1] https://lore.kernel.org/all/[email protected]/

Perhaps the WARN is simply too strict there :)

Migration already holds the folio lock on dst, while the competing rmap-removal path runs under the page-table lock. So once remove_migration_ptes(src, dst, 0) makes dst visible again, this race looks hard to avoid.

So maybe the simplest fix is just to drop the WARN in the !partially_mapped path:

---8<---
Subject: [PATCH 1/1] mm/thp: avoid false warning in deferred_split_folio()

From: Lance Yang <[email protected]>

migrate_folio_move() snapshots src_partially_mapped from src before migration and later requeues dst after remove_migration_ptes(src, dst, 0).

Once dst is visible again, a competing rmap-removal path can legally set PG_partially_mapped before the migration path reaches deferred_split_folio(dst, src_partially_mapped).

Migration already holds the folio lock on dst, while the competing rmap-removal path runs under the page-table lock. So once remove_migration_ptes(src, dst, 0) makes dst visible again, this race looks hard to avoid.

So just drop the WARN in the !partially_mapped path and preserve an already-set PG_partially_mapped bit.

Fixes 8a8ca142a488 ("mm: migrate: requeue destination folio on deferred split queue")
Reported-by [email protected]
---
 mm/huge_memory.c | 3 ---
 1 file changed, 3 deletions(-)
mm/huge_memory.c +0 -0
index 745eb3d0d4a7..8ea8e293dc7c 100644
@@ -4433,9 +4433,6 @@ void deferred_split_folio(struct folio *folio, bool partially_mapped)
 			mod_mthp_stat(folio_order(folio), MTHP_STAT_NR_ANON_PARTIALLY_MAPPED, 1);
 
Quoted 7 lines

mm/huge_memory.c void deferred_split_folio(struct folio *folio, bool partially_mapped) · line 4436

Quoted 5 lines
mm/huge_memory.c +0 -3
 		}
-	} else {
-		/* partially mapped folios cannot become non-partially mapped */
-		VM_WARN_ON_FOLIO(folio_test_partially_mapped(folio), folio);
 	}

Can't we simply move the setting before restoring migration ptes?

Afraid not, it closes the remove_migration_ptes() -> deferred_split_folio() race, but opens a new one with the shrinker, IIUC

Once dst is on the deferred split queue, deferred_split_scan() can pick it up immediately. The shrinker unconditionally dequeues every folio it visits:

	list_del_init(&folio->_deferred_list);   /* always */

Then for a non-partially-mapped folio, if folio_trylock() fails (dst is still locked by migration), it falls through to:

next:

		if (did_split || !folio_test_partially_mapped(folio))
			continue;  /* not requeued, dst silently lost */

so it is *not* requeued.

That seems to recreate the original issue commit[1] was fixing: letting underused THPs silently fall off the deferred split queue again ...

Hopefully, I didn't miss something important :)

[1] https://lore.kernel.org/all/[email protected]/

Quoted 39 lines
mm/migrate.c +9 -10
index 05cb408846f2..5f222cb0ca90 100644
@@ -1385,6 +1385,15 @@ static int migrate_folio_move(free_folio_t put_new_folio, unsigned long private,
       if (rc)
               goto out;
 
+       /*
+        * Requeue the destination folio on the deferred split queue if
+        * the source was on the queue.  The source is unqueued in
+        * __folio_migrate_mapping(), so we recorded the state from
+        * before move_to_new_folio().
+        */
+       if (src_deferred_split)
+               deferred_split_folio(dst, src_partially_mapped);
+
       /*
        * When successful, push dst to LRU immediately: so that if it
        * turns out to be an mlocked page, remove_migration_ptes() will
@@ -1400,16 +1409,6 @@ static int migrate_folio_move(free_folio_t put_new_folio, unsigned long private,
 
       if (old_page_state & PAGE_WAS_MAPPED)
               remove_migration_ptes(src, dst, 0);
-
-       /*
-        * Requeue the destination folio on the deferred split queue if
-        * the source was on the queue.  The source is unqueued in
-        * __folio_migrate_mapping(), so we recorded the state from
-        * before move_to_new_folio().
-        */
-       if (src_deferred_split)
-               deferred_split_folio(dst, src_partially_mapped);
-

out_unlock_both:

       folio_unlock(dst);
       folio_set_owner_migrate_reason(dst, reason);
#8 lore raw
Quoted 70 lines · Lance Yang

+Cc Deepanshu

Quoted 49 lines · David Hildenbrand (Arm)
Quoted 48 lines · Lance Yang
Quoted 2 lines

from another sharer can then remove some of those mappings and reach

Perhaps the WARN is simply too strict there :)

Migration already holds the folio lock on dst, while the competing rmap-removal path runs under the page-table lock. So once remove_migration_ptes(src, dst, 0) makes dst visible again, this race looks hard to avoid.

So maybe the simplest fix is just to drop the WARN in the !partially_mapped path:

---8<---
Subject: [PATCH 1/1] mm/thp: avoid false warning in deferred_split_folio()

From: Lance Yang <[email protected]>

migrate_folio_move() snapshots src_partially_mapped from src before migration and later requeues dst after remove_migration_ptes(src, dst, 0).

Once dst is visible again, a competing rmap-removal path can legally set PG_partially_mapped before the migration path reaches deferred_split_folio(dst, src_partially_mapped).

Migration already holds the folio lock on dst, while the competing rmap-removal path runs under the page-table lock. So once remove_migration_ptes(src, dst, 0) makes dst visible again, this race looks hard to avoid.

So just drop the WARN in the !partially_mapped path and preserve an already-set PG_partially_mapped bit.

Fixes 8a8ca142a488 ("mm: migrate: requeue destination folio on deferred split queue")
Reported-by [email protected]
---
 mm/huge_memory.c | 3 ---
 1 file changed, 3 deletions(-)
mm/huge_memory.c +0 -0
index 745eb3d0d4a7..8ea8e293dc7c 100644
@@ -4433,9 +4433,6 @@ void deferred_split_folio(struct folio *folio, bool partially_mapped)
 			mod_mthp_stat(folio_order(folio), MTHP_STAT_NR_ANON_PARTIALLY_MAPPED, 1);
 
Quoted 7 lines

mm/huge_memory.c void deferred_split_folio(struct folio *folio, bool partially_mapped) · line 4436

Quoted 5 lines
mm/huge_memory.c +0 -3
 		}
-	} else {
-		/* partially mapped folios cannot become non-partially mapped */
-		VM_WARN_ON_FOLIO(folio_test_partially_mapped(folio), folio);
 	}

Can't we simply move the setting before restoring migration ptes?

Afraid not, it closes the remove_migration_ptes() -> deferred_split_folio() race, but opens a new one with the shrinker, IIUC

Once dst is on the deferred split queue, deferred_split_scan() can pick it up immediately. The shrinker unconditionally dequeues every folio it visits:

	list_del_init(&folio->_deferred_list);   /* always */
Quoted 8 lines

Then for a non-partially-mapped folio, if folio_trylock() fails (dst is still locked by migration), it falls through to:

next:

		if (did_split || !folio_test_partially_mapped(folio))
			continue;  /* not requeued, dst silently lost */

so it is *not* requeued.

How is that different to the shrinker just trying to lock the folio before we unlock it and failing? The race already exists?

To sort out that race a trylock must not result in the folio getting discarded.

mm/huge_memory.c +2 -1
index ff9a42abd1b6..521989517cd1 100644
@@ -4558,7 +4558,7 @@ static unsigned long deferred_split_scan(struct shrinker *shrink,
                                goto next;
                }
                if (!folio_trylock(folio))
-                       goto next;
+                       goto requeue:
                if (!split_folio(folio)) {
                        did_split = true;
                        if (underused)
@@ -4569,6 +4569,7 @@ static unsigned long deferred_split_scan(struct shrinker *shrink,
 next:
                if (did_split || !folio_test_partially_mapped(folio))
                        continue;
+requeue:
                /*
                 * Only add back to the queue if folio is partially mapped.
                 * If thp_underused returns false, or if split_folio fails
Signature · 3 lines
Cheers,

David
reviewer
#9 lore raw
Quoted 74 lines · David Hildenbrand (Arm)
Quoted 73 lines · Lance Yang

+Cc Deepanshu

Quoted 49 lines · David Hildenbrand (Arm)
Quoted 48 lines · Lance Yang
Quoted 2 lines

from another sharer can then remove some of those mappings and reach

Perhaps the WARN is simply too strict there :)

Migration already holds the folio lock on dst, while the competing rmap-removal path runs under the page-table lock. So once remove_migration_ptes(src, dst, 0) makes dst visible again, this race looks hard to avoid.

So maybe the simplest fix is just to drop the WARN in the !partially_mapped path:

---8<---
Subject: [PATCH 1/1] mm/thp: avoid false warning in deferred_split_folio()

From: Lance Yang <[email protected]>

migrate_folio_move() snapshots src_partially_mapped from src before migration and later requeues dst after remove_migration_ptes(src, dst, 0).

Once dst is visible again, a competing rmap-removal path can legally set PG_partially_mapped before the migration path reaches deferred_split_folio(dst, src_partially_mapped).

Migration already holds the folio lock on dst, while the competing rmap-removal path runs under the page-table lock. So once remove_migration_ptes(src, dst, 0) makes dst visible again, this race looks hard to avoid.

So just drop the WARN in the !partially_mapped path and preserve an already-set PG_partially_mapped bit.

Fixes 8a8ca142a488 ("mm: migrate: requeue destination folio on deferred split queue")
Reported-by [email protected]
---
 mm/huge_memory.c | 3 ---
 1 file changed, 3 deletions(-)
mm/huge_memory.c +0 -0
index 745eb3d0d4a7..8ea8e293dc7c 100644
@@ -4433,9 +4433,6 @@ void deferred_split_folio(struct folio *folio, bool partially_mapped)
 			mod_mthp_stat(folio_order(folio), MTHP_STAT_NR_ANON_PARTIALLY_MAPPED, 1);
 
Quoted 7 lines

mm/huge_memory.c void deferred_split_folio(struct folio *folio, bool partially_mapped) · line 4436

Quoted 5 lines
mm/huge_memory.c +0 -3
 		}
-	} else {
-		/* partially mapped folios cannot become non-partially mapped */
-		VM_WARN_ON_FOLIO(folio_test_partially_mapped(folio), folio);
 	}

Can't we simply move the setting before restoring migration ptes?

Afraid not, it closes the remove_migration_ptes() -> deferred_split_folio() race, but opens a new one with the shrinker, IIUC

Once dst is on the deferred split queue, deferred_split_scan() can pick it up immediately. The shrinker unconditionally dequeues every folio it visits:

	list_del_init(&folio->_deferred_list);   /* always */

Then for a non-partially-mapped folio, if folio_trylock() fails (dst is still locked by migration), it falls through to:

Quoted 8 lines
Quoted 5 lines

next:

		if (did_split || !folio_test_partially_mapped(folio))
			continue;  /* not requeued, dst silently lost */

so it is *not* requeued.

How is that different to the shrinker just trying to lock the folio before we unlock it and failing? The race already exists?

Ouch, you're right, I was wrong - the trylock drop is a pre-existing issue, not caused by the reorder ;)

Quoted 3 lines

To sort out that race a trylock must not result in the folio getting discarded.

Nice, LGTM!

Given that the "trylock -> drop" behavior seems to exist already today, do you think it's worth fixing that together with the reorder?

Quoted 23 lines
mm/huge_memory.c +1 -1
index ff9a42abd1b6..521989517cd1 100644
@@ -4558,7 +4558,7 @@ static unsigned long deferred_split_scan(struct shrinker *shrink,
                               goto next;
               }
               if (!folio_trylock(folio))
-                       goto next;
+                       goto requeue:
               if (!split_folio(folio)) {
                       did_split = true;
                       if (underused)
@@ -4569,6 +4569,7 @@ static unsigned long deferred_split_scan(struct shrinker *shrink,

next:

               if (did_split || !folio_test_partially_mapped(folio))
                       continue;

+requeue:
/*
* Only add back to the queue if folio is partially mapped.
* If thp_underused returns false, or if split_folio fails

#10 lore raw
Quoted 32 lines · Lance Yang
Quoted 17 lines · David Hildenbrand (Arm)
Quoted 16 lines · Lance Yang

+Cc Deepanshu

Afraid not, it closes the remove_migration_ptes() -> deferred_split_folio() race, but opens a new one with the shrinker, IIUC

Once dst is on the deferred split queue, deferred_split_scan() can pick it up immediately. The shrinker unconditionally dequeues every folio it visits:

	list_del_init(&folio->_deferred_list);   /* always */

Then for a non-partially-mapped folio, if folio_trylock() fails (dst is still locked by migration), it falls through to:

Quoted 8 lines
Quoted 5 lines

next:

		if (did_split || !folio_test_partially_mapped(folio))
			continue;  /* not requeued, dst silently lost */

so it is *not* requeued.

How is that different to the shrinker just trying to lock the folio before we unlock it and failing? The race already exists?

Ouch, you're right, I was wrong - the trylock drop is a pre-existing issue, not caused by the reorder ;)

Quoted 1 line
Quoted 7 lines
Quoted 2 lines

To sort out that race a trylock must not result in the folio getting discarded.

Nice, LGTM!

Given that the "trylock -> drop" behavior seems to exist already today, do you think it's worth fixing that together with the reorder?

I'd do it in a single shot if possible.

Can you craft something? (cc stable etc)

Signature · 3 lines
Cheers,

David
reviewer
#11 lore raw
Quoted 36 lines · David Hildenbrand (Arm)
Quoted 35 lines · Lance Yang
Quoted 17 lines · David Hildenbrand (Arm)
Quoted 16 lines · Lance Yang

+Cc Deepanshu

Afraid not, it closes the remove_migration_ptes() -> deferred_split_folio() race, but opens a new one with the shrinker, IIUC

Once dst is on the deferred split queue, deferred_split_scan() can pick it up immediately. The shrinker unconditionally dequeues every folio it visits:

	list_del_init(&folio->_deferred_list);   /* always */

Then for a non-partially-mapped folio, if folio_trylock() fails (dst is still locked by migration), it falls through to:

Quoted 8 lines
Quoted 5 lines

next:

		if (did_split || !folio_test_partially_mapped(folio))
			continue;  /* not requeued, dst silently lost */

so it is *not* requeued.

How is that different to the shrinker just trying to lock the folio before we unlock it and failing? The race already exists?

Ouch, you're right, I was wrong - the trylock drop is a pre-existing issue, not caused by the reorder ;)

Quoted 3 lines

To sort out that race a trylock must not result in the folio getting discarded.

Quoted 6 lines
Quoted 4 lines

Nice, LGTM!

Given that the "trylock -> drop" behavior seems to exist already today, do you think it's worth fixing that together with the reorder?

I'd do it in a single shot if possible.

ACK.

Quoted 1 line

Can you craft something? (cc stable etc)

certainly, will do! But commit[1] ("mm: migrate: requeue destination folio on deferred split queue") is only in mm-stable now, not yet upstream/stable ...

[1] https://lore.kernel.org/mm-commits/[email protected]/

#12 lore raw
Quoted 16 lines · Lance Yang
Quoted 7 lines · David Hildenbrand (Arm)
Quoted 6 lines · Lance Yang

Ouch, you're right, I was wrong - the trylock drop is a pre-existing issue, not caused by the reorder ;)

Quoted 6 lines
Quoted 4 lines

Nice, LGTM!

Given that the "trylock -> drop" behavior seems to exist already today, do you think it's worth fixing that together with the reorder?

I'd do it in a single shot if possible.

Quoted 7 lines

ACK.

Quoted 1 line

Can you craft something? (cc stable etc)

certainly, will do! But commit[1] ("mm: migrate: requeue destination folio on deferred split queue") is only in mm-stable now, not yet upstream/stable ...

It's tricky. The original commit will be backported to stable kernels, so we want also the fix to be backported to the same stable kernels.

The commit id in mm-stable is "stable" now.

Signature · 3 lines
Cheers,

David
reviewer
#13 lore raw
Quoted 21 lines · David Hildenbrand (Arm)
Quoted 20 lines · Lance Yang
Quoted 7 lines · David Hildenbrand (Arm)
Quoted 6 lines · Lance Yang

Ouch, you're right, I was wrong - the trylock drop is a pre-existing issue, not caused by the reorder ;)

Quoted 6 lines
Quoted 4 lines

Nice, LGTM!

Given that the "trylock -> drop" behavior seems to exist already today, do you think it's worth fixing that together with the reorder?

I'd do it in a single shot if possible.

ACK.

Quoted 1 line

Can you craft something? (cc stable etc)

Quoted 8 lines
Quoted 3 lines

certainly, will do! But commit[1] ("mm: migrate: requeue destination folio on deferred split queue") is only in mm-stable now, not yet upstream/stable ...

It's tricky. The original commit will be backported to stable kernels, so we want also the fix to be backported to the same stable kernels.

The commit id in mm-stable is "stable" now.

Emm... Not a big deal, I guess. We can always submit a stable backport once that fix gets merged into stable :D

#14 lore raw
Quoted 13 lines · Lance Yang
Quoted 10 lines · David Hildenbrand (Arm)
Quoted 8 lines · Lance Yang

ACK.

certainly, will do! But commit[1] ("mm: migrate: requeue destination folio on deferred split queue") is only in mm-stable now, not yet upstream/stable ...

Quoted 7 lines
Quoted 4 lines

It's tricky. The original commit will be backported to stable kernels, so we want also the fix to be backported to the same stable kernels.

The commit id in mm-stable is "stable" now.

Emm... Not a big deal, I guess. We can always submit a stable backport once that fix gets merged into stable :D

What's the problem with tagging the commit right way as Cc: stable?

Signature · 3 lines
Cheers,

David
reviewer
#15 lore raw
Quoted 17 lines · David Hildenbrand (Arm)
Quoted 16 lines · Lance Yang
Quoted 13 lines · David Hildenbrand (Arm)
Quoted 8 lines · Lance Yang

ACK.

certainly, will do! But commit[1] ("mm: migrate: requeue destination folio on deferred split queue") is only in mm-stable now, not yet upstream/stable ...

It's tricky. The original commit will be backported to stable kernels, so we want also the fix to be backported to the same stable kernels.

Quoted 6 lines
Quoted 4 lines
Quoted 1 line

The commit id in mm-stable is "stable" now.

Emm... Not a big deal, I guess. We can always submit a stable backport once that fix gets merged into stable :D

What's the problem with tagging the commit right way as Cc: stable?

Sure, that makes sense. I'll add "Cc: stable" to the fix as well, so it can follow 8a8ca142a488 into stable ;)

reviewer
#16 lore raw
Quoted 22 lines · Lance Yang
Quoted 19 lines · David Hildenbrand (Arm)
Quoted 18 lines · Lance Yang
Quoted 14 lines · David Hildenbrand (Arm)
Quoted 8 lines · Lance Yang

ACK.

certainly, will do! But commit[1] ("mm: migrate: requeue destination folio on deferred split queue") is only in mm-stable now, not yet upstream/stable ...

It's tricky. The original commit will be backported to stable kernels, so we want also the fix to be backported to the same stable kernels.

The commit id in mm-stable is "stable" now.

Quoted 7 lines
Quoted 4 lines
Quoted 2 lines

Emm... Not a big deal, I guess. We can always submit a stable backport once that fix gets merged into stable :D

What's the problem with tagging the commit right way as Cc: stable?

Sure, that makes sense. I'll add "Cc: stable" to the fix as well, so it can follow 8a8ca142a488 into stable ;)

8a8ca142a488 refers to commit "mm: migrate: requeue destination folio on deferred split queue".