* [PATCH v2] mm: memory: fix truncation removing mapped folios
@ 2026-10-10 7:22 Baolin Wang
2026-10-12 10:23 ` David Hildenbrand (Arm)
2026-10-13 10:24 ` Pedro Falcato
0 siblings, 2 replies; 9+ messages in thread
From: Baolin Wang @ 2026-10-10 7:22 UTC (permalink / raw)
To: akpm, david
Cc: ljs, liam, vbabka, rppt, surenb, mhocko, qi.zheng, jack,
pfalcato, ayushr, baolin.wang, linux-mm, linux-kernel
Ayush reported an issue that shmem/tmpfs folios are still mapped when
truncating them on production:
"
BUG: Bad page cache in process ... pfn:...
page dumped because: still mapped when deleted
"
I reproduced the issue with Ayush's reproducer [1] with the latest mm-unstable
branch, and I got the following crash info. From the dump message, we can
see that truncate_inode_folio() is really trying to remove mapped folios,
which is incorrect.
"
[ 189.171977] page: refcount:3 mapcount:1 mapping:00000000fe168ee1
index:0x3880 pfn:0x191647
[ 189.171995] memcg:ffff0000cc073d40
[ 189.171997] aops:shmem_aops ino:3c01 dentry name(?):"memfd:runsc-memory"
[ 189.172006] flags: 0x17fffef0002022d(locked|referenced|uptodate|lru|
workingset|swapbacked|node=0|zone=2|lastcpupid=0x3ffff)
.......
[ 189.172017] page dumped because: VM_BUG_ON_FOLIO(folio_mapped(folio))
[ 189.172026] ------------[ cut here ]------------
[ 189.172027] kernel BUG at mm/filemap.c:155!
[ 189.172057] Internal error: Oops - BUG: 00000000f2000800 [#1] SMP
.......
[ 189.177721] CPU: 3 UID: 0 PID: 5521 Comm: repro Kdump: loaded Tainted:
7.3.0-rc4+ #277 PREEMPT(lazy)
.......
[ 189.183986] Call trace:
[ 189.184124] filemap_unaccount_folio+0xf0/0x1e8 (P)
[ 189.184391] __filemap_remove_folio+0x34/0x160
[ 189.184633] filemap_remove_folio+0x4c/0xb0
[ 189.184859] truncate_inode_folio+0x34/0x58
[ 189.185087] shmem_undo_range+0x220/0x658
[ 189.185308] shmem_fallocate+0x2f0/0x470
[ 189.185527] vfs_fallocate+0x128/0x328
......
[ 189.187078] el0t_64_sync+0x184/0x188
"
After analysis, I believe the race exists between truncation and MADV_DONTNEED,
and shmem's ->map_pages() merely makes the issue easier to reproduce. Since
MADV_DONTNEED synchronously releases the pagetable page before calling
tlb_flush_rmaps(), this could cause another thread's truncation to skip
zap_pte_range() (due to pmd is none) but still observe the folio's mapcount
as non-zero. A possible race scenario is as follows:
CPU 0 CPU 1
shmem_fallocate madvise_dontneed_single_vma
unmap_mapping_range .......
......
(filemap_map_pages() remap folios)
zap_pte_range
truncate_inode_folio zap_empty_pte_table(pmd clear)
unmap_mapping_folio
......
zap_pmd_range (saw pmd none, skip)
filemap_remove_folio
BUG_ON(folio_mapped)
tlb_flush_rmaps(remove rmaps)
To fix this issue, we should move the pmd clear operation (via zap_empty_pte_table())
to after tlb_flush_rmaps() and add smp_wmb()/smp_rmb() memory barriers, so that
when we find a pmd_none() while unmapping a folio without holding the PTL, the
folio's mappings are guaranteed to have been removed via tlb_flush_rmaps().
[1] https://lore.kernel.org/all/20260925065013.3682431-1-ayushr@modal.com/
Reported-by: Ayush Ranjan <ayushr@modal.com>
Closes: https://lore.kernel.org/all/20260924061708.1645968-1-ayushr@modal.com/
Fixes: 6375e95f381e ("mm: pgtable: reclaim empty PTE page in madvise(MADV_DONTNEED)")
Signed-off-by: Baolin Wang <baolin.wang@linux.alibaba.com>
---
Changes from v1:
- Add an smp_rmb() to pair with the smp_wmb() (Per Lorenzo and Pedro).
---
mm/memory.c | 29 ++++++++++++++++++++---------
1 file changed, 20 insertions(+), 9 deletions(-)
diff --git a/mm/memory.c b/mm/memory.c
index 6a8e7772b8d6..c3a1faf580c8 100644
--- a/mm/memory.c
+++ b/mm/memory.c
@@ -2036,6 +2036,15 @@ static unsigned long zap_pte_range(struct mmu_gather *tlb,
}
} while (pte += nr, addr += PAGE_SIZE * nr, addr != end);
+ add_mm_rss_vec(mm, rss);
+ lazy_mmu_mode_disable();
+
+ /* Do the actual TLB flush before dropping ptl */
+ if (force_flush) {
+ tlb_flush_mmu_tlbonly(tlb);
+ tlb_flush_rmaps(tlb, vma);
+ }
+
/*
* Fast path: try to hold the pmd lock and unmap the PTE page.
*
@@ -2044,16 +2053,13 @@ static unsigned long zap_pte_range(struct mmu_gather *tlb,
* to ensure they are still none, thereby preventing the pte entries
* from being repopulated by another thread.
*/
- if (can_reclaim_pt && direct_reclaim && addr == end)
+ if (can_reclaim_pt && direct_reclaim && addr == end) {
+ /*
+ * The rmap changes need to be observed before PTEs get zapped.
+ * Pairs with smp_rmb() in zap_pmd_range().
+ */
+ smp_wmb();
direct_reclaim = zap_empty_pte_table(mm, pmd, ptl, &pmdval);
-
- add_mm_rss_vec(mm, rss);
- lazy_mmu_mode_disable();
-
- /* Do the actual TLB flush before dropping ptl */
- if (force_flush) {
- tlb_flush_mmu_tlbonly(tlb);
- tlb_flush_rmaps(tlb, vma);
}
pte_unmap_unlock(start_pte, ptl);
@@ -2108,6 +2114,11 @@ static inline unsigned long zap_pmd_range(struct mmu_gather *tlb,
sync_with_folio_pmd_zap(tlb->mm, pmd);
}
if (pmd_none(*pmd)) {
+ /*
+ * Order this PMD read before the folio's mapcount read.
+ * Pairs with smp_wmb() in zap_pte_range().
+ */
+ smp_rmb();
addr = next;
continue;
}
--
2.47.3
^ permalink raw reply [flat|nested] 9+ messages in thread
* Re: [PATCH v2] mm: memory: fix truncation removing mapped folios
2026-10-10 7:22 [PATCH v2] mm: memory: fix truncation removing mapped folios Baolin Wang
@ 2026-10-12 10:23 ` David Hildenbrand (Arm)
2026-10-13 9:36 ` Baolin Wang
2026-10-13 10:24 ` Pedro Falcato
1 sibling, 1 reply; 9+ messages in thread
From: David Hildenbrand (Arm) @ 2026-10-12 10:23 UTC (permalink / raw)
To: Baolin Wang, akpm
Cc: ljs, liam, vbabka, rppt, surenb, mhocko, qi.zheng, jack,
pfalcato, ayushr, linux-mm, linux-kernel
On 10/10/26 09:22, Baolin Wang wrote:
> Ayush reported an issue that shmem/tmpfs folios are still mapped when
> truncating them on production:
> "
> BUG: Bad page cache in process ... pfn:...
> page dumped because: still mapped when deleted
> "
>
> I reproduced the issue with Ayush's reproducer [1] with the latest mm-unstable
> branch, and I got the following crash info. From the dump message, we can
> see that truncate_inode_folio() is really trying to remove mapped folios,
> which is incorrect.
>
> "
> [ 189.171977] page: refcount:3 mapcount:1 mapping:00000000fe168ee1
> index:0x3880 pfn:0x191647
> [ 189.171995] memcg:ffff0000cc073d40
> [ 189.171997] aops:shmem_aops ino:3c01 dentry name(?):"memfd:runsc-memory"
> [ 189.172006] flags: 0x17fffef0002022d(locked|referenced|uptodate|lru|
> workingset|swapbacked|node=0|zone=2|lastcpupid=0x3ffff)
> .......
> [ 189.172017] page dumped because: VM_BUG_ON_FOLIO(folio_mapped(folio))
> [ 189.172026] ------------[ cut here ]------------
> [ 189.172027] kernel BUG at mm/filemap.c:155!
> [ 189.172057] Internal error: Oops - BUG: 00000000f2000800 [#1] SMP
> .......
> [ 189.177721] CPU: 3 UID: 0 PID: 5521 Comm: repro Kdump: loaded Tainted:
> 7.3.0-rc4+ #277 PREEMPT(lazy)
> .......
> [ 189.183986] Call trace:
> [ 189.184124] filemap_unaccount_folio+0xf0/0x1e8 (P)
> [ 189.184391] __filemap_remove_folio+0x34/0x160
> [ 189.184633] filemap_remove_folio+0x4c/0xb0
> [ 189.184859] truncate_inode_folio+0x34/0x58
> [ 189.185087] shmem_undo_range+0x220/0x658
> [ 189.185308] shmem_fallocate+0x2f0/0x470
> [ 189.185527] vfs_fallocate+0x128/0x328
> ......
> [ 189.187078] el0t_64_sync+0x184/0x188
> "
>
> After analysis, I believe the race exists between truncation and MADV_DONTNEED,
> and shmem's ->map_pages() merely makes the issue easier to reproduce. Since
> MADV_DONTNEED synchronously releases the pagetable page before calling
> tlb_flush_rmaps(), this could cause another thread's truncation to skip
> zap_pte_range() (due to pmd is none) but still observe the folio's mapcount
> as non-zero. A possible race scenario is as follows:
>
> CPU 0 CPU 1
> shmem_fallocate madvise_dontneed_single_vma
> unmap_mapping_range .......
> ......
> (filemap_map_pages() remap folios)
> zap_pte_range
> truncate_inode_folio zap_empty_pte_table(pmd clear)
> unmap_mapping_folio
> ......
> zap_pmd_range (saw pmd none, skip)
>
> filemap_remove_folio
> BUG_ON(folio_mapped)
That reminds me of sync_with_folio_pmd_zap() usage in page_vma_mapped_walk().
Isn't that the better approach?
--
Cheers,
David
^ permalink raw reply [flat|nested] 9+ messages in thread
* Re: [PATCH v2] mm: memory: fix truncation removing mapped folios
2026-10-12 10:23 ` David Hildenbrand (Arm)
@ 2026-10-13 9:36 ` Baolin Wang
2026-10-14 13:25 ` David Hildenbrand (Arm)
0 siblings, 1 reply; 9+ messages in thread
From: Baolin Wang @ 2026-10-13 9:36 UTC (permalink / raw)
To: David Hildenbrand (Arm), akpm
Cc: ljs, liam, vbabka, rppt, surenb, mhocko, qi.zheng, jack,
pfalcato, ayushr, linux-mm, linux-kernel
On 10/12/26 6:23 PM, David Hildenbrand (Arm) wrote:
> On 10/10/26 09:22, Baolin Wang wrote:
>> Ayush reported an issue that shmem/tmpfs folios are still mapped when
>> truncating them on production:
>> "
>> BUG: Bad page cache in process ... pfn:...
>> page dumped because: still mapped when deleted
>> "
>>
>> I reproduced the issue with Ayush's reproducer [1] with the latest mm-unstable
>> branch, and I got the following crash info. From the dump message, we can
>> see that truncate_inode_folio() is really trying to remove mapped folios,
>> which is incorrect.
>>
>> "
>> [ 189.171977] page: refcount:3 mapcount:1 mapping:00000000fe168ee1
>> index:0x3880 pfn:0x191647
>> [ 189.171995] memcg:ffff0000cc073d40
>> [ 189.171997] aops:shmem_aops ino:3c01 dentry name(?):"memfd:runsc-memory"
>> [ 189.172006] flags: 0x17fffef0002022d(locked|referenced|uptodate|lru|
>> workingset|swapbacked|node=0|zone=2|lastcpupid=0x3ffff)
>> .......
>> [ 189.172017] page dumped because: VM_BUG_ON_FOLIO(folio_mapped(folio))
>> [ 189.172026] ------------[ cut here ]------------
>> [ 189.172027] kernel BUG at mm/filemap.c:155!
>> [ 189.172057] Internal error: Oops - BUG: 00000000f2000800 [#1] SMP
>> .......
>> [ 189.177721] CPU: 3 UID: 0 PID: 5521 Comm: repro Kdump: loaded Tainted:
>> 7.3.0-rc4+ #277 PREEMPT(lazy)
>> .......
>> [ 189.183986] Call trace:
>> [ 189.184124] filemap_unaccount_folio+0xf0/0x1e8 (P)
>> [ 189.184391] __filemap_remove_folio+0x34/0x160
>> [ 189.184633] filemap_remove_folio+0x4c/0xb0
>> [ 189.184859] truncate_inode_folio+0x34/0x58
>> [ 189.185087] shmem_undo_range+0x220/0x658
>> [ 189.185308] shmem_fallocate+0x2f0/0x470
>> [ 189.185527] vfs_fallocate+0x128/0x328
>> ......
>> [ 189.187078] el0t_64_sync+0x184/0x188
>> "
>>
>> After analysis, I believe the race exists between truncation and MADV_DONTNEED,
>> and shmem's ->map_pages() merely makes the issue easier to reproduce. Since
>> MADV_DONTNEED synchronously releases the pagetable page before calling
>> tlb_flush_rmaps(), this could cause another thread's truncation to skip
>> zap_pte_range() (due to pmd is none) but still observe the folio's mapcount
>> as non-zero. A possible race scenario is as follows:
>>
>> CPU 0 CPU 1
>> shmem_fallocate madvise_dontneed_single_vma
>> unmap_mapping_range .......
>> ......
>> (filemap_map_pages() remap folios)
>> zap_pte_range
>> truncate_inode_folio zap_empty_pte_table(pmd clear)
>> unmap_mapping_folio
>> ......
>> zap_pmd_range (saw pmd none, skip)
>>
>> filemap_remove_folio
>> BUG_ON(folio_mapped)
>
> That reminds me of sync_with_folio_pmd_zap() usage in page_vma_mapped_walk().
>
> Isn't that the better approach?
I had considered this approach before, but it is not feasible. IIUC, the
sync_with_folio_pmd_zap() is used to address the race with
zap_huge_pmd(), and both the PMD clear and the folio mapcount decrement
are performed under the PMD lock in zap_huge_pmd().
However, the race here is different: tlb_flush_rmaps() is under the PTE
lock, while zap_empty_pte_table() is under the PMD lock, and reordering
between the two functions is also possible. That is, the folio mapcount
operation could execute after the acquire barrier (spin_lock()) of
zap_empty_pte_table(), allowing a reader on CPU 0 to incorrectly observe
that the PMD is none while mapcount > 0.
Also, we need to consider the possible reordering on the reader side on
CPU 0, as discussed earlier [1].
[1]
https://lore.kernel.org/all/1b77c2d7-acc6-476a-b5cd-547471a46409@linux.alibaba.com/
^ permalink raw reply [flat|nested] 9+ messages in thread
* Re: [PATCH v2] mm: memory: fix truncation removing mapped folios
2026-10-10 7:22 [PATCH v2] mm: memory: fix truncation removing mapped folios Baolin Wang
2026-10-12 10:23 ` David Hildenbrand (Arm)
@ 2026-10-13 10:24 ` Pedro Falcato
2026-10-14 9:51 ` Baolin Wang
1 sibling, 1 reply; 9+ messages in thread
From: Pedro Falcato @ 2026-10-13 10:24 UTC (permalink / raw)
To: Baolin Wang
Cc: akpm, david, ljs, liam, vbabka, rppt, surenb, mhocko, qi.zheng,
jack, ayushr, linux-mm, linux-kernel
On Sat, Oct 10, 2026 at 03:22:18PM +0800, Baolin Wang wrote:
> Ayush reported an issue that shmem/tmpfs folios are still mapped when
> truncating them on production:
> "
> BUG: Bad page cache in process ... pfn:...
> page dumped because: still mapped when deleted
> "
>
> I reproduced the issue with Ayush's reproducer [1] with the latest mm-unstable
> branch, and I got the following crash info. From the dump message, we can
> see that truncate_inode_folio() is really trying to remove mapped folios,
> which is incorrect.
>
> "
> [ 189.171977] page: refcount:3 mapcount:1 mapping:00000000fe168ee1
> index:0x3880 pfn:0x191647
> [ 189.171995] memcg:ffff0000cc073d40
> [ 189.171997] aops:shmem_aops ino:3c01 dentry name(?):"memfd:runsc-memory"
> [ 189.172006] flags: 0x17fffef0002022d(locked|referenced|uptodate|lru|
> workingset|swapbacked|node=0|zone=2|lastcpupid=0x3ffff)
> .......
> [ 189.172017] page dumped because: VM_BUG_ON_FOLIO(folio_mapped(folio))
> [ 189.172026] ------------[ cut here ]------------
> [ 189.172027] kernel BUG at mm/filemap.c:155!
> [ 189.172057] Internal error: Oops - BUG: 00000000f2000800 [#1] SMP
> .......
> [ 189.177721] CPU: 3 UID: 0 PID: 5521 Comm: repro Kdump: loaded Tainted:
> 7.3.0-rc4+ #277 PREEMPT(lazy)
> .......
> [ 189.183986] Call trace:
> [ 189.184124] filemap_unaccount_folio+0xf0/0x1e8 (P)
> [ 189.184391] __filemap_remove_folio+0x34/0x160
> [ 189.184633] filemap_remove_folio+0x4c/0xb0
> [ 189.184859] truncate_inode_folio+0x34/0x58
> [ 189.185087] shmem_undo_range+0x220/0x658
> [ 189.185308] shmem_fallocate+0x2f0/0x470
> [ 189.185527] vfs_fallocate+0x128/0x328
> ......
> [ 189.187078] el0t_64_sync+0x184/0x188
> "
>
> After analysis, I believe the race exists between truncation and MADV_DONTNEED,
> and shmem's ->map_pages() merely makes the issue easier to reproduce. Since
> MADV_DONTNEED synchronously releases the pagetable page before calling
> tlb_flush_rmaps(), this could cause another thread's truncation to skip
> zap_pte_range() (due to pmd is none) but still observe the folio's mapcount
> as non-zero. A possible race scenario is as follows:
>
> CPU 0 CPU 1
> shmem_fallocate madvise_dontneed_single_vma
> unmap_mapping_range .......
> ......
> (filemap_map_pages() remap folios)
> zap_pte_range
> truncate_inode_folio zap_empty_pte_table(pmd clear)
> unmap_mapping_folio
> ......
> zap_pmd_range (saw pmd none, skip)
>
> filemap_remove_folio
> BUG_ON(folio_mapped)
> tlb_flush_rmaps(remove rmaps)
>
> To fix this issue, we should move the pmd clear operation (via zap_empty_pte_table())
> to after tlb_flush_rmaps() and add smp_wmb()/smp_rmb() memory barriers, so that
> when we find a pmd_none() while unmapping a folio without holding the PTL, the
> folio's mappings are guaranteed to have been removed via tlb_flush_rmaps().
>
> [1] https://lore.kernel.org/all/20260925065013.3682431-1-ayushr@modal.com/
> Reported-by: Ayush Ranjan <ayushr@modal.com>
> Closes: https://lore.kernel.org/all/20260924061708.1645968-1-ayushr@modal.com/
> Fixes: 6375e95f381e ("mm: pgtable: reclaim empty PTE page in madvise(MADV_DONTNEED)")
> Signed-off-by: Baolin Wang <baolin.wang@linux.alibaba.com>
Reviewed-by: Pedro Falcato <pfalcato@suse.de>
Mostly LGTM, thanks!
> ---
> Changes from v1:
> - Add an smp_rmb() to pair with the smp_wmb() (Per Lorenzo and Pedro).
> ---
> mm/memory.c | 29 ++++++++++++++++++++---------
> 1 file changed, 20 insertions(+), 9 deletions(-)
>
> diff --git a/mm/memory.c b/mm/memory.c
> index 6a8e7772b8d6..c3a1faf580c8 100644
> --- a/mm/memory.c
> +++ b/mm/memory.c
> @@ -2036,6 +2036,15 @@ static unsigned long zap_pte_range(struct mmu_gather *tlb,
> }
> } while (pte += nr, addr += PAGE_SIZE * nr, addr != end);
>
> + add_mm_rss_vec(mm, rss);
> + lazy_mmu_mode_disable();
> +
> + /* Do the actual TLB flush before dropping ptl */
> + if (force_flush) {
> + tlb_flush_mmu_tlbonly(tlb);
> + tlb_flush_rmaps(tlb, vma);
> + }
> +
> /*
> * Fast path: try to hold the pmd lock and unmap the PTE page.
> *
> @@ -2044,16 +2053,13 @@ static unsigned long zap_pte_range(struct mmu_gather *tlb,
> * to ensure they are still none, thereby preventing the pte entries
> * from being repopulated by another thread.
> */
> - if (can_reclaim_pt && direct_reclaim && addr == end)
> + if (can_reclaim_pt && direct_reclaim && addr == end) {
> + /*
> + * The rmap changes need to be observed before PTEs get zapped.
> + * Pairs with smp_rmb() in zap_pmd_range().
> + */
I'd amend this comment to something like:
/*
* rmap changes need to be observed before page tables get zapped.
* Pairs with smp_rmb() in zap_pmd_range().
*/
> + smp_wmb();
> direct_reclaim = zap_empty_pte_table(mm, pmd, ptl, &pmdval);
> -
> - add_mm_rss_vec(mm, rss);
> - lazy_mmu_mode_disable();
> -
> - /* Do the actual TLB flush before dropping ptl */
> - if (force_flush) {
> - tlb_flush_mmu_tlbonly(tlb);
> - tlb_flush_rmaps(tlb, vma);
> }
> pte_unmap_unlock(start_pte, ptl);
>
> @@ -2108,6 +2114,11 @@ static inline unsigned long zap_pmd_range(struct mmu_gather *tlb,
> sync_with_folio_pmd_zap(tlb->mm, pmd);
> }
> if (pmd_none(*pmd)) {
> + /*
> + * Order this PMD read before the folio's mapcount read.
> + * Pairs with smp_wmb() in zap_pte_range().
> + */
and this one to something like:
/*
* Order this PMD read before any (possible) mapcount/rmap read.
* This isn't a problem in most cases, due to us synchronizing on
* the PTE lock, but such doesn't happen when PTE tables are zapped.
* Pairs with smp_wmb() in zap_pte_range().
*/
> + smp_rmb();
> addr = next;
> continue;
> }
Also, as a note, I would suggest we hold off on this fix until the merge window.
I don't think it's that urgent, and it seems like there is a possibility of a
nicer, cleaner fix. That and we need to audit pmd_none() sites for other
possible smp_rmb()'s...
--
Pedro
^ permalink raw reply [flat|nested] 9+ messages in thread
* Re: [PATCH v2] mm: memory: fix truncation removing mapped folios
2026-10-13 10:24 ` Pedro Falcato
@ 2026-10-14 9:51 ` Baolin Wang
0 siblings, 0 replies; 9+ messages in thread
From: Baolin Wang @ 2026-10-14 9:51 UTC (permalink / raw)
To: Pedro Falcato
Cc: akpm, david, ljs, liam, vbabka, rppt, surenb, mhocko, qi.zheng,
jack, ayushr, linux-mm, linux-kernel
On 10/13/26 6:24 PM, Pedro Falcato wrote:
> On Sat, Oct 10, 2026 at 03:22:18PM +0800, Baolin Wang wrote:
>> Ayush reported an issue that shmem/tmpfs folios are still mapped when
>> truncating them on production:
>> "
>> BUG: Bad page cache in process ... pfn:...
>> page dumped because: still mapped when deleted
>> "
>>
>> I reproduced the issue with Ayush's reproducer [1] with the latest mm-unstable
>> branch, and I got the following crash info. From the dump message, we can
>> see that truncate_inode_folio() is really trying to remove mapped folios,
>> which is incorrect.
>>
>> "
>> [ 189.171977] page: refcount:3 mapcount:1 mapping:00000000fe168ee1
>> index:0x3880 pfn:0x191647
>> [ 189.171995] memcg:ffff0000cc073d40
>> [ 189.171997] aops:shmem_aops ino:3c01 dentry name(?):"memfd:runsc-memory"
>> [ 189.172006] flags: 0x17fffef0002022d(locked|referenced|uptodate|lru|
>> workingset|swapbacked|node=0|zone=2|lastcpupid=0x3ffff)
>> .......
>> [ 189.172017] page dumped because: VM_BUG_ON_FOLIO(folio_mapped(folio))
>> [ 189.172026] ------------[ cut here ]------------
>> [ 189.172027] kernel BUG at mm/filemap.c:155!
>> [ 189.172057] Internal error: Oops - BUG: 00000000f2000800 [#1] SMP
>> .......
>> [ 189.177721] CPU: 3 UID: 0 PID: 5521 Comm: repro Kdump: loaded Tainted:
>> 7.3.0-rc4+ #277 PREEMPT(lazy)
>> .......
>> [ 189.183986] Call trace:
>> [ 189.184124] filemap_unaccount_folio+0xf0/0x1e8 (P)
>> [ 189.184391] __filemap_remove_folio+0x34/0x160
>> [ 189.184633] filemap_remove_folio+0x4c/0xb0
>> [ 189.184859] truncate_inode_folio+0x34/0x58
>> [ 189.185087] shmem_undo_range+0x220/0x658
>> [ 189.185308] shmem_fallocate+0x2f0/0x470
>> [ 189.185527] vfs_fallocate+0x128/0x328
>> ......
>> [ 189.187078] el0t_64_sync+0x184/0x188
>> "
>>
>> After analysis, I believe the race exists between truncation and MADV_DONTNEED,
>> and shmem's ->map_pages() merely makes the issue easier to reproduce. Since
>> MADV_DONTNEED synchronously releases the pagetable page before calling
>> tlb_flush_rmaps(), this could cause another thread's truncation to skip
>> zap_pte_range() (due to pmd is none) but still observe the folio's mapcount
>> as non-zero. A possible race scenario is as follows:
>>
>> CPU 0 CPU 1
>> shmem_fallocate madvise_dontneed_single_vma
>> unmap_mapping_range .......
>> ......
>> (filemap_map_pages() remap folios)
>> zap_pte_range
>> truncate_inode_folio zap_empty_pte_table(pmd clear)
>> unmap_mapping_folio
>> ......
>> zap_pmd_range (saw pmd none, skip)
>>
>> filemap_remove_folio
>> BUG_ON(folio_mapped)
>> tlb_flush_rmaps(remove rmaps)
>>
>> To fix this issue, we should move the pmd clear operation (via zap_empty_pte_table())
>> to after tlb_flush_rmaps() and add smp_wmb()/smp_rmb() memory barriers, so that
>> when we find a pmd_none() while unmapping a folio without holding the PTL, the
>> folio's mappings are guaranteed to have been removed via tlb_flush_rmaps().
>>
>> [1] https://lore.kernel.org/all/20260925065013.3682431-1-ayushr@modal.com/
>> Reported-by: Ayush Ranjan <ayushr@modal.com>
>> Closes: https://lore.kernel.org/all/20260924061708.1645968-1-ayushr@modal.com/
>> Fixes: 6375e95f381e ("mm: pgtable: reclaim empty PTE page in madvise(MADV_DONTNEED)")
>> Signed-off-by: Baolin Wang <baolin.wang@linux.alibaba.com>
>
> Reviewed-by: Pedro Falcato <pfalcato@suse.de>
Thanks.
> Mostly LGTM, thanks!
>
>> ---
>> Changes from v1:
>> - Add an smp_rmb() to pair with the smp_wmb() (Per Lorenzo and Pedro).
>> ---
>> mm/memory.c | 29 ++++++++++++++++++++---------
>> 1 file changed, 20 insertions(+), 9 deletions(-)
>>
>> diff --git a/mm/memory.c b/mm/memory.c
>> index 6a8e7772b8d6..c3a1faf580c8 100644
>> --- a/mm/memory.c
>> +++ b/mm/memory.c
>> @@ -2036,6 +2036,15 @@ static unsigned long zap_pte_range(struct mmu_gather *tlb,
>> }
>> } while (pte += nr, addr += PAGE_SIZE * nr, addr != end);
>>
>> + add_mm_rss_vec(mm, rss);
>> + lazy_mmu_mode_disable();
>> +
>> + /* Do the actual TLB flush before dropping ptl */
>> + if (force_flush) {
>> + tlb_flush_mmu_tlbonly(tlb);
>> + tlb_flush_rmaps(tlb, vma);
>> + }
>> +
>> /*
>> * Fast path: try to hold the pmd lock and unmap the PTE page.
>> *
>> @@ -2044,16 +2053,13 @@ static unsigned long zap_pte_range(struct mmu_gather *tlb,
>> * to ensure they are still none, thereby preventing the pte entries
>> * from being repopulated by another thread.
>> */
>> - if (can_reclaim_pt && direct_reclaim && addr == end)
>> + if (can_reclaim_pt && direct_reclaim && addr == end) {
>> + /*
>> + * The rmap changes need to be observed before PTEs get zapped.
>> + * Pairs with smp_rmb() in zap_pmd_range().
>> + */
>
> I'd amend this comment to something like:
> /*
> * rmap changes need to be observed before page tables get zapped.
> * Pairs with smp_rmb() in zap_pmd_range().
> */
Ack.
>> + smp_wmb();
>> direct_reclaim = zap_empty_pte_table(mm, pmd, ptl, &pmdval);
>> -
>> - add_mm_rss_vec(mm, rss);
>> - lazy_mmu_mode_disable();
>> -
>> - /* Do the actual TLB flush before dropping ptl */
>> - if (force_flush) {
>> - tlb_flush_mmu_tlbonly(tlb);
>> - tlb_flush_rmaps(tlb, vma);
>> }
>> pte_unmap_unlock(start_pte, ptl);
>>
>> @@ -2108,6 +2114,11 @@ static inline unsigned long zap_pmd_range(struct mmu_gather *tlb,
>> sync_with_folio_pmd_zap(tlb->mm, pmd);
>> }
>> if (pmd_none(*pmd)) {
>> + /*
>> + * Order this PMD read before the folio's mapcount read.
>> + * Pairs with smp_wmb() in zap_pte_range().
>> + */
>
> and this one to something like:
> /*
> * Order this PMD read before any (possible) mapcount/rmap read.
> * This isn't a problem in most cases, due to us synchronizing on
> * the PTE lock, but such doesn't happen when PTE tables are zapped.
> * Pairs with smp_wmb() in zap_pte_range().
> */
Ack.
>> + smp_rmb();
>> addr = next;
>> continue;
>> }
>
> Also, as a note, I would suggest we hold off on this fix until the merge window.
No strong opinion.
> I don't think it's that urgent, and it seems like there is a possibility of a
> nicer, cleaner fix. That and we need to audit pmd_none() sites for other
> possible smp_rmb()'s...
^ permalink raw reply [flat|nested] 9+ messages in thread
* Re: [PATCH v2] mm: memory: fix truncation removing mapped folios
2026-10-13 9:36 ` Baolin Wang
@ 2026-10-14 13:25 ` David Hildenbrand (Arm)
2026-10-14 14:05 ` Pedro Falcato
2026-10-15 6:41 ` Baolin Wang
0 siblings, 2 replies; 9+ messages in thread
From: David Hildenbrand (Arm) @ 2026-10-14 13:25 UTC (permalink / raw)
To: Baolin Wang, akpm
Cc: ljs, liam, vbabka, rppt, surenb, mhocko, qi.zheng, jack,
pfalcato, ayushr, linux-mm, linux-kernel
On 10/13/26 11:36, Baolin Wang wrote:
>
>
> On 10/12/26 6:23 PM, David Hildenbrand (Arm) wrote:
>> On 10/10/26 09:22, Baolin Wang wrote:
>>> Ayush reported an issue that shmem/tmpfs folios are still mapped when
>>> truncating them on production:
>>> "
>>> BUG: Bad page cache in process ... pfn:...
>>> page dumped because: still mapped when deleted
>>> "
>>>
>>> I reproduced the issue with Ayush's reproducer [1] with the latest mm-unstable
>>> branch, and I got the following crash info. From the dump message, we can
>>> see that truncate_inode_folio() is really trying to remove mapped folios,
>>> which is incorrect.
>>>
>>> "
>>> [ 189.171977] page: refcount:3 mapcount:1 mapping:00000000fe168ee1
>>> index:0x3880 pfn:0x191647
>>> [ 189.171995] memcg:ffff0000cc073d40
>>> [ 189.171997] aops:shmem_aops ino:3c01 dentry name(?):"memfd:runsc-memory"
>>> [ 189.172006] flags: 0x17fffef0002022d(locked|referenced|uptodate|lru|
>>> workingset|swapbacked|node=0|zone=2|lastcpupid=0x3ffff)
>>> .......
>>> [ 189.172017] page dumped because: VM_BUG_ON_FOLIO(folio_mapped(folio))
>>> [ 189.172026] ------------[ cut here ]------------
>>> [ 189.172027] kernel BUG at mm/filemap.c:155!
>>> [ 189.172057] Internal error: Oops - BUG: 00000000f2000800 [#1] SMP
>>> .......
>>> [ 189.177721] CPU: 3 UID: 0 PID: 5521 Comm: repro Kdump: loaded Tainted:
>>> 7.3.0-rc4+ #277 PREEMPT(lazy)
>>> .......
>>> [ 189.183986] Call trace:
>>> [ 189.184124] filemap_unaccount_folio+0xf0/0x1e8 (P)
>>> [ 189.184391] __filemap_remove_folio+0x34/0x160
>>> [ 189.184633] filemap_remove_folio+0x4c/0xb0
>>> [ 189.184859] truncate_inode_folio+0x34/0x58
>>> [ 189.185087] shmem_undo_range+0x220/0x658
>>> [ 189.185308] shmem_fallocate+0x2f0/0x470
>>> [ 189.185527] vfs_fallocate+0x128/0x328
>>> ......
>>> [ 189.187078] el0t_64_sync+0x184/0x188
>>> "
>>>
>>> After analysis, I believe the race exists between truncation and MADV_DONTNEED,
>>> and shmem's ->map_pages() merely makes the issue easier to reproduce. Since
>>> MADV_DONTNEED synchronously releases the pagetable page before calling
>>> tlb_flush_rmaps(), this could cause another thread's truncation to skip
>>> zap_pte_range() (due to pmd is none) but still observe the folio's mapcount
>>> as non-zero. A possible race scenario is as follows:
>>>
>>> CPU 0 CPU 1
>>> shmem_fallocate madvise_dontneed_single_vma
>>> unmap_mapping_range .......
>>> ......
>>> (filemap_map_pages() remap folios)
>>> zap_pte_range
>>> truncate_inode_folio zap_empty_pte_table(pmd clear)
>>> unmap_mapping_folio
>>> ......
>>> zap_pmd_range (saw pmd none, skip)
>>>
>>> filemap_remove_folio
>>> BUG_ON(folio_mapped)
>>
>> That reminds me of sync_with_folio_pmd_zap() usage in page_vma_mapped_walk().
>>
>> Isn't that the better approach?
>
> I had considered this approach before, but it is not feasible. IIUC, the
> sync_with_folio_pmd_zap() is used to address the race with zap_huge_pmd(), and
> both the PMD clear and the folio mapcount decrement are performed under the PMD
> lock in zap_huge_pmd().
Just to be clear, I am thinking about something like the following on the
zap_pmd_range() side:
diff --git a/mm/internal.h b/mm/internal.h
index 38b1165212c94..a8820ad7e2f21 100644
--- a/mm/internal.h
+++ b/mm/internal.h
@@ -584,14 +584,15 @@ void free_pgtables(struct mmu_gather *tlb, struct
unmap_desc *desc);
void pmd_install(struct mm_struct *mm, pmd_t *pmd, pgtable_t *pte);
/**
- * sync_with_folio_pmd_zap - sync with concurrent zapping of a folio PMD
+ * sync_with_folio_pmd_zap - sync with concurrent zapping of folios
* @mm: The mm_struct.
* @pmdp: Pointer to the pmd that was found to be pmd_none().
*
* When we find a pmd_none() while unmapping a folio without holding the PTL,
- * zap_huge_pmd() may have cleared the PMD but not yet modified the folio to
- * indicate that it's unmapped. Skipping the PMD without synchronization could
- * make folio unmapping code assume that unmapping failed.
+ * zap_huge_pmd() or zap_empty_pte_table() may have cleared the PMD but not
+ * yet modified the mapped folios to indicate they are unmapped. Skipping the
+ * PMD without synchronization could make folio unmapping code assume that
+ * unmapping failed.
*
* Wait for concurrent zapping to complete by grabbing the PTL.
*/
diff --git a/mm/memory.c b/mm/memory.c
index 8b0c2c735d3de..c585f5eb255c9 100644
--- a/mm/memory.c
+++ b/mm/memory.c
@@ -2139,9 +2139,7 @@ static inline unsigned long zap_pmd_range(struct
mmu_gather *tlb,
continue;
}
/* fall through */
- } else if (details && details->single_folio &&
- folio_test_pmd_mappable(details->single_folio) &&
- next - addr == HPAGE_PMD_SIZE && pmd_none(*pmd)) {
+ } else if (details && details->single_folio && pmd_none(*pmd)) {
sync_with_folio_pmd_zap(tlb->mm, pmd);
}
if (pmd_none(*pmd)) {
>
> However, the race here is different: tlb_flush_rmaps() is under the PTE lock,
> while zap_empty_pte_table() is under the PMD lock, and reordering between the
> two functions is also possible. That is, the folio mapcount operation could
> execute after the acquire barrier (spin_lock()) of zap_empty_pte_table(),
> allowing a reader on CPU 0 to incorrectly observe that the PMD is none while
> mapcount > 0.
I think we really just have to make sure to flush rmaps before zapping the PMD.
Then it's really just the same thing we had before with sync_with_folio_pmd_zap().
So I agree about doing the tlb_flush_rmaps() before zapping the PMD. But on the
zap_pmd_range() front that's really just what sync_with_folio_pmd_zap() wants to
handle.
--
Cheers,
David
^ permalink raw reply [flat|nested] 9+ messages in thread
* Re: [PATCH v2] mm: memory: fix truncation removing mapped folios
2026-10-14 13:25 ` David Hildenbrand (Arm)
@ 2026-10-14 14:05 ` Pedro Falcato
2026-10-14 18:44 ` David Hildenbrand (Arm)
2026-10-15 6:41 ` Baolin Wang
1 sibling, 1 reply; 9+ messages in thread
From: Pedro Falcato @ 2026-10-14 14:05 UTC (permalink / raw)
To: David Hildenbrand (Arm)
Cc: Baolin Wang, akpm, ljs, liam, vbabka, rppt, surenb, mhocko,
qi.zheng, jack, ayushr, linux-mm, linux-kernel
On Wed, Oct 14, 2026 at 03:25:33PM +0200, David Hildenbrand (Arm) wrote:
> On 10/13/26 11:36, Baolin Wang wrote:
> >
> >
> > On 10/12/26 6:23 PM, David Hildenbrand (Arm) wrote:
> >> On 10/10/26 09:22, Baolin Wang wrote:
> >>> Ayush reported an issue that shmem/tmpfs folios are still mapped when
> >>> truncating them on production:
> >>> "
> >>> BUG: Bad page cache in process ... pfn:...
> >>> page dumped because: still mapped when deleted
> >>> "
> >>>
> >>> I reproduced the issue with Ayush's reproducer [1] with the latest mm-unstable
> >>> branch, and I got the following crash info. From the dump message, we can
> >>> see that truncate_inode_folio() is really trying to remove mapped folios,
> >>> which is incorrect.
> >>>
> >>> "
> >>> [ 189.171977] page: refcount:3 mapcount:1 mapping:00000000fe168ee1
> >>> index:0x3880 pfn:0x191647
> >>> [ 189.171995] memcg:ffff0000cc073d40
> >>> [ 189.171997] aops:shmem_aops ino:3c01 dentry name(?):"memfd:runsc-memory"
> >>> [ 189.172006] flags: 0x17fffef0002022d(locked|referenced|uptodate|lru|
> >>> workingset|swapbacked|node=0|zone=2|lastcpupid=0x3ffff)
> >>> .......
> >>> [ 189.172017] page dumped because: VM_BUG_ON_FOLIO(folio_mapped(folio))
> >>> [ 189.172026] ------------[ cut here ]------------
> >>> [ 189.172027] kernel BUG at mm/filemap.c:155!
> >>> [ 189.172057] Internal error: Oops - BUG: 00000000f2000800 [#1] SMP
> >>> .......
> >>> [ 189.177721] CPU: 3 UID: 0 PID: 5521 Comm: repro Kdump: loaded Tainted:
> >>> 7.3.0-rc4+ #277 PREEMPT(lazy)
> >>> .......
> >>> [ 189.183986] Call trace:
> >>> [ 189.184124] filemap_unaccount_folio+0xf0/0x1e8 (P)
> >>> [ 189.184391] __filemap_remove_folio+0x34/0x160
> >>> [ 189.184633] filemap_remove_folio+0x4c/0xb0
> >>> [ 189.184859] truncate_inode_folio+0x34/0x58
> >>> [ 189.185087] shmem_undo_range+0x220/0x658
> >>> [ 189.185308] shmem_fallocate+0x2f0/0x470
> >>> [ 189.185527] vfs_fallocate+0x128/0x328
> >>> ......
> >>> [ 189.187078] el0t_64_sync+0x184/0x188
> >>> "
> >>>
> >>> After analysis, I believe the race exists between truncation and MADV_DONTNEED,
> >>> and shmem's ->map_pages() merely makes the issue easier to reproduce. Since
> >>> MADV_DONTNEED synchronously releases the pagetable page before calling
> >>> tlb_flush_rmaps(), this could cause another thread's truncation to skip
> >>> zap_pte_range() (due to pmd is none) but still observe the folio's mapcount
> >>> as non-zero. A possible race scenario is as follows:
> >>>
> >>> CPU 0 CPU 1
> >>> shmem_fallocate madvise_dontneed_single_vma
> >>> unmap_mapping_range .......
> >>> ......
> >>> (filemap_map_pages() remap folios)
> >>> zap_pte_range
> >>> truncate_inode_folio zap_empty_pte_table(pmd clear)
> >>> unmap_mapping_folio
> >>> ......
> >>> zap_pmd_range (saw pmd none, skip)
> >>>
> >>> filemap_remove_folio
> >>> BUG_ON(folio_mapped)
> >>
> >> That reminds me of sync_with_folio_pmd_zap() usage in page_vma_mapped_walk().
> >>
> >> Isn't that the better approach?
> >
> > I had considered this approach before, but it is not feasible. IIUC, the
> > sync_with_folio_pmd_zap() is used to address the race with zap_huge_pmd(), and
> > both the PMD clear and the folio mapcount decrement are performed under the PMD
> > lock in zap_huge_pmd().
>
> Just to be clear, I am thinking about something like the following on the
> zap_pmd_range() side:
>
> diff --git a/mm/internal.h b/mm/internal.h
> index 38b1165212c94..a8820ad7e2f21 100644
> --- a/mm/internal.h
> +++ b/mm/internal.h
> @@ -584,14 +584,15 @@ void free_pgtables(struct mmu_gather *tlb, struct
> unmap_desc *desc);
> void pmd_install(struct mm_struct *mm, pmd_t *pmd, pgtable_t *pte);
>
> /**
> - * sync_with_folio_pmd_zap - sync with concurrent zapping of a folio PMD
> + * sync_with_folio_pmd_zap - sync with concurrent zapping of folios
> * @mm: The mm_struct.
> * @pmdp: Pointer to the pmd that was found to be pmd_none().
> *
> * When we find a pmd_none() while unmapping a folio without holding the PTL,
> - * zap_huge_pmd() may have cleared the PMD but not yet modified the folio to
> - * indicate that it's unmapped. Skipping the PMD without synchronization could
> - * make folio unmapping code assume that unmapping failed.
> + * zap_huge_pmd() or zap_empty_pte_table() may have cleared the PMD but not
> + * yet modified the mapped folios to indicate they are unmapped. Skipping the
> + * PMD without synchronization could make folio unmapping code assume that
> + * unmapping failed.
> *
> * Wait for concurrent zapping to complete by grabbing the PTL.
> */
> diff --git a/mm/memory.c b/mm/memory.c
> index 8b0c2c735d3de..c585f5eb255c9 100644
> --- a/mm/memory.c
> +++ b/mm/memory.c
> @@ -2139,9 +2139,7 @@ static inline unsigned long zap_pmd_range(struct
> mmu_gather *tlb,
> continue;
> }
> /* fall through */
> - } else if (details && details->single_folio &&
> - folio_test_pmd_mappable(details->single_folio) &&
> - next - addr == HPAGE_PMD_SIZE && pmd_none(*pmd)) {
> + } else if (details && details->single_folio && pmd_none(*pmd)) {
> sync_with_folio_pmd_zap(tlb->mm, pmd);
> }
> if (pmd_none(*pmd)) {
>
> >
> > However, the race here is different: tlb_flush_rmaps() is under the PTE lock,
> > while zap_empty_pte_table() is under the PMD lock, and reordering between the
> > two functions is also possible. That is, the folio mapcount operation could
> > execute after the acquire barrier (spin_lock()) of zap_empty_pte_table(),
> > allowing a reader on CPU 0 to incorrectly observe that the PMD is none while
> > mapcount > 0.
>
> I think we really just have to make sure to flush rmaps before zapping the PMD.
> Then it's really just the same thing we had before with sync_with_folio_pmd_zap().
Hmm, could we viably replace sync_with_folio_pmd_zap() with the memory barriers
instead? Seems lighter-weight and functionally equivalent?
--
Pedro
^ permalink raw reply [flat|nested] 9+ messages in thread
* Re: [PATCH v2] mm: memory: fix truncation removing mapped folios
2026-10-14 14:05 ` Pedro Falcato
@ 2026-10-14 18:44 ` David Hildenbrand (Arm)
0 siblings, 0 replies; 9+ messages in thread
From: David Hildenbrand (Arm) @ 2026-10-14 18:44 UTC (permalink / raw)
To: Pedro Falcato
Cc: Baolin Wang, akpm, ljs, liam, vbabka, rppt, surenb, mhocko,
qi.zheng, jack, ayushr, linux-mm, linux-kernel
On 10/14/26 16:05, Pedro Falcato wrote:
> On Wed, Oct 14, 2026 at 03:25:33PM +0200, David Hildenbrand (Arm) wrote:
>> On 10/13/26 11:36, Baolin Wang wrote:
>>>
>>>
>>>
>>> I had considered this approach before, but it is not feasible. IIUC, the
>>> sync_with_folio_pmd_zap() is used to address the race with zap_huge_pmd(), and
>>> both the PMD clear and the folio mapcount decrement are performed under the PMD
>>> lock in zap_huge_pmd().
>>
>> Just to be clear, I am thinking about something like the following on the
>> zap_pmd_range() side:
>>
>> diff --git a/mm/internal.h b/mm/internal.h
>> index 38b1165212c94..a8820ad7e2f21 100644
>> --- a/mm/internal.h
>> +++ b/mm/internal.h
>> @@ -584,14 +584,15 @@ void free_pgtables(struct mmu_gather *tlb, struct
>> unmap_desc *desc);
>> void pmd_install(struct mm_struct *mm, pmd_t *pmd, pgtable_t *pte);
>>
>> /**
>> - * sync_with_folio_pmd_zap - sync with concurrent zapping of a folio PMD
>> + * sync_with_folio_pmd_zap - sync with concurrent zapping of folios
>> * @mm: The mm_struct.
>> * @pmdp: Pointer to the pmd that was found to be pmd_none().
>> *
>> * When we find a pmd_none() while unmapping a folio without holding the PTL,
>> - * zap_huge_pmd() may have cleared the PMD but not yet modified the folio to
>> - * indicate that it's unmapped. Skipping the PMD without synchronization could
>> - * make folio unmapping code assume that unmapping failed.
>> + * zap_huge_pmd() or zap_empty_pte_table() may have cleared the PMD but not
>> + * yet modified the mapped folios to indicate they are unmapped. Skipping the
>> + * PMD without synchronization could make folio unmapping code assume that
>> + * unmapping failed.
>> *
>> * Wait for concurrent zapping to complete by grabbing the PTL.
>> */
>> diff --git a/mm/memory.c b/mm/memory.c
>> index 8b0c2c735d3de..c585f5eb255c9 100644
>> --- a/mm/memory.c
>> +++ b/mm/memory.c
>> @@ -2139,9 +2139,7 @@ static inline unsigned long zap_pmd_range(struct
>> mmu_gather *tlb,
>> continue;
>> }
>> /* fall through */
>> - } else if (details && details->single_folio &&
>> - folio_test_pmd_mappable(details->single_folio) &&
>> - next - addr == HPAGE_PMD_SIZE && pmd_none(*pmd)) {
>> + } else if (details && details->single_folio && pmd_none(*pmd)) {
>> sync_with_folio_pmd_zap(tlb->mm, pmd);
>> }
>> if (pmd_none(*pmd)) {
>>
>>>
>>> However, the race here is different: tlb_flush_rmaps() is under the PTE lock,
>>> while zap_empty_pte_table() is under the PMD lock, and reordering between the
>>> two functions is also possible. That is, the folio mapcount operation could
>>> execute after the acquire barrier (spin_lock()) of zap_empty_pte_table(),
>>> allowing a reader on CPU 0 to incorrectly observe that the PMD is none while
>>> mapcount > 0.
>>
>> I think we really just have to make sure to flush rmaps before zapping the PMD.
>> Then it's really just the same thing we had before with sync_with_folio_pmd_zap().
>
> Hmm, could we viably replace sync_with_folio_pmd_zap() with the memory barriers
> instead? Seems lighter-weight and functionally equivalent?
For the ordinary PMD zap case it's really:
1) Clear PMD
2) Drop refcount+mapcount
So I don't think it's a direct replacement. If we had a proper way to invalidate
+ adjust + clear it could work. Like
1) Invalidate
2) Drop refcount+mapcount
3) Clear
But in general I think:
a) Likely all places where we have existing sync_with_folio_pmd_zap() have this
problem (2).
b) I don't want us to throw around memory barriers that way. This all belongs
into a helper (like sync_with_folio_pmd_zap()) which we only call when
required (e.g., details && details->single_folio) and that properly documents
all the nasty details.
c) Maybe there is some smart way to decide whether we need a memory barrier or a
proper lock for now inside sync_with_folio_pmd_zap(). Based on the folio size
should do (PMD vs. < PMD). But I assume grabbing the PMD lock briefly is not
really a concern: if there would be something mapped we would have to lock
it, and if it would be a PTE table we would lock that instead.
[insert rant about our PMD table locking]
--
Cheers,
David
^ permalink raw reply [flat|nested] 9+ messages in thread
* Re: [PATCH v2] mm: memory: fix truncation removing mapped folios
2026-10-14 13:25 ` David Hildenbrand (Arm)
2026-10-14 14:05 ` Pedro Falcato
@ 2026-10-15 6:41 ` Baolin Wang
1 sibling, 0 replies; 9+ messages in thread
From: Baolin Wang @ 2026-10-15 6:41 UTC (permalink / raw)
To: David Hildenbrand (Arm), akpm
Cc: ljs, liam, vbabka, rppt, surenb, mhocko, qi.zheng, jack,
pfalcato, ayushr, linux-mm, linux-kernel
On 10/14/26 9:25 PM, David Hildenbrand (Arm) wrote:
> On 10/13/26 11:36, Baolin Wang wrote:
>>
>>
>> On 10/12/26 6:23 PM, David Hildenbrand (Arm) wrote:
>>> On 10/10/26 09:22, Baolin Wang wrote:
>>>> Ayush reported an issue that shmem/tmpfs folios are still mapped when
>>>> truncating them on production:
>>>> "
>>>> BUG: Bad page cache in process ... pfn:...
>>>> page dumped because: still mapped when deleted
>>>> "
>>>>
>>>> I reproduced the issue with Ayush's reproducer [1] with the latest mm-unstable
>>>> branch, and I got the following crash info. From the dump message, we can
>>>> see that truncate_inode_folio() is really trying to remove mapped folios,
>>>> which is incorrect.
>>>>
>>>> "
>>>> [ 189.171977] page: refcount:3 mapcount:1 mapping:00000000fe168ee1
>>>> index:0x3880 pfn:0x191647
>>>> [ 189.171995] memcg:ffff0000cc073d40
>>>> [ 189.171997] aops:shmem_aops ino:3c01 dentry name(?):"memfd:runsc-memory"
>>>> [ 189.172006] flags: 0x17fffef0002022d(locked|referenced|uptodate|lru|
>>>> workingset|swapbacked|node=0|zone=2|lastcpupid=0x3ffff)
>>>> .......
>>>> [ 189.172017] page dumped because: VM_BUG_ON_FOLIO(folio_mapped(folio))
>>>> [ 189.172026] ------------[ cut here ]------------
>>>> [ 189.172027] kernel BUG at mm/filemap.c:155!
>>>> [ 189.172057] Internal error: Oops - BUG: 00000000f2000800 [#1] SMP
>>>> .......
>>>> [ 189.177721] CPU: 3 UID: 0 PID: 5521 Comm: repro Kdump: loaded Tainted:
>>>> 7.3.0-rc4+ #277 PREEMPT(lazy)
>>>> .......
>>>> [ 189.183986] Call trace:
>>>> [ 189.184124] filemap_unaccount_folio+0xf0/0x1e8 (P)
>>>> [ 189.184391] __filemap_remove_folio+0x34/0x160
>>>> [ 189.184633] filemap_remove_folio+0x4c/0xb0
>>>> [ 189.184859] truncate_inode_folio+0x34/0x58
>>>> [ 189.185087] shmem_undo_range+0x220/0x658
>>>> [ 189.185308] shmem_fallocate+0x2f0/0x470
>>>> [ 189.185527] vfs_fallocate+0x128/0x328
>>>> ......
>>>> [ 189.187078] el0t_64_sync+0x184/0x188
>>>> "
>>>>
>>>> After analysis, I believe the race exists between truncation and MADV_DONTNEED,
>>>> and shmem's ->map_pages() merely makes the issue easier to reproduce. Since
>>>> MADV_DONTNEED synchronously releases the pagetable page before calling
>>>> tlb_flush_rmaps(), this could cause another thread's truncation to skip
>>>> zap_pte_range() (due to pmd is none) but still observe the folio's mapcount
>>>> as non-zero. A possible race scenario is as follows:
>>>>
>>>> CPU 0 CPU 1
>>>> shmem_fallocate madvise_dontneed_single_vma
>>>> unmap_mapping_range .......
>>>> ......
>>>> (filemap_map_pages() remap folios)
>>>> zap_pte_range
>>>> truncate_inode_folio zap_empty_pte_table(pmd clear)
>>>> unmap_mapping_folio
>>>> ......
>>>> zap_pmd_range (saw pmd none, skip)
>>>>
>>>> filemap_remove_folio
>>>> BUG_ON(folio_mapped)
>>>
>>> That reminds me of sync_with_folio_pmd_zap() usage in page_vma_mapped_walk().
>>>
>>> Isn't that the better approach?
>>
>> I had considered this approach before, but it is not feasible. IIUC, the
>> sync_with_folio_pmd_zap() is used to address the race with zap_huge_pmd(), and
>> both the PMD clear and the folio mapcount decrement are performed under the PMD
>> lock in zap_huge_pmd().
>
> Just to be clear, I am thinking about something like the following on the
> zap_pmd_range() side:
>
> diff --git a/mm/internal.h b/mm/internal.h
> index 38b1165212c94..a8820ad7e2f21 100644
> --- a/mm/internal.h
> +++ b/mm/internal.h
> @@ -584,14 +584,15 @@ void free_pgtables(struct mmu_gather *tlb, struct
> unmap_desc *desc);
> void pmd_install(struct mm_struct *mm, pmd_t *pmd, pgtable_t *pte);
>
> /**
> - * sync_with_folio_pmd_zap - sync with concurrent zapping of a folio PMD
> + * sync_with_folio_pmd_zap - sync with concurrent zapping of folios
> * @mm: The mm_struct.
> * @pmdp: Pointer to the pmd that was found to be pmd_none().
> *
> * When we find a pmd_none() while unmapping a folio without holding the PTL,
> - * zap_huge_pmd() may have cleared the PMD but not yet modified the folio to
> - * indicate that it's unmapped. Skipping the PMD without synchronization could
> - * make folio unmapping code assume that unmapping failed.
> + * zap_huge_pmd() or zap_empty_pte_table() may have cleared the PMD but not
> + * yet modified the mapped folios to indicate they are unmapped. Skipping the
> + * PMD without synchronization could make folio unmapping code assume that
> + * unmapping failed.
> *
> * Wait for concurrent zapping to complete by grabbing the PTL.
> */
> diff --git a/mm/memory.c b/mm/memory.c
> index 8b0c2c735d3de..c585f5eb255c9 100644
> --- a/mm/memory.c
> +++ b/mm/memory.c
> @@ -2139,9 +2139,7 @@ static inline unsigned long zap_pmd_range(struct
> mmu_gather *tlb,
> continue;
> }
> /* fall through */
> - } else if (details && details->single_folio &&
> - folio_test_pmd_mappable(details->single_folio) &&
> - next - addr == HPAGE_PMD_SIZE && pmd_none(*pmd)) {
> + } else if (details && details->single_folio && pmd_none(*pmd)) {
> sync_with_folio_pmd_zap(tlb->mm, pmd);
> }
> if (pmd_none(*pmd)) {
>
Yes, I've considered that. However, implicit memory barriers give me a
headache, so I ended up going with explicit memory barriers (suggested
by Pedro).
If you don't like memory barriers, let me re-check whether the implicit
memory barriers here can ensure race safety with
sync_with_folio_pmd_zap(). A simplified logic is as follows:
CPU 0 (truncation) CPU 1 (zapping)
down_read() ->Acquire A1. pte spin_lock() ->Acquire A4
clear mapcount
pmd spin_lock() ->Acquire A5
clear pmd
pmd spin_unlock() ->Release R5
if (pmd_none()) { //read pmd
pte spin_unlock() ->Release R4
pmd spin_lock() ->Acquire A2
pmd spin_unlock() ->Release R2
}
up_read() -> Release R1
spin_lock(y) ->Acquire A3
folio_mapped() // read mapcount
spin_unlock(y) ->Release R3
CPU 0 constraints:
1. R2 is a release, so "read pmd" completes before R2.
2. A2 completes before A3 (ACQUIRE vs ACQUIRE).
3. "read mapcount" completes after A3, hence after A2.
CPU 1 constraints:
1. R5 is a release. Both "clear mapcount" and "clear pmd" complete
before R5.
According to above constraints, if CPU0 sees pmd_none() caused by CPU1's
clear pmd, and CPU0 then acquires the same pmd lock released by CPU1 at
R5, then CPU1's clear mapcount happens before CPU0's later folio_mapped().
Therefore folio_mapped() is guaranteed not to read the old mapcount in
this case. So I think sync_with_folio_pmd_zap() can work, but needs a
small change to avoid the double pmd_none() check.
David, does the following diff look good to you?
diff --git a/mm/memory.c b/mm/memory.c
index 6a8e7772b8d6..c0aadab1b59e 100644
--- a/mm/memory.c
+++ b/mm/memory.c
@@ -2036,6 +2036,15 @@ static unsigned long zap_pte_range(struct
mmu_gather *tlb,
}
} while (pte += nr, addr += PAGE_SIZE * nr, addr != end);
+ add_mm_rss_vec(mm, rss);
+ lazy_mmu_mode_disable();
+
+ /* Do the actual TLB flush before dropping ptl */
+ if (force_flush) {
+ tlb_flush_mmu_tlbonly(tlb);
+ tlb_flush_rmaps(tlb, vma);
+ }
+
/*
* Fast path: try to hold the pmd lock and unmap the PTE page.
*
@@ -2044,16 +2053,8 @@ static unsigned long zap_pte_range(struct
mmu_gather *tlb,
* to ensure they are still none, thereby preventing the pte
entries
* from being repopulated by another thread.
*/
- if (can_reclaim_pt && direct_reclaim && addr == end)
+ if (can_reclaim_pt && direct_reclaim && addr == end) {
direct_reclaim = zap_empty_pte_table(mm, pmd, ptl,
&pmdval);
-
- add_mm_rss_vec(mm, rss);
- lazy_mmu_mode_disable();
-
- /* Do the actual TLB flush before dropping ptl */
- if (force_flush) {
- tlb_flush_mmu_tlbonly(tlb);
- tlb_flush_rmaps(tlb, vma);
}
pte_unmap_unlock(start_pte, ptl);
@@ -2102,12 +2103,11 @@ static inline unsigned long zap_pmd_range(struct
mmu_gather *tlb,
continue;
}
/* fall through */
- } else if (details && details->single_folio &&
- folio_test_pmd_mappable(details->single_folio) &&
- next - addr == HPAGE_PMD_SIZE &&
pmd_none(*pmd)) {
- sync_with_folio_pmd_zap(tlb->mm, pmd);
}
+
if (pmd_none(*pmd)) {
+ if (details && details->single_folio)
+ sync_with_folio_pmd_zap(tlb->mm, pmd);
addr = next;
continue;
}
^ permalink raw reply [flat|nested] 9+ messages in thread
end of thread, other threads:[~2026-10-15 6:41 UTC | newest]
Thread overview: 9+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2026-10-10 7:22 [PATCH v2] mm: memory: fix truncation removing mapped folios Baolin Wang
2026-10-12 10:23 ` David Hildenbrand (Arm)
2026-10-13 9:36 ` Baolin Wang
2026-10-14 13:25 ` David Hildenbrand (Arm)
2026-10-14 14:05 ` Pedro Falcato
2026-10-14 18:44 ` David Hildenbrand (Arm)
2026-10-15 6:41 ` Baolin Wang
2026-10-13 10:24 ` Pedro Falcato
2026-10-14 9:51 ` Baolin Wang
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox
all inboxes | Powered by JetHome®