* [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
* Re: [PATCH v3] selftests/cgroup: test pids.events notification
2026-10-13 6:48 [PATCH v3] selftests/cgroup: test pids.events notification Sahaj Chaudhari
@ 2026-10-14 18:54 ` Tejun Heo
0 siblings, 0 replies; 2+ messages in thread
From: Tejun Heo @ 2026-10-14 18:54 UTC (permalink / raw)
To: Sahaj Chaudhari
Cc: hannes, mkoutny, shuah, cgroups, linux-kselftest, linux-kernel
Hello, Sahaj.
The following is a Claude-generated review.
On Tue, Oct 13, 2026 at 12:18:12PM +0530, Sahaj Chaudhari wrote:
> 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.
Can you mention 1765a153d985 ("cgroup/pids: Restore pids.events
notifications in local mode") here as the fix this test covers?
> 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.
This describes the v1 to v2 change. As v1 never landed, it belongs only in
the changelog below the --- line.
> +TEST_GEN_PROGS += test_pids_local_events
Can this be a test in test_pids.c instead? Its main() already does the
same setup, so the new test fits as another T() entry next to
test_pids_events() without the Makefile and .gitignore changes. It can
then use cg_run_nowait() with run_success() for the fork and cg_control()
for the pids.events path.
> + 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;
> + }
This repeats the counter check before poll(). Nothing can change the
counter in between, so maybe drop it?
> + if (proc_mount_contains("pids_localevents") <= 0)
> + ksft_exit_skip("mount cgroup2 with pids_localevents to run this test\n");
Is this skip necessary? In the default mode, the rejected fork also
increments the test cgroup's pids.events and notifies it, as the test
cgroup is the one over the limit. The same checks hold, so without the
skip the test covers the notification in both modes and still catches
the regression on a pids_localevents mount.
Thanks.
--
tejun
^ permalink raw reply [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