[PATCH] mm/memcg: clear folio memcg after changing per memcg stats
From: Bingfang Guo via B4 Relay
Date: Sun Aug 30 2026 - 23:35:57 EST
From: Bingfang Guo <bingfangguo@xxxxxxxxxxx>
__memcg1_swapout() transfers the memsw charge of a folio to its swap
entry and clears folio->memcg_data as part of that. In the vmscan
swapout path it runs before __swap_cache_del_folio(), which then
decrements the swapcache stats through lruvec_stat_mod_folio(). Since
folio->memcg_data has already been cleared, folio_memcg() returns NULL
and the NR_SWAPCACHE decrement only updates the node-level counter
instead of the memcg's lruvec, leaking the per-memcg swapcache count.
Move the __memcg1_swapout() call into __swap_cache_del_folio(), after
the NR_FILE_PAGES and NR_SWAPCACHE updates but before
__swap_cache_do_del_folio() removes the folio from the swap cache. This
keeps the stats attributed to the folio's memcg while still recording
the swap cgroup with a valid folio->swap. Add a swapout parameter so
the plain swap_cache_del_folio() path is left unchanged.
Fixes: b197d41462c20 ("mm/memcg, swap: store cgroup id in cluster table directly")
Signed-off-by: Bingfang Guo <bingfangguo@xxxxxxxxxxx>
---
The problem is reproducible using the following script and program:
```
#!/bin/bash
set -e
CG=/sys/fs/cgroup/memory/swapcache-leak-test
SIZE=$((256 * 1024 * 1024)) # 256 MiB of anon memory
[ "$(id -u)" -eq 0 ] || { echo "must run as root"; exit 1; }
grep -q . /proc/swaps <<<"$(tail -n +2 /proc/swaps)" || { echo "no swap active; run: swapon <dev>"; exit 1; }
cleanup() { rmdir "$CG" 2>/dev/null || true; }
trap cleanup EXIT
cc -O2 swapout.c -o swapout
mkdir -p "$CG"
echo "+memory" > /sys/fs/cgroup/cgroup.subtree_control 2>/dev/null || true
echo "== before reclaim =="
grep -E '^(swapcached|anon) ' "$CG/memory.stat"
# Put ourselves in the cgroup, allocate & touch anon memory, then wait to be reclaimed.
(
echo $BASHPID > "$CG/cgroup.procs"
# Allocate and dirty SIZE bytes of anonymous memory.
./swapout
) &
WORKER=$!
sleep 2
echo "== after reclaim (swap cache should drain to ~0) =="
grep -E '^(swapcached|anon) ' "$CG/memory.stat"
SWAPCACHED=$(awk '/^swapcached /{print $2}' "$CG/memory.stat")
echo
if [ "$SWAPCACHED" -gt $((1024 * 1024)) ]; then
echo "LEAK DETECTED: swapcached = $SWAPCACHED bytes (expected ~0) [BUGGY kernel]"
RC=1
else
echo "OK: swapcached = $SWAPCACHED bytes [FIXED kernel]"
RC=0
fi
kill "$WORKER" 2>/dev/null || true
wait "$WORKER" 2>/dev/null || true
exit $RC
```
swapout.c:
```
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/mman.h>
int main(int argc, char **argv)
{
size_t mib = (argc > 1) ? strtoul(argv[1], NULL, 10) : 256;
size_t size = mib * 1024UL * 1024UL;
long page = sysconf(_SC_PAGESIZE);
char *buf;
size_t i;
buf = mmap(NULL, size, PROT_READ | PROT_WRITE,
MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
if (buf == MAP_FAILED) {
perror("mmap");
return 1;
}
/* Fault in and dirty every page so it becomes reclaimable anon. */
for (i = 0; i < size; i += page)
buf[i] = 1;
printf("allocated and dirtied %zu MiB, paging out...\n", mib);
/* Force the whole range out to swap. */
if (madvise(buf, size, MADV_PAGEOUT)) {
perror("madvise(MADV_PAGEOUT)");
return 1;
}
/* Give reclaim a moment, then stay alive so the cgroup can be inspected. */
printf("paged out; sleeping so memory.stat can be read. pid=%d\n", getpid());
sleep(30);
munmap(buf, size);
return 0;
}
```
Test result:
before:
```
== before reclaim ==
swapcached 0
allocated and dirtied 256 MiB, paging out...
paged out; sleeping so memory.stat can be read. pid=4778
== after reclaim (swap cache should drain to ~0) ==
swapcached 268435456
LEAK DETECTED: swapcached = 268435456 bytes (expected ~0) [BUGGY kernel]
```
after the patch:
```
== before reclaim ==
swapcached 0
allocated and dirtied 256 MiB, paging out...
paged out; sleeping so memory.stat can be read. pid=2601
== after reclaim (swap cache should drain to ~0) ==
swapcached 0
OK: swapcached = 0 bytes [FIXED kernel]
```
---
mm/swap.h | 3 ++-
mm/swap_state.c | 10 +++++++---
mm/vmscan.c | 3 +--
3 files changed, 10 insertions(+), 6 deletions(-)
diff --git a/mm/swap.h b/mm/swap.h
index 90a551a88df63..c5a49b0ed3430 100644
--- a/mm/swap.h
+++ b/mm/swap.h
@@ -319,7 +319,8 @@ struct folio *swap_cache_alloc_folio(swp_entry_t target_entry, gfp_t gfp_mask,
void __swap_cache_add_folio(struct swap_cluster_info *ci,
struct folio *folio, swp_entry_t entry);
void __swap_cache_del_folio(struct swap_cluster_info *ci,
- struct folio *folio, swp_entry_t entry, void *shadow);
+ struct folio *folio, swp_entry_t entry, void *shadow,
+ bool swapout);
void __swap_cache_replace_folio(struct swap_cluster_info *ci,
struct folio *old, struct folio *new);
diff --git a/mm/swap_state.c b/mm/swap_state.c
index f3961fdd857dc..825e623b03d16 100644
--- a/mm/swap_state.c
+++ b/mm/swap_state.c
@@ -314,13 +314,17 @@ static void __swap_cache_do_del_folio(struct swap_cluster_info *ci,
* using the index of @entry, and lock the cluster that holds the entries.
*/
void __swap_cache_del_folio(struct swap_cluster_info *ci, struct folio *folio,
- swp_entry_t entry, void *shadow)
+ swp_entry_t entry, void *shadow, bool swapout)
{
unsigned long nr_pages = folio_nr_pages(folio);
- __swap_cache_do_del_folio(ci, folio, entry, shadow);
node_stat_mod_folio(folio, NR_FILE_PAGES, -nr_pages);
lruvec_stat_mod_folio(folio, NR_SWAPCACHE, -nr_pages);
+
+ if (swapout)
+ __memcg1_swapout(folio, ci);
+
+ __swap_cache_do_del_folio(ci, folio, entry, shadow);
}
/**
@@ -339,7 +343,7 @@ void swap_cache_del_folio(struct folio *folio)
swp_entry_t entry = folio->swap;
ci = swap_cluster_lock(__swap_entry_to_info(entry), swp_offset(entry));
- __swap_cache_del_folio(ci, folio, entry, NULL);
+ __swap_cache_del_folio(ci, folio, entry, NULL, false);
swap_cluster_unlock(ci);
folio_ref_sub(folio, folio_nr_pages(folio));
diff --git a/mm/vmscan.c b/mm/vmscan.c
index f11491ee9ed5c..17ab01693ae8d 100644
--- a/mm/vmscan.c
+++ b/mm/vmscan.c
@@ -735,8 +735,7 @@ static int __remove_mapping(struct address_space *mapping, struct folio *folio,
if (reclaimed && !mapping_exiting(mapping))
shadow = workingset_eviction(folio, target_memcg);
- __memcg1_swapout(folio, ci);
- __swap_cache_del_folio(ci, folio, swap, shadow);
+ __swap_cache_del_folio(ci, folio, swap, shadow, true);
swap_cluster_unlock_irq(ci);
} else {
void (*free_folio)(struct folio *);
---
base-commit: 42d64d4fef83a241c919c8693fdf0a21b2cb6061
change-id: 20260828-memcg-swapcache-stats-fix-1beef3dc3afb
Best regards,
--
Bingfang Guo <bingfangguo@xxxxxxxxxxx>