The Linux Kernel Mailing List
 help / color / mirror / Atom feed
* [PATCH 0/2] taskstats: fix cgroupstats invalid fd handling and add selftests
@ 2026-07-10 20:07 Yiyang Chen
  2026-07-10 20:07 ` [PATCH 1/2] taskstats: return -EBADF when cgroupstats receives an invalid fd Yiyang Chen
                   ` (2 more replies)
  0 siblings, 3 replies; 4+ messages in thread
From: Yiyang Chen @ 2026-07-10 20:07 UTC (permalink / raw)
  To: Balbir Singh, Andrew Morton; +Cc: linux-kernel, Dr . Thomas Orgis, Yiyang Chen

This series fixes an issue where cgroupstats mishandles invalid file
descriptors, and introduces a functional kselftest to prevent regressions.

When an invalid file descriptor is passed via CGROUPSTATS_CMD_ATTR_FD,
cgroupstats_user_cmd() returns 0 instead of an error code. This leads to
two broken behaviors depending on netlink flags:
- Callers without NLM_F_ACK block indefinitely on recv().
- Callers with NLM_F_ACK receive a misleading success ACK (errno == 0)
  but no actual statistics payload.

The first patch addresses this by returning -EBADF when the fd cannot be
resolved. The second patch adds a comprehensive kselftest covering both
the valid cgroup v1 query and the invalid fd paths (with and without
NLM_F_ACK) to ensure the fixes work as intended.

Yiyang Chen (2):
  taskstats: return -EBADF when cgroupstats receives an invalid fd
  selftests/acct: add cgroupstats functional test

 kernel/taskstats.c                         |   2 +-
 tools/testing/selftests/acct/Makefile      |   1 +
 tools/testing/selftests/acct/cgroupstats.c | 354 +++++++++++++++++++++
 3 files changed, 356 insertions(+), 1 deletion(-)
 create mode 100644 tools/testing/selftests/acct/cgroupstats.c

-- 
2.43.0


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

* [PATCH 1/2] taskstats: return -EBADF when cgroupstats receives an invalid fd
  2026-07-10 20:07 [PATCH 0/2] taskstats: fix cgroupstats invalid fd handling and add selftests Yiyang Chen
@ 2026-07-10 20:07 ` Yiyang Chen
  2026-07-10 20:07 ` [PATCH 2/2] selftests/acct: add cgroupstats functional test Yiyang Chen
  2026-07-10 22:59 ` [PATCH 0/2] taskstats: fix cgroupstats invalid fd handling and add selftests Andrew Morton
  2 siblings, 0 replies; 4+ messages in thread
From: Yiyang Chen @ 2026-07-10 20:07 UTC (permalink / raw)
  To: Balbir Singh, Andrew Morton; +Cc: linux-kernel, Dr . Thomas Orgis, Yiyang Chen

cgroupstats_user_cmd() returns 0 without sending a reply or a netlink
error when the fd passed via CGROUPSTATS_CMD_ATTR_FD does not resolve
to an open file in the caller's table.  As a result:

- clients that did not set NLM_F_ACK block on recv() indefinitely
waiting for a CGROUPSTATS_CMD_NEW message that is never emitted;
- clients that set NLM_F_ACK receive a misleading "success" ACK
(errno == 0) with no statistics payload.

Return -EBADF instead so the netlink layer propagates the error to
userspace as expected.

Signed-off-by: Yiyang Chen <cyyzero16@gmail.com>
---
 kernel/taskstats.c | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/kernel/taskstats.c b/kernel/taskstats.c
index 2cd0172d0516..8d115670a3ca 100644
--- a/kernel/taskstats.c
+++ b/kernel/taskstats.c
@@ -423,7 +423,7 @@ static int cgroupstats_user_cmd(struct sk_buff *skb, struct genl_info *info)
 	fd = nla_get_u32(info->attrs[CGROUPSTATS_CMD_ATTR_FD]);
 	CLASS(fd, f)(fd);
 	if (fd_empty(f))
-		return 0;
+		return -EBADF;
 
 	size = nla_total_size(sizeof(struct cgroupstats));
 
-- 
2.43.0


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

* [PATCH 2/2] selftests/acct: add cgroupstats functional test
  2026-07-10 20:07 [PATCH 0/2] taskstats: fix cgroupstats invalid fd handling and add selftests Yiyang Chen
  2026-07-10 20:07 ` [PATCH 1/2] taskstats: return -EBADF when cgroupstats receives an invalid fd Yiyang Chen
@ 2026-07-10 20:07 ` Yiyang Chen
  2026-07-10 22:59 ` [PATCH 0/2] taskstats: fix cgroupstats invalid fd handling and add selftests Andrew Morton
  2 siblings, 0 replies; 4+ messages in thread
From: Yiyang Chen @ 2026-07-10 20:07 UTC (permalink / raw)
  To: Balbir Singh, Andrew Morton; +Cc: linux-kernel, Dr . Thomas Orgis, Yiyang Chen

Add a self-contained test for the CGROUPSTATS_CMD_GET genl command
that covers three cases:

- querying a cgroup v1 hierarchy and verifying the response
contains non-zero task counts
- rejecting an invalid fd without NLM_F_ACK
- rejecting an invalid fd with NLM_F_ACK

Signed-off-by: Yiyang Chen <cyyzero16@gmail.com>
---
 tools/testing/selftests/acct/Makefile      |   1 +
 tools/testing/selftests/acct/cgroupstats.c | 354 +++++++++++++++++++++
 2 files changed, 355 insertions(+)
 create mode 100644 tools/testing/selftests/acct/cgroupstats.c

diff --git a/tools/testing/selftests/acct/Makefile b/tools/testing/selftests/acct/Makefile
index 083cab5ddb72..db88d65f5581 100644
--- a/tools/testing/selftests/acct/Makefile
+++ b/tools/testing/selftests/acct/Makefile
@@ -1,6 +1,7 @@
 # SPDX-License-Identifier: GPL-2.0
 TEST_GEN_PROGS := acct_syscall
 TEST_GEN_PROGS += taskstats_fill_stats_tgid
+TEST_GEN_PROGS += cgroupstats
 
 CFLAGS += -Wall
 LDLIBS += -lpthread
diff --git a/tools/testing/selftests/acct/cgroupstats.c b/tools/testing/selftests/acct/cgroupstats.c
new file mode 100644
index 000000000000..e2836383ed50
--- /dev/null
+++ b/tools/testing/selftests/acct/cgroupstats.c
@@ -0,0 +1,354 @@
+// SPDX-License-Identifier: GPL-2.0
+#define _GNU_SOURCE
+
+#include <errno.h>
+#include <fcntl.h>
+#include <linux/cgroupstats.h>
+#include <linux/genetlink.h>
+#include <linux/netlink.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/socket.h>
+#include <sys/types.h>
+#include <sys/mount.h>
+#include <time.h>
+#include <unistd.h>
+
+#include "kselftest.h"
+
+#ifndef NLA_ALIGN
+#define NLA_ALIGNTO 4
+#define NLA_ALIGN(len) (((len) + NLA_ALIGNTO - 1) & ~(NLA_ALIGNTO - 1))
+#define NLA_HDRLEN ((int)NLA_ALIGN(sizeof(struct nlattr)))
+#endif
+
+#define RECV_TIMEOUT_SEC 2
+
+static void *nla_data(const struct nlattr *na)
+{
+	return (void *)((char *)na + NLA_HDRLEN);
+}
+
+static int netlink_open(void)
+{
+	struct timeval tv = { .tv_sec = RECV_TIMEOUT_SEC };
+	struct sockaddr_nl addr = {
+		.nl_family = AF_NETLINK,
+		.nl_pid = getpid(),
+	};
+	int fd;
+
+	fd = socket(AF_NETLINK, SOCK_RAW, NETLINK_GENERIC);
+	if (fd < 0)
+		return -errno;
+
+	/*
+	 * Ensure that a missing kernel reply fails the individual test
+	 * case instead of hanging the whole test binary.
+	 */
+	if (setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv)) < 0) {
+		int err = -errno;
+
+		close(fd);
+		return err;
+	}
+
+	if (bind(fd, (struct sockaddr *)&addr, sizeof(addr)) < 0) {
+		int err = -errno;
+
+		close(fd);
+		return err;
+	}
+
+	return fd;
+}
+
+static int send_request(int fd, void *buf, size_t len)
+{
+	struct sockaddr_nl addr = {
+		.nl_family = AF_NETLINK,
+	};
+
+	if (sendto(fd, buf, len, 0, (struct sockaddr *)&addr, sizeof(addr)) < 0)
+		return -errno;
+
+	return 0;
+}
+
+static int get_family_id(int fd, const char *name)
+{
+	struct {
+		struct nlmsghdr nlh;
+		struct genlmsghdr genl;
+		char buf[256];
+	} req = { 0 };
+	char resp[8192];
+	struct nlmsghdr *nlh;
+	struct genlmsghdr *genl;
+	struct nlattr *na;
+	int len;
+	int rem;
+	int ret;
+
+	req.nlh.nlmsg_len = NLMSG_LENGTH(GENL_HDRLEN);
+	req.nlh.nlmsg_type = GENL_ID_CTRL;
+	req.nlh.nlmsg_flags = NLM_F_REQUEST;
+	req.nlh.nlmsg_seq = 1;
+	req.nlh.nlmsg_pid = getpid();
+
+	req.genl.cmd = CTRL_CMD_GETFAMILY;
+	req.genl.version = 1;
+
+	na = (struct nlattr *)((char *)&req + NLMSG_ALIGN(req.nlh.nlmsg_len));
+	na->nla_type = CTRL_ATTR_FAMILY_NAME;
+	na->nla_len = NLA_HDRLEN + strlen(name) + 1;
+	memcpy(nla_data(na), name, strlen(name) + 1);
+	req.nlh.nlmsg_len = NLMSG_ALIGN(req.nlh.nlmsg_len) + NLA_ALIGN(na->nla_len);
+
+	ret = send_request(fd, &req, req.nlh.nlmsg_len);
+	if (ret)
+		return ret;
+
+	len = recv(fd, resp, sizeof(resp), 0);
+	if (len < 0)
+		return -errno;
+
+	for (nlh = (struct nlmsghdr *)resp; NLMSG_OK(nlh, len);
+	     nlh = NLMSG_NEXT(nlh, len)) {
+		if (nlh->nlmsg_type == NLMSG_ERROR) {
+			struct nlmsgerr *err = NLMSG_DATA(nlh);
+
+			return err->error ? err->error : -ENOENT;
+		}
+
+		genl = (struct genlmsghdr *)NLMSG_DATA(nlh);
+		rem = nlh->nlmsg_len - NLMSG_HDRLEN - GENL_HDRLEN;
+		na = (struct nlattr *)((char *)genl + GENL_HDRLEN);
+		while (rem >= (int)sizeof(*na) &&
+		       na->nla_len >= (int)sizeof(*na) &&
+		       na->nla_len <= rem) {
+			if (na->nla_type == CTRL_ATTR_FAMILY_ID)
+				return *(uint16_t *)nla_data(na);
+			rem -= NLA_ALIGN(na->nla_len);
+			na = (struct nlattr *)((char *)na + NLA_ALIGN(na->nla_len));
+		}
+	}
+
+	return -ENOENT;
+}
+
+static int send_cgroupstats_cmd(int fd, int family_id, uint32_t cgroup_fd,
+				int flags)
+{
+	struct {
+		struct nlmsghdr nlh;
+		struct genlmsghdr genl;
+		char buf[256];
+	} req = { 0 };
+	struct nlattr *na;
+
+	req.nlh.nlmsg_len = NLMSG_LENGTH(GENL_HDRLEN);
+	req.nlh.nlmsg_type = family_id;
+	req.nlh.nlmsg_flags = NLM_F_REQUEST | flags;
+	req.nlh.nlmsg_seq = 2;
+	req.nlh.nlmsg_pid = getpid();
+
+	req.genl.cmd = CGROUPSTATS_CMD_GET;
+	req.genl.version = 1;
+
+	na = (struct nlattr *)((char *)&req + NLMSG_ALIGN(req.nlh.nlmsg_len));
+	na->nla_type = CGROUPSTATS_CMD_ATTR_FD;
+	na->nla_len = NLA_HDRLEN + sizeof(cgroup_fd);
+	memcpy(nla_data(na), &cgroup_fd, sizeof(cgroup_fd));
+	req.nlh.nlmsg_len = NLMSG_ALIGN(req.nlh.nlmsg_len) + NLA_ALIGN(na->nla_len);
+
+	return send_request(fd, &req, req.nlh.nlmsg_len);
+}
+
+/*
+ * Receive and decode a cgroupstats response.
+ *
+ * Returns:
+ *   0             — success, stats filled from CGROUPSTATS_CMD_NEW reply
+ *   <0            — NLMSG_ERROR errno (e.g. -EBADF, -EINVAL)
+ */
+static int recv_cgroupstats_response(int fd, struct cgroupstats *stats)
+{
+	char resp[8192];
+	struct nlmsghdr *nlh;
+	struct genlmsghdr *genl;
+	struct nlattr *na;
+	int len;
+	int rem;
+
+	memset(stats, 0, sizeof(*stats));
+
+	len = recv(fd, resp, sizeof(resp), 0);
+	if (len < 0)
+		return -errno;
+
+	for (nlh = (struct nlmsghdr *)resp; NLMSG_OK(nlh, len);
+	     nlh = NLMSG_NEXT(nlh, len)) {
+		if (nlh->nlmsg_type == NLMSG_ERROR) {
+			struct nlmsgerr *err = NLMSG_DATA(nlh);
+
+			return err->error;
+		}
+
+		genl = (struct genlmsghdr *)NLMSG_DATA(nlh);
+		if (genl->cmd != CGROUPSTATS_CMD_NEW)
+			continue;
+
+		rem = nlh->nlmsg_len - NLMSG_HDRLEN - GENL_HDRLEN;
+		na = (struct nlattr *)((char *)genl + GENL_HDRLEN);
+		while (rem >= (int)sizeof(*na) &&
+		       na->nla_len >= (int)sizeof(*na) &&
+		       na->nla_len <= rem) {
+			if (na->nla_type == CGROUPSTATS_TYPE_CGROUP_STATS) {
+				memcpy(stats, nla_data(na), sizeof(*stats));
+				return 0;
+			}
+			rem -= NLA_ALIGN(na->nla_len);
+			na = (struct nlattr *)((char *)na + NLA_ALIGN(na->nla_len));
+		}
+	}
+
+	return -EIO;
+}
+
+/* mkdtemp() modifies the template in place, so this cannot be const. */
+static char cg_mountpoint[32];
+static bool cg_mounted;
+
+static int setup_cgroup_v1(void)
+{
+	strcpy(cg_mountpoint, "/tmp/cgstats_test_XXXXXX");
+
+	if (!mkdtemp(cg_mountpoint))
+		return -errno;
+
+	if (mount("cgstats_test", cg_mountpoint, "cgroup", 0,
+		  "none,name=cgstats_test") < 0) {
+		int ret = -errno;
+
+		rmdir(cg_mountpoint);
+		return ret;
+	}
+
+	cg_mounted = true;
+	return 0;
+}
+
+static void cleanup_cgroup_v1(void)
+{
+	if (!cg_mounted)
+		return;
+	umount2(cg_mountpoint, MNT_DETACH);
+	rmdir(cg_mountpoint);
+	cg_mounted = false;
+}
+
+int main(void)
+{
+	struct cgroupstats stats;
+	uint64_t total_tasks;
+	int family_id;
+	int nl_fd;
+	int cg_fd;
+	int ret;
+
+	ksft_print_header();
+
+	nl_fd = netlink_open();
+	if (nl_fd < 0)
+		ksft_exit_skip("failed to open generic netlink socket: %s\n",
+			       strerror(-nl_fd));
+
+	family_id = get_family_id(nl_fd, TASKSTATS_GENL_NAME);
+	if (family_id < 0)
+		ksft_exit_skip("taskstats generic netlink family unavailable: %s\n",
+			       strerror(-family_id));
+
+	ksft_set_plan(3);
+
+	/*
+	 * Test 1: mount a private cgroup v1 hierarchy, query it, and
+	 * verify the response contains sane task counts. If the test
+	 * environment cannot create a private cgroup v1 mount, skip this
+	 * case and continue with the unprivileged regression checks below.
+	 */
+	ret = setup_cgroup_v1();
+	if (ret) {
+		ksft_test_result_skip("cgroupstats query: cannot mount cgroup v1: %s\n",
+				      strerror(-ret));
+	} else {
+		cg_fd = open(cg_mountpoint, O_RDONLY | O_DIRECTORY);
+		if (cg_fd < 0) {
+			ksft_test_result_fail("cgroupstats query: open mountpoint: %s\n",
+					      strerror(errno));
+		} else {
+			ret = send_cgroupstats_cmd(nl_fd, family_id,
+						   (uint32_t)cg_fd, 0);
+			if (ret) {
+				ksft_test_result_fail("cgroupstats query: send: %s\n",
+						      strerror(-ret));
+			} else {
+				ret = recv_cgroupstats_response(nl_fd, &stats);
+				if (ret < 0) {
+					ksft_test_result_fail("cgroupstats query: %s\n",
+							      strerror(-ret));
+				} else {
+					total_tasks = (uint64_t)stats.nr_sleeping +
+						      (uint64_t)stats.nr_running +
+						      (uint64_t)stats.nr_stopped +
+						      (uint64_t)stats.nr_uninterruptible +
+						      (uint64_t)stats.nr_io_wait;
+
+					ksft_print_msg("cgroupstats query: total_tasks=%llu\n",
+						       (unsigned long long)total_tasks);
+
+					ksft_test_result(total_tasks > 0,
+							 "cgroupstats query returns valid stats\n");
+				}
+			}
+			close(cg_fd);
+		}
+	}
+	cleanup_cgroup_v1();
+
+	/*
+	 * Test 2: invalid fd without NLM_F_ACK.  The kernel should
+	 * return -EBADF via NLMSG_ERROR regardless of whether the
+	 * client requested an explicit ACK.
+	 */
+	ret = send_cgroupstats_cmd(nl_fd, family_id, 0xFFFFFFFF, 0);
+	if (ret)
+		ksft_exit_fail_msg("send test 2 failed: %s\n", strerror(-ret));
+
+	ret = recv_cgroupstats_response(nl_fd, &stats);
+	ksft_print_msg("bad fd (no ACK): response=%d (%s)\n",
+		       ret, ret < 0 ? strerror(-ret) : "unexpected success");
+	ksft_test_result(ret == -EBADF,
+			 "cgroupstats rejects bad fd without NLM_F_ACK\n");
+
+	/*
+	 * Test 3: invalid fd with NLM_F_ACK.  Same expectation as
+	 * test 2, but exercised through a different netlink flag
+	 * path in the kernel's ack/error handling.
+	 */
+	ret = send_cgroupstats_cmd(nl_fd, family_id, 0xFFFFFFFF, NLM_F_ACK);
+	if (ret)
+		ksft_exit_fail_msg("send test 3 failed: %s\n", strerror(-ret));
+
+	ret = recv_cgroupstats_response(nl_fd, &stats);
+	ksft_print_msg("bad fd (with ACK): response=%d (%s)\n",
+		       ret, ret < 0 ? strerror(-ret) : "unexpected success");
+	ksft_test_result(ret == -EBADF,
+			 "cgroupstats rejects bad fd with NLM_F_ACK\n");
+
+	close(nl_fd);
+	ksft_finished();
+	return ksft_get_fail_cnt() ? KSFT_FAIL : KSFT_PASS;
+}
-- 
2.43.0


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

* Re: [PATCH 0/2] taskstats: fix cgroupstats invalid fd handling and add selftests
  2026-07-10 20:07 [PATCH 0/2] taskstats: fix cgroupstats invalid fd handling and add selftests Yiyang Chen
  2026-07-10 20:07 ` [PATCH 1/2] taskstats: return -EBADF when cgroupstats receives an invalid fd Yiyang Chen
  2026-07-10 20:07 ` [PATCH 2/2] selftests/acct: add cgroupstats functional test Yiyang Chen
@ 2026-07-10 22:59 ` Andrew Morton
  2 siblings, 0 replies; 4+ messages in thread
From: Andrew Morton @ 2026-07-10 22:59 UTC (permalink / raw)
  To: Yiyang Chen; +Cc: Balbir Singh, linux-kernel, Dr . Thomas Orgis

On Sat, 11 Jul 2026 04:07:35 +0800 Yiyang Chen <cyyzero16@gmail.com> wrote:

> This series fixes an issue where cgroupstats mishandles invalid file
> descriptors, and introduces a functional kselftest to prevent regressions.
> 
> When an invalid file descriptor is passed via CGROUPSTATS_CMD_ATTR_FD,
> cgroupstats_user_cmd() returns 0 instead of an error code. This leads to
> two broken behaviors depending on netlink flags:
> - Callers without NLM_F_ACK block indefinitely on recv().
> - Callers with NLM_F_ACK receive a misleading success ACK (errno == 0)
>   but no actual statistics payload.
> 
> The first patch addresses this by returning -EBADF when the fd cannot be
> resolved. The second patch adds a comprehensive kselftest covering both
> the valid cgroup v1 query and the invalid fd paths (with and without
> NLM_F_ACK) to ensure the fixes work as intended.

lgtm, thanks.

It's a little odd to group two unrelated(?) changes in one series like
this.  Oh well, let's not redo things for this.

Should the netlink_open() in taskstats_fill_stats_tgid.c also be
setting that timeout?  If so, we can share a single implementation - a
little cleanup.

There's a lot of code duplication in selftests/ :(

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

end of thread, other threads:[~2026-07-10 22:59 UTC | newest]

Thread overview: 4+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-07-10 20:07 [PATCH 0/2] taskstats: fix cgroupstats invalid fd handling and add selftests Yiyang Chen
2026-07-10 20:07 ` [PATCH 1/2] taskstats: return -EBADF when cgroupstats receives an invalid fd Yiyang Chen
2026-07-10 20:07 ` [PATCH 2/2] selftests/acct: add cgroupstats functional test Yiyang Chen
2026-07-10 22:59 ` [PATCH 0/2] taskstats: fix cgroupstats invalid fd handling and add selftests Andrew Morton

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