Linux cgroups development
 help / color / mirror / Atom feed
* [PATCH v3] selftests/cgroup: test pids.events notification
@ 2026-10-13  6:48 Sahaj Chaudhari
  2026-10-14 18:54 ` Tejun Heo
  0 siblings, 1 reply; 2+ messages in thread
From: Sahaj Chaudhari @ 2026-10-13  6:48 UTC (permalink / raw)
  To: tj, hannes, mkoutny, shuah; +Cc: cgroups, linux-kselftest, linux-kernel

Add a selftest for pids.events notifications. When pids_localevents is
enabled, a fork rejected by pids.max must increment pids.events and
generate an IN_MODIFY inotify event on that file.

The test creates a cgroup with pids.max set to 1, watches its pids.events
file, moves itself into the cgroup and forces a rejected fork. It then
checks both the counter and the notification. It skips when cgroup v2 is
not mounted, pids_localevents is not enabled or the pids controller is
unavailable.

Check for a failed read() of the inotify event before comparing its
signed ssize_t result with sizeof(struct inotify_event). Otherwise -1 is
converted to a large unsigned length and the uninitialized event buffer
is inspected.

Signed-off-by: Sahaj Chaudhari <sahaj123.sc@gmail.com>
---
Changes since v2:
- v2 was mistakenly sent as an incremental delta on top of v1. This v3 is
  the complete patch, replacing both v1 and v2.
- Check for a failed inotify read() before comparing the signed result
  with sizeof(struct inotify_event) (the v2 change).

Testing:
- Built with make -C tools/testing/selftests TARGETS=cgroup.
- On an x86 host the test skips: its cgroup2 mount lacks pids_localevents.
- On an AM642 board (6.12.57) the test fails with a timeout waiting for
  the notification. That kernel lacks commit 1765a153d985 ("cgroup/pids:
  Restore pids.events notifications in local mode"), so this is expected.

v1: https://lore.kernel.org/all/20261007063516.1192279-1-sahaj123.sc@gmail.com/
v2: https://lore.kernel.org/all/20261012095011.3188830-1-sahaj123.sc@gmail.com/

 tools/testing/selftests/cgroup/.gitignore     |   1 +
 tools/testing/selftests/cgroup/Makefile       |   2 +
 .../selftests/cgroup/test_pids_local_events.c | 143 ++++++++++++++++++
 3 files changed, 146 insertions(+)
 create mode 100644 tools/testing/selftests/cgroup/test_pids_local_events.c

diff --git a/tools/testing/selftests/cgroup/.gitignore b/tools/testing/selftests/cgroup/.gitignore
index 952e4448bf070..0a00eaab14cb1 100644
--- a/tools/testing/selftests/cgroup/.gitignore
+++ b/tools/testing/selftests/cgroup/.gitignore
@@ -8,5 +8,6 @@ test_kill
 test_kmem
 test_memcontrol
 test_pids
+test_pids_local_events
 test_zswap
 wait_inotify
diff --git a/tools/testing/selftests/cgroup/Makefile b/tools/testing/selftests/cgroup/Makefile
index e01584c2189ac..e494487307418 100644
--- a/tools/testing/selftests/cgroup/Makefile
+++ b/tools/testing/selftests/cgroup/Makefile
@@ -16,6 +16,7 @@ TEST_GEN_PROGS += test_kill
 TEST_GEN_PROGS += test_kmem
 TEST_GEN_PROGS += test_memcontrol
 TEST_GEN_PROGS += test_pids
+TEST_GEN_PROGS += test_pids_local_events
 TEST_GEN_PROGS += test_zswap
 
 LOCAL_HDRS += $(selfdir)/clone3/clone3_selftests.h $(selfdir)/pidfd/pidfd.h
@@ -32,4 +33,5 @@ $(OUTPUT)/test_kill: $(LIBCGROUP_O)
 $(OUTPUT)/test_kmem: $(LIBCGROUP_O)
 $(OUTPUT)/test_memcontrol: $(LIBCGROUP_O)
 $(OUTPUT)/test_pids: $(LIBCGROUP_O)
+$(OUTPUT)/test_pids_local_events: $(LIBCGROUP_O)
 $(OUTPUT)/test_zswap: $(LIBCGROUP_O)
diff --git a/tools/testing/selftests/cgroup/test_pids_local_events.c b/tools/testing/selftests/cgroup/test_pids_local_events.c
new file mode 100644
index 0000000000000..59885901fbf51
--- /dev/null
+++ b/tools/testing/selftests/cgroup/test_pids_local_events.c
@@ -0,0 +1,143 @@
+// SPDX-License-Identifier: GPL-2.0
+#define _GNU_SOURCE
+
+#include <errno.h>
+#include <fcntl.h>
+#include <linux/limits.h>
+#include <poll.h>
+#include <stdbool.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/inotify.h>
+#include <sys/wait.h>
+#include <unistd.h>
+
+#include "cgroup_util.h"
+#include "kselftest.h"
+
+static int test_pids_events_notify(const char *root)
+{
+	union {
+		struct inotify_event align;
+		char bytes[sizeof(struct inotify_event) + NAME_MAX + 1];
+	} event_buf;
+	struct pollfd pfd;
+	char *cgroup = NULL, *events_path = NULL;
+	const char *reason = "pids.events did not notify after a rejected fork";
+	bool moved = false;
+	int inotify_fd = -1, ret = KSFT_FAIL, watch;
+	ssize_t n;
+	pid_t child;
+
+	cgroup = cg_name(root, "pids_local_events_notify");
+	if (!cgroup) {
+		reason = "failed to allocate cgroup path";
+		goto cleanup;
+	}
+	if (cg_create(cgroup)) {
+		reason = "failed to create test cgroup";
+		goto cleanup;
+	}
+	if (cg_write(cgroup, "pids.max", "1")) {
+		reason = "failed to set pids.max";
+		goto cleanup;
+	}
+
+	if (asprintf(&events_path, "%s/pids.events", cgroup) < 0) {
+		reason = "failed to allocate pids.events path";
+		goto cleanup;
+	}
+	inotify_fd = inotify_init1(IN_CLOEXEC | IN_NONBLOCK);
+	if (inotify_fd < 0) {
+		reason = "inotify_init1 failed";
+		goto cleanup;
+	}
+	watch = inotify_add_watch(inotify_fd, events_path, IN_MODIFY);
+	if (watch < 0) {
+		reason = "inotify_add_watch on pids.events failed";
+		goto cleanup;
+	}
+
+	if (cg_enter_current(cgroup)) {
+		reason = "failed to move test into cgroup";
+		goto cleanup;
+	}
+	moved = true;
+
+	child = fork();
+	if (child == 0)
+		_exit(0);
+	if (child > 0) {
+		waitpid(child, NULL, 0);
+		reason = "fork succeeded despite pids.max";
+		goto cleanup;
+	}
+	if (errno != EAGAIN) {
+		reason = "fork failed for a reason other than pids.max";
+		goto cleanup;
+	}
+
+	if (cg_read_key_long(cgroup, "pids.events", "max ") != 1) {
+		reason = "pids.events did not count the rejected fork";
+		goto cleanup;
+	}
+
+	pfd.fd = inotify_fd;
+	pfd.events = POLLIN;
+	if (poll(&pfd, 1, 1000) <= 0) {
+		reason = "timed out waiting for pids.events notification";
+		goto cleanup;
+	}
+	n = read(inotify_fd, event_buf.bytes, sizeof(event_buf.bytes));
+	if (n < 0) {
+		reason = "failed to read pids.events notification";
+		goto cleanup;
+	}
+	if ((size_t)n < sizeof(struct inotify_event) ||
+	    !(event_buf.align.mask & IN_MODIFY)) {
+		reason = "received no pids.events modify notification";
+		goto cleanup;
+	}
+	if (cg_read_key_long(cgroup, "pids.events", "max ") != 1) {
+		reason = "pids.events did not count the rejected fork";
+		goto cleanup;
+	}
+
+	ret = KSFT_PASS;
+
+cleanup:
+	if (inotify_fd >= 0)
+		close(inotify_fd);
+	if (moved)
+		cg_enter_current(root);
+	if (cgroup)
+		cg_destroy(cgroup);
+	free(events_path);
+	free(cgroup);
+	if (ret != KSFT_PASS)
+		ksft_print_msg("%s\n", reason);
+	return ret;
+}
+
+int main(void)
+{
+	char root[PATH_MAX];
+	int ret;
+
+	ksft_print_header();
+	if (cg_find_unified_root(root, sizeof(root), NULL))
+		ksft_exit_skip("cgroup v2 isn't mounted\n");
+	if (proc_mount_contains("pids_localevents") <= 0)
+		ksft_exit_skip("mount cgroup2 with pids_localevents to run this test\n");
+	if (cg_read_strstr(root, "cgroup.controllers", "pids"))
+		ksft_exit_skip("pids controller isn't available\n");
+	if (cg_read_strstr(root, "cgroup.subtree_control", "pids") &&
+	    cg_write(root, "cgroup.subtree_control", "+pids"))
+		ksft_exit_skip("failed to enable pids controller\n");
+
+	ksft_set_plan(1);
+	ret = test_pids_events_notify(root);
+	ksft_test_result_report(ret, "pids.events notifies on a rejected fork\n");
+	ksft_finished();
+}
-- 
2.43.0


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

end of thread, other threads:[~2026-10-14 18:54 UTC | newest]

Thread overview: 2+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-10-13  6:48 [PATCH v3] selftests/cgroup: test pids.events notification Sahaj Chaudhari
2026-10-14 18:54 ` Tejun Heo

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox