mm: thp: unlock i_mmap before releasing split folios
Subsystems (2)
PATCH 1/1 mm: thp: unlock i_mmap before releasing split folios
From: Hao Zhang <[email protected]>
Date: Thu, 9 Jul 2026 16:30:00 +0800
__folio_split() takes mapping->i_mmap_rwsem while unmapping and splitting a file-backed large folio. The lock is currently released only after the split folios that are not returned locked to the caller have been unlocked and put.
That leaves a lifetime hole. Once the split folios are unlocked and their references are dropped, inode eviction can make progress through the final page-cache truncation path. After the folios are removed from the page cache and the last inode reference is dropped, the inode that embeds the address_space can be freed after an RCU grace period. A later i_mmap_unlock_read(mapping) then dereferences mapping->i_mmap_rwsem from freed memory.
A possible race is:
CPU0 CPU1
__folio_split()
i_mmap_lock_read(mapping)
...
remap_page()
folio_unlock(new_folio)
free_folio_and_swap_cache(new_folio)
evict inode
truncate_inode_pages_final()
destroy_inode()
call_rcu()
RCU callback frees inode
i_mmap_unlock_read(mapping)Release mapping->i_mmap_rwsem after the page-cache and reverse-mapping work has completed, but before unlocking and putting any of the split folios. Clear the local mapping pointer after the early unlock so the common exit path does not unlock it a second time.
Tested with syz-repro on the two available crash logs; neither reproduced the __folio_split KASAN report on the patched kernel.
--- mm/huge_memory.c | 5 +++++ 1 file changed, 5 insertions(+)
mm/huge_memory.c +5 -0
@@ -4109,6 +4109,11 @@ static int __folio_split(struct folio *folio, unsigned int new_order,remap_page (folio,1 << old_order, ttu_flags); +if (mapping) { +i_mmap_unlock_read (mapping); + mapping =NULL ; + } +/* * Unlock all after-split folios except the one containing * @lock_at page. If @folio is not split, it will be kept locked.
base-commit: 0e35b9b6ec0ffcc5e23cbdec09f5c622ad532b53
Signature · 1 line
2.15.0
Quoted 32 lines · Hao Zhang
From: Hao Zhang <[email protected]>
Date: Thu, 9 Jul 2026 16:30:00 +0800
__folio_split()takesmapping->i_mmap_rwsemwhile unmapping and splitting a file-backed large folio. The lock is currently released only after the split folios that are not returned locked to the caller have been unlocked and put.That leaves a lifetime hole. Once the split folios are unlocked and their references are dropped, inode eviction can make progress through the final page-cache truncation path. After the folios are removed from the page cache and the last inode reference is dropped, the inode that embeds the
address_spacecan be freed after an RCU grace period. A lateri_mmap_unlock_read(mapping)then dereferencesmapping->i_mmap_rwsemfrom freed memory.A possible race is:
CPU0 CPU1
__folio_split()i_mmap_lock_read(mapping) ... remap_page() folio_unlock(new_folio) free_folio_and_swap_cache(new_folio) evict inode truncate_inode_pages_final() destroy_inode() call_rcu() RCU callback frees inode i_mmap_unlock_read(mapping)
Quoted 4 lines
Release
mapping->i_mmap_rwsemafter the page-cache and reverse-mapping work has completed, but before unlocking and putting any of the split folios. Clear the local mapping pointer after the early unlock so the common exit path does not unlock it a second time.
I don't buy this analysis. We still have the lock_at page which is locked and still in the page cache. Inode eviction cannot complete past truncate_inode_pages_final() until it is unlocked, which only happens after __folio_split() has released i_mmap_rwsem.
One possible path how this can be hit is if lock_at ends up in an after-split folio beyond EOF and __folio_freeze_and_split_unmapped() removes it from the page cache. Note the drop loop starts at folio_next(folio), so this requires splitting at a tail page (e.g. memory-failure) racing with truncate.
But that's not what your analysis claims. Please share the actual crash reports. Let's get to the bottom of the situation before changing the code.
Signature · 1 line
Kiryl Shutsemau / Kirill A. Shutemov
Quoted 49 lines · Kiryl Shutsemau
Quoted 32 lines · Hao Zhang
From: Hao Zhang <[email protected]>
Date: Thu, 9 Jul 2026 16:30:00 +0800
__folio_split()takesmapping->i_mmap_rwsemwhile unmapping and splitting a file-backed large folio. The lock is currently released only after the split folios that are not returned locked to the caller have been unlocked and put.That leaves a lifetime hole. Once the split folios are unlocked and their references are dropped, inode eviction can make progress through the final page-cache truncation path. After the folios are removed from the page cache and the last inode reference is dropped, the inode that embeds the
address_spacecan be freed after an RCU grace period. A lateri_mmap_unlock_read(mapping)then dereferencesmapping->i_mmap_rwsemfrom freed memory.A possible race is:
CPU0 CPU1
__folio_split()i_mmap_lock_read(mapping) ... remap_page() folio_unlock(new_folio) free_folio_and_swap_cache(new_folio) evict inode truncate_inode_pages_final() destroy_inode() call_rcu() RCU callback frees inode i_mmap_unlock_read(mapping)Quoted 4 lines
Release
mapping->i_mmap_rwsemafter the page-cache and reverse-mapping work has completed, but before unlocking and putting any of the split folios. Clear the local mapping pointer after the early unlock so the common exit path does not unlock it a second time.I don't buy this analysis. We still have the
lock_atpage which is locked and still in the page cache. Inode eviction cannot complete pasttruncate_inode_pages_final()until it is unlocked, which only happens after__folio_split()has releasedi_mmap_rwsem.One possible path how this can be hit is if
lock_atends up in an after-split folio beyond EOF and__folio_freeze_and_split_unmapped()removes it from the page cache. Note the drop loop starts atfolio_next(folio), so this requires splitting at a tail page (e.g. memory-failure) racing with truncate.
Quoted 7 lines
But that's not what your analysis claims. Please share the actual crash reports. Let's get to the bottom of the situation before changing the code.
Signature · 1 line
Kiryl Shutsemau / Kirill A. Shutemov
Memory failure: 0x5a6a7: recovery action for clean LRU page: Recovered Injecting memory failure for pfn 0x81bfe at process virtual address 0x200000ffe000
==================================================================
BUG: KASAN: slab-use-after-free in __up_read+0x634/0x790 data/linux/kernel/locking/rwsem.c:1353 Read of size 8 at addr ffff88800b81c5e8 by task syz.2.11034/45874 CPU: 0 UID: 0 PID: 45874 Comm: syz.2.11034 Not tainted 7.0.0-rc3base+ #62 PREEMPT(full) Hardware name: QEMU Standard PC (i440FX + PIIX, 1996), BIOS rel-1.16.2-0-gea1b7a073390-prebuilt.qemu.org 04/01/2014 Call Trace: <TASK> __dump_stack data/linux/lib/dump_stack.c:94 [inline] dump_stack_lvl+0xbe/0x130 data/linux/lib/dump_stack.c:120 print_address_description data/linux/mm/kasan/report.c:378 [inline] print_report+0xd1/0x660 data/linux/mm/kasan/report.c:482 kasan_report+0xec/0x130 data/linux/mm/kasan/report.c:595 __asan_report_load8_noabort+0x14/0x30 data/linux/mm/kasan/report_generic.c:381 __up_read+0x634/0x790 data/linux/kernel/locking/rwsem.c:1353 up_read+0x22/0x30 data/linux/kernel/locking/rwsem.c:1633 i_mmap_unlock_read data/linux/include/linux/fs.h:537 [inline] __folio_split+0x732/0x1640 data/linux/mm/huge_memory.c:4100 __split_huge_page_to_list_to_order+0x7b/0x140 data/linux/mm/huge_memory.c:4203 split_huge_page_to_list_to_order data/linux/include/linux/huge_mm.h:385 [inline] split_huge_page_to_order data/linux/include/linux/huge_mm.h:389 [inline] try_to_split_thp_page+0xab/0x390 data/linux/mm/memory-failure.c:1675 memory_failure+0x1394/0x26e0 data/linux/mm/memory-failure.c:2470 madvise_inject_error data/linux/mm/madvise.c:1487 [inline] madvise_do_behavior+0x4ae/0x8a0 data/linux/mm/madvise.c:1925 do_madvise+0x162/0x230 data/linux/mm/madvise.c:2028 __do_sys_madvise data/linux/mm/madvise.c:2037 [inline] __se_sys_madvise data/linux/mm/madvise.c:2035 [inline] __x64_sys_madvise+0xae/0x120 data/linux/mm/madvise.c:2035 x64_sys_call+0x1bed/0x25e0 data/linux/arch/x86/include/generated/asm/syscalls_64.h:29 do_syscall_x64 data/linux/arch/x86/entry/syscall_64.c:63 [inline] do_syscall_64+0xe2/0x14e0 data/linux/arch/x86/entry/syscall_64.c:94 entry_SYSCALL_64_after_hwframe+0x76/0x7e RIP: 0033:0x762e87f8f109 Code: ff ff c3 66 2e 0f 1f 84 00 00 00 00 00 0f 1f 40 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 a8 ff ff ff f7 d8 64 89 01 48 RSP: 002b:0000762e88d6d038 EFLAGS: 00000246 ORIG_RAX: 000000000000001c RAX: ffffffffffffffda RBX: 0000762e881e5fa0 RCX: 0000762e87f8f109 RDX: 0000000000000064 RSI: 0000000000003000 RDI: 0000200000ffd000 RBP: 0000762e88011f91 R08: 0000000000000000 R09: 0000000000000000 R10: 0000000000000000 R11: 0000000000000246 R12: 0000000000000000 R13: 0000762e881e6038 R14: 0000762e881e5fa0 R15: 00007ffdc9863dc8 </TASK> Allocated by task 45874: kasan_save_stack+0x39/0x70 data/linux/mm/kasan/common.c:57 kasan_save_track+0x14/0x40 data/linux/mm/kasan/common.c:78 kasan_save_alloc_info+0x37/0x60 data/linux/mm/kasan/generic.c:570 unpoison_slab_object data/linux/mm/kasan/common.c:340 [inline] __kasan_slab_alloc+0x9d/0xa0 data/linux/mm/kasan/common.c:366 kasan_slab_alloc data/linux/include/linux/kasan.h:253 [inline] slab_post_alloc_hook data/linux/mm/slub.c:4542 [inline] slab_alloc_node data/linux/mm/slub.c:4869 [inline] kmem_cache_alloc_lru_noprof+0x1e6/0x6b0 data/linux/mm/slub.c:4888 shmem_alloc_inode+0x2a/0x60 data/linux/mm/shmem.c:5167 alloc_inode+0x6b/0x230 data/linux/fs/inode.c:347 new_inode+0x26/0x1c0 data/linux/fs/inode.c:1185 __shmem_get_inode data/linux/mm/shmem.c:3079 [inline] shmem_get_inode+0x1de/0x1020 data/linux/mm/shmem.c:3154 __shmem_file_setup+0x328/0x3e0 data/linux/mm/shmem.c:5852 shmem_kernel_file_setup data/linux/mm/shmem.c:5883 [inline] __shmem_zero_setup data/linux/mm/shmem.c:5924 [inline] shmem_zero_setup+0x9c/0x1b0 data/linux/mm/shmem.c:5934 __mmap_new_vma data/linux/mm/vma.c:2534 [inline] __mmap_region+0x2046/0x2a60 data/linux/mm/vma.c:2759 mmap_region+0x162/0x320 data/linux/mm/vma.c:2837 do_mmap+0x892/0x1000 data/linux/mm/mmap.c:559 vm_mmap_pgoff+0x2a3/0x450 data/linux/mm/util.c:581 ksys_mmap_pgoff+0xc8/0x5e0 data/linux/mm/mmap.c:605 __do_sys_mmap data/linux/arch/x86/kernel/sys_x86_64.c:89 [inline] __se_sys_mmap data/linux/arch/x86/kernel/sys_x86_64.c:82 [inline] __x64_sys_mmap+0x11a/0x1b0 data/linux/arch/x86/kernel/sys_x86_64.c:82 x64_sys_call+0x1afd/0x25e0 data/linux/arch/x86/include/generated/asm/syscalls_64.h:10 do_syscall_x64 data/linux/arch/x86/entry/syscall_64.c:63 [inline] do_syscall_64+0xe2/0x14e0 data/linux/arch/x86/entry/syscall_64.c:94 entry_SYSCALL_64_after_hwframe+0x76/0x7e Freed by task 4601: kasan_save_stack+0x39/0x70 data/linux/mm/kasan/common.c:57 kasan_save_track+0x14/0x40 data/linux/mm/kasan/common.c:78 kasan_save_free_info+0x3b/0x60 data/linux/mm/kasan/generic.c:584 poison_slab_object data/linux/mm/kasan/common.c:253 [inline] __kasan_slab_free+0x6f/0xa0 data/linux/mm/kasan/common.c:285 kasan_slab_free data/linux/include/linux/kasan.h:235 [inline] slab_free_hook data/linux/mm/slub.c:2692 [inline] slab_free data/linux/mm/slub.c:6168 [inline] kmem_cache_free+0x2c4/0x650 data/linux/mm/slub.c:6298 shmem_free_in_core_inode+0x54/0xb0 data/linux/mm/shmem.c:5177 i_callback+0x4c/0xa0 data/linux/fs/inode.c:326 rcu_do_batch+0x3af/0xdf0 data/linux/kernel/rcu/tree.c:2617 rcu_core+0x4af/0x6f0 data/linux/kernel/rcu/tree.c:2869 rcu_core_si+0xe/0x20 data/linux/kernel/rcu/tree.c:2886 handle_softirqs+0x200/0x940 data/linux/kernel/softirq.c:622 __do_softirq data/linux/kernel/softirq.c:656 [inline] invoke_softirq data/linux/kernel/softirq.c:496 [inline] __irq_exit_rcu+0x1ca/0x220 data/linux/kernel/softirq.c:723 irq_exit_rcu+0xe/0x20 data/linux/kernel/softirq.c:739 instr_sysvec_apic_timer_interrupt data/linux/arch/x86/kernel/apic/apic.c:1056 [inline] sysvec_apic_timer_interrupt+0x92/0xd0 data/linux/arch/x86/kernel/apic/apic.c:1056 asm_sysvec_apic_timer_interrupt+0x1b/0x20 data/linux/arch/x86/include/asm/idtentry.h:569 Last potentially related work creation: kasan_save_stack+0x39/0x70 data/linux/mm/kasan/common.c:57 kasan_record_aux_stack+0xae/0xd0 data/linux/mm/kasan/generic.c:556 __call_rcu_common+0xcc/0x1310 data/linux/kernel/rcu/tree.c:3131 call_rcu+0x34/0x50 data/linux/kernel/rcu/tree.c:3251 destroy_inode+0x144/0x1e0 data/linux/fs/inode.c:402 evict+0x57f/0xac0 data/linux/fs/inode.c:870 iput_final data/linux/fs/inode.c:1966 [inline] iput data/linux/fs/inode.c:2015 [inline] iput+0x6a0/0xb90 data/linux/fs/inode.c:1978 dentry_unlink_inode+0x2a4/0x460 data/linux/fs/dcache.c:467 __dentry_kill+0x1b2/0x5b0 data/linux/fs/dcache.c:670 finish_dput+0x69/0x3e0 data/linux/fs/dcache.c:879 dput.part.0+0x3c2/0x4d0 data/linux/fs/dcache.c:928 dput+0x24/0x40 data/linux/fs/dcache.c:920 __fput+0x49b/0xaa0 data/linux/fs/file_table.c:477 ____fput+0x1e/0x30 data/linux/fs/file_table.c:497 task_work_run+0x16a/0x270 data/linux/kernel/task_work.c:233 resume_user_mode_work data/linux/include/linux/resume_user_mode.h:50 [inline] __exit_to_user_mode_loop data/linux/kernel/entry/common.c:67 [inline] exit_to_user_mode_loop+0x185/0x600 data/linux/kernel/entry/common.c:98 __exit_to_user_mode_prepare data/linux/include/linux/irq-entry-common.h:226 [inline] syscall_exit_to_user_mode_prepare data/linux/include/linux/irq-entry-common.h:256 [inline] syscall_exit_to_user_mode data/linux/include/linux/entry-common.h:325 [inline] do_syscall_64+0x5dc/0x14e0 data/linux/arch/x86/entry/syscall_64.c:100 entry_SYSCALL_64_after_hwframe+0x76/0x7e The buggy address belongs to the object at ffff88800b81c060 which belongs to the cache shmem_inode_cache of size 1520
The buggy address is located 1416 bytes inside of
freed 1520-byte region [ffff88800b81c060, ffff88800b81c650)
The buggy address belongs to the physical page: page: refcount:0 mapcount:0 mapping:0000000000000000 index:0x0 pfn:0xb818
head: order:3 mapcount:0 entire_mapcount:0 nr_pages_mapped:0 pincount:0 memcg:ffff88800b81fa51
flags: 0xfffffc0000040(head|node=0|zone=1|lastcpupid=0x1fffff)page_type: f5(slab)
raw: 000fffffc0000040 ffff8880018618c0 dead000000000100 dead000000000122 raw: 0000000000000000 0000000800130013 00000000f5000000 ffff88800b81fa51
head: 000fffffc0000040 ffff8880018618c0 dead000000000100 dead000000000122
head: 0000000000000000 0000000800130013 00000000f5000000 ffff88800b81fa51
head: 000fffffc0000003 ffffea00002e0601 00000000ffffffff 00000000ffffffff
head: ffffffffffffffff 0000000000000000 00000000ffffffff 0000000000000008
page dumped because: kasan: bad access detected
Memory state around the buggy address:
ffff88800b81c480: fb fb fb fb fb fb fb fb fb fb fb fb fb fb fb fb
ffff88800b81c500: fb fb fb fb fb fb fb fb fb fb fb fb fb fb fb fb
>ffff88800b81c580: fb fb fb fb fb fb fb fb fb fb fb fb fb fb fb fb
^
ffff88800b81c600: fb fb fb fb fb fb fb fb fb fb fc fc fc fc fc fc
ffff88800b81c680: fc fc fc fc fc fc fc fc fc fc fa fb fb fb fb fb
==================================================================Memory failure: 0x81bfe: recovery action for already truncated LRU page: Ignored Soft offlining pfn 0x7a60f at process virtual address 0x200000841000
Soft offlining pfn 0x729b9 at process virtual address 0x200000842000
Soft offlining pfn 0x771f9 at process virtual address 0x200000843000
Injecting memory failure for pfn 0x7b9f2 at process virtual address 0x200000ffe000 Memory failure: 0x7b9f2: Sending SIGBUS to syz.1.11044:45913 due to hardware memory corruption Memory failure: 0x7b9f2: recovery action for dirty LRU page: Recovered Injecting memory failure for pfn 0x7a1ff at process virtual address 0x200000fff000 Memory failure: 0x7a1ff: Sending SIGBUS to syz.1.11044:45913 due to hardware memory corruption Memory failure: 0x7a1ff: recovery action for dirty LRU page: Recovered
<<<<<<<<<<<<<<< tail report >>>>>>>>>>>>>>>
This is the crash report, please review.
I hate to have to keep pointing this out to people and maybe you were trying to limit the mailing list rather to maintainers only buuuut :)
$ scripts/get_maintainer.pl mm/huge_memory.c Andrew Morton <[email protected]> (maintainer:MEMORY MANAGEMENT - THP (TRANSPARENT HUGE PAGE)) David Hildenbrand <[email protected]> (maintainer:MEMORY MANAGEMENT - THP (TRANSPARENT HUGE PAGE)) Lorenzo Stoakes <[email protected]> (maintainer:MEMORY MANAGEMENT - THP (TRANSPARENT HUGE PAGE)) Zi Yan <[email protected]> (reviewer:MEMORY MANAGEMENT - THP (TRANSPARENT HUGE PAGE)) Baolin Wang <[email protected]> (reviewer:MEMORY MANAGEMENT - THP (TRANSPARENT HUGE PAGE)) "Liam R. Howlett" <[email protected]> (reviewer:MEMORY MANAGEMENT - THP (TRANSPARENT HUGE PAGE)) Nico Pache <[email protected]> (reviewer:MEMORY MANAGEMENT - THP (TRANSPARENT HUGE PAGE)) Ryan Roberts <[email protected]> (reviewer:MEMORY MANAGEMENT - THP (TRANSPARENT HUGE PAGE)) Dev Jain <[email protected]> (reviewer:MEMORY MANAGEMENT - THP (TRANSPARENT HUGE PAGE)) Barry Song <[email protected]> (reviewer:MEMORY MANAGEMENT - THP (TRANSPARENT HUGE PAGE)) Lance Yang <[email protected]> (reviewer:MEMORY MANAGEMENT - THP (TRANSPARENT HUGE PAGE)) Usama Arif <[email protected]> (reviewer:MEMORY MANAGEMENT - THP (TRANSPARENT HUGE PAGE)) [email protected] (open list:MEMORY MANAGEMENT - THP (TRANSPARENT HUGE PAGE)) [email protected] (open list)
It definitely helps to cc everybody relevant when it comes to getting review.
Quoted 12 lines · Hao Zhang
Memory failure:
0x5a6a7: recovery action for clean LRU page: Recovered Injecting memory failure for pfn0x81bfeat process virtual address0x200000ffe000
==================================================================BUG: KASAN: slab-use-after-free in __up_read+0x634/0x790 data/linux/kernel/locking/rwsem.c:1353 Read of size 8 at addr ffff88800b81c5e8 by task syz.2.11034/45874...
<<<<<<<<<<<<<<< tail report >>>>>>>>>>>>>>>
This is the crash report, please review.
Thanks but do you have any of the other pieces around this to help? Like a reproducer, .config, the arch you ran it on, etc. etc.?
In general please do ensure you evidence things like this without needing to be asked for it, we can't just take your word for it and the more you do to make our lives easier the quicker we can get the review done.
We've had real issues in the past with private syzbot reports where it's turned out on more than one occasion to not actually be a genuine issue.
So your help at a time where mm review is _totally_ overwhelmed would be much appreciated :)
Thanks, Lorenzo
Quoted 93 lines · Hao Zhang
Quoted 49 lines · Kiryl Shutsemau
Quoted 32 lines · Hao Zhang
From: Hao Zhang <[email protected]>
Date: Thu, 9 Jul 2026 16:30:00 +0800
__folio_split()takesmapping->i_mmap_rwsemwhile unmapping and splitting a file-backed large folio. The lock is currently released only after the split folios that are not returned locked to the caller have been unlocked and put.That leaves a lifetime hole. Once the split folios are unlocked and their references are dropped, inode eviction can make progress through the final page-cache truncation path. After the folios are removed from the page cache and the last inode reference is dropped, the inode that embeds the
address_spacecan be freed after an RCU grace period. A lateri_mmap_unlock_read(mapping)then dereferencesmapping->i_mmap_rwsemfrom freed memory.A possible race is:
CPU0 CPU1
__folio_split()i_mmap_lock_read(mapping) ... remap_page() folio_unlock(new_folio) free_folio_and_swap_cache(new_folio) evict inode truncate_inode_pages_final() destroy_inode() call_rcu() RCU callback frees inode i_mmap_unlock_read(mapping)Quoted 4 lines
Release
mapping->i_mmap_rwsemafter the page-cache and reverse-mapping work has completed, but before unlocking and putting any of the split folios. Clear the local mapping pointer after the early unlock so the common exit path does not unlock it a second time.I don't buy this analysis. We still have the
lock_atpage which is locked and still in the page cache. Inode eviction cannot complete pasttruncate_inode_pages_final()until it is unlocked, which only happens after__folio_split()has releasedi_mmap_rwsem.One possible path how this can be hit is if
lock_atends up in an after-split folio beyond EOF and__folio_freeze_and_split_unmapped()removes it from the page cache. Note the drop loop starts atfolio_next(folio), so this requires splitting at a tail page (e.g. memory-failure) racing with truncate.Quoted 7 lines
But that's not what your analysis claims. Please share the actual crash reports. Let's get to the bottom of the situation before changing the code.
Signature · 1 line
Kiryl Shutsemau / Kirill A. ShutemovMemory failure:
0x5a6a7: recovery action for clean LRU page: Recovered Injecting memory failure for pfn0x81bfeat process virtual address0x200000ffe000
==================================================================BUG: KASAN: slab-use-after-free in __up_read+0x634/0x790 data/linux/kernel/locking/rwsem.c:1353 Read of size 8 at addr ffff88800b81c5e8 by task syz.2.11034/45874 CPU: 0 UID: 0 PID: 45874 Comm: syz.2.11034 Not tainted 7.0.0-rc3base+ #62 PREEMPT(full) Hardware name: QEMU Standard PC (i440FX + PIIX, 1996), BIOS rel-1.16.2-0-gea1b7a073390-prebuilt.qemu.org 04/01/2014 Call Trace: <TASK> __dump_stack data/linux/lib/dump_stack.c:94 [inline] dump_stack_lvl+0xbe/0x130 data/linux/lib/dump_stack.c:120 print_address_description data/linux/mm/kasan/report.c:378 [inline] print_report+0xd1/0x660 data/linux/mm/kasan/report.c:482 kasan_report+0xec/0x130 data/linux/mm/kasan/report.c:595 __asan_report_load8_noabort+0x14/0x30 data/linux/mm/kasan/report_generic.c:381 __up_read+0x634/0x790 data/linux/kernel/locking/rwsem.c:1353 up_read+0x22/0x30 data/linux/kernel/locking/rwsem.c:1633 i_mmap_unlock_read data/linux/include/linux/fs.h:537 [inline] __folio_split+0x732/0x1640 data/linux/mm/huge_memory.c:4100 __split_huge_page_to_list_to_order+0x7b/0x140 data/linux/mm/huge_memory.c:4203 split_huge_page_to_list_to_order data/linux/include/linux/huge_mm.h:385 [inline] split_huge_page_to_order data/linux/include/linux/huge_mm.h:389 [inline] try_to_split_thp_page+0xab/0x390 data/linux/mm/memory-failure.c:1675 memory_failure+0x1394/0x26e0 data/linux/mm/memory-failure.c:2470 madvise_inject_error data/linux/mm/madvise.c:1487 [inline] madvise_do_behavior+0x4ae/0x8a0 data/linux/mm/madvise.c:1925 do_madvise+0x162/0x230 data/linux/mm/madvise.c:2028 __do_sys_madvise data/linux/mm/madvise.c:2037 [inline] __se_sys_madvise data/linux/mm/madvise.c:2035 [inline] __x64_sys_madvise+0xae/0x120 data/linux/mm/madvise.c:2035 x64_sys_call+0x1bed/0x25e0 data/linux/arch/x86/include/generated/asm/syscalls_64.h:29 do_syscall_x64 data/linux/arch/x86/entry/syscall_64.c:63 [inline] do_syscall_64+0xe2/0x14e0 data/linux/arch/x86/entry/syscall_64.c:94 entry_SYSCALL_64_after_hwframe+0x76/0x7e
...
Quoted 1 line
Memory failure:
0x81bfe: recovery action for already truncated LRU page: Ignored
This log line confirms my suspicion.
The UAF fires at i_mmap_unlock_read() while try_to_split_thp_page() still holds the poisoned page locked. For the inode to be freed at that point, eviction must have completed truncation, which is impossible while a locked folio remains in the page cache. So the after-split folio containing lock_at was no longer in the page cache — and the only way to remove a locked folio is the split itself: the new_folio->index >= end drop in __folio_freeze_and_split_unmapped(). It requires lock_at to be a tail page, which is what memory-failure passes.
CPU0 CPU1
i_mmap_lock_read(mapping)
__folio_freeze_and_split_unmapped()
__filemap_remove_folio(lock_at) <-- beyond EOF: leaves cache, stays LOCKED
/* no locked folio pins the inode */
evict()
truncate_inode_pages_final() /* nothing to block on */
call_rcu() -> free inode i_mmap_unlock_read(mapping) <-- UAF: rwsem in freed inode
Your fix moves i_mmap_unlock_read() out of the window, but I don't think it is complete: shmem_uncharge(mapping->host) a few lines up also touches the inode, and its nr_shmem_dropped guard is exactly the beyond-EOF drop that triggers this. So it is in the same freed window; the patch just relocates the one dereference KASAN caught.
This is a lifetime problem, not a lock-ordering one: __folio_split() relies on the caller's locked folio in the page cache to keep the inode alive, then removes that very folio. I'd rather fix it by pinning the inode explicitly -- igrab(mapping->host) up front, so the inode is alive), iput() after i_mmap_unlock_read(). That covers shmem_uncharge() and the rwsem both, and won't regress if someone adds another mapping deref later.
See the patch below. Untested.
Any opinions?
Hao, could you give it a try?
mm/huge_memory.c +17 -0
@@ -3982,6 +3982,7 @@ static int __folio_split(struct folio *folio, unsigned int new_order,bool is_anon =folio_test_anon (folio);struct address_space *mapping =NULL ;struct anon_vma *anon_vma =NULL ; +struct inode *inode =NULL ;int old_order =folio_order (folio);struct folio *new_folio, *next;int nr_shmem_dropped =0 ;
@@ -4053,6 +4054,20 @@ static int __folio_split(struct folio *folio, unsigned int new_order, } anon_vma =NULL ; + +/* +* The locked @lock_at folio keeps the inode alive: eviction +* cannot remove it from the page cache while it is locked. But +* the split drops it if it lies beyond EOF, after which we +* still touch @mapping (shmem_uncharge(), i_mmap_unlock_read()). +* Hold an inode reference across the split to be safe. +*/ + inode =igrab (mapping->host); +if (!inode) { +/* Inode is being evicted; nothing to split. */ + ret = -EBUSY ; + goto out; + }i_mmap_lock_read (mapping);/*
@@ -4135,6 +4150,8 @@ static int __folio_split(struct folio *folio, unsigned int new_order, }if (mapping)i_mmap_unlock_read (mapping); +if (inode) +iput (inode); out:xas_destroy (&xas);if (is_pmd_order (old_order))
Signature · 1 line
Kiryl Shutsemau / Kirill A. Shutemov
Zi Yan
reviewerQuoted 120 lines · Kiryl Shutsemau
Quoted 93 lines · Hao Zhang
Quoted 49 lines · Kiryl Shutsemau
Quoted 32 lines · Hao Zhang
From: Hao Zhang <[email protected]>
Date: Thu, 9 Jul 2026 16:30:00 +0800
__folio_split()takesmapping->i_mmap_rwsemwhile unmapping and splitting a file-backed large folio. The lock is currently released only after the split folios that are not returned locked to the caller have been unlocked and put.That leaves a lifetime hole. Once the split folios are unlocked and their references are dropped, inode eviction can make progress through the final page-cache truncation path. After the folios are removed from the page cache and the last inode reference is dropped, the inode that embeds the
address_spacecan be freed after an RCU grace period. A lateri_mmap_unlock_read(mapping)then dereferencesmapping->i_mmap_rwsemfrom freed memory.A possible race is:
CPU0 CPU1
__folio_split()i_mmap_lock_read(mapping) ... remap_page() folio_unlock(new_folio) free_folio_and_swap_cache(new_folio) evict inode truncate_inode_pages_final() destroy_inode() call_rcu() RCU callback frees inode i_mmap_unlock_read(mapping)Quoted 4 lines
Release
mapping->i_mmap_rwsemafter the page-cache and reverse-mapping work has completed, but before unlocking and putting any of the split folios. Clear the local mapping pointer after the early unlock so the common exit path does not unlock it a second time.I don't buy this analysis. We still have the
lock_atpage which is locked and still in the page cache. Inode eviction cannot complete pasttruncate_inode_pages_final()until it is unlocked, which only happens after__folio_split()has releasedi_mmap_rwsem.One possible path how this can be hit is if
lock_atends up in an after-split folio beyond EOF and__folio_freeze_and_split_unmapped()removes it from the page cache. Note the drop loop starts atfolio_next(folio), so this requires splitting at a tail page (e.g. memory-failure) racing with truncate.Quoted 7 lines
But that's not what your analysis claims. Please share the actual crash reports. Let's get to the bottom of the situation before changing the code.
Signature · 1 line
Kiryl Shutsemau / Kirill A. ShutemovMemory failure:
0x5a6a7: recovery action for clean LRU page: Recovered Injecting memory failure for pfn0x81bfeat process virtual address0x200000ffe000
==================================================================BUG: KASAN: slab-use-after-free in __up_read+0x634/0x790 data/linux/kernel/locking/rwsem.c:1353 Read of size 8 at addr ffff88800b81c5e8 by task syz.2.11034/45874 CPU: 0 UID: 0 PID: 45874 Comm: syz.2.11034 Not tainted 7.0.0-rc3base+ #62 PREEMPT(full) Hardware name: QEMU Standard PC (i440FX + PIIX, 1996), BIOS rel-1.16.2-0-gea1b7a073390-prebuilt.qemu.org 04/01/2014 Call Trace: <TASK> __dump_stack data/linux/lib/dump_stack.c:94 [inline] dump_stack_lvl+0xbe/0x130 data/linux/lib/dump_stack.c:120 print_address_description data/linux/mm/kasan/report.c:378 [inline] print_report+0xd1/0x660 data/linux/mm/kasan/report.c:482 kasan_report+0xec/0x130 data/linux/mm/kasan/report.c:595 __asan_report_load8_noabort+0x14/0x30 data/linux/mm/kasan/report_generic.c:381 __up_read+0x634/0x790 data/linux/kernel/locking/rwsem.c:1353 up_read+0x22/0x30 data/linux/kernel/locking/rwsem.c:1633 i_mmap_unlock_read data/linux/include/linux/fs.h:537 [inline] __folio_split+0x732/0x1640 data/linux/mm/huge_memory.c:4100 __split_huge_page_to_list_to_order+0x7b/0x140 data/linux/mm/huge_memory.c:4203 split_huge_page_to_list_to_order data/linux/include/linux/huge_mm.h:385 [inline] split_huge_page_to_order data/linux/include/linux/huge_mm.h:389 [inline] try_to_split_thp_page+0xab/0x390 data/linux/mm/memory-failure.c:1675 memory_failure+0x1394/0x26e0 data/linux/mm/memory-failure.c:2470 madvise_inject_error data/linux/mm/madvise.c:1487 [inline] madvise_do_behavior+0x4ae/0x8a0 data/linux/mm/madvise.c:1925 do_madvise+0x162/0x230 data/linux/mm/madvise.c:2028 __do_sys_madvise data/linux/mm/madvise.c:2037 [inline] __se_sys_madvise data/linux/mm/madvise.c:2035 [inline] __x64_sys_madvise+0xae/0x120 data/linux/mm/madvise.c:2035 x64_sys_call+0x1bed/0x25e0 data/linux/arch/x86/include/generated/asm/syscalls_64.h:29 do_syscall_x64 data/linux/arch/x86/entry/syscall_64.c:63 [inline] do_syscall_64+0xe2/0x14e0 data/linux/arch/x86/entry/syscall_64.c:94 entry_SYSCALL_64_after_hwframe+0x76/0x7e...
Quoted 1 line
Memory failure:
0x81bfe: recovery action for already truncated LRU page: IgnoredThis log line confirms my suspicion.
The UAF fires at
i_mmap_unlock_read()whiletry_to_split_thp_page()still holds the poisoned page locked. For the inode to be freed at that point, eviction must have completed truncation, which is impossible while a locked folio remains in the page cache. So the after-split folio containinglock_atwas no longer in the page cache — and the only way to remove a locked folio is the split itself: thenew_folio->index>= end drop in__folio_freeze_and_split_unmapped(). It requireslock_atto be a tail page, which is what memory-failure passes.CPU0 CPU1
i_mmap_lock_read(mapping)
__folio_freeze_and_split_unmapped()
__filemap_remove_folio(lock_at)<-- beyond EOF: leaves cache, stays LOCKED/* no locked folio pins the inode */
evict() truncate_inode_pages_final() /* nothing to block on */ call_rcu() -> free inode
Quoted 7 lines
i_mmap_unlock_read(mapping)<-- UAF: rwsem in freed inodeYour fix moves
i_mmap_unlock_read()out of the window, but I don't think it is complete:shmem_uncharge(mapping->host)a few lines up also touches the inode, and itsnr_shmem_droppedguard is exactly the beyond-EOF drop that triggers this. So it is in the same freed window; the patch just relocates the one dereference KASAN caught.
I was discussing this issue with Codex after I saw Kriyl's patch and the iput() issue raised by Sashiko[1]. Hao's patch might be still valid for shmem_uncharge(), since all after-split folios are locked at shmem_uncharge() and prevents inode from going away. So the rule is no mapping/inode dereferences after the after-split folios unlock loop begins.
But let me know if I miss anything. Thanks.
[1]
https://sashiko.dev/#/patchset/[email protected]
Quoted 57 lines
This is a lifetime problem, not a lock-ordering one:
__folio_split()relies on the caller's locked folio in the page cache to keep the inode alive, then removes that very folio. I'd rather fix it by pinning the inode explicitly --igrab(mapping->host)up front, so the inode is alive),iput()afteri_mmap_unlock_read(). That coversshmem_uncharge()and the rwsem both, and won't regress if someone adds another mapping deref later.See the patch below. Untested.
Any opinions?
Hao, could you give it a try?
mm/huge_memory.c +17
-0@@ -3982,6 +3982,7 @@ static int __folio_split(struct folio *folio, unsigned int new_order,bool is_anon =folio_test_anon (folio);struct address_space *mapping =NULL ;struct anon_vma *anon_vma =NULL ; +struct inode *inode =NULL ;int old_order =folio_order (folio);struct folio *new_folio, *next;int nr_shmem_dropped =0 ;@@ -4053,6 +4054,20 @@ static int __folio_split(struct folio *folio, unsigned int new_order, } anon_vma =NULL ; + +/* +* The locked @lock_at folio keeps the inode alive: eviction +* cannot remove it from the page cache while it is locked. But +* the split drops it if it lies beyond EOF, after which we +* still touch @mapping (shmem_uncharge(), i_mmap_unlock_read()). +* Hold an inode reference across the split to be safe. +*/ + inode =igrab (mapping->host); +if (!inode) { +/* Inode is being evicted; nothing to split. */ + ret = -EBUSY ; + goto out; + }i_mmap_lock_read (mapping);/* @@ -4135,6 +4150,8 @@ static int __folio_split(struct folio *folio, unsigned int new_order, }if (mapping)i_mmap_unlock_read (mapping); +if (inode) +iput (inode); out:xas_destroy (&xas);if (is_pmd_order (old_order))
Signature · 2 lines
Best Regards, Yan, Zi
Zi Yan
reviewerRemoved [email protected] from Cc.
Quoted 120 lines · Kiryl Shutsemau
Quoted 93 lines · Hao Zhang
Quoted 49 lines · Kiryl Shutsemau
Quoted 32 lines · Hao Zhang
From: Hao Zhang <[email protected]>
Date: Thu, 9 Jul 2026 16:30:00 +0800
__folio_split()takesmapping->i_mmap_rwsemwhile unmapping and splitting a file-backed large folio. The lock is currently released only after the split folios that are not returned locked to the caller have been unlocked and put.That leaves a lifetime hole. Once the split folios are unlocked and their references are dropped, inode eviction can make progress through the final page-cache truncation path. After the folios are removed from the page cache and the last inode reference is dropped, the inode that embeds the
address_spacecan be freed after an RCU grace period. A lateri_mmap_unlock_read(mapping)then dereferencesmapping->i_mmap_rwsemfrom freed memory.A possible race is:
CPU0 CPU1
__folio_split()i_mmap_lock_read(mapping) ... remap_page() folio_unlock(new_folio) free_folio_and_swap_cache(new_folio) evict inode truncate_inode_pages_final() destroy_inode() call_rcu() RCU callback frees inode i_mmap_unlock_read(mapping)Quoted 4 lines
Release
mapping->i_mmap_rwsemafter the page-cache and reverse-mapping work has completed, but before unlocking and putting any of the split folios. Clear the local mapping pointer after the early unlock so the common exit path does not unlock it a second time.I don't buy this analysis. We still have the
lock_atpage which is locked and still in the page cache. Inode eviction cannot complete pasttruncate_inode_pages_final()until it is unlocked, which only happens after__folio_split()has releasedi_mmap_rwsem.One possible path how this can be hit is if
lock_atends up in an after-split folio beyond EOF and__folio_freeze_and_split_unmapped()removes it from the page cache. Note the drop loop starts atfolio_next(folio), so this requires splitting at a tail page (e.g. memory-failure) racing with truncate.Quoted 7 lines
But that's not what your analysis claims. Please share the actual crash reports. Let's get to the bottom of the situation before changing the code.
Signature · 1 line
Kiryl Shutsemau / Kirill A. ShutemovMemory failure:
0x5a6a7: recovery action for clean LRU page: Recovered Injecting memory failure for pfn0x81bfeat process virtual address0x200000ffe000
==================================================================BUG: KASAN: slab-use-after-free in __up_read+0x634/0x790 data/linux/kernel/locking/rwsem.c:1353 Read of size 8 at addr ffff88800b81c5e8 by task syz.2.11034/45874 CPU: 0 UID: 0 PID: 45874 Comm: syz.2.11034 Not tainted 7.0.0-rc3base+ #62 PREEMPT(full) Hardware name: QEMU Standard PC (i440FX + PIIX, 1996), BIOS rel-1.16.2-0-gea1b7a073390-prebuilt.qemu.org 04/01/2014 Call Trace: <TASK> __dump_stack data/linux/lib/dump_stack.c:94 [inline] dump_stack_lvl+0xbe/0x130 data/linux/lib/dump_stack.c:120 print_address_description data/linux/mm/kasan/report.c:378 [inline] print_report+0xd1/0x660 data/linux/mm/kasan/report.c:482 kasan_report+0xec/0x130 data/linux/mm/kasan/report.c:595 __asan_report_load8_noabort+0x14/0x30 data/linux/mm/kasan/report_generic.c:381 __up_read+0x634/0x790 data/linux/kernel/locking/rwsem.c:1353 up_read+0x22/0x30 data/linux/kernel/locking/rwsem.c:1633 i_mmap_unlock_read data/linux/include/linux/fs.h:537 [inline] __folio_split+0x732/0x1640 data/linux/mm/huge_memory.c:4100 __split_huge_page_to_list_to_order+0x7b/0x140 data/linux/mm/huge_memory.c:4203 split_huge_page_to_list_to_order data/linux/include/linux/huge_mm.h:385 [inline] split_huge_page_to_order data/linux/include/linux/huge_mm.h:389 [inline] try_to_split_thp_page+0xab/0x390 data/linux/mm/memory-failure.c:1675 memory_failure+0x1394/0x26e0 data/linux/mm/memory-failure.c:2470 madvise_inject_error data/linux/mm/madvise.c:1487 [inline] madvise_do_behavior+0x4ae/0x8a0 data/linux/mm/madvise.c:1925 do_madvise+0x162/0x230 data/linux/mm/madvise.c:2028 __do_sys_madvise data/linux/mm/madvise.c:2037 [inline] __se_sys_madvise data/linux/mm/madvise.c:2035 [inline] __x64_sys_madvise+0xae/0x120 data/linux/mm/madvise.c:2035 x64_sys_call+0x1bed/0x25e0 data/linux/arch/x86/include/generated/asm/syscalls_64.h:29 do_syscall_x64 data/linux/arch/x86/entry/syscall_64.c:63 [inline] do_syscall_64+0xe2/0x14e0 data/linux/arch/x86/entry/syscall_64.c:94 entry_SYSCALL_64_after_hwframe+0x76/0x7e...
Quoted 1 line
Memory failure:
0x81bfe: recovery action for already truncated LRU page: IgnoredThis log line confirms my suspicion.
The UAF fires at
i_mmap_unlock_read()whiletry_to_split_thp_page()still holds the poisoned page locked. For the inode to be freed at that point, eviction must have completed truncation, which is impossible while a locked folio remains in the page cache. So the after-split folio containinglock_atwas no longer in the page cache — and the only way to remove a locked folio is the split itself: thenew_folio->index>= end drop in__folio_freeze_and_split_unmapped(). It requireslock_atto be a tail page, which is what memory-failure passes.CPU0 CPU1
i_mmap_lock_read(mapping)
__folio_freeze_and_split_unmapped()
__filemap_remove_folio(lock_at)<-- beyond EOF: leaves cache, stays LOCKED/* no locked folio pins the inode */
evict() truncate_inode_pages_final() /* nothing to block on */ call_rcu() -> free inode
Quoted 7 lines
i_mmap_unlock_read(mapping)<-- UAF: rwsem in freed inodeYour fix moves
i_mmap_unlock_read()out of the window, but I don't think it is complete:shmem_uncharge(mapping->host)a few lines up also touches the inode, and itsnr_shmem_droppedguard is exactly the beyond-EOF drop that triggers this. So it is in the same freed window; the patch just relocates the one dereference KASAN caught.
I was discussing this issue with Codex after I saw Kriyl's patch and the iput() issue raised by Sashiko[1]. Hao's patch might be still valid for shmem_uncharge(), since all after-split folios are locked at shmem_uncharge() and prevents inode from going away. So the rule is no mapping/inode dereferences after the after-split folios unlock loop begins.
But let me know if I miss anything. Thanks.
[1]
https://sashiko.dev/#/patchset/[email protected]
Quoted 57 lines
This is a lifetime problem, not a lock-ordering one:
__folio_split()relies on the caller's locked folio in the page cache to keep the inode alive, then removes that very folio. I'd rather fix it by pinning the inode explicitly --igrab(mapping->host)up front, so the inode is alive),iput()afteri_mmap_unlock_read(). That coversshmem_uncharge()and the rwsem both, and won't regress if someone adds another mapping deref later.See the patch below. Untested.
Any opinions?
Hao, could you give it a try?
mm/huge_memory.c +17
-0@@ -3982,6 +3982,7 @@ static int __folio_split(struct folio *folio, unsigned int new_order,bool is_anon =folio_test_anon (folio);struct address_space *mapping =NULL ;struct anon_vma *anon_vma =NULL ; +struct inode *inode =NULL ;int old_order =folio_order (folio);struct folio *new_folio, *next;int nr_shmem_dropped =0 ;@@ -4053,6 +4054,20 @@ static int __folio_split(struct folio *folio, unsigned int new_order, } anon_vma =NULL ; + +/* +* The locked @lock_at folio keeps the inode alive: eviction +* cannot remove it from the page cache while it is locked. But +* the split drops it if it lies beyond EOF, after which we +* still touch @mapping (shmem_uncharge(), i_mmap_unlock_read()). +* Hold an inode reference across the split to be safe. +*/ + inode =igrab (mapping->host); +if (!inode) { +/* Inode is being evicted; nothing to split. */ + ret = -EBUSY ; + goto out; + }i_mmap_lock_read (mapping);/* @@ -4135,6 +4150,8 @@ static int __folio_split(struct folio *folio, unsigned int new_order, }if (mapping)i_mmap_unlock_read (mapping); +if (inode) +iput (inode); out:xas_destroy (&xas);if (is_pmd_order (old_order))
Signature · 2 lines
Best Regards, Yan, Zi
Baolin Wang
reviewerQuoted 138 lines · Zi Yan
Removed [email protected] from Cc.
Quoted 122 lines · Kiryl Shutsemau
Quoted 93 lines · Hao Zhang
Quoted 49 lines · Kiryl Shutsemau
Quoted 32 lines · Hao Zhang
From: Hao Zhang <[email protected]>
Date: Thu, 9 Jul 2026 16:30:00 +0800
__folio_split()takesmapping->i_mmap_rwsemwhile unmapping and splitting a file-backed large folio. The lock is currently released only after the split folios that are not returned locked to the caller have been unlocked and put.That leaves a lifetime hole. Once the split folios are unlocked and their references are dropped, inode eviction can make progress through the final page-cache truncation path. After the folios are removed from the page cache and the last inode reference is dropped, the inode that embeds the
address_spacecan be freed after an RCU grace period. A lateri_mmap_unlock_read(mapping)then dereferencesmapping->i_mmap_rwsemfrom freed memory.A possible race is:
CPU0 CPU1 __folio_split() i_mmap_lock_read(mapping) ... remap_page() folio_unlock(new_folio) free_folio_and_swap_cache(new_folio) evict inode truncate_inode_pages_final() destroy_inode() call_rcu() RCU callback frees inode i_mmap_unlock_read(mapping)Quoted 4 lines
Release
mapping->i_mmap_rwsemafter the page-cache and reverse-mapping work has completed, but before unlocking and putting any of the split folios. Clear the local mapping pointer after the early unlock so the common exit path does not unlock it a second time.I don't buy this analysis. We still have the
lock_atpage which is locked and still in the page cache. Inode eviction cannot complete pasttruncate_inode_pages_final()until it is unlocked, which only happens after__folio_split()has releasedi_mmap_rwsem.One possible path how this can be hit is if
lock_atends up in an after-split folio beyond EOF and__folio_freeze_and_split_unmapped()removes it from the page cache. Note the drop loop starts atfolio_next(folio), so this requires splitting at a tail page (e.g. memory-failure) racing with truncate.Quoted 7 lines
But that's not what your analysis claims. Please share the actual crash reports. Let's get to the bottom of the situation before changing the code.
Signature · 1 line
Kiryl Shutsemau / Kirill A. ShutemovMemory failure:
0x5a6a7: recovery action for clean LRU page: Recovered Injecting memory failure for pfn0x81bfeat process virtual address0x200000ffe000
==================================================================BUG: KASAN: slab-use-after-free in __up_read+0x634/0x790 data/linux/kernel/locking/rwsem.c:1353 Read of size 8 at addr ffff88800b81c5e8 by task syz.2.11034/45874 CPU: 0 UID: 0 PID: 45874 Comm: syz.2.11034 Not tainted 7.0.0-rc3base+ #62 PREEMPT(full) Hardware name: QEMU Standard PC (i440FX + PIIX, 1996), BIOS rel-1.16.2-0-gea1b7a073390-prebuilt.qemu.org 04/01/2014 Call Trace: <TASK> __dump_stack data/linux/lib/dump_stack.c:94 [inline] dump_stack_lvl+0xbe/0x130 data/linux/lib/dump_stack.c:120 print_address_description data/linux/mm/kasan/report.c:378 [inline] print_report+0xd1/0x660 data/linux/mm/kasan/report.c:482 kasan_report+0xec/0x130 data/linux/mm/kasan/report.c:595 __asan_report_load8_noabort+0x14/0x30 data/linux/mm/kasan/report_generic.c:381 __up_read+0x634/0x790 data/linux/kernel/locking/rwsem.c:1353 up_read+0x22/0x30 data/linux/kernel/locking/rwsem.c:1633 i_mmap_unlock_read data/linux/include/linux/fs.h:537 [inline] __folio_split+0x732/0x1640 data/linux/mm/huge_memory.c:4100 __split_huge_page_to_list_to_order+0x7b/0x140 data/linux/mm/huge_memory.c:4203 split_huge_page_to_list_to_order data/linux/include/linux/huge_mm.h:385 [inline] split_huge_page_to_order data/linux/include/linux/huge_mm.h:389 [inline] try_to_split_thp_page+0xab/0x390 data/linux/mm/memory-failure.c:1675 memory_failure+0x1394/0x26e0 data/linux/mm/memory-failure.c:2470 madvise_inject_error data/linux/mm/madvise.c:1487 [inline] madvise_do_behavior+0x4ae/0x8a0 data/linux/mm/madvise.c:1925 do_madvise+0x162/0x230 data/linux/mm/madvise.c:2028 __do_sys_madvise data/linux/mm/madvise.c:2037 [inline] __se_sys_madvise data/linux/mm/madvise.c:2035 [inline] __x64_sys_madvise+0xae/0x120 data/linux/mm/madvise.c:2035 x64_sys_call+0x1bed/0x25e0 data/linux/arch/x86/include/generated/asm/syscalls_64.h:29 do_syscall_x64 data/linux/arch/x86/entry/syscall_64.c:63 [inline] do_syscall_64+0xe2/0x14e0 data/linux/arch/x86/entry/syscall_64.c:94 entry_SYSCALL_64_after_hwframe+0x76/0x7e...
Quoted 1 line
Memory failure:
0x81bfe: recovery action for already truncated LRU page: IgnoredThis log line confirms my suspicion.
The UAF fires at
i_mmap_unlock_read()whiletry_to_split_thp_page()still holds the poisoned page locked. For the inode to be freed at that point, eviction must have completed truncation, which is impossible while a locked folio remains in the page cache. So the after-split folio containinglock_atwas no longer in the page cache — and the only way to remove a locked folio is the split itself: thenew_folio->index>= end drop in__folio_freeze_and_split_unmapped(). It requireslock_atto be a tail page, which is what memory-failure passes.CPU0 CPU1
i_mmap_lock_read(mapping)
__folio_freeze_and_split_unmapped()
__filemap_remove_folio(lock_at)<-- beyond EOF: leaves cache, stays LOCKED/* no locked folio pins the inode */ evict() truncate_inode_pages_final() /* nothing to block on */ call_rcu() -> free inode i_mmap_unlock_read(mapping) <-- UAF: rwsem in freed inodeQuoted 5 lines
Your fix moves
i_mmap_unlock_read()out of the window, but I don't think it is complete:shmem_uncharge(mapping->host)a few lines up also touches the inode, and itsnr_shmem_droppedguard is exactly the beyond-EOF drop that triggers this. So it is in the same freed window; the patch just relocates the one dereference KASAN caught.I was discussing this issue with Codex after I saw Kriyl's patch and the
iput()issue raised by Sashiko[1]. Hao's patch might be still valid forshmem_uncharge(), since all after-split folios are locked atshmem_uncharge()and prevents inode from going away. So the rule is no mapping/inode dereferences after the after-split folios unlock loop begins.
Quoted 3 lines
But let me know if I miss anything. Thanks.
[1]
https://sashiko.dev/#/patchset/[email protected]
After reading the analysis, I agree with you. Hao's fix looks simple and effective, unless I've also missed anything. :)
Quoted 29 lines · Zi Yan
Quoted 21 lines
This log line confirms my suspicion.
The UAF fires at
i_mmap_unlock_read()whiletry_to_split_thp_page()still holds the poisoned page locked. For the inode to be freed at that point, eviction must have completed truncation, which is impossible while a locked folio remains in the page cache. So the after-split folio containinglock_atwas no longer in the page cache — and the only way to remove a locked folio is the split itself: thenew_folio->index>= end drop in__folio_freeze_and_split_unmapped(). It requireslock_atto be a tail page, which is what memory-failure passes.CPU0 CPU1
i_mmap_lock_read(mapping)
__folio_freeze_and_split_unmapped()
__filemap_remove_folio(lock_at)<-- beyond EOF: leaves cache, stays LOCKED/* no locked folio pins the inode */
evict() truncate_inode_pages_final() /* nothing to block on */ call_rcu() -> free inodeQuoted 7 lines
i_mmap_unlock_read(mapping)<-- UAF: rwsem in freed inodeYour fix moves
i_mmap_unlock_read()out of the window, but I don't think it is complete:shmem_uncharge(mapping->host)a few lines up also touches the inode, and itsnr_shmem_droppedguard is exactly the beyond-EOF drop that triggers this. So it is in the same freed window; the patch just relocates the one dereference KASAN caught.
Quoted 6 lines
I was discussing this issue with Codex after I saw Kriyl's patch and the
iput()issue raised by Sashiko[1]. Hao's patch might be still valid forshmem_uncharge(), since all after-split folios are locked atshmem_uncharge()and prevents inode from going away. So the rule is no mapping/inode dereferences after the after-split folios unlock loop begins.
I agree that Hao's fix works, but I don't think "all after-split folios are locked" is the right rule: the beyond-EOF folios are locked, yet no longer in the page cache, so they don't block eviction. What actually pins the inode is @folio itself: the drop loop in __folio_freeze_and_split_unmapped() starts at folio_next(folio), so @folio always stays in the page cache -- even if it is entirely beyond EOF -- and stays locked until the unlock loop.
But maybe we should sidestep the problem altogether and refuse the split with -EBUSY if @lock_at is beyond @end. Then the caller's locked folio is guaranteed to stay in the page cache and pins the inode for the whole function. No ordering rules needed.
It is normally only reachable as a race with truncate (or eviction), where failing the split is harmless. The exception is a folio left straddling EOF by a failed split in truncate_inode_partial_folio():memory_failure() on a beyond-EOF subpage of it would now fail to split instead of dropping the poisoned page. That range is zeroed and unreachable, so I think it is acceptable.
Any opinions?
Signature · 1 line
Kiryl Shutsemau / Kirill A. Shutemov
David Hildenbrand (Arm)
maintainerQuoted 50 lines · Kiryl Shutsemau
Quoted 29 lines · Zi Yan
Quoted 21 lines
This log line confirms my suspicion.
The UAF fires at
i_mmap_unlock_read()whiletry_to_split_thp_page()still holds the poisoned page locked. For the inode to be freed at that point, eviction must have completed truncation, which is impossible while a locked folio remains in the page cache. So the after-split folio containinglock_atwas no longer in the page cache — and the only way to remove a locked folio is the split itself: thenew_folio->index>= end drop in__folio_freeze_and_split_unmapped(). It requireslock_atto be a tail page, which is what memory-failure passes.CPU0 CPU1
i_mmap_lock_read(mapping)
__folio_freeze_and_split_unmapped()
__filemap_remove_folio(lock_at)<-- beyond EOF: leaves cache, stays LOCKED/* no locked folio pins the inode */
evict() truncate_inode_pages_final() /* nothing to block on */ call_rcu() -> free inodeQuoted 7 lines
i_mmap_unlock_read(mapping)<-- UAF: rwsem in freed inodeYour fix moves
i_mmap_unlock_read()out of the window, but I don't think it is complete:shmem_uncharge(mapping->host)a few lines up also touches the inode, and itsnr_shmem_droppedguard is exactly the beyond-EOF drop that triggers this. So it is in the same freed window; the patch just relocates the one dereference KASAN caught.Quoted 6 lines
I was discussing this issue with Codex after I saw Kriyl's patch and the
iput()issue raised by Sashiko[1]. Hao's patch might be still valid forshmem_uncharge(), since all after-split folios are locked atshmem_uncharge()and prevents inode from going away. So the rule is no mapping/inode dereferences after the after-split folios unlock loop begins.I agree that Hao's fix works, but I don't think "all after-split folios are locked" is the right rule: the beyond-EOF folios are locked, yet no longer in the page cache, so they don't block eviction. What actually pins the inode is @folio itself: the drop loop in
__folio_freeze_and_split_unmapped()starts atfolio_next(folio), so @folio always stays in the page cache -- even if it is entirely beyond EOF -- and stays locked until the unlock loop.But maybe we should sidestep the problem altogether and refuse the split with
-EBUSYif @lock_at is beyond @end. Then the caller's locked folio is guaranteed to stay in the page cache and pins the inode for the whole function. No ordering rules needed.
Quoted 8 lines
It is normally only reachable as a race with truncate (or eviction), where failing the split is harmless. The exception is a folio left straddling EOF by a failed split in
truncate_inode_partial_folio():memory_failure()on a beyond-EOF subpage of it would now fail to split instead of dropping the poisoned page. That range is zeroed and unreachable, so I think it is acceptable.Any opinions?
I guess a deferred iput through some workqueue is not an option?
I'd prefer if we could keep folio splitting working even when the passed page is EOF.
Signature · 3 lines
Cheers, David
Quoted 3 lines · David Hildenbrand (Arm)
Quoted 1 line
Any opinions?
I guess a deferred iput through some workqueue is not an option?
Hm. This feels like a dirty hack to me.
Quoted 2 lines
I'd prefer if we could keep folio splitting working even when the passed page is EOF.
Can you think of legitimate use-case?
I will get rid of hwpoison case. It has no good reason to lock tail page in the first place.
The only other case I can see is race with truncate. Loosing to it should be fine to split caller.
Signature · 1 line
Kiryl Shutsemau / Kirill A. Shutemov
David Hildenbrand (Arm)
maintainerQuoted 7 lines · Kiryl Shutsemau
Quoted 3 lines · David Hildenbrand (Arm)
Quoted 1 line
Any opinions?
I guess a deferred iput through some workqueue is not an option?
Hm. This feels like a dirty hack to me.
Quoted 7 lines
Quoted 2 lines
I'd prefer if we could keep folio splitting working even when the passed page is EOF.
Can you think of legitimate use-case?
I will get rid of hwpoison case. It has no good reason to lock tail page in the first place.
If we can get that out of the way, fine with me.
Signature · 3 lines
Cheers, David
Quoted 136 lines · Kiryl Shutsemau
Quoted 93 lines · Hao Zhang
Quoted 49 lines · Kiryl Shutsemau
Quoted 32 lines · Hao Zhang
From: Hao Zhang <[email protected]>
Date: Thu, 9 Jul 2026 16:30:00 +0800
__folio_split()takesmapping->i_mmap_rwsemwhile unmapping and splitting a file-backed large folio. The lock is currently released only after the split folios that are not returned locked to the caller have been unlocked and put.That leaves a lifetime hole. Once the split folios are unlocked and their references are dropped, inode eviction can make progress through the final page-cache truncation path. After the folios are removed from the page cache and the last inode reference is dropped, the inode that embeds the
address_spacecan be freed after an RCU grace period. A lateri_mmap_unlock_read(mapping)then dereferencesmapping->i_mmap_rwsemfrom freed memory.A possible race is:
CPU0 CPU1
__folio_split()i_mmap_lock_read(mapping) ... remap_page() folio_unlock(new_folio) free_folio_and_swap_cache(new_folio) evict inode truncate_inode_pages_final() destroy_inode() call_rcu() RCU callback frees inode i_mmap_unlock_read(mapping)Quoted 4 lines
Release
mapping->i_mmap_rwsemafter the page-cache and reverse-mapping work has completed, but before unlocking and putting any of the split folios. Clear the local mapping pointer after the early unlock so the common exit path does not unlock it a second time.I don't buy this analysis. We still have the
lock_atpage which is locked and still in the page cache. Inode eviction cannot complete pasttruncate_inode_pages_final()until it is unlocked, which only happens after__folio_split()has releasedi_mmap_rwsem.One possible path how this can be hit is if
lock_atends up in an after-split folio beyond EOF and__folio_freeze_and_split_unmapped()removes it from the page cache. Note the drop loop starts atfolio_next(folio), so this requires splitting at a tail page (e.g. memory-failure) racing with truncate.Quoted 7 lines
But that's not what your analysis claims. Please share the actual crash reports. Let's get to the bottom of the situation before changing the code.
Signature · 1 line
Kiryl Shutsemau / Kirill A. ShutemovMemory failure:
0x5a6a7: recovery action for clean LRU page: Recovered Injecting memory failure for pfn0x81bfeat process virtual address0x200000ffe000
==================================================================BUG: KASAN: slab-use-after-free in __up_read+0x634/0x790 data/linux/kernel/locking/rwsem.c:1353 Read of size 8 at addr ffff88800b81c5e8 by task syz.2.11034/45874 CPU: 0 UID: 0 PID: 45874 Comm: syz.2.11034 Not tainted 7.0.0-rc3base+ #62 PREEMPT(full) Hardware name: QEMU Standard PC (i440FX + PIIX, 1996), BIOS rel-1.16.2-0-gea1b7a073390-prebuilt.qemu.org 04/01/2014 Call Trace: <TASK> __dump_stack data/linux/lib/dump_stack.c:94 [inline] dump_stack_lvl+0xbe/0x130 data/linux/lib/dump_stack.c:120 print_address_description data/linux/mm/kasan/report.c:378 [inline] print_report+0xd1/0x660 data/linux/mm/kasan/report.c:482 kasan_report+0xec/0x130 data/linux/mm/kasan/report.c:595 __asan_report_load8_noabort+0x14/0x30 data/linux/mm/kasan/report_generic.c:381 __up_read+0x634/0x790 data/linux/kernel/locking/rwsem.c:1353 up_read+0x22/0x30 data/linux/kernel/locking/rwsem.c:1633 i_mmap_unlock_read data/linux/include/linux/fs.h:537 [inline] __folio_split+0x732/0x1640 data/linux/mm/huge_memory.c:4100 __split_huge_page_to_list_to_order+0x7b/0x140 data/linux/mm/huge_memory.c:4203 split_huge_page_to_list_to_order data/linux/include/linux/huge_mm.h:385 [inline] split_huge_page_to_order data/linux/include/linux/huge_mm.h:389 [inline] try_to_split_thp_page+0xab/0x390 data/linux/mm/memory-failure.c:1675 memory_failure+0x1394/0x26e0 data/linux/mm/memory-failure.c:2470 madvise_inject_error data/linux/mm/madvise.c:1487 [inline] madvise_do_behavior+0x4ae/0x8a0 data/linux/mm/madvise.c:1925 do_madvise+0x162/0x230 data/linux/mm/madvise.c:2028 __do_sys_madvise data/linux/mm/madvise.c:2037 [inline] __se_sys_madvise data/linux/mm/madvise.c:2035 [inline] __x64_sys_madvise+0xae/0x120 data/linux/mm/madvise.c:2035 x64_sys_call+0x1bed/0x25e0 data/linux/arch/x86/include/generated/asm/syscalls_64.h:29 do_syscall_x64 data/linux/arch/x86/entry/syscall_64.c:63 [inline] do_syscall_64+0xe2/0x14e0 data/linux/arch/x86/entry/syscall_64.c:94 entry_SYSCALL_64_after_hwframe+0x76/0x7e...
Quoted 1 line
Memory failure:
0x81bfe: recovery action for already truncated LRU page: IgnoredThis log line confirms my suspicion.
The UAF fires at
i_mmap_unlock_read()whiletry_to_split_thp_page()still holds the poisoned page locked. For the inode to be freed at that point, eviction must have completed truncation, which is impossible while a locked folio remains in the page cache. So the after-split folio containinglock_atwas no longer in the page cache — and the only way to remove a locked folio is the split itself: thenew_folio->index>= end drop in__folio_freeze_and_split_unmapped(). It requireslock_atto be a tail page, which is what memory-failure passes.CPU0 CPU1
i_mmap_lock_read(mapping)
__folio_freeze_and_split_unmapped()
__filemap_remove_folio(lock_at)<-- beyond EOF: leaves cache, stays LOCKED/* no locked folio pins the inode */
evict() truncate_inode_pages_final() /* nothing to block on */ call_rcu() -> free inode
i_mmap_unlock_read(mapping)<-- UAF: rwsem in freed inodeYour fix moves
i_mmap_unlock_read()out of the window, but I don't think it is complete:shmem_uncharge(mapping->host)a few lines up also touches the inode, and itsnr_shmem_droppedguard is exactly the beyond-EOF drop that triggers this. So it is in the same freed window; the patch just relocates the one dereference KASAN caught.This is a lifetime problem, not a lock-ordering one:
__folio_split()relies on the caller's locked folio in the page cache to keep the inode alive, then removes that very folio. I'd rather fix it by pinning the inode explicitly --igrab(mapping->host)up front, so the inode is alive),iput()afteri_mmap_unlock_read(). That coversshmem_uncharge()and the rwsem both, and won't regress if someone adds another mapping deref later.
Quoted 6 lines
See the patch below. Untested.
Any opinions?
Hao, could you give it a try?
Hi,
The original KASAN report was found during a ~6 hour syzkaller fuzzing run, not from a small deterministic reproducer. After applying the patch, I also ran another ~6 hour fuzzing/replay test with the same syzkaller setup, and I did not see the original KASAN UAF again.
Test setup:
target: linux/amd64
kernel: 7.0-rc3 based tree, commit 1f318b96cc84 ("Linux 7.0-rc3") config highlights:
CONFIG_KASAN=y
CONFIG_KCOV=y
CONFIG_DEBUG_VM=y
CONFIG_PREEMPT=y
CONFIG_TRANSPARENT_HUGEPAGE=y CONFIG_MEMORY_FAILURE=y
CONFIG_KVM=y
CONFIG_SHMEM=y
CONFIG_TMPFS=y
VM:
qemu, -enable-kvm -cpu host,migratable=off
4 VMs, 2 vCPUs each, 3072M memory
syz-manager procs: 6
I also reran the original syzkaller crash logs with syz-repro.
Relevant replay results:
fs_log1_20260713-110727.log:
reproducing took 19m50s
program did not crash
replaying the whole log did not cause a kernel crash EXIT=0
fs_log1_rerun_20260713-160230.log:
reproducing took 19m51s
program did not crash
replaying the whole log did not cause a kernel crash EXIT=0
fs_log0_rerun_20260713-154007.log:
reproducing took 19m09s
program did not crash
replaying the whole log did not cause a kernel crash EXIT=0
So far I have not reproduced the original report after applying your patch.
I do not have a stable generated .prog/.c reproducer artifact left from these runs.
Thanks,
Hao
Quoted 45 lines
mm/huge_memory.c +17
-0@@ -3982,6 +3982,7 @@ static int __folio_split(struct folio *folio, unsigned int new_order,bool is_anon =folio_test_anon (folio);struct address_space *mapping =NULL ;struct anon_vma *anon_vma =NULL ; +struct inode *inode =NULL ;int old_order =folio_order (folio);struct folio *new_folio, *next;int nr_shmem_dropped =0 ;@@ -4053,6 +4054,20 @@ static int __folio_split(struct folio *folio, unsigned int new_order, } anon_vma =NULL ; + +/* +* The locked @lock_at folio keeps the inode alive: eviction +* cannot remove it from the page cache while it is locked. But +* the split drops it if it lies beyond EOF, after which we +* still touch @mapping (shmem_uncharge(), i_mmap_unlock_read()). +* Hold an inode reference across the split to be safe. +*/ + inode =igrab (mapping->host); +if (!inode) { +/* Inode is being evicted; nothing to split. */ + ret = -EBUSY ; + goto out; + }i_mmap_lock_read (mapping);/* @@ -4135,6 +4150,8 @@ static int __folio_split(struct folio *folio, unsigned int new_order, }if (mapping)i_mmap_unlock_read (mapping); +if (inode) +iput (inode); out:xas_destroy (&xas);if (is_pmd_order (old_order))Signature · 1 line
Kiryl Shutsemau / Kirill A. Shutemov