All of lore.kernel.org
 help / color / mirror / Atom feed
* [BUG] mm: mglru: stale aging batch triggers lru_gen_exit_memcg warning
@ 2026-06-21 13:50 Peiyang He
  2026-06-22  3:12 ` Qi Zheng
  2026-06-22  7:37 ` [PATCH] mm: mglru: fix stale batch updates after memcg reparenting Qi Zheng
  0 siblings, 2 replies; 5+ messages in thread
From: Peiyang He @ 2026-06-21 13:50 UTC (permalink / raw)
  To: akpm, hannes, linux-mm
  Cc: mhocko, roman.gushchin, shakeel.butt, muchun.song, qi.zheng,
	kasong, baohua, axelrasmussen, yuanchu, weixugc, david, ljs,
	cgroups, linux-kernel, syzkaller

[-- Attachment #1: Type: text/plain, Size: 9372 bytes --]

Hello,

I hit the following warning while fuzzing other kernel code with Syzkaller.

The original Syzkaller report:

WARNING: mm/vmscan.c:5867 at lru_gen_exit_memcg+0x26f/0x300 
mm/vmscan.c:5867, CPU#0: kworker/0:0/9
Modules linked in:
CPU: 0 UID: 0 PID: 9 Comm: kworker/0:0 Not tainted 7.1.0 #2 PREEMPT(full)
Hardware name: QEMU Ubuntu 24.04 PC v2 (i440FX + PIIX, arch_caps fix, 
1996), BIOS 1.16.3-debian-1.16.3-2 04/01/2014
Workqueue: cgroup_free css_free_rwork_fn
RIP: 0010:lru_gen_exit_memcg+0x26f/0x300 mm/vmscan.c:5867
Code: 89 de e8 d4 62 ba ff 49 83 fd 3f 0f 86 9c fe ff ff 48 83 c4 08 5b 
5d 41 5c 41 5d 41 5e 41 5f e9 17 68 ba ff e8 12 68 ba ff 90 <0f> 0b 90 
e9 b0 fe ff ff e8 04 68 ba ff 66 90 e8 fd 67 ba ff 90 0f
RSP: 0018:ffffc900001afb78 EFLAGS: 00010293
RAX: 0000000000000000 RBX: 0000000000000000 RCX: ffffffff82049e88
RDX: ffff888016f35c40 RSI: ffffffff8204a02e RDI: ffff88801d4103b8
RBP: dffffc0000000000 R08: 0000000000000005 R09: 0000000000000040
R10: 0000000000000000 R11: 0000000000002ba4 R12: ffff8880481f1600
R13: ffff88801d410650 R14: ffff88801d410040 R15: dead000000000100
FS:  0000000000000000(0000) GS:ffff888098d91000(0000) knlGS:0000000000000000
CS:  0010 DS: 0000 ES: 0000 CR0: 0000000080050033
CR2: 000055ac6490c1d8 CR3: 00000000249b0000 CR4: 0000000000350ef0
Call Trace:
  <TASK>
  mem_cgroup_free mm/memcontrol.c:3972 [inline]
  mem_cgroup_css_free+0x76/0xb0 mm/memcontrol.c:4241
  css_free_rwork_fn+0x125/0x1260 kernel/cgroup/cgroup.c:5575
  process_one_work+0xa0d/0x1c30 kernel/workqueue.c:3314
  process_scheduled_works kernel/workqueue.c:3397 [inline]
  worker_thread+0x645/0xe80 kernel/workqueue.c:3478
  kthread+0x367/0x480 kernel/kthread.c:436
  ret_from_fork+0x72b/0xd50 arch/x86/kernel/process.c:158
  ret_from_fork_asm+0x1a/0x30 arch/x86/entry/entry_64.S:245
  </TASK>

Kernel version: commit 8cd9520d35a6c38db6567e97dd93b1f11f185dc6 (tag v7.1)

Relevant kernel config:

   CONFIG_MEMCG=y
   CONFIG_LRU_GEN=y
   CONFIG_LRU_GEN_ENABLED=y
   CONFIG_LRU_GEN_WALKS_MMU=y
   CONFIG_NUMA=y

Root Cause:

The bug is a race between two code paths that each hold 
`lruvec->lru_lock`, but at
non-overlapping times.

Component 1 - `reset_batch_size()`:

During `walk_mm()`, `update_batch_size()` accumulates per-generation 
page deltas into
`walk->nr_pages` WITHOUT holding `lruvec_lock`.  After 
`mmap_read_unlock(mm)`, the
walker reacquires `lruvec_lock` and `reset_batch_size()` writes those deltas
UNCONDITIONALLY into `lrugen->nr_pages`.

Component 2 - `lru_gen_reparent_memcg()`:

When a memcg is offlined, `lru_gen_reparent_memcg()` moves all folios to 
the parent
lruvec and zeros the child's `lrugen->nr_pages`, all under `lruvec_lock`.

I have not bisected the issue.  Based on code inspection, the important 
interaction
appears to be the reparenting path that clears the child's `nr_pages` while
`reset_batch_size()` can still commit a batch that was generated before 
the memcg
went offline.  This looks related to f304652609ea ("mm: vmscan: prepare for
reparenting MGLRU folios").

Race sequence:

     1. The aging path enters walk_mm() for the child memcg lruvec.

     2. walk_page_range() scans PTEs and update_batch_size() stores 
deltas in
        walk->nr_pages.  At this point the deltas have not been committed to
        lruvec->lrugen.nr_pages yet.

     3. walk_mm() drops mmap_read_lock(mm).  Before it reaches
        reset_batch_size(), the child memcg is killed and removed.

     4. The memcg offline path runs lru_gen_reparent_memcg().  Under
        lruvec_lock, it moves the child folios to the parent and clears the
        child's lrugen.nr_pages.

     5. The old aging walk resumes, takes lruvec_lock, and 
reset_batch_size()
        writes the stale walk->nr_pages deltas back into the original child
        lruvec.

     6. Later, lru_gen_exit_memcg(child) checks the child's 
lrugen.nr_pages with
        memchr_inv(...).  Since the stale batch made some slots non-zero 
again,
        VM_WARN_ON_ONCE() triggers.

The two critical sections are serialized by `lruvec_lock`, but the batch 
accumulation
in `walk->nr_pages` happens outside that lock, so there is no ordering 
between the
accumulation and the reparenting zeroing.

The relevant code path:

   mm/vmscan.c:
     run_cmd('+')              selects the target memcg and child lruvec
     try_to_inc_max_seq()      stores the child lruvec in walk->lruvec
     update_batch_size()       accumulates deltas in walk->nr_pages
     walk_mm()                 calls walk_page_range(), then later 
reset_batch_size()
     reset_batch_size()        writes cached deltas into 
walk->lruvec->lrugen.nr_pages
     lru_gen_reparent_memcg()  reparents child MGLRU state and clears 
child nr_pages
     lru_gen_exit_memcg()      warns if the exiting memcg has non-zero 
nr_pages

   mm/memcontrol.c:
     mem_cgroup_css_offline()  calls memcg_reparent_objcgs() and 
lru_gen_offline_memcg()
     mem_cgroup_free()         calls lru_gen_exit_memcg()

Reproducer:

The C reproducer and the helper script for running it are provided in 
the attachments.

The PoC creates a leaf memory cgroup, moves a victim process into it, 
and makes the victim fault and continuously touch file-backed pages so 
MGLRU aging can produce cached generation deltas for that memcg. A 
separate `lru_ager` thread repeatedly writes aging commands to 
`/sys/kernel/debug/lru_gen`; when the instrumentation reports that the 
ager is delayed just before `reset_batch_size()`, the PoC kills the 
victim and removes the leaf cgroup, forcing memcg offline/reparenting 
before the stale batch is committed.

The helper script builds the PoC, creates a temporary qcow2 overlay, 
boots the instrumented kernel in QEMU with fake NUMA and SSH port 
forwarding, copies the PoC into the guest, runs it, and scans the serial 
console for `exit_nonzero`, `WARNING: mm/vmscan.c`, or `Kernel panic`. 
It writes the full serial console, extracted kernel events, and guest 
stdout/stderr under the chosen output directory.

The example command:

   ./repros/lru_gen_exit_memcg/run_poc_qemu.sh /tmp/lru_gen_poc_manual 
10450 20 32

The arguments are:

   /tmp/lru_gen_poc_manual  output directory for the overlay, console log,
                            extracted events and guest log
   10450                    host TCP port forwarded to guest SSH
   20                       number of PoC iterations to run
   32                       file-backed working-set size in MiB per 
iteration

The script uses default `KERNEL`, `IMAGE` and `SSH_KEY` paths, or they 
can be
overridden with environment variables.

Since this bug requires a specific race window, kernel instrumentation 
is needed
to enlarge the race window in order to reproduce the bug more reliably.  The
instrumentation patch is also included in the attachments.

The patch only instruments `mm/vmscan.c`: it delays the PoC aging task just
before `reset_batch_size()`, logs when a stale batch is written into an 
already
offlined and zeroed memcg lruvec, and dumps the non-zero 
`lrugen.nr_pages` slots
before `lru_gen_exit_memcg()` triggers the warning.

A successful run reports `status=repro_triggered`, and the extracted events
include a warning like:

   WARNING: mm/vmscan.c:5943 at lru_gen_exit_memcg+0x420/0x520

Proposed Fix:

One possible fix direction is to make `reset_batch_size()` skip writing 
back the
stale delta when the memcg is no longer online. `reset_batch_size()` is 
called
under `lruvec_lock`, the same lock that `lru_gen_reparent_memcg()` holds 
when it
zeroes `nr_pages`, so this should avoid committing a batch after 
reparenting has
completed.

Possible fix direction, not a tested patch:

--- a/mm/vmscan.c
+++ b/mm/vmscan.c
@@ -... reset_batch_size() ...
  static void reset_batch_size(struct lru_gen_mm_walk *walk)
  {
      int gen, type, zone;
      struct lruvec *lruvec = walk->lruvec;
      struct lru_gen_folio *lrugen = &lruvec->lrugen;
+    struct mem_cgroup *memcg = lruvec_memcg(lruvec);

      walk->batched = 0;

      for_each_gen_type_zone(gen, type, zone) {
          enum lru_list lru = type * LRU_INACTIVE_FILE;
          int delta = walk->nr_pages[gen][type][zone];

          if (!delta)
              continue;

          walk->nr_pages[gen][type][zone] = 0;
+
+        /*
+         * If the memcg went offline while we were walking page tables,
+         * lru_gen_reparent_memcg() has already zeroed nr_pages and moved
+         * all folios to the parent.  Writing our stale batch delta back
+         * would corrupt the offline child and trigger WARN_ON in
+         * lru_gen_exit_memcg().  Discard the delta; the parent lruvec
+         * already owns the pages and accounts for them correctly.
+         */
+        if (memcg && !mem_cgroup_online(memcg))
+            continue;
+
          WRITE_ONCE(lrugen->nr_pages[gen][type][zone],
                 lrugen->nr_pages[gen][type][zone] + delta);

          if (lru_gen_is_active(lruvec, gen))
              lru += LRU_ACTIVE;
          __update_lru_size(lruvec, lru, zone, delta);
      }
  }

Thanks

[-- Attachment #2: poc_lru_race.c --]
[-- Type: text/plain, Size: 10419 bytes --]

/*
 * Minimal MGLRU memcg reparent race PoC.
 *
 * This program expects the companion instrumentation patch to add a short
 * delay before reset_batch_size() for cgroups named /lru_gen_race_* and to log
 * "delay_before_reset".  The program waits for that log line, tears down the
 * target memcg, and lets the stale MGLRU batch commit into the offlined child.
 */

#define _GNU_SOURCE
#include <errno.h>
#include <fcntl.h>
#include <pthread.h>
#include <signal.h>
#include <stdatomic.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/mman.h>
#include <sys/mount.h>
#include <sys/prctl.h>
#include <sys/stat.h>
#include <sys/wait.h>
#include <unistd.h>

#define CGROUP_ROOT "/sys/fs/cgroup"
#define LRU_GEN_FILE "/sys/kernel/debug/lru_gen"
#define PAGE_BYTES 4096UL
#define MAX_NODES 8
#define DEFAULT_ITERS 20
#define DEFAULT_FILE_MIB 32
#define WINDOW_TIMEOUT_MS 90000
#define CONFIRM_TIMEOUT_MS 5000

enum {
	PHASE_IDLE = 0,
	PHASE_WINDOW = 1,
	PHASE_DONE = 2,
};

/* LruInfo stores the debugfs MGLRU ids needed to issue aging commands. */
struct lru_info {
	unsigned long memcg_id; /* Memcg id accepted by /sys/kernel/debug/lru_gen. */
	int nr_nodes; /* Number of NUMA nodes parsed for this memcg. */
	int nodes[MAX_NODES]; /* Node ids parsed from /sys/kernel/debug/lru_gen. */
	unsigned long max_seq[MAX_NODES]; /* Latest generation sequence for each node. */
};

/* RaceState is shared by the ager, kmsg reader, and main iteration. */
struct race_state {
	char leaf[1024]; /* Absolute cgroup path for the current leaf. */
	char leaf_rel[1024]; /* Cgroup path as printed by cgroup_path(). */
	char file_path[512]; /* File mapped by the victim process. */
	pid_t victim; /* Victim pid charged to the leaf memcg. */
	atomic_int phase; /* Current synchronization phase. */
	int iter; /* Iteration index used only for concise progress output. */
};

/* Die prints a syscall failure and exits the process. */
static void die(const char *what)
{
	perror(what);
	exit(1);
}

/* WriteFile writes a short string into a sysfs/cgroupfs control file. */
static int write_file(const char *path, const char *value)
{
	int fd = open(path, O_WRONLY | O_CLOEXEC);

	if (fd < 0)
		return -1;

	ssize_t ret = write(fd, value, strlen(value));
	int saved_errno = errno;

	close(fd);
	errno = saved_errno;
	return ret < 0 ? -1 : 0;
}

/* MkdirIfMissing creates a cgroup directory if it is not already present. */
static int mkdir_if_missing(const char *path)
{
	if (!mkdir(path, 0755) || errno == EEXIST)
		return 0;

	return -1;
}

/* EnableMemoryController enables memory accounting below a cgroup. */
static void enable_memory_controller(const char *cg)
{
	char path[640];

	snprintf(path, sizeof(path), "%s/cgroup.subtree_control", cg);
	(void)write_file(path, "+memory");
}

/* MovePid moves a process into the target cgroup. */
static int move_pid(const char *cg, pid_t pid)
{
	char path[640];
	char value[32];

	snprintf(path, sizeof(path), "%s/cgroup.procs", cg);
	snprintf(value, sizeof(value), "%d", (int)pid);
	return write_file(path, value);
}

/* RmdirRetry removes a cgroup after css teardown has made it removable. */
static int rmdir_retry(const char *path)
{
	for (int i = 0; i < 600; i++) {
		if (!rmdir(path))
			return 0;
		if (errno != EBUSY && errno != EINVAL)
			return -1;
		usleep(5000);
	}

	return -1;
}

/* WaitPhase waits until the shared phase reaches the requested value. */
static bool wait_phase(struct race_state *st, int want, int timeout_ms)
{
	for (int i = 0; i < timeout_ms; i++) {
		if (atomic_load(&st->phase) >= want)
			return true;
		usleep(1000);
	}

	return false;
}

/* VictimMain faults file-backed pages after it has been moved into the leaf. */
static void victim_main(int start_fd, int ready_fd, const char *path, size_t bytes)
{
	char ch;

	if (read(start_fd, &ch, 1) != 1)
		_exit(10);
	close(start_fd);

	int fd = open(path, O_CREAT | O_TRUNC | O_RDWR | O_CLOEXEC, 0600);

	if (fd < 0)
		_exit(11);
	if (ftruncate(fd, (off_t)bytes))
		_exit(12);

	volatile uint8_t *mapping = mmap(NULL, bytes, PROT_READ | PROT_WRITE,
					 MAP_SHARED, fd, 0);
	if (mapping == MAP_FAILED)
		_exit(13);
	close(fd);

	for (size_t off = 0; off < bytes; off += PAGE_BYTES)
		mapping[off] = (uint8_t)(off >> 12);

	ssize_t ready_ret = write(ready_fd, "R", 1);

	(void)ready_ret;
	close(ready_fd);

	for (uint8_t seed = 1;; seed++) {
		for (size_t off = 0; off < bytes; off += PAGE_BYTES)
			mapping[off] ^= seed;
	}
}

/* ReadLruInfo parses the target memcg section from /sys/kernel/debug/lru_gen. */
static int read_lru_info(const char *leaf_rel, struct lru_info *info)
{
	FILE *file = fopen(LRU_GEN_FILE, "r");
	char *line = NULL;
	size_t cap = 0;
	bool in_target = false;
	int current = -1;
	int ret = -1;

	if (!file)
		return -1;

	memset(info, 0, sizeof(*info));

	while (getline(&line, &cap, file) > 0) {
		unsigned long id;
		unsigned long seq;
		char path[1024];
		int node;

		if (sscanf(line, " memcg %lu %1023s", &id, path) == 2) {
			in_target = !strcmp(path, leaf_rel);
			current = -1;
			if (in_target) {
				info->memcg_id = id;
				ret = 0;
			}
			continue;
		}

		if (!in_target)
			continue;

		if (sscanf(line, " node %d", &node) == 1) {
			if (info->nr_nodes >= MAX_NODES)
				continue;
			current = info->nr_nodes++;
			info->nodes[current] = node;
			continue;
		}

		if (current >= 0 && sscanf(line, " %lu", &seq) == 1 &&
		    seq > info->max_seq[current])
			info->max_seq[current] = seq;
	}

	free(line);
	fclose(file);
	return ret;
}

/* AgerThread repeatedly asks MGLRU debugfs to age the target memcg. */
static void *ager_thread(void *arg)
{
	struct race_state *st = arg;
	int fd;

	prctl(PR_SET_NAME, "lru_ager", 0, 0, 0);

	fd = open(LRU_GEN_FILE, O_WRONLY | O_CLOEXEC);
	if (fd < 0)
		return NULL;

	while (atomic_load(&st->phase) < PHASE_WINDOW) {
		struct lru_info info;

		if (read_lru_info(st->leaf_rel, &info) || !info.memcg_id)
			break;

		for (int i = 0; i < info.nr_nodes; i++) {
			char cmd[128];

			if (atomic_load(&st->phase) >= PHASE_WINDOW)
				break;

			snprintf(cmd, sizeof(cmd), "+ %lu %d %lu 1 1\n",
				 info.memcg_id, info.nodes[i], info.max_seq[i]);
			ssize_t write_ret = write(fd, cmd, strlen(cmd));

			(void)write_ret;
		}
	}

	close(fd);
	return NULL;
}

/* KmsgThread watches for the instrumentation lines used for synchronization. */
static void *kmsg_thread(void *arg)
{
	struct race_state *st = arg;
	char buf[8192];
	int fd = open("/dev/kmsg", O_RDONLY | O_NONBLOCK | O_CLOEXEC);

	if (fd < 0)
		return NULL;

	lseek(fd, 0, SEEK_END);

	while (atomic_load(&st->phase) < PHASE_DONE) {
		ssize_t len = read(fd, buf, sizeof(buf) - 1);
		const char *msg;
		bool ours;

		if (len <= 0) {
			usleep(500);
			continue;
		}

		buf[len] = '\0';
		msg = strchr(buf, ';');
		if (msg)
			msg++;
		else
			msg = buf;

		ours = strstr(msg, st->leaf_rel) != NULL;
		if (ours && strstr(msg, "delay_before_reset")) {
			int idle = PHASE_IDLE;

			atomic_compare_exchange_strong(&st->phase, &idle, PHASE_WINDOW);
		}

		if ((ours && strstr(msg, "exit_nonzero")) ||
		    (atomic_load(&st->phase) >= PHASE_WINDOW &&
		     strstr(msg, "WARNING: mm/vmscan.c")))
			atomic_store(&st->phase, PHASE_DONE);
	}

	close(fd);
	return NULL;
}

/* RunIteration creates one child memcg and races its teardown against aging. */
static bool run_iteration(const char *base, int iter, size_t file_mib)
{
	struct race_state st;
	int start_pipe[2];
	int ready_pipe[2];
	pthread_t ager;
	pthread_t kmsg;
	bool got_window;
	bool confirmed = false;

	memset(&st, 0, sizeof(st));
	st.iter = iter;
	snprintf(st.leaf, sizeof(st.leaf), "%s/leaf_%03d", base, iter);
	snprintf(st.leaf_rel, sizeof(st.leaf_rel), "%s/leaf_%03d",
		 base + strlen(CGROUP_ROOT), iter);
	snprintf(st.file_path, sizeof(st.file_path), "/root/lru_race_%d_%03d.dat",
		 getpid(), iter);
	atomic_store(&st.phase, PHASE_IDLE);

	if (mkdir_if_missing(st.leaf))
		return false;
	if (pipe(start_pipe) || pipe(ready_pipe))
		die("pipe");

	st.victim = fork();
	if (!st.victim) {
		close(start_pipe[1]);
		close(ready_pipe[0]);
		victim_main(start_pipe[0], ready_pipe[1], st.file_path,
			    file_mib << 20);
		_exit(0);
	}

	close(start_pipe[0]);
	close(ready_pipe[1]);

	if (move_pid(st.leaf, st.victim)) {
		ssize_t start_ret = write(start_pipe[1], "g", 1);

		(void)start_ret;
		close(start_pipe[1]);
		kill(st.victim, SIGKILL);
		waitpid(st.victim, NULL, 0);
		rmdir_retry(st.leaf);
		return false;
	}

	ssize_t start_ret = write(start_pipe[1], "g", 1);

	(void)start_ret;
	close(start_pipe[1]);

	char ready;
	ssize_t ready_ret = read(ready_pipe[0], &ready, 1);

	(void)ready_ret;
	close(ready_pipe[0]);

	for (int retry = 0; retry < 400; retry++) {
		struct lru_info info;

		if (!read_lru_info(st.leaf_rel, &info) && info.memcg_id &&
		    info.nr_nodes > 0)
			break;
		usleep(5000);
	}

	pthread_create(&kmsg, NULL, kmsg_thread, &st);
	pthread_create(&ager, NULL, ager_thread, &st);

	got_window = wait_phase(&st, PHASE_WINDOW, WINDOW_TIMEOUT_MS);
	if (got_window) {
		kill(st.victim, SIGKILL);
		waitpid(st.victim, NULL, 0);
		st.victim = 0;
		rmdir_retry(st.leaf);
		confirmed = wait_phase(&st, PHASE_DONE, CONFIRM_TIMEOUT_MS);
	}

	atomic_store(&st.phase, PHASE_DONE);
	pthread_join(ager, NULL);
	pthread_join(kmsg, NULL);

	if (st.victim) {
		kill(st.victim, SIGKILL);
		waitpid(st.victim, NULL, 0);
	}
	rmdir_retry(st.leaf);
	unlink(st.file_path);

	printf("iter %d: %s\n", iter, confirmed ? "confirmed" :
	       got_window ? "window-only" : "miss");
	return confirmed;
}

/* Main prepares cgroup/debugfs state and runs bounded race attempts. */
int main(int argc, char **argv)
{
	int iters = argc > 1 ? atoi(argv[1]) : DEFAULT_ITERS;
	size_t file_mib = argc > 2 ? strtoul(argv[2], NULL, 0) : DEFAULT_FILE_MIB;
	char base[512];
	int confirmed = 0;

	if (geteuid()) {
		fprintf(stderr, "must run as root\n");
		return 1;
	}

	if (mount("debugfs", "/sys/kernel/debug", "debugfs", 0, NULL) && errno != EBUSY)
		perror("mount debugfs");

	enable_memory_controller(CGROUP_ROOT);
	snprintf(base, sizeof(base), CGROUP_ROOT "/lru_gen_race_%d", getpid());
	if (mkdir_if_missing(base))
		die("mkdir base cgroup");
	enable_memory_controller(base);

	for (int i = 0; i < iters; i++) {
		if (run_iteration(base, i, file_mib))
			confirmed++;
	}

	rmdir_retry(base);
	printf("confirmed=%d/%d\n", confirmed, iters);
	return confirmed ? 0 : 1;
}

[-- Attachment #3: lru_gen_exit_memcg.patch --]
[-- Type: text/plain, Size: 4169 bytes --]

diff --git a/mm/vmscan.c b/mm/vmscan.c
index bd1b1aa12581..6206ce41de3b 100644
--- a/mm/vmscan.c
+++ b/mm/vmscan.c
@@ -3265,24 +3265,96 @@ static void update_batch_size(struct lru_gen_mm_walk *walk, struct folio *folio,
 	walk->nr_pages[new_gen][type][zone] += delta;
 }
 
+/* Return whether any MGLRU size slot is still charged. */
+static bool lru_gen_has_nr_pages(struct lruvec *lruvec)
+{
+	int gen, type, zone;
+	struct lru_gen_folio *lrugen = &lruvec->lrugen;
+
+	for_each_gen_type_zone(gen, type, zone) {
+		if (READ_ONCE(lrugen->nr_pages[gen][type][zone]))
+			return true;
+	}
+
+	return false;
+}
+
+/* Dump nonzero MGLRU size slots for the target memcg. */
+static void lru_gen_dump_nr_pages(const char *tag, struct mem_cgroup *memcg,
+				  int nid, struct lruvec *lruvec, bool show_path)
+{
+	int gen, type, zone;
+	char path[256] = "";
+	struct lru_gen_folio *lrugen = &lruvec->lrugen;
+
+	if (show_path && memcg)
+		cgroup_path(memcg->css.cgroup, path, sizeof(path));
+
+	pr_warn("lru_gen_debug: %s task=%s/%d memcg=%llu online=%d dying=%d nid=%d path=%s\n",
+		tag, current->comm, task_pid_nr(current), mem_cgroup_id(memcg),
+		memcg ? mem_cgroup_online(memcg) : 1, memcg_is_dying(memcg),
+		nid, path);
+
+	for_each_gen_type_zone(gen, type, zone) {
+		long nr_pages = READ_ONCE(lrugen->nr_pages[gen][type][zone]);
+
+		if (!nr_pages)
+			continue;
+
+		pr_warn("lru_gen_debug: %s slot memcg=%llu nid=%d gen=%d type=%d zone=%d nr=%ld list_empty=%d\n",
+			tag, mem_cgroup_id(memcg), nid, gen, type, zone,
+			nr_pages, list_empty(&lrugen->folios[gen][type][zone]));
+	}
+}
+
+/* Delay the PoC aging task so memcg offline can race with batch reset. */
+static void lru_gen_delay_test_reset(struct lruvec *lruvec)
+{
+	char path[128] = "";
+	struct mem_cgroup *memcg = lruvec_memcg(lruvec);
+
+	if (!memcg || strcmp(current->comm, "lru_ager"))
+		return;
+
+	cgroup_path(memcg->css.cgroup, path, sizeof(path));
+	if (!str_has_prefix(path, "/lru_gen_race_"))
+		return;
+
+	pr_warn("lru_gen_debug: delay_before_reset task=%s/%d memcg=%llu online=%d dying=%d path=%s\n",
+		current->comm, task_pid_nr(current), mem_cgroup_id(memcg),
+		mem_cgroup_online(memcg), memcg_is_dying(memcg), path);
+	msleep(3000);
+}
+
 static void reset_batch_size(struct lru_gen_mm_walk *walk)
 {
 	int gen, type, zone;
 	struct lruvec *lruvec = walk->lruvec;
 	struct lru_gen_folio *lrugen = &lruvec->lrugen;
+	struct mem_cgroup *memcg = lruvec_memcg(lruvec);
+	bool offline = memcg && (!mem_cgroup_online(memcg) || memcg_is_dying(memcg));
+	bool zeroed = offline && !lru_gen_has_nr_pages(lruvec);
 
 	walk->batched = 0;
 
 	for_each_gen_type_zone(gen, type, zone) {
 		enum lru_list lru = type * LRU_INACTIVE_FILE;
 		int delta = walk->nr_pages[gen][type][zone];
+		long old;
 
 		if (!delta)
 			continue;
 
 		walk->nr_pages[gen][type][zone] = 0;
-		WRITE_ONCE(lrugen->nr_pages[gen][type][zone],
-			   lrugen->nr_pages[gen][type][zone] + delta);
+		old = READ_ONCE(lrugen->nr_pages[gen][type][zone]);
+		WRITE_ONCE(lrugen->nr_pages[gen][type][zone], old + delta);
+
+		if (zeroed)
+			pr_warn("lru_gen_debug: reset_batch_to_zeroed_offline task=%s/%d memcg=%llu online=%d dying=%d nid=%d seq=%lu gen=%d type=%d zone=%d delta=%d old=%ld new=%ld\n",
+				current->comm, task_pid_nr(current), mem_cgroup_id(memcg),
+				mem_cgroup_online(memcg), memcg_is_dying(memcg),
+				lruvec_pgdat(lruvec)->node_id, walk->seq, gen, type,
+				zone, delta, old, old + delta);
 
 		if (lru_gen_is_active(lruvec, gen))
 			lru += LRU_ACTIVE;
@@ -3783,6 +3855,7 @@ static void walk_mm(struct mm_struct *mm, struct lru_gen_mm_walk *walk)
 		}
 
 		if (walk->batched) {
+			lru_gen_delay_test_reset(lruvec);
 			lruvec_lock_irq(lruvec);
 			reset_batch_size(walk);
 			lruvec_unlock_irq(lruvec);
@@ -5864,6 +5937,9 @@ void lru_gen_exit_memcg(struct mem_cgroup *memcg)
 		struct lruvec *lruvec = get_lruvec(memcg, nid);
 		struct lru_gen_mm_state *mm_state = get_mm_state(lruvec);
 
+		if (lru_gen_has_nr_pages(lruvec))
+			lru_gen_dump_nr_pages("exit_nonzero", memcg, nid, lruvec, true);
+
 		VM_WARN_ON_ONCE(memchr_inv(lruvec->lrugen.nr_pages, 0,
 					   sizeof(lruvec->lrugen.nr_pages)));
 

[-- Attachment #4: run_poc_qemu.sh --]
[-- Type: application/x-sh, Size: 3160 bytes --]

^ permalink raw reply related	[flat|nested] 5+ messages in thread

end of thread, other threads:[~2026-06-22  8:32 UTC | newest]

Thread overview: 5+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-06-21 13:50 [BUG] mm: mglru: stale aging batch triggers lru_gen_exit_memcg warning Peiyang He
2026-06-22  3:12 ` Qi Zheng
2026-06-22  7:37 ` [PATCH] mm: mglru: fix stale batch updates after memcg reparenting Qi Zheng
2026-06-22  8:24   ` Peiyang He
2026-06-22  8:31     ` Qi Zheng

This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.