* [BUG] shmem: FALLOC_FL_PUNCH_HOLE vs fault-around race corrupts page cache / rss counters
@ 2026-09-24 6:16 Ayush Ranjan
2026-09-24 7:12 ` David Hildenbrand (Arm)
2026-09-24 8:34 ` Pedro Falcato
0 siblings, 2 replies; 9+ messages in thread
From: Ayush Ranjan @ 2026-09-24 6:16 UTC (permalink / raw)
To: Hugh Dickins, Matthew Wilcox, Andrew Morton, Jan Kara
Cc: Ayush Ranjan, Baolin Wang, David Hildenbrand, Gregory Price,
Pedro Falcato, linux-mm, linux-fsdevel, linux-kernel
Hi,
We are seeing shmem/tmpfs page cache corruption on production hosts
running a workload that punches holes in a memfd (hole-punch based
memory reclaim) while other threads and forked children fault the
same MAP_SHARED mapping. The kernel taints but does not oops:
BUG: Bad page cache in process ... pfn:...
page dumped because: still mapped when deleted
...
dentry name(?): "memfd:..."
and, more frequently, a paired rss-counter imbalance when the mm is
torn down, always exactly one PMD-order folio (512 pages):
BUG: Bad rss-counter state mm:... type:MM_FILEPAGES val:-512
BUG: Bad rss-counter state mm:... type:MM_SHMEMPAGES val:512
Seen on 6.12 and 6.18, x86_64, bare metal and VM, with
/sys/kernel/mm/transparent_hugepage/shmem_enabled = always.
This looks like the same corruption Gregory reported in March, which
as far as I can tell stalled without a fix; that report needed ~100
ballooning VMs to reproduce:
https://patchew.org/linux/20260326162611.693539-1-gourry@gourry.net/
The reproducer at the end of this mail trips it with a single memfd,
no VMs or ballooning, within a few minutes on a large machine, so
hopefully it makes the race easier to confirm (and to test a fix
against).
Here is my best understanding of the race -- corrections welcome:
shmem guards faults against an in-progress hole punch with
inode->i_private: shmem_fault() -> shmem_falloc_wait() waits while
shmem_fallocate(PUNCH_HOLE) holds i_private. But shmem's .map_pages
is the generic filemap_map_pages() (shmem_vm_ops /
shmem_anon_vm_ops), which does not consult i_private and does not
take invalidate_lock, and shmem does not use invalidate_lock to
serialize faults against truncation the way regular filesystems do --
the i_private + waitq scheme stands in for it, but only shmem_fault()
participates in that scheme.
So while shmem_fallocate(PUNCH_HOLE) is between
unmap_mapping_range() and shmem_truncate_range(), a concurrent
fault-around can (re-)install PTEs for folios that are about to be
truncated:
- filemap_map_pages() samples mm_counter_file(folio) once per batch
and applies it with add_mm_counter() after mapping; if the
folio's swapbacked state changes while it is concurrently torn
down, the map-time counter (MM_FILEPAGES) and the zap-time
counter (MM_SHMEMPAGES) disagree by exactly one folio -- the
+/-512 imbalance above.
- a folio re-mapped in this window (by fault-around directly, or
via a child VMA whose PTEs copy_page_range() installs after
unmap_mapping_range() has already walked the i_mmap tree -- the
dup_mmap() variant discussed in the earlier thread) can be
deleted from the page cache while still mapped -> "still mapped
when deleted".
Reproducer
----------
The race is on PMD-order folios, so khugepaged needs to scan
aggressively (with the default 10s scan interval the punched ranges
are not re-collapsed fast enough to reproduce quickly):
echo always > /sys/kernel/mm/transparent_hugepage/shmem_enabled
cd /sys/kernel/mm/transparent_hugepage/khugepaged
echo 1 > scan_sleep_millisecs
echo 4096 > pages_to_scan
echo 511 > max_ptes_none
cc -O2 -pthread -o repro repro_shmem_punch_race.c
for i in $(seq $(( $(nproc) / 3 ))); do ./repro 60 & done; wait
# watch: dmesg -w
On a 112-CPU host this trips within ~2-5 minutes; this capture is
from 6.12.0-204.92.4.4.3.el9uek.x86_64:
BUG: Bad rss-counter state mm:0000000078314ee0 type:MM_FILEPAGES val:-512
BUG: Bad rss-counter state mm:0000000078314ee0 type:MM_SHMEMPAGES val:512
The rss-counter form is the most frequent. The "still mapped when
deleted" form is what we mostly see in production but is rarer under
the reproducer (as in the earlier thread); I do not have a fresh
capture of it to paste here and will follow up with a full splat if I
catch one.
For background: we originally hit this under a memfd-backed sandbox
runtime (gVisor), which reclaims memory by punching holes in a
MAP_SHARED memfd while it is being faulted -- hence the "memfd:..."
dentry in the splats. The reproducer below has no such dependency: it
only uses memfd_create + mmap(MAP_SHARED) + fallocate(PUNCH_HOLE) +
madvise, so this appears to be a plain shmem issue rather than
anything specific to our setup.
Thanks,
Ayush
---- repro_shmem_punch_race.c ----
/*
* Race FALLOC_FL_PUNCH_HOLE against fault-around on a MAP_SHARED
* memfd mapping.
*
* One memfd is mapped MAP_SHARED into the main process and several
* forked peer processes. All of them fault (and MADV_DONTNEED,
* forcing re-fault via fault-around) random windows of the file while
* the main process punches holes at random offsets. Short-lived
* fork() children exercise the dup_mmap()/copy_page_range() variant.
*
* Usage: ./repro [seconds] [file_MiB] [peers]
*/
#define _GNU_SOURCE
#include <errno.h>
#include <fcntl.h>
#include <pthread.h>
#include <stdatomic.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/mman.h>
#include <sys/wait.h>
#include <time.h>
#include <unistd.h>
#include <linux/falloc.h>
#include <linux/memfd.h>
#define PAGE 4096UL
#define HPAGE (2UL << 20) /* PMD-order folio */
static unsigned char *map;
static int fd;
static size_t file_sz;
static volatile int stop;
static _Atomic long n_punch;
static inline uint64_t xorshift(uint64_t *s) {
*s ^= *s << 13; *s ^= *s >> 7; *s ^= *s << 17; return *s;
}
static uint64_t seed(void) {
struct timespec t; clock_gettime(CLOCK_MONOTONIC, &t);
return (t.tv_nsec ^ ((uint64_t)getpid() << 20) ^ (uint64_t)pthread_self()) | 1;
}
/* Read a random ~128 KiB span (spanning several fault-around batches), then drop
* it so the next touch faults again through filemap_map_pages(). */
static void *faulter(void *arg) {
uint64_t s = seed();
size_t span = 32 * PAGE;
while (!stop) {
size_t off = (xorshift(&s) % ((file_sz - span) / PAGE)) * PAGE;
volatile unsigned char sink = 0;
for (size_t o = 0; o < span; o += PAGE)
sink += map[off + o];
if (xorshift(&s) & 1)
madvise(map + off, span, MADV_DONTNEED);
}
return NULL;
}
/* Keep folios present for the puncher to race against. */
static void *writer(void *arg) {
uint64_t s = seed();
size_t span = 64 * PAGE;
while (!stop) {
size_t off = (xorshift(&s) % ((file_sz - span) / PAGE)) * PAGE;
memset(map + off, 0x5a, span);
}
return NULL;
}
/* Punch holes at random offsets, mixing PMD-aligned and unaligned/sub-PMD
* ranges. */
static void *puncher(void *arg) {
uint64_t s = seed();
while (!stop) {
size_t len, off;
if (xorshift(&s) & 1) { /* unaligned, 4K..2M */
len = ((xorshift(&s) % 512) + 1) * PAGE;
off = (xorshift(&s) % ((file_sz - len) / PAGE)) * PAGE;
} else { /* PMD-aligned, 2M/4M */
len = ((xorshift(&s) % 2) + 1) * HPAGE;
off = (xorshift(&s) % ((file_sz - len) / HPAGE)) * HPAGE;
}
fallocate(fd, FALLOC_FL_PUNCH_HOLE | FALLOC_FL_KEEP_SIZE,
(off_t)off, (off_t)len);
atomic_fetch_add(&n_punch, 1);
}
return NULL;
}
/* dup_mmap()/copy_page_range() variant; bounded to one child per thread. */
static void *forker(void *arg) {
uint64_t s = seed();
size_t pages = file_sz / PAGE;
while (!stop) {
pid_t p = fork();
if (p == 0) {
volatile unsigned char sink = 0;
for (int i = 0; i < 16; i++)
sink += map[(xorshift(&s) % pages) * PAGE];
_exit(0);
}
if (p > 0) waitpid(p, NULL, 0);
else usleep(200);
}
return NULL;
}
/* A peer process: maps the same memfd and faults/forks it concurrently. */
static void peer(int secs) {
pthread_t th[4];
pthread_create(&th[0], NULL, faulter, NULL);
pthread_create(&th[1], NULL, faulter, NULL);
pthread_create(&th[2], NULL, writer, NULL);
pthread_create(&th[3], NULL, forker, NULL);
sleep(secs + 2);
_exit(0);
}
int main(int argc, char **argv) {
int secs = argc > 1 ? atoi(argv[1]) : 60;
file_sz = (argc > 2 ? (size_t)atol(argv[2]) : 64) << 20;
int peers = argc > 3 ? atoi(argv[3]) : 2;
fd = memfd_create("repro", MFD_CLOEXEC);
if (fd < 0) { perror("memfd_create"); return 1; }
if (ftruncate(fd, file_sz)) { perror("ftruncate"); return 1; }
map = mmap(NULL, file_sz, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
if (map == MAP_FAILED) { perror("mmap"); return 1; }
madvise(map, file_sz, MADV_HUGEPAGE); /* request PMD-order folios */
memset(map, 1, file_sz);
pid_t pid[64];
if (peers > 64) peers = 64;
for (int i = 0; i < peers; i++) {
pid[i] = fork();
if (pid[i] == 0) peer(secs); /* child shares the memfd + mapping */
}
enum { NFAULT = 3, NFORK = 1 };
pthread_t th[NFAULT + NFORK + 2];
int n = 0;
for (int i = 0; i < NFAULT; i++) pthread_create(&th[n++], NULL, faulter, NULL);
for (int i = 0; i < NFORK; i++) pthread_create(&th[n++], NULL, forker, NULL);
pthread_create(&th[n++], NULL, writer, NULL);
pthread_create(&th[n++], NULL, puncher, NULL);
sleep(secs);
stop = 1;
for (int i = 0; i < n; i++) pthread_join(th[i], NULL);
for (int i = 0; i < peers; i++) { kill(pid[i], SIGKILL); waitpid(pid[i], NULL, 0); }
while (waitpid(-1, NULL, WNOHANG) > 0) {}
fprintf(stderr, "pid %d: punches=%ld\n", getpid(), atomic_load(&n_punch));
munmap(map, file_sz);
close(fd);
return 0;
}
^ permalink raw reply [flat|nested] 9+ messages in thread
* Re: [BUG] shmem: FALLOC_FL_PUNCH_HOLE vs fault-around race corrupts page cache / rss counters
2026-09-24 6:16 [BUG] shmem: FALLOC_FL_PUNCH_HOLE vs fault-around race corrupts page cache / rss counters Ayush Ranjan
@ 2026-09-24 7:12 ` David Hildenbrand (Arm)
2026-09-24 8:34 ` Pedro Falcato
1 sibling, 0 replies; 9+ messages in thread
From: David Hildenbrand (Arm) @ 2026-09-24 7:12 UTC (permalink / raw)
To: Ayush Ranjan, Hugh Dickins, Matthew Wilcox, Andrew Morton, Jan Kara
Cc: Baolin Wang, Gregory Price, Pedro Falcato, linux-mm,
linux-fsdevel, linux-kernel
On 9/24/26 08:16, Ayush Ranjan wrote:
> Hi,
>
> We are seeing shmem/tmpfs page cache corruption on production hosts
> running a workload that punches holes in a memfd (hole-punch based
> memory reclaim) while other threads and forked children fault the
> same MAP_SHARED mapping. The kernel taints but does not oops:
>
> BUG: Bad page cache in process ... pfn:...
> page dumped because: still mapped when deleted
> ...
> dentry name(?): "memfd:..."
>
> and, more frequently, a paired rss-counter imbalance when the mm is
> torn down, always exactly one PMD-order folio (512 pages):
>
> BUG: Bad rss-counter state mm:... type:MM_FILEPAGES val:-512
> BUG: Bad rss-counter state mm:... type:MM_SHMEMPAGES val:512
>
> Seen on 6.12 and 6.18, x86_64, bare metal and VM, with
> /sys/kernel/mm/transparent_hugepage/shmem_enabled = always.
>
> This looks like the same corruption Gregory reported in March, which
> as far as I can tell stalled without a fix; that report needed ~100
> ballooning VMs to reproduce:
>
> https://patchew.org/linux/20260326162611.693539-1-gourry@gourry.net/
Indeed, it looks like what was reported in
https://lore.kernel.org/all/20260326162611.693539-1-gourry@gourry.net/
--
Cheers,
David
^ permalink raw reply [flat|nested] 9+ messages in thread
* Re: [BUG] shmem: FALLOC_FL_PUNCH_HOLE vs fault-around race corrupts page cache / rss counters
2026-09-24 6:16 [BUG] shmem: FALLOC_FL_PUNCH_HOLE vs fault-around race corrupts page cache / rss counters Ayush Ranjan
2026-09-24 7:12 ` David Hildenbrand (Arm)
@ 2026-09-24 8:34 ` Pedro Falcato
2026-09-24 9:15 ` Jan Kara
` (2 more replies)
1 sibling, 3 replies; 9+ messages in thread
From: Pedro Falcato @ 2026-09-24 8:34 UTC (permalink / raw)
To: Ayush Ranjan
Cc: Hugh Dickins, Matthew Wilcox, Andrew Morton, Jan Kara,
Baolin Wang, David Hildenbrand, Gregory Price, linux-mm,
linux-fsdevel, linux-kernel
(please use the email I actually use for work, thanks; not sure how
you got to that one)
Hi,
On Thu, Sep 24, 2026 at 06:16:21AM +0000, Ayush Ranjan wrote:
> Hi,
>
> We are seeing shmem/tmpfs page cache corruption on production hosts
> running a workload that punches holes in a memfd (hole-punch based
> memory reclaim) while other threads and forked children fault the
> same MAP_SHARED mapping. The kernel taints but does not oops:
>
> BUG: Bad page cache in process ... pfn:...
> page dumped because: still mapped when deleted
> ...
> dentry name(?): "memfd:..."
>
> and, more frequently, a paired rss-counter imbalance when the mm is
> torn down, always exactly one PMD-order folio (512 pages):
>
> BUG: Bad rss-counter state mm:... type:MM_FILEPAGES val:-512
> BUG: Bad rss-counter state mm:... type:MM_SHMEMPAGES val:512
>
> Seen on 6.12 and 6.18, x86_64, bare metal and VM, with
> /sys/kernel/mm/transparent_hugepage/shmem_enabled = always.
>
> This looks like the same corruption Gregory reported in March, which
> as far as I can tell stalled without a fix; that report needed ~100
> ballooning VMs to reproduce:
Well, I thought (from the lack of replies) that it was probably a bug on
their side. Perhaps that's not true :/
>
> https://patchew.org/linux/20260326162611.693539-1-gourry@gourry.net/
>
> The reproducer at the end of this mail trips it with a single memfd,
> no VMs or ballooning, within a few minutes on a large machine, so
> hopefully it makes the race easier to confirm (and to test a fix
> against).
>
> Here is my best understanding of the race -- corrections welcome:
>
> shmem guards faults against an in-progress hole punch with
> inode->i_private: shmem_fault() -> shmem_falloc_wait() waits while
> shmem_fallocate(PUNCH_HOLE) holds i_private. But shmem's .map_pages
> is the generic filemap_map_pages() (shmem_vm_ops /
> shmem_anon_vm_ops), which does not consult i_private and does not
> take invalidate_lock, and shmem does not use invalidate_lock to
> serialize faults against truncation the way regular filesystems do --
> the i_private + waitq scheme stands in for it, but only shmem_fault()
> participates in that scheme.
>
> So while shmem_fallocate(PUNCH_HOLE) is between
> unmap_mapping_range() and shmem_truncate_range(), a concurrent
> fault-around can (re-)install PTEs for folios that are about to be
> truncated:
>
> - filemap_map_pages() samples mm_counter_file(folio) once per batch
> and applies it with add_mm_counter() after mapping; if the
> folio's swapbacked state changes while it is concurrently torn
But that cannot happen? We hold the folio lock in filemap_map_pages().
The folio (naturally) cannot be torn down while we have the folio lock.
> down, the map-time counter (MM_FILEPAGES) and the zap-time
> counter (MM_SHMEMPAGES) disagree by exactly one folio -- the
> +/-512 imbalance above.
>
> - a folio re-mapped in this window (by fault-around directly, or
> via a child VMA whose PTEs copy_page_range() installs after
> unmap_mapping_range() has already walked the i_mmap tree -- the
> dup_mmap() variant discussed in the earlier thread) can be
> deleted from the page cache while still mapped -> "still mapped
> when deleted".
No, I don't think this paragraph is true. Page cache truncation (via
truncate, or fallocate PUNCH_HOLE) takes the folio lock for each folio
that is about to be truncated out. Mapping folios takes the folio lock
as well, except in the fork() case where a myriad of weird interval tree
+ PTE lock interactions make it safe (AIUI).
>
> Reproducer
> ----------
>
> The race is on PMD-order folios, so khugepaged needs to scan
> aggressively (with the default 10s scan interval the punched ranges
> are not re-collapsed fast enough to reproduce quickly):
>
> echo always > /sys/kernel/mm/transparent_hugepage/shmem_enabled
> cd /sys/kernel/mm/transparent_hugepage/khugepaged
> echo 1 > scan_sleep_millisecs
> echo 4096 > pages_to_scan
> echo 511 > max_ptes_none
>
> cc -O2 -pthread -o repro repro_shmem_punch_race.c
> for i in $(seq $(( $(nproc) / 3 ))); do ./repro 60 & done; wait
> # watch: dmesg -w
>
> On a 112-CPU host this trips within ~2-5 minutes; this capture is
> from 6.12.0-204.92.4.4.3.el9uek.x86_64:
Awesome that you have a reproducer! Have you reproduced this on a mainline
kernel? Enterprise kernels are not supported upstream.
In any case, I'll take a closer look ASAP.
--
Pedro
^ permalink raw reply [flat|nested] 9+ messages in thread
* Re: [BUG] shmem: FALLOC_FL_PUNCH_HOLE vs fault-around race corrupts page cache / rss counters
2026-09-24 8:34 ` Pedro Falcato
@ 2026-09-24 9:15 ` Jan Kara
2026-09-25 5:32 ` Ayush Ranjan
2026-09-24 9:30 ` Baolin Wang
2026-09-25 5:30 ` Ayush Ranjan
2 siblings, 1 reply; 9+ messages in thread
From: Jan Kara @ 2026-09-24 9:15 UTC (permalink / raw)
To: Pedro Falcato
Cc: Ayush Ranjan, Hugh Dickins, Matthew Wilcox, Andrew Morton,
Jan Kara, Baolin Wang, David Hildenbrand, Gregory Price,
linux-mm, linux-fsdevel, linux-kernel
On Thu 24-09-26 09:34:24, Pedro Falcato wrote:
> On Thu, Sep 24, 2026 at 06:16:21AM +0000, Ayush Ranjan wrote:
> > Here is my best understanding of the race -- corrections welcome:
> >
> > shmem guards faults against an in-progress hole punch with
> > inode->i_private: shmem_fault() -> shmem_falloc_wait() waits while
> > shmem_fallocate(PUNCH_HOLE) holds i_private. But shmem's .map_pages
> > is the generic filemap_map_pages() (shmem_vm_ops /
> > shmem_anon_vm_ops), which does not consult i_private and does not
> > take invalidate_lock, and shmem does not use invalidate_lock to
> > serialize faults against truncation the way regular filesystems do --
> > the i_private + waitq scheme stands in for it, but only shmem_fault()
> > participates in that scheme.
> >
> > So while shmem_fallocate(PUNCH_HOLE) is between
> > unmap_mapping_range() and shmem_truncate_range(), a concurrent
> > fault-around can (re-)install PTEs for folios that are about to be
> > truncated:
> >
> > - filemap_map_pages() samples mm_counter_file(folio) once per batch
> > and applies it with add_mm_counter() after mapping; if the
> > folio's swapbacked state changes while it is concurrently torn
>
> But that cannot happen? We hold the folio lock in filemap_map_pages().
> The folio (naturally) cannot be torn down while we have the folio lock.
>
> > down, the map-time counter (MM_FILEPAGES) and the zap-time
> > counter (MM_SHMEMPAGES) disagree by exactly one folio -- the
> > +/-512 imbalance above.
> >
> > - a folio re-mapped in this window (by fault-around directly, or
> > via a child VMA whose PTEs copy_page_range() installs after
> > unmap_mapping_range() has already walked the i_mmap tree -- the
> > dup_mmap() variant discussed in the earlier thread) can be
> > deleted from the page cache while still mapped -> "still mapped
> > when deleted".
>
> No, I don't think this paragraph is true. Page cache truncation (via
> truncate, or fallocate PUNCH_HOLE) takes the folio lock for each folio
> that is about to be truncated out. Mapping folios takes the folio lock
> as well, except in the fork() case where a myriad of weird interval tree
> + PTE lock interactions make it safe (AIUI).
Can this be perhaps somehow related to the fixes in partial large folio
truncation Zhang Yi is working on, possibly even the tmpfs bug in handling
of folio split I've found [1]? It seems large folios are used here so that
matches, I just don't immediately see how those bugs would lead to the
errors reported here...
Honza
[1] https://lore.kernel.org/all/5pthbyxtn7q6xi4fmkofvksmcjzfnujcw2g4fxmxjzfin5pbgf@zui3vcimb4cv
--
Jan Kara <jack@suse.com>
SUSE Labs, CR
^ permalink raw reply [flat|nested] 9+ messages in thread
* Re: [BUG] shmem: FALLOC_FL_PUNCH_HOLE vs fault-around race corrupts page cache / rss counters
2026-09-24 8:34 ` Pedro Falcato
2026-09-24 9:15 ` Jan Kara
@ 2026-09-24 9:30 ` Baolin Wang
2026-09-25 5:33 ` Ayush Ranjan
2026-09-25 5:30 ` Ayush Ranjan
2 siblings, 1 reply; 9+ messages in thread
From: Baolin Wang @ 2026-09-24 9:30 UTC (permalink / raw)
To: Pedro Falcato, Ayush Ranjan
Cc: Hugh Dickins, Matthew Wilcox, Andrew Morton, Jan Kara,
David Hildenbrand, Gregory Price, linux-mm, linux-fsdevel,
linux-kernel
On 9/24/26 4:34 PM, Pedro Falcato wrote:
> (please use the email I actually use for work, thanks; not sure how
> you got to that one)
>
> Hi,
>
> On Thu, Sep 24, 2026 at 06:16:21AM +0000, Ayush Ranjan wrote:
>> Hi,
>>
>> We are seeing shmem/tmpfs page cache corruption on production hosts
>> running a workload that punches holes in a memfd (hole-punch based
>> memory reclaim) while other threads and forked children fault the
>> same MAP_SHARED mapping. The kernel taints but does not oops:
>>
>> BUG: Bad page cache in process ... pfn:...
>> page dumped because: still mapped when deleted
>> ...
>> dentry name(?): "memfd:..."
>>
>> and, more frequently, a paired rss-counter imbalance when the mm is
>> torn down, always exactly one PMD-order folio (512 pages):
>>
>> BUG: Bad rss-counter state mm:... type:MM_FILEPAGES val:-512
>> BUG: Bad rss-counter state mm:... type:MM_SHMEMPAGES val:512
>>
>> Seen on 6.12 and 6.18, x86_64, bare metal and VM, with
>> /sys/kernel/mm/transparent_hugepage/shmem_enabled = always.
>>
>> This looks like the same corruption Gregory reported in March, which
>> as far as I can tell stalled without a fix; that report needed ~100
>> ballooning VMs to reproduce:
>
> Well, I thought (from the lack of replies) that it was probably a bug on
> their side. Perhaps that's not true :/
>
>>
>> https://patchew.org/linux/20260326162611.693539-1-gourry@gourry.net/
>>
>> The reproducer at the end of this mail trips it with a single memfd,
>> no VMs or ballooning, within a few minutes on a large machine, so
>> hopefully it makes the race easier to confirm (and to test a fix
>> against).
>>
>> Here is my best understanding of the race -- corrections welcome:
>>
>> shmem guards faults against an in-progress hole punch with
>> inode->i_private: shmem_fault() -> shmem_falloc_wait() waits while
>> shmem_fallocate(PUNCH_HOLE) holds i_private. But shmem's .map_pages
>> is the generic filemap_map_pages() (shmem_vm_ops /
>> shmem_anon_vm_ops), which does not consult i_private and does not
>> take invalidate_lock, and shmem does not use invalidate_lock to
>> serialize faults against truncation the way regular filesystems do --
>> the i_private + waitq scheme stands in for it, but only shmem_fault()
>> participates in that scheme.
>>
>> So while shmem_fallocate(PUNCH_HOLE) is between
>> unmap_mapping_range() and shmem_truncate_range(), a concurrent
>> fault-around can (re-)install PTEs for folios that are about to be
>> truncated:
>>
>> - filemap_map_pages() samples mm_counter_file(folio) once per batch
>> and applies it with add_mm_counter() after mapping; if the
>> folio's swapbacked state changes while it is concurrently torn
>
> But that cannot happen? We hold the folio lock in filemap_map_pages().
> The folio (naturally) cannot be torn down while we have the folio lock.
>
>> down, the map-time counter (MM_FILEPAGES) and the zap-time
>> counter (MM_SHMEMPAGES) disagree by exactly one folio -- the
>> +/-512 imbalance above.
>>
>> - a folio re-mapped in this window (by fault-around directly, or
>> via a child VMA whose PTEs copy_page_range() installs after
>> unmap_mapping_range() has already walked the i_mmap tree -- the
>> dup_mmap() variant discussed in the earlier thread) can be
>> deleted from the page cache while still mapped -> "still mapped
>> when deleted".
>
> No, I don't think this paragraph is true. Page cache truncation (via
> truncate, or fallocate PUNCH_HOLE) takes the folio lock for each folio
> that is about to be truncated out. Mapping folios takes the folio lock
> as well, except in the fork() case where a myriad of weird interval tree
> + PTE lock interactions make it safe (AIUI).
Agree.
However, I did previously fix a race between filemap_map_pages() and
truncation that caused incorrect folio mappings, and I believe this race
also exists in shmem. Ayush, could you check whether that fix is present
in your kernel?
f58df566524e ("mm: filemap: fix nr_pages calculation overflow in
filemap_map_pages()")
>> Reproducer
>> ----------
>>
>> The race is on PMD-order folios, so khugepaged needs to scan
>> aggressively (with the default 10s scan interval the punched ranges
>> are not re-collapsed fast enough to reproduce quickly):
>>
>> echo always > /sys/kernel/mm/transparent_hugepage/shmem_enabled
>> cd /sys/kernel/mm/transparent_hugepage/khugepaged
>> echo 1 > scan_sleep_millisecs
>> echo 4096 > pages_to_scan
>> echo 511 > max_ptes_none
>>
>> cc -O2 -pthread -o repro repro_shmem_punch_race.c
>> for i in $(seq $(( $(nproc) / 3 ))); do ./repro 60 & done; wait
>> # watch: dmesg -w
>>
>> On a 112-CPU host this trips within ~2-5 minutes; this capture is
>> from 6.12.0-204.92.4.4.3.el9uek.x86_64:
>
> Awesome that you have a reproducer! Have you reproduced this on a mainline
> kernel? Enterprise kernels are not supported upstream.
I've been trying to reproduce the issue on v7.3.0-rc1 for half an hour
now with Ayush's reproducer, but haven't been able to trigger it.
^ permalink raw reply [flat|nested] 9+ messages in thread
* Re: [BUG] shmem: FALLOC_FL_PUNCH_HOLE vs fault-around race corrupts page cache / rss counters
2026-09-24 8:34 ` Pedro Falcato
2026-09-24 9:15 ` Jan Kara
2026-09-24 9:30 ` Baolin Wang
@ 2026-09-25 5:30 ` Ayush Ranjan
2026-09-25 6:50 ` Ayush Ranjan
2 siblings, 1 reply; 9+ messages in thread
From: Ayush Ranjan @ 2026-09-25 5:30 UTC (permalink / raw)
To: Pedro Falcato
Cc: Ayush Ranjan, Hugh Dickins, Matthew Wilcox, Andrew Morton,
Jan Kara, Baolin Wang, David Hildenbrand, Gregory Price,
linux-mm, linux-fsdevel, linux-kernel
On Thu, Sep 24, 2026 at 08:34 +0000, Pedro Falcato wrote:
> (please use the email I actually use for work, thanks; not sure how
> you got to that one)
Sorry about that. I put the Cc list together with the help of an AI
assistant, and it filled in the gmail address from your older list
postings; I should have checked it against MAINTAINERS. Using this
one from now on.
> > - filemap_map_pages() samples mm_counter_file(folio) once per batch
> > and applies it with add_mm_counter() after mapping; if the
> > folio's swapbacked state changes while it is concurrently torn
>
> But that cannot happen? We hold the folio lock in filemap_map_pages().
> The folio (naturally) cannot be torn down while we have the folio lock.
[...]
> No, I don't think this paragraph is true. Page cache truncation (via
> truncate, or fallocate PUNCH_HOLE) takes the folio lock for each folio
> that is about to be truncated out. Mapping folios takes the folio lock
> as well, except in the fork() case where a myriad of weird interval tree
> + PTE lock interactions make it safe (AIUI).
You're right... Thanks for the correction.
One empirical hint that may help: the reproducer strictly requires
khugepaged to be re-collapsing the punched ranges (it never trips with
the default 10s scan interval), so the large-folio / partial-truncation
angle Jan raised elsewhere in the thread may be the more promising one.
> Awesome that you have a reproducer! Have you reproduced this on a
> mainline kernel? Enterprise kernels are not supported upstream.
Partially. The production workload trips both BUGs on:
- 6.12.96 (Ubuntu 24.04, mainline stable build)
- 6.18.46 + two writeback backports (31c1d19ead2c "writeback: use a
per-sb counter to drain inode wb switches at umount" and
f6988c90671e "writeback: bound cleanup_offline_cgwb() rescans by
rotating scanned inodes") (Ubuntu 24.04)
- 6.12.0-204.92.4.4.3.el9uek (Oracle Linux 9, UEK8)
The standalone reproducer, however, has so far only triggered the
rss-counter one, and only on the UEK8 kernel. Not on our 6.18.46
hosts, and it has never triggered the "Bad page cache" one for me.
So it clearly does not capture everything the production workload
does. The production workload which triggers this is gVisor, which
heavily utilizes memfd to implement application memory for the
sandboxed application and punches holes into it to decommit/release
memory on the host.
For completeness, here is a production capture of the "still mapped
when deleted" bug on the 6.18.46 kernel (the gVisor workload
mentioned in the report; the taint is from our out-of-tree module which
was not being used here):
BUG: Bad page cache in process exe pfn:1be0e380
page: refcount:17 mapcount:1 mapping:00000000ceb7a77f index:0x153980 pfn:0x1be0e380
head: order:3 mapcount:8 entire_mapcount:0 nr_pages_mapped:8 pincount:0
memcg:ff25c58e86be5480
aops:shmem_aops ino:3180f dentry name(?):"memfd:runsc-memory"
flags: 0x57ffffd802006d(locked|referenced|uptodate|lru|head|swapbacked|node=1|zone=2|lastcpupid=0x1fffff)
raw: 0057ffffd802006d ff8c38e33838e208 ff8c38e33838a008 ff25c5f1950be208
raw: 0000000000153980 0000000000000000 0000001100000000 ff25c58e86be5480
head: 0057ffffd802006d ff8c38e33838e208 ff8c38e33838a008 ff25c5f1950be208
head: 0000000000153980 0000000000000000 0000001100000000 ff25c58e86be5480
head: 0057ffffc0000203 ff8c38e33838e001 0000000800000007 00000000ffffffff
head: ffffffff00000007 00000000000000d4 0000000000000000 0000000000000008
page dumped because: still mapped when deleted
CPU: 170 UID: 0 PID: 947113 Comm: exe Kdump: loaded Tainted: G OE 6.18.46-modal2 #2 PREEMPT(voluntary)
Tainted: [O]=OOT_MODULE, [E]=UNSIGNED_MODULE
Hardware name: Oracle Corporation ORACLE SERVER E6-2c/Asm,MB+Tray,E6-2c, BIOS 89070200 04/03/2026
Call Trace:
<TASK>
dump_stack_lvl+0x76/0xa0
dump_stack+0x10/0x20
filemap_unaccount_folio+0xf7/0x240
__filemap_remove_folio+0x3c/0x1e0
? vma_interval_tree_iter_next+0xaa/0xc0
? unmap_mapping_folio+0x70/0x130
? __folio_cancel_dirty+0x29/0x110
filemap_remove_folio+0x47/0xf0
truncate_inode_partial_folio+0x15e/0x2d0
shmem_undo_range+0x6bb/0x930
shmem_fallocate+0x1ab/0x530
vfs_fallocate+0x17b/0x3b0
__x64_sys_fallocate+0x4a/0xc0
x64_sys_call+0x1fe1/0x26a0
do_syscall_64+0x82/0xf80
? seccomp_notify_ioctl+0x3dd/0x7a0
? __seccomp_filter+0x10b/0x610
? __x64_sys_ioctl+0xbf/0x100
entry_SYSCALL_64_after_hwframe+0x76/0x7e
RIP: 0033:0x40d00e
</TASK>
followed later, when that process exited, by:
BUG: Bad rss-counter state mm:00000000283589c7 type:MM_SHMEMPAGES val:40 Comm:exe Pid:939691
Thanks for taking a look.
Thanks,
Ayush
^ permalink raw reply [flat|nested] 9+ messages in thread
* Re: [BUG] shmem: FALLOC_FL_PUNCH_HOLE vs fault-around race corrupts page cache / rss counters
2026-09-24 9:15 ` Jan Kara
@ 2026-09-25 5:32 ` Ayush Ranjan
0 siblings, 0 replies; 9+ messages in thread
From: Ayush Ranjan @ 2026-09-25 5:32 UTC (permalink / raw)
To: Jan Kara
Cc: Ayush Ranjan, Hugh Dickins, Matthew Wilcox, Andrew Morton,
Pedro Falcato, Baolin Wang, David Hildenbrand, Gregory Price,
linux-mm, linux-fsdevel, linux-kernel
On Thu, Sep 24, 2026 at 09:15 +0000, Jan Kara wrote:
> Can this be perhaps somehow related to the fixes in partial large folio
> truncation Zhang Yi is working on, possibly even the tmpfs bug in handling
> of folio split I've found [1]? It seems large folios are used here so that
> matches, I just don't immediately see how those bugs would lead to the
> errors reported here...
That would fit what I see, for what it's worth:
- the rss-counter imbalance from the reproducer is always exactly
one PMD-order folio (+/-512), which looks more like large-folio
split/accounting going wrong than lost or extra PTEs;
- the reproducer's punching thread deliberately mixes unaligned
sub-PMD ranges, so partial truncation of large folios is exercised
constantly, and the production "still mapped when deleted" splat
is reported from truncate_inode_partial_folio() (full stack in my
reply to Pedro).
Thanks,
Ayush
^ permalink raw reply [flat|nested] 9+ messages in thread
* Re: [BUG] shmem: FALLOC_FL_PUNCH_HOLE vs fault-around race corrupts page cache / rss counters
2026-09-24 9:30 ` Baolin Wang
@ 2026-09-25 5:33 ` Ayush Ranjan
0 siblings, 0 replies; 9+ messages in thread
From: Ayush Ranjan @ 2026-09-25 5:33 UTC (permalink / raw)
To: Baolin Wang
Cc: Ayush Ranjan, Hugh Dickins, Matthew Wilcox, Andrew Morton,
Jan Kara, Pedro Falcato, David Hildenbrand, Gregory Price,
linux-mm, linux-fsdevel, linux-kernel
On Thu, Sep 24, 2026 at 09:30 +0000, Baolin Wang wrote:
> However, I did previously fix a race between filemap_map_pages() and
> truncation that caused incorrect folio mappings, and I believe this race
> also exists in shmem. Ayush, could you check whether that fix is present
> in your kernel?
>
> f58df566524e ("mm: filemap: fix nr_pages calculation overflow in
> filemap_map_pages()")
It is present on the UEK 6.12.0-204; its changelog lists f58df566524e
(as the CVE-2026-31648 fix), and the rss-counter imbalance still
reproduces on that kernel.
One more data point that may help: the reproducer punches with
FALLOC_FL_PUNCH_HOLE | FALLOC_FL_KEEP_SIZE and never changes i_size
(and all faults are below i_size), so the i_size-shrink window that
commit closes should not be in play at all. That seems consistent with
your suspicion that a shmem analogue of the race remains unfixed.
> I've been trying to reproduce the issue on v7.3.0-rc1 for half an hour
> now with Ayush's reproducer, but haven't been able to trigger it.
Thank you for trying. Two things that were essential on my side, in
case either did not make it into your run:
- the khugepaged tunables from the report (scan_sleep_millisecs=1,
pages_to_scan=4096, max_ptes_none=511): with the default 10s scan
interval it never reproduced for me either;
- shmem_enabled=always, and many parallel instances on a large
machine (nproc/3 instances on a 112-CPU box; it takes ~2-5 minutes
to trip).
That said, I should be upfront that the reproducer has so far only
triggered the corruption on the UEK8 6.12 kernel -- not on our 6.18.46
hosts, even though the production workload hits both splat forms there
(kernel list in my reply to Pedro). So the reproducer is clearly
missing some ingredient of the production workload...
Thanks,
Ayush
^ permalink raw reply [flat|nested] 9+ messages in thread
* Re: [BUG] shmem: FALLOC_FL_PUNCH_HOLE vs fault-around race corrupts page cache / rss counters
2026-09-25 5:30 ` Ayush Ranjan
@ 2026-09-25 6:50 ` Ayush Ranjan
0 siblings, 0 replies; 9+ messages in thread
From: Ayush Ranjan @ 2026-09-25 6:50 UTC (permalink / raw)
To: Pedro Falcato
Cc: Ayush Ranjan, Hugh Dickins, Matthew Wilcox, Andrew Morton,
Jan Kara, Baolin Wang, David Hildenbrand, Gregory Price,
linux-mm, linux-fsdevel, linux-kernel
On Fri, Sep 25, 2026 at 05:30 +0000, I wrote:
> The standalone reproducer, however, has so far only triggered the
> rss-counter one, and only on the UEK8 kernel. Not on our 6.18.46
> hosts, and it has never triggered the "Bad page cache" one for me.
> So it clearly does not capture everything the production workload
> does.
Following up: a reworked reproducer (at the end of this mail) now
triggers the "Bad page cache ... still mapped when deleted" bug on
6.18.46 (plus 31c1d19ead2c "writeback: use a per-sb counter to drain
inode wb switches at umount"). The new reproducer typically works
within 2 minutes on a 128-CPU box. It also still produces the
rss-counter imbalance when the process exits.
Two changes over the previous version made the difference:
1. Every punch now forces a partial-folio split: it punches
[pmd_start, pmd_start + k * PAGE) with 1 <= k < 512 (PMD-aligned
start, mid-PMD end), which sends the straddling huge folio
through truncate_inode_partial_folio() and a folio split.
2. Fault-around is steered at just-punched ranges: the punching
thread publishes the PMD index it just punched into a small
shared ring, and the faulting threads preferentially read across
those PMDs, so filemap_map_pages() keeps re-installing PTEs over
the range being torn down.
Two data points from this version:
- fork() is not needed: a single-process variant (one memfd, one
MAP_SHARED mapping, faulting threads plus one punching thread)
trips it as well, so the dup_mmap() angle can be ruled out
entirely.
- it still strictly requires shmem_enabled=always plus aggressive
khugepaged (scan_sleep_millisecs=1, pages_to_scan=4096,
max_ptes_none=511); with default khugepaged settings it does not
trip within 150s. The constant re-collapse of punched ranges back
into PMD folios is essential.
Pedro: I think this is consistent with your folio-lock point. Both
mapping and truncation do hold the folio lock, but not across the
whole punch: on a partial punch, truncate_inode_partial_folio()
splits the straddling folio, and the sub-folios inside the hole are
only removed by shmem_undo_range()'s subsequent lookup pass. In
between, they sit unlocked in the page cache, where
filemap_map_pages() -- which, unlike shmem_fault(), knows nothing of
the shmem_falloc guard -- can lock and map them; the later removal
then finds them mapped.
Baolin: given the above, this version may be worth another try on
v7.3-rc1 with the khugepaged settings applied; I would expect the
same behaviour there but have only verified 6.18 so far.
Run recipe (same as before, plus alloc_sleep_millisecs):
echo always > /sys/kernel/mm/transparent_hugepage/shmem_enabled
cd /sys/kernel/mm/transparent_hugepage/khugepaged
echo 1 > scan_sleep_millisecs
echo 1 > alloc_sleep_millisecs
echo 4096 > pages_to_scan
echo 511 > max_ptes_none
cc -O2 -pthread -o repro shmem_punch_fault_race.c
for i in $(seq $(( $(nproc) / 4 ))); do ./repro 120 & done
# watch: dmesg -w
Thanks,
Ayush
---- shmem_punch_fault_race.c ----
// SPDX-License-Identifier: GPL-2.0
/*
* Reproducer: shmem/tmpfs hole-punch vs fault-around race on huge
* folios ("BUG: Bad page cache ... still mapped when deleted").
*
* One memfd, mapped MAP_SHARED. The punching thread punches
* [pmd_start, pmd_start + k * PAGE), 1 <= k < 512, to force a split
* of the straddling huge folio, and publishes the punched PMD index
* to a shared ring; faulting threads read across recently punched
* PMDs so fault-around re-populates them. Peer processes only
* accelerate the race: a single process suffices.
*
* Usage: ./shmem_punch_fault_race [seconds] [file_MiB] [peer_procs]
*/
#define _GNU_SOURCE
#include <fcntl.h>
#include <pthread.h>
#include <stdatomic.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/mman.h>
#include <sys/wait.h>
#include <time.h>
#include <unistd.h>
#include <linux/falloc.h>
#include <linux/memfd.h>
#define PAGE 4096UL
#define HPAGE (2UL << 20) /* PMD-order folio */
#define PMD_PAGES (HPAGE / PAGE) /* 512 */
/* Shared across all peer processes; steers faulters onto just-punched PMDs. */
struct ctl {
_Atomic uint64_t hot[64]; /* recently punched PMD indices */
_Atomic uint64_t seq;
_Atomic long n_punch;
volatile int stop;
};
static unsigned char *map;
static int fd;
static size_t file_sz, n_pmd;
static struct ctl *ctl;
static inline uint64_t xs(uint64_t *s)
{
*s ^= *s << 13; *s ^= *s >> 7; *s ^= *s << 17; return *s;
}
static uint64_t seed(void)
{
struct timespec t;
clock_gettime(CLOCK_MONOTONIC, &t);
return (t.tv_nsec ^ ((uint64_t)getpid() << 20) ^
(uint64_t)pthread_self()) | 1;
}
static void push_hot(uint64_t pmd)
{
uint64_t i = atomic_fetch_add(&ctl->seq, 1) & 63;
atomic_store(&ctl->hot[i], pmd + 1); /* 0 == empty */
}
static uint64_t pick_hot(uint64_t *s)
{
uint64_t v = atomic_load(&ctl->hot[xs(s) & 63]);
return v ? v - 1 : (xs(s) % n_pmd);
}
/* Read across a (recently punched) PMD so fault-around re-populates it, then
* drop it to force the next touch to fault in again through map_pages. */
static void *faulter(void *a)
{
uint64_t s = seed();
while (!ctl->stop) {
uint64_t p = pick_hot(&s);
size_t base = p * HPAGE;
volatile unsigned char sink = 0;
for (size_t o = 0; o < HPAGE; o += PAGE)
sink += map[base + o];
(void)sink;
if (xs(&s) & 1)
madvise(map + base, HPAGE, MADV_DONTNEED);
}
return NULL;
}
/* Keep PMD folios present and dirty so the puncher always has one to split. */
static void *writer(void *a)
{
uint64_t s = seed();
while (!ctl->stop) {
uint64_t p = xs(&s) % n_pmd;
memset(map + p * HPAGE, 0x5a, HPAGE);
}
return NULL;
}
/* Punch [pmd_start, pmd_start + k*PAGE), 1 <= k < 512: forces a folio_split()
* of the trailing partial PMD folio. Occasionally drop a whole PMD to keep the
* allocator/khugepaged churning fresh huge folios. */
static void *puncher(void *a)
{
uint64_t s = seed();
while (!ctl->stop) {
uint64_t p = xs(&s) % n_pmd;
size_t off = p * HPAGE, len;
if (xs(&s) % 4 == 0)
len = HPAGE;
else
len = (1 + (xs(&s) % (PMD_PAGES - 1))) * PAGE;
fallocate(fd, FALLOC_FL_PUNCH_HOLE | FALLOC_FL_KEEP_SIZE,
(off_t)off, (off_t)len);
push_hot(p);
atomic_fetch_add(&ctl->n_punch, 1);
}
return NULL;
}
static void peer(int secs)
{
pthread_t t[4];
pthread_create(&t[0], NULL, faulter, NULL);
pthread_create(&t[1], NULL, faulter, NULL);
pthread_create(&t[2], NULL, faulter, NULL);
pthread_create(&t[3], NULL, writer, NULL);
sleep(secs + 2);
_exit(0);
}
int main(int argc, char **argv)
{
int secs = argc > 1 ? atoi(argv[1]) : 120;
file_sz = (argc > 2 ? (size_t)atol(argv[2]) : 256) << 20;
int peers = argc > 3 ? atoi(argv[3]) : 3;
file_sz = (file_sz / HPAGE) * HPAGE;
n_pmd = file_sz / HPAGE;
ctl = mmap(NULL, sizeof(*ctl), PROT_READ | PROT_WRITE,
MAP_SHARED | MAP_ANONYMOUS, -1, 0);
fd = memfd_create("runsc-memory", MFD_CLOEXEC);
if (fd < 0) { perror("memfd_create"); return 1; }
if (ftruncate(fd, file_sz)) { perror("ftruncate"); return 1; }
map = mmap(NULL, file_sz, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
if (map == MAP_FAILED) { perror("mmap"); return 1; }
madvise(map, file_sz, MADV_HUGEPAGE);
memset(map, 1, file_sz);
pid_t pid[64];
if (peers > 64) peers = 64;
for (int i = 0; i < peers; i++) {
pid[i] = fork();
if (pid[i] == 0) peer(secs);
}
pthread_t t[5];
int n = 0;
pthread_create(&t[n++], NULL, faulter, NULL);
pthread_create(&t[n++], NULL, faulter, NULL);
pthread_create(&t[n++], NULL, faulter, NULL);
pthread_create(&t[n++], NULL, writer, NULL);
pthread_create(&t[n++], NULL, puncher, NULL);
sleep(secs);
ctl->stop = 1;
for (int i = 0; i < n; i++) pthread_join(t[i], NULL);
for (int i = 0; i < peers; i++) { kill(pid[i], SIGKILL); waitpid(pid[i], NULL, 0); }
while (waitpid(-1, NULL, WNOHANG) > 0) {}
fprintf(stderr, "pid %d: punches=%ld\n", getpid(),
atomic_load(&ctl->n_punch));
return 0;
}
^ permalink raw reply [flat|nested] 9+ messages in thread
end of thread, other threads:[~2026-09-25 6:50 UTC | newest]
Thread overview: 9+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2026-09-24 6:16 [BUG] shmem: FALLOC_FL_PUNCH_HOLE vs fault-around race corrupts page cache / rss counters Ayush Ranjan
2026-09-24 7:12 ` David Hildenbrand (Arm)
2026-09-24 8:34 ` Pedro Falcato
2026-09-24 9:15 ` Jan Kara
2026-09-25 5:32 ` Ayush Ranjan
2026-09-24 9:30 ` Baolin Wang
2026-09-25 5:33 ` Ayush Ranjan
2026-09-25 5:30 ` Ayush Ranjan
2026-09-25 6:50 ` Ayush Ranjan
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®