* [PATCH] mm: memcontrol: don't miss processes whose leader has exited in OOM scans
@ 2026-10-26 12:02 Zhan Xusheng
2026-10-27 8:39 ` [PATCH] selftests: cgroup: test memcg OOM on a process whose leader has exited Zhan Xusheng
0 siblings, 1 reply; 2+ messages in thread
From: Zhan Xusheng @ 2026-10-26 12:02 UTC (permalink / raw)
To: akpm
Cc: hannes, mhocko, roman.gushchin, shakeel.butt, muchun.song,
penguin-kernel, tj, mkoutny, cgroups, linux-mm, linux-kernel,
zhanxusheng, stable
From: Zhan Xusheng <zhanxusheng@xiaomi.com>
mem_cgroup_scan_tasks() walks with CSS_TASK_ITER_PROCS, so it sees only
thread group leaders, plus a dying leader that still has live threads.
Moving a process whose leader has exited, by writing its PID to
cgroup.procs, moves the live threads but leaves the leader behind, as
cgroup_migrate_add_task() skips exiting tasks.
The destination's OOM killer then cannot see the process. If it is
alone there, every attempt ends with "Out of memory and no killable
processes..." and the charge is retried indefinitely, and
memory.oom.group kills nothing; otherwise another process is killed in
its place. The source's OOM killer still finds the leader, reaches the
moved threads' mm through find_lock_task_mm(), and kills a process that
has left the memcg.
Walk all tasks and take one per thread group: the leader, or the first
live thread once the leader is exiting. Skip exiting threads, which may
belong to a group that has moved out.
Tested in a VM with memory.max=64M: in each case above the process using
the memory is now killed, and a process whose exited leader was not
moved, or whose leader is alive, is killed as before, with and without
memory.oom.group.
Fixes: f168a9a54ec3 ("mm: memcontrol: use CSS_TASK_ITER_PROCS at mem_cgroup_scan_tasks()")
Cc: stable@vger.kernel.org
Signed-off-by: Zhan Xusheng <zhanxusheng@xiaomi.com>
---
mm/memcontrol.c | 35 +++++++++++++++++++++++++++++++++--
1 file changed, 33 insertions(+), 2 deletions(-)
diff --git a/mm/memcontrol.c b/mm/memcontrol.c
index 4d00748c8a5b..8823c549b6af 100644
--- a/mm/memcontrol.c
+++ b/mm/memcontrol.c
@@ -1427,6 +1427,36 @@ static void invalidate_reclaim_iterators(struct mem_cgroup *dead_memcg)
dead_memcg);
}
+/*
+ * Take one task per thread group: the leader, or the first live thread once
+ * the leader is exiting, since migrating the group leaves an exiting leader
+ * behind. Exiting threads are skipped, as one left behind may belong to a
+ * group that has moved out.
+ */
+static bool mem_cgroup_scan_skip(struct task_struct *task)
+{
+ struct task_struct *leader = task->group_leader;
+ struct task_struct *t;
+ bool skip = true;
+
+ if (READ_ONCE(task->flags) & PF_EXITING)
+ return true;
+ if (task == leader)
+ return false;
+ if (!(READ_ONCE(leader->flags) & PF_EXITING))
+ return true;
+
+ rcu_read_lock();
+ for_each_thread(leader, t) {
+ if (!(READ_ONCE(t->flags) & PF_EXITING)) {
+ skip = t != task;
+ break;
+ }
+ }
+ rcu_read_unlock();
+ return skip;
+}
+
/**
* mem_cgroup_scan_tasks - iterate over tasks of a memory cgroup hierarchy
* @memcg: hierarchy root
@@ -1452,9 +1482,10 @@ void mem_cgroup_scan_tasks(struct mem_cgroup *memcg,
struct css_task_iter it;
struct task_struct *task;
- css_task_iter_start(&iter->css, CSS_TASK_ITER_PROCS, &it);
+ css_task_iter_start(&iter->css, 0, &it);
while (!ret && (task = css_task_iter_next(&it))) {
- ret = fn(task, arg);
+ if (!mem_cgroup_scan_skip(task))
+ ret = fn(task, arg);
/* Avoid potential softlockup warning */
cond_resched();
}
base-commit: 6b4b543fa18757297ac756a6fe6e64f506b7844a
--
2.43.0
^ permalink raw reply [flat|nested] 2+ messages in thread
* [PATCH] selftests: cgroup: test memcg OOM on a process whose leader has exited
2026-10-26 12:02 [PATCH] mm: memcontrol: don't miss processes whose leader has exited in OOM scans Zhan Xusheng
@ 2026-10-27 8:39 ` Zhan Xusheng
0 siblings, 0 replies; 2+ messages in thread
From: Zhan Xusheng @ 2026-10-27 8:39 UTC (permalink / raw)
To: akpm
Cc: hannes, mhocko, roman.gushchin, shakeel.butt, muchun.song,
penguin-kernel, tj, mkoutny, cgroups, linux-mm, linux-kselftest,
shuah, linux-kernel, zhanxusheng
From: Zhan Xusheng <zhanxusheng@xiaomi.com>
Moving a process whose thread group leader has exited leaves the leader
behind, so its live threads sit in the destination memcg with nothing
listed in cgroup.procs. Check that an OOM there still kills the process.
Without "mm: memcontrol: don't miss processes whose leader has exited in
OOM scans" no task is selected and the test fails after five seconds.
Signed-off-by: Zhan Xusheng <zhanxusheng@xiaomi.com>
---
.../selftests/cgroup/test_memcontrol.c | 123 ++++++++++++++++++
1 file changed, 123 insertions(+)
diff --git a/tools/testing/selftests/cgroup/test_memcontrol.c b/tools/testing/selftests/cgroup/test_memcontrol.c
index bb673012dcf0..bddbe923fa42 100644
--- a/tools/testing/selftests/cgroup/test_memcontrol.c
+++ b/tools/testing/selftests/cgroup/test_memcontrol.c
@@ -4,6 +4,8 @@
#include <linux/limits.h>
#include <linux/oom.h>
#include <fcntl.h>
+#include <sched.h>
+#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
@@ -18,6 +20,7 @@
#include <netdb.h>
#include <errno.h>
#include <sys/mman.h>
+#include <sys/syscall.h>
#include <sys/sysinfo.h>
#include "kselftest.h"
@@ -1417,6 +1420,125 @@ static bool memcg_sockets_disabled(void)
* The TCP client interleaves reads from the socket with check whether
* memory.current and memory.stat.sock are similar.
*/
+static int exited_leader_pipe[2];
+
+static int exited_leader_thread(void *arg)
+{
+ char c;
+
+ /* Wait until the process has been moved, then fill memory. */
+ if (read(exited_leader_pipe[0], &c, 1) == 1)
+ alloc_anon(NULL, (void *)MB(100));
+ for (;;)
+ pause();
+ return 0;
+}
+
+/* Start a thread and exit only the thread group leader. */
+static int exited_leader_fn(const char *cgroup, void *arg)
+{
+ const size_t stack_size = 64 * 1024;
+ char *stack = malloc(stack_size);
+
+ if (!stack)
+ return EXIT_FAILURE;
+
+ if (clone(exited_leader_thread, stack + stack_size,
+ CLONE_VM | CLONE_FS | CLONE_FILES | CLONE_SIGHAND |
+ CLONE_THREAD | CLONE_SYSVSEM, NULL) < 0)
+ return EXIT_FAILURE;
+
+ syscall(SYS_exit, 0);
+ return EXIT_FAILURE;
+}
+
+/*
+ * Moving a process whose leader has exited leaves the leader behind, so
+ * the destination's cgroup.procs is empty. Check that the destination's
+ * OOM killer still finds the process using its memory.
+ */
+static int test_memcg_oom_exited_leader(const char *root)
+{
+ char *parent = NULL, *src = NULL, *dst = NULL;
+ int ret = KSFT_FAIL;
+ pid_t pid = -ESRCH;
+ int i;
+
+ if (pipe(exited_leader_pipe))
+ return KSFT_FAIL;
+
+ parent = cg_name(root, "memcg_test_exited_leader");
+ if (!parent)
+ goto cleanup;
+ src = cg_name(parent, "src");
+ if (!src)
+ goto cleanup;
+ dst = cg_name(parent, "dst");
+ if (!dst)
+ goto cleanup;
+
+ if (cg_create(parent))
+ goto cleanup;
+ if (cg_write(parent, "cgroup.subtree_control", "+memory"))
+ goto cleanup;
+ if (cg_create(src) || cg_create(dst))
+ goto cleanup;
+ if (cg_write(dst, "memory.max", "30M"))
+ goto cleanup;
+ if (cg_write(dst, "memory.swap.max", "0"))
+ goto cleanup;
+
+ pid = cg_run_nowait(src, exited_leader_fn, NULL);
+ if (pid < 0)
+ goto cleanup;
+
+ for (i = 0; i < 100; i++) {
+ if (!proc_read_strstr(pid, false, "stat", ") Z "))
+ break;
+ usleep(10000);
+ }
+ if (i == 100)
+ goto cleanup;
+
+ /* The exited leader stays in src, the live thread moves. */
+ if (cg_enter(dst, pid))
+ goto cleanup;
+ if (cg_read_strcmp(dst, "cgroup.procs", ""))
+ goto cleanup;
+
+ if (write(exited_leader_pipe[1], "x", 1) != 1)
+ goto cleanup;
+
+ if (cg_read_key_long_poll(dst, "memory.events", "oom_kill ", 1,
+ 50, 100000) != 1)
+ goto cleanup;
+
+ if (cg_read_strcmp_wait(dst, "cgroup.events", "populated 0\n"))
+ goto cleanup;
+
+ ret = KSFT_PASS;
+
+cleanup:
+ if (dst)
+ cg_write(dst, "cgroup.kill", "1");
+ if (pid > 0) {
+ kill(pid, SIGKILL);
+ waitpid(pid, NULL, 0);
+ }
+ close(exited_leader_pipe[0]);
+ close(exited_leader_pipe[1]);
+ if (dst)
+ cg_destroy(dst);
+ if (src)
+ cg_destroy(src);
+ if (parent)
+ cg_destroy(parent);
+ free(dst);
+ free(src);
+ free(parent);
+ return ret;
+}
+
static int test_memcg_sock(const char *root)
{
int bind_retries = 5, ret = KSFT_FAIL, pid, err;
@@ -1816,6 +1938,7 @@ struct memcg_test {
T(test_memcg_oom_group_leaf_events),
T(test_memcg_oom_group_parent_events),
T(test_memcg_oom_group_score_events),
+ T(test_memcg_oom_exited_leader),
T(test_memcg_inotify_delete_file),
T(test_memcg_inotify_delete_dir),
};
base-commit: d135ab453a7400a34930bdda1ef4bdc8acfac80e
--
2.43.0
^ permalink raw reply [flat|nested] 2+ messages in thread
end of thread, other threads:[~2026-10-27 8:39 UTC | newest]
Thread overview: 2+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2026-10-26 12:02 [PATCH] mm: memcontrol: don't miss processes whose leader has exited in OOM scans Zhan Xusheng
2026-10-27 8:39 ` [PATCH] selftests: cgroup: test memcg OOM on a process whose leader has exited Zhan Xusheng
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®