[PATCH 2/2] selftests/cgroup: test clone3() into a previously killed cgroup

From: Etienne Perot

Date: Fri Aug 28 2026 - 17:53:51 EST


Once cgroup.kill had been written to a cgroup, a stale kill_seq
snapshot (taken in cgroup_css_set_fork() before the target cgroup was
resolved) caused every child subsequently cloned into that cgroup with
clone3(CLONE_INTO_CGROUP) to be SIGKILLed on the spot.

Add a regression test: create a cgroup, kill it while it is empty,
then clone a child into it and check that the child runs and exits
cleanly. On a kernel without the fix, the test fails:

not ok 4 test_cgkill_clone_into_killed

The test is skipped on kernels without clone3() or without
CLONE_INTO_CGROUP.

Cc: Shakeel Butt <shakeel.butt@xxxxxxxxx>
Assisted-by: LLM
Signed-off-by: Etienne Perot <eperot@xxxxxxxxxx>
---
tools/testing/selftests/cgroup/test_kill.c | 55 ++++++++++++++++++++++
1 file changed, 55 insertions(+)

diff --git a/tools/testing/selftests/cgroup/test_kill.c b/tools/testing/selftests/cgroup/test_kill.c
index f6cd23a8ecc7..5ba8e285b2f3 100644
--- a/tools/testing/selftests/cgroup/test_kill.c
+++ b/tools/testing/selftests/cgroup/test_kill.c
@@ -7,6 +7,7 @@
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
+#include <sys/wait.h>
#include <unistd.h>

#include "kselftest.h"
@@ -261,6 +262,59 @@ static int test_cgkill_forkbomb(const char *root)
return ret;
}

+/*
+ * Test that a cgroup that was killed in the past can still be the target
+ * of clone3(CLONE_INTO_CGROUP): writing cgroup.kill must only kill the
+ * tasks in the cgroup at the time of the write, not tasks cloned into
+ * it afterwards.
+ */
+static int test_cgkill_clone_into_killed(const char *root)
+{
+ pid_t pid;
+ int cgroup_fd = -EBADF;
+ int ret = KSFT_FAIL;
+ char *cgroup = NULL;
+
+ cgroup = cg_name(root, "cg_test_clone_into_killed");
+ if (!cgroup)
+ goto cleanup;
+
+ if (cg_create(cgroup))
+ goto cleanup;
+
+ /* Kill the cgroup while it is still empty. */
+ if (cg_write(cgroup, "cgroup.kill", "1"))
+ goto cleanup;
+
+ cgroup_fd = dirfd_open_opath(cgroup);
+ if (cgroup_fd < 0)
+ goto cleanup;
+
+ pid = clone_into_cgroup(cgroup_fd);
+ if (pid < 0) {
+ if (errno == ENOSYS)
+ ret = KSFT_SKIP;
+ goto cleanup;
+ }
+
+ if (pid == 0)
+ exit(EXIT_SUCCESS);
+
+ /* The child must not be SIGKILLed; it has to exit cleanly. */
+ if (clone_reap(pid, WEXITED) != EXIT_SUCCESS)
+ goto cleanup;
+
+ ret = KSFT_PASS;
+
+cleanup:
+ if (cgroup_fd >= 0)
+ close(cgroup_fd);
+ if (cgroup)
+ cg_destroy(cgroup);
+ free(cgroup);
+ return ret;
+}
+
#define T(x) { x, #x }
struct cgkill_test {
int (*fn)(const char *root);
@@ -269,6 +323,7 @@ struct cgkill_test {
T(test_cgkill_simple),
T(test_cgkill_tree),
T(test_cgkill_forkbomb),
+ T(test_cgkill_clone_into_killed),
};
#undef T

--
2.55.0.897.gb25b4bd76c-goog