* [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 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 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 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-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-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®