Re: [BUG] shmem: FALLOC_FL_PUNCH_HOLE vs fault-around race corrupts page cache / rss counters
From: Andrew Morton
Date: Mon Oct 05 2026 - 01:29:23 EST
On Sat, 3 Oct 2026 03:31:03 +0000 Ayush Ranjan <ayushr@xxxxxxxxx> wrote:
> Hi all,
>
> Gentle ping on this. The reproducer in my previous mail [1] triggers
> "Bad page cache ... still mapped when deleted" on 6.18.46 within a
> couple of minutes on a 128-CPU bare-metal box, with no fork() and no
> gVisor involved.
>
> We continue to hit this in production at low frequency, so I am happy
> to test patches or collect more data if that would help.
>
fwiw I made gpt and gemini argue about this for a while and ended up
with the below.
From: Andrew Morton <akpm@xxxxxxxxxxxxxxxxxxxx>
Subject: mm: shmem: serialize fault-around against hole punching
Date: Sun Oct 4 09:05:31 PM PDT 2026
shmem uses filemap_map_pages() for fault-around. Unlike shmem_fault(),
filemap_map_pages() does not participate in shmem's fallocate exclusion
protocol.
During a partial hole punch of a large shmem folio, the folio can be split
and the resulting smaller folios can remain temporarily visible in the page
cache while shmem_undo_range() restarts its walk. Fault-around can then map
one of those folios again before the hole-punch path removes it.
shmem_undo_range() may subsequently delete that folio from the page cache
despite the new userspace mapping. This can trigger "still mapped when
deleted" warnings and leave stale mappings or inconsistent RSS/page-table
accounting behind.
The race dates back to d7c1755179b8 ("mm: implement ->map_pages for
shmem/tmpfs"), which enabled generic fault-around for shmem without making
it participate in shmem's hole-punch exclusion protocol.
Serialize shmem fault-around against the hole-punch unmap/truncate sequence
with mapping->invalidate_lock. The hole-punch side holds the lock
exclusively while unmapping and removing pages.
do_fault_around() invokes ->map_pages() under rcu_read_lock(), so the
fault-around side cannot block on invalidate_lock. Use the shared trylock
instead. If the trylock fails, skip fault-around and let the normal shmem
fault path handle the fault. Otherwise hold the shared lock while
filemap_map_pages() installs mappings.
This preserves fault-around in the uncontended case while ensuring that
pages in a punched range cannot be remapped between unmap_mapping_range()
and shmem_truncate_range().
Fixes: d7c1755179b8 ("mm: implement ->map_pages for shmem/tmpfs")
Signed-off-by: Andrew Morton <akpm@xxxxxxxxxxxxxxxxxxxx>
---
mm/shmem.c | 27 +++++++++++++++++++++++++--
1 file changed, 25 insertions(+), 2 deletions(-)
--- a/mm/shmem.c~a
+++ a/mm/shmem.c
@@ -2938,6 +2938,27 @@ static vm_fault_t shmem_fault(struct vm_
return ret;
}
+/*
+ * A hole punch can temporarily leave split folios visible in the page cache
+ * after it has unmapped the range. Do not let fault-around map them again
+ * before shmem_truncate_range() removes them. ->map_pages() runs under
+ * rcu_read_lock(), so this exclusion must be non-blocking.
+ */
+static vm_fault_t shmem_map_pages(struct vm_fault *vmf,
+ pgoff_t start_pgoff, pgoff_t end_pgoff)
+{
+ struct address_space *mapping = vmf->vma->vm_file->f_mapping;
+ vm_fault_t ret;
+
+ if (!filemap_invalidate_trylock_shared(mapping))
+ return 0;
+
+ ret = filemap_map_pages(vmf, start_pgoff, end_pgoff);
+ filemap_invalidate_unlock_shared(mapping);
+
+ return ret;
+}
+
unsigned long shmem_get_unmapped_area(struct file *file,
unsigned long uaddr, unsigned long len,
unsigned long pgoff, unsigned long flags)
@@ -3865,10 +3886,12 @@ static long shmem_fallocate(struct file
WRITE_ONCE(inode->i_private, &shmem_falloc);
spin_unlock(&inode->i_lock);
+ filemap_invalidate_lock(mapping);
if ((u64)unmap_end > (u64)unmap_start)
unmap_mapping_range(mapping, unmap_start,
1 + unmap_end - unmap_start, 0);
shmem_truncate_range(inode, offset, offset + len - 1);
+ filemap_invalidate_unlock(mapping);
/* No need to unmap again: hole-punching leaves COWed pages */
spin_lock(&inode->i_lock);
@@ -5467,7 +5490,7 @@ static const struct super_operations shm
static const struct vm_operations_struct shmem_vm_ops = {
.fault = shmem_fault,
- .map_pages = filemap_map_pages,
+ .map_pages = shmem_map_pages,
#ifdef CONFIG_NUMA
.set_policy = shmem_set_policy,
.get_policy = shmem_get_policy,
@@ -5479,7 +5502,7 @@ static const struct vm_operations_struct
static const struct vm_operations_struct shmem_anon_vm_ops = {
.fault = shmem_fault,
- .map_pages = filemap_map_pages,
+ .map_pages = shmem_map_pages,
#ifdef CONFIG_NUMA
.set_policy = shmem_set_policy,
.get_policy = shmem_get_policy,
_