mirror of https://lore.kernel.org/lkml/
 help / color / mirror / Atom feed
From: Bradley Morgan <brads@mainlining.org>
To: tjdqudcks0424@naver.com
Cc: akpm@linux-foundation.org, bsingharora@gmail.com,
	cyyzero16@gmail.com, gregkh@linuxfoundation.org,
	linux-kernel@vger.kernel.org, linux-kselftest@vger.kernel.org,
	shuah@kernel.org, xu.xin@linux.dev
Subject: Re: [PATCH v3 2/2] selftests: acct: test taskstats listener netns routing
Date: Fri, 02 Oct 2026 17:43:57 +0100	[thread overview]
Message-ID: <98826F7B-9448-4585-89CA-55590AE29280@mainlining.org> (raw)
In-Reply-To: <20261002105614.195370-3-tjdqudcks0424@naver.com>

On 2 October 2026 11:56:14 BST, tjdqudcks0424@naver.com wrote:
>From: 성병찬 <tjdqudcks0424@naver.com>
>
>Add a regression test for CPU-mask listener routing across network
>namespaces. The test registers a privileged listener in a child network
>namespace and binds an unprivileged init_net socket to the same numeric
>Generic Netlink port ID. A single pinned victim exit is observed by that
>pair and by the normal controls.
>
>On the unmodified kernel the child-netns listener misses the record and
>the
>colliding init_net socket receives it, so the test fails. It passes when
>the
>record reaches the child-netns listener and not the colliding socket.
>Decode
>and validate the victim PID, TGID, UID, GID, comm, and exit code.
>
>Also verify that an init_net administrator listener receives the record,
>that explicit deregistration suppresses it, that an unrelated port ID does
>not receive it, and that the unprivileged socket's direct query and
>listener
>registration both fail with EPERM.
>
>Use pipe handshakes instead of sleeps and bound every receive. Skip when
>the
>test lacks root, CAP_NET_ADMIN in the initial user namespace, network
>namespace support, or the taskstats Generic Netlink family.

NAK, Have you run this in a loop? I mean like 10 back to back runs on the
same boot, not one run per fresh QEMU boot.

It failed, but:

1: the test can't tell me which failure it was. receive_victim()
returns 0 both when there was no record and when the 3s deadline hit,
so "no record" and "recv timeout" print the same. Something like this
would separate them:

	int ret;
	...
	ret = wait_fd(fd, POLLIN, left);
	if (ret == -ETIMEDOUT) {
		result->recv_errno = -ret;   /* new field */
		return 0;
	}

or just print the Drops column from /proc/net/netlink for the socket
after the run, that would show a kernel sised drop.


2: first run, the
colliding socket got the victim record and the test failed exactly as
described. But every run after the first went silent: all four
listeners report found=0, including the init_net admin one that passed
in run 1

So NAK for now, I don't want this merged yet, sorry.

>Assisted-by: OpenAI Codex
>Signed-off-by: 성병찬 <tjdqudcks0424@naver.com>
>---
> tools/testing/selftests/acct/Makefile         |   1 +
> .../selftests/acct/taskstats_netns_listener.c | 945 ++++++++++++++++++
> 2 files changed, 946 insertions(+)
> create mode 100644
> tools/testing/selftests/acct/taskstats_netns_listener.c
>
>diff --git a/tools/testing/selftests/acct/Makefile b/tools/testing/selftests/acct/Makefile
>index 93a11a28a6361..e1a79f57fe6df 100644
>--- a/tools/testing/selftests/acct/Makefile
>+++ b/tools/testing/selftests/acct/Makefile
>@@ -2,6 +2,7 @@
> TEST_GEN_PROGS := acct_syscall
> TEST_GEN_PROGS += taskstats_fill_stats_tgid
> TEST_GEN_PROGS += cgroupstats
>+TEST_GEN_PROGS += taskstats_netns_listener
> 
> NETLINK_HELPER_PROGS := cgroupstats taskstats_fill_stats_tgid
> 
>diff --git a/tools/testing/selftests/acct/taskstats_netns_listener.c b/tools/testing/selftests/acct/taskstats_netns_listener.c
>new file mode 100644
>index 0000000000000..134ecb269c9a0
>--- /dev/null
>+++ b/tools/testing/selftests/acct/taskstats_netns_listener.c
>@@ -0,0 +1,945 @@
>+// SPDX-License-Identifier: GPL-2.0
>+#define _GNU_SOURCE
>+
>+#include <errno.h>
>+#include <fcntl.h>
>+#include <grp.h>
>+#include <linux/capability.h>
>+#include <linux/genetlink.h>
>+#include <linux/netlink.h>
>+#include <linux/taskstats.h>
>+#include <limits.h>
>+#include <poll.h>
>+#include <sched.h>
>+#include <stdbool.h>
>+#include <stdint.h>
>+#include <stdio.h>
>+#include <stdlib.h>
>+#include <string.h>
>+#include <sys/prctl.h>
>+#include <sys/socket.h>
>+#include <sys/stat.h>
>+#include <sys/syscall.h>
>+#include <sys/types.h>
>+#include <sys/wait.h>
>+#include <time.h>
>+#include <unistd.h>
>+
>+#include "kselftest.h"
>+
>+#define TEST_UID 65533
>+#define TEST_GID 65533
>+#define ATTACKER_UID 65534
>+#define ATTACKER_GID 65534
>+#define VICTIM_COMM "ts-netns-victim"
>+#define VICTIM_EXIT_CODE 37
>+#define RECV_TIMEOUT_MS 3000
>+#define PIPE_TIMEOUT_MS 5000
>+#define MAX_MSG 16384
>+
>+struct wire_result {
>+	int rc;
>+	int err;
>+	int query_rc;
>+	int register_rc;
>+	uint32_t portid;
>+	uint32_t uid;
>+	uint32_t gid;
>+	unsigned long long netns_ino;
>+	unsigned long long cap_eff;
>+	bool found;
>+	uint32_t id;
>+	uint32_t ac_pid;
>+	uint32_t ac_tgid;
>+	uint32_t ac_uid;
>+	uint32_t ac_gid;
>+	uint32_t exit_code;
>+	char comm[TS_COMM_LEN + 1];
>+};
>+
>+static uint32_t nl_seq;
>+
>+static long long now_ms(void)
>+{
>+	struct timespec ts;
>+
>+	if (clock_gettime(CLOCK_MONOTONIC, &ts))
>+		return -1;
>+	return (long long)ts.tv_sec * 1000 + ts.tv_nsec / 1000000;
>+}
>+
>+static int wait_fd(int fd, short events, int timeout_ms)
>+{
>+	struct pollfd pfd = { .fd = fd, .events = events };
>+	int ret;
>+
>+	do {
>+		ret = poll(&pfd, 1, timeout_ms);
>+	} while (ret < 0 && errno == EINTR);
>+	if (!ret)
>+		return -ETIMEDOUT;
>+	if (ret < 0)
>+		return -errno;
>+	if (pfd.revents & events)
>+		return 0;
>+	if (pfd.revents & (POLLERR | POLLHUP | POLLNVAL))
>+		return -EIO;
>+	return -EIO;
>+}
>+
>+static int read_full_timeout(int fd, void *buf, size_t len, int timeout_ms)
>+{
>+	char *pos = buf;
>+	long long deadline = now_ms() + timeout_ms;
>+
>+	while (len) {
>+		long long left = deadline - now_ms();
>+		ssize_t ret;
>+		int err;
>+
>+		if (left <= 0)
>+			return -ETIMEDOUT;
>+		err = wait_fd(fd, POLLIN, left > INT32_MAX ? INT32_MAX : (int)left);
>+		if (err)
>+			return err;
>+		ret = read(fd, pos, len);
>+		if (ret < 0) {
>+			if (errno == EINTR)
>+				continue;
>+			return -errno;
>+		}
>+		if (!ret)
>+			return -EPIPE;
>+		pos += ret;
>+		len -= ret;
>+	}
>+	return 0;
>+}
>+
>+static int write_full(int fd, const void *buf, size_t len)
>+{
>+	const char *pos = buf;
>+
>+	while (len) {
>+		ssize_t ret = write(fd, pos, len);
>+
>+		if (ret < 0) {
>+			if (errno == EINTR)
>+				continue;
>+			return -errno;
>+		}
>+		pos += ret;
>+		len -= ret;
>+	}
>+	return 0;
>+}
>+
>+static bool nla_valid(const struct nlattr *attr, size_t remaining)
>+{
>+	return remaining >= sizeof(*attr) && attr->nla_len >= NLA_HDRLEN &&
>+	       attr->nla_len <= remaining;
>+}
>+
>+static void *nla_payload(const struct nlattr *attr)
>+{
>+	return (char *)attr + NLA_HDRLEN;
>+}
>+
>+static size_t nla_payload_len(const struct nlattr *attr)
>+{
>+	return attr->nla_len - NLA_HDRLEN;
>+}
>+
>+static int open_netlink(uint32_t requested, uint32_t *portid)
>+{
>+	struct sockaddr_nl addr = { .nl_family = AF_NETLINK, .nl_pid = requested };
>+	socklen_t len = sizeof(addr);
>+	int fd;
>+
>+	fd = socket(AF_NETLINK, SOCK_RAW | SOCK_CLOEXEC, NETLINK_GENERIC);
>+	if (fd < 0)
>+		return -errno;
>+	if (bind(fd, (struct sockaddr *)&addr, sizeof(addr)) ||
>+	    getsockname(fd, (struct sockaddr *)&addr, &len)) {
>+		int err = -errno;
>+
>+		close(fd);
>+		return err;
>+	}
>+	if (len != sizeof(addr) || addr.nl_family != AF_NETLINK) {
>+		close(fd);
>+		return -EPROTO;
>+	}
>+	*portid = addr.nl_pid;
>+	return fd;
>+}
>+
>+static int add_attr(char *buf, size_t size, size_t *len, uint16_t type,
>+		    const void *data, size_t data_len)
>+{
>+	struct nlattr *attr;
>+	size_t attr_len = NLA_HDRLEN + data_len;
>+	size_t aligned = NLA_ALIGN(attr_len);
>+
>+	if (*len > size || aligned > size - *len)
>+		return -EMSGSIZE;
>+	attr = (struct nlattr *)(buf + *len);
>+	attr->nla_type = type;
>+	attr->nla_len = attr_len;
>+	memcpy(nla_payload(attr), data, data_len);
>+	memset((char *)attr + attr_len, 0, aligned - attr_len);
>+	*len += aligned;
>+	return 0;
>+}
>+
>+static int send_cmd(int fd, uint32_t portid, uint16_t family, uint8_t cmd,
>+		    uint16_t attr_type, const void *data, size_t data_len,
>+		    uint32_t *seq)
>+{
>+	struct sockaddr_nl kernel = { .nl_family = AF_NETLINK };
>+	char buf[512] = {};
>+	struct nlmsghdr *nlh = (struct nlmsghdr *)buf;
>+	struct genlmsghdr *genl = (struct genlmsghdr *)(buf + NLMSG_HDRLEN);
>+	size_t len = NLMSG_HDRLEN + GENL_HDRLEN;
>+	ssize_t sent;
>+	int ret;
>+
>+	nlh->nlmsg_type = family;
>+	nlh->nlmsg_flags = NLM_F_REQUEST | NLM_F_ACK;
>+	nlh->nlmsg_seq = ++nl_seq;
>+	nlh->nlmsg_pid = portid;
>+	genl->cmd = cmd;
>+	genl->version = TASKSTATS_GENL_VERSION;
>+	ret = add_attr(buf, sizeof(buf), &len, attr_type, data, data_len);
>+	if (ret)
>+		return ret;
>+	nlh->nlmsg_len = len;
>+	sent = sendto(fd, buf, len, 0, (struct sockaddr *)&kernel,
>+		      sizeof(kernel));
>+	if (sent < 0)
>+		return -errno;
>+	if ((size_t)sent != len)
>+		return -EIO;
>+	*seq = nlh->nlmsg_seq;
>+	return 0;
>+}
>+
>+static ssize_t recv_msg(int fd, void *buf, size_t len, int timeout_ms)
>+{
>+	struct sockaddr_nl sender = {};
>+	struct iovec iov = { .iov_base = buf, .iov_len = len };
>+	struct msghdr msg = {
>+		.msg_name = &sender,
>+		.msg_namelen = sizeof(sender),
>+		.msg_iov = &iov,
>+		.msg_iovlen = 1,
>+	};
>+	ssize_t ret;
>+	int err;
>+
>+	err = wait_fd(fd, POLLIN, timeout_ms);
>+	if (err)
>+		return err;
>+	ret = recvmsg(fd, &msg, 0);
>+	if (ret < 0)
>+		return -errno;
>+	if (msg.msg_flags & MSG_TRUNC)
>+		return -EMSGSIZE;
>+	if (sender.nl_family != AF_NETLINK || sender.nl_pid)
>+		return -EPROTO;
>+	return ret;
>+}
>+
>+static int transact(int fd, uint32_t portid, uint16_t family,
>+		    uint16_t attr_type, const void *data, size_t data_len)
>+{
>+	char buf[MAX_MSG];
>+	uint32_t seq;
>+	long long deadline;
>+	int ret;
>+
>+	ret = send_cmd(fd, portid, family, TASKSTATS_CMD_GET, attr_type,
>+		       data, data_len, &seq);
>+	if (ret)
>+		return ret;
>+	deadline = now_ms() + RECV_TIMEOUT_MS;
>+	for (;;) {
>+		struct nlmsghdr *nlh;
>+		long long left = deadline - now_ms();
>+		ssize_t received;
>+		int remaining;
>+
>+		if (left <= 0)
>+			return -ETIMEDOUT;
>+		received = recv_msg(fd, buf, sizeof(buf), (int)left);
>+		if (received < 0)
>+			return received;
>+		remaining = received;
>+		for (nlh = (struct nlmsghdr *)buf; NLMSG_OK(nlh, remaining);
>+		     nlh = NLMSG_NEXT(nlh, remaining)) {
>+			struct nlmsgerr *err;
>+
>+			if (nlh->nlmsg_seq != seq)
>+				continue;
>+			if (nlh->nlmsg_type != NLMSG_ERROR)
>+				continue;
>+			if (nlh->nlmsg_len < NLMSG_LENGTH(sizeof(*err)))
>+				return -EBADMSG;
>+			err = NLMSG_DATA(nlh);
>+			return err->error;
>+		}
>+	}
>+}
>+
>+static int resolve_family(int fd, uint32_t portid, uint16_t *family)
>+{
>+	const char name[] = TASKSTATS_GENL_NAME;
>+	char buf[MAX_MSG];
>+	uint32_t seq;
>+	ssize_t received;
>+	struct nlmsghdr *nlh;
>+	int remaining;
>+	int ret;
>+
>+	ret = send_cmd(fd, portid, GENL_ID_CTRL, CTRL_CMD_GETFAMILY,
>+		       CTRL_ATTR_FAMILY_NAME, name, sizeof(name), &seq);
>+	if (ret)
>+		return ret;
>+	received = recv_msg(fd, buf, sizeof(buf), RECV_TIMEOUT_MS);
>+	if (received < 0)
>+		return received;
>+	remaining = received;
>+	for (nlh = (struct nlmsghdr *)buf; NLMSG_OK(nlh, remaining);
>+	     nlh = NLMSG_NEXT(nlh, remaining)) {
>+		struct genlmsghdr *genl;
>+		struct nlattr *attr;
>+		size_t attrs;
>+
>+		if (nlh->nlmsg_seq != seq)
>+			continue;
>+		if (nlh->nlmsg_type == NLMSG_ERROR) {
>+			struct nlmsgerr *err;
>+
>+			if (nlh->nlmsg_len < NLMSG_LENGTH(sizeof(*err)))
>+				return -EBADMSG;
>+			err = NLMSG_DATA(nlh);
>+			return err->error ? err->error : -ENOENT;
>+		}
>+		if (nlh->nlmsg_type != GENL_ID_CTRL ||
>+		    nlh->nlmsg_len < NLMSG_HDRLEN + GENL_HDRLEN)
>+			return -EBADMSG;
>+		genl = NLMSG_DATA(nlh);
>+		if (genl->cmd != CTRL_CMD_NEWFAMILY)
>+			return -EBADMSG;
>+		attrs = nlh->nlmsg_len - NLMSG_HDRLEN - GENL_HDRLEN;
>+		attr = (struct nlattr *)((char *)genl + GENL_HDRLEN);
>+		while (nla_valid(attr, attrs)) {
>+			size_t step = NLA_ALIGN(attr->nla_len);
>+
>+			if ((attr->nla_type & NLA_TYPE_MASK) == CTRL_ATTR_FAMILY_ID &&
>+			    nla_payload_len(attr) == sizeof(*family)) {
>+				memcpy(family, nla_payload(attr), sizeof(*family));
>+				return 0;
>+			}
>+			if (step > attrs)
>+				return -EBADMSG;
>+			attrs -= step;
>+			attr = (struct nlattr *)((char *)attr + step);
>+		}
>+		if (attrs)
>+			return -EBADMSG;
>+	}
>+	return -ENOENT;
>+}
>+
>+static int listener_cmd(int fd, uint32_t portid, uint16_t family, int cpu,
>+			bool add)
>+{
>+	char mask[32];
>+	int len;
>+
>+	len = snprintf(mask, sizeof(mask), "%d", cpu);
>+	if (len < 0 || len >= (int)sizeof(mask))
>+		return -EOVERFLOW;
>+	return transact(fd, portid, family,
>+			add ? TASKSTATS_CMD_ATTR_REGISTER_CPUMASK :
>+			      TASKSTATS_CMD_ATTR_DEREGISTER_CPUMASK,
>+			mask, len + 1);
>+}
>+
>+static int query_pid(int fd, uint32_t portid, uint16_t family, uint32_t pid)
>+{
>+	return transact(fd, portid, family, TASKSTATS_CMD_ATTR_PID,
>+			&pid, sizeof(pid));
>+}
>+
>+static int decode_nested(struct nlattr *outer, pid_t victim,
>+			 struct wire_result *result)
>+{
>+	struct nlattr *attr = nla_payload(outer);
>+	size_t remaining = nla_payload_len(outer);
>+	struct taskstats stats = {};
>+	uint32_t id = 0;
>+	bool have_id = false;
>+	bool have_stats = false;
>+
>+	while (nla_valid(attr, remaining)) {
>+		uint16_t type = attr->nla_type & NLA_TYPE_MASK;
>+		size_t step = NLA_ALIGN(attr->nla_len);
>+
>+		if ((type == TASKSTATS_TYPE_PID || type == TASKSTATS_TYPE_TGID) &&
>+		    nla_payload_len(attr) == sizeof(id)) {
>+			memcpy(&id, nla_payload(attr), sizeof(id));
>+			have_id = true;
>+		} else if (type == TASKSTATS_TYPE_STATS &&
>+			   nla_payload_len(attr) >= sizeof(stats)) {
>+			memcpy(&stats, nla_payload(attr), sizeof(stats));
>+			have_stats = true;
>+		}
>+		if (step > remaining)
>+			return -EBADMSG;
>+		remaining -= step;
>+		attr = (struct nlattr *)((char *)attr + step);
>+	}
>+	if (remaining)
>+		return -EBADMSG;
>+	if (!have_id || !have_stats || stats.ac_pid != (uint32_t)victim ||
>+	    stats.ac_tgid != (uint32_t)victim || id != (uint32_t)victim ||
>+	    strncmp(stats.ac_comm, VICTIM_COMM, TS_COMM_LEN))
>+		return 0;
>+	result->found = true;
>+	result->id = id;
>+	result->ac_pid = stats.ac_pid;
>+	result->ac_tgid = stats.ac_tgid;
>+	result->ac_uid = stats.ac_uid;
>+	result->ac_gid = stats.ac_gid;
>+	result->exit_code = stats.ac_exitcode >> 8;
>+	memcpy(result->comm, stats.ac_comm, TS_COMM_LEN);
>+	result->comm[TS_COMM_LEN] = '\0';
>+	return 1;
>+}
>+
>+static int receive_victim(int fd, uint16_t family, pid_t victim,
>+			  struct wire_result *result, int timeout_ms)
>+{
>+	char buf[MAX_MSG];
>+	long long deadline = now_ms() + timeout_ms;
>+
>+	for (;;) {
>+		struct nlmsghdr *nlh;
>+		long long left = deadline - now_ms();
>+		ssize_t received;
>+		int remaining;
>+
>+		if (left <= 0)
>+			return 0;
>+		received = recv_msg(fd, buf, sizeof(buf), (int)left);
>+		if (received == -ETIMEDOUT)
>+			return 0;
>+		if (received < 0)
>+			return received;
>+		remaining = received;
>+		for (nlh = (struct nlmsghdr *)buf; NLMSG_OK(nlh, remaining);
>+		     nlh = NLMSG_NEXT(nlh, remaining)) {
>+			struct genlmsghdr *genl;
>+			struct nlattr *attr;
>+			size_t attrs;
>+
>+			if (nlh->nlmsg_type != family || nlh->nlmsg_pid ||
>+			    nlh->nlmsg_seq ||
>+			    nlh->nlmsg_len < NLMSG_HDRLEN + GENL_HDRLEN)
>+				continue;
>+			genl = NLMSG_DATA(nlh);
>+			if (genl->cmd != TASKSTATS_CMD_NEW)
>+				continue;
>+			attrs = nlh->nlmsg_len - NLMSG_HDRLEN - GENL_HDRLEN;
>+			attr = (struct nlattr *)((char *)genl + GENL_HDRLEN);
>+			while (nla_valid(attr, attrs)) {
>+				size_t step = NLA_ALIGN(attr->nla_len);
>+				uint16_t type = attr->nla_type & NLA_TYPE_MASK;
>+				int ret;
>+
>+				if (type == TASKSTATS_TYPE_AGGR_PID ||
>+				    type == TASKSTATS_TYPE_AGGR_TGID) {
>+					ret = decode_nested(attr, victim, result);
>+					if (ret)
>+						return ret;
>+				}
>+				if (step > attrs)
>+					return -EBADMSG;
>+				attrs -= step;
>+				attr = (struct nlattr *)((char *)attr + step);
>+			}
>+			if (attrs)
>+				return -EBADMSG;
>+		}
>+		if (remaining)
>+			return -EBADMSG;
>+	}
>+}
>+
>+static unsigned long long read_cap_eff(void)
>+{
>+	FILE *file = fopen("/proc/self/status", "re");
>+	char line[256];
>+	unsigned long long value = ULLONG_MAX;
>+
>+	if (!file)
>+		return value;
>+	while (fgets(line, sizeof(line), file)) {
>+		if (sscanf(line, "CapEff:\t%llx", &value) == 1)
>+			break;
>+	}
>+	fclose(file);
>+	return value;
>+}
>+
>+static unsigned long long netns_inode(void)
>+{
>+	struct stat st;
>+
>+	if (stat("/proc/self/ns/net", &st))
>+		return 0;
>+	return st.st_ino;
>+}
>+
>+static int drop_privileges(uid_t uid, gid_t gid)
>+{
>+	struct __user_cap_header_struct header = {
>+		.version = _LINUX_CAPABILITY_VERSION_3,
>+	};
>+	struct __user_cap_data_struct data[2] = {};
>+
>+	if (setgroups(0, NULL) || setresgid(gid, gid, gid) ||
>+	    setresuid(uid, uid, uid) || syscall(SYS_capset, &header, data))
>+		return -errno;
>+	return 0;
>+}
>+
>+static void fill_identity(struct wire_result *result)
>+{
>+	result->uid = getuid();
>+	result->gid = getgid();
>+	result->netns_ino = netns_inode();
>+	result->cap_eff = read_cap_eff();
>+}
>+
>+static void listener_child(int result_fd, int go_fd, int cpu,
>+			   bool child_netns, bool attacker, uint32_t requested,
>+			   uint32_t query_target, pid_t victim, bool deregister)
>+{
>+	struct wire_result result = {};
>+	uint16_t family;
>+	char go;
>+	int fd = -1;
>+	int ret;
>+
>+	result.query_rc = INT32_MIN;
>+	result.register_rc = INT32_MIN;
>+	if (child_netns && unshare(CLONE_NEWNET)) {
>+		result.rc = -errno;
>+		goto out;
>+	}
>+	if (attacker) {
>+		ret = drop_privileges(ATTACKER_UID, ATTACKER_GID);
>+		if (ret) {
>+			result.rc = ret;
>+			goto out;
>+		}
>+	}
>+	fill_identity(&result);
>+	fd = open_netlink(requested, &result.portid);
>+	if (fd < 0) {
>+		result.rc = fd;
>+		goto out;
>+	}
>+	ret = resolve_family(fd, result.portid, &family);
>+	if (ret) {
>+		result.rc = ret;
>+		goto out;
>+	}
>+	if (attacker) {
>+		result.query_rc = query_pid(fd, result.portid, family, query_target);
>+		result.register_rc = listener_cmd(fd, result.portid, family, cpu,
>+						  true);
>+	} else {
>+		result.register_rc = listener_cmd(fd, result.portid, family, cpu,
>+						  true);
>+		if (result.register_rc) {
>+			result.rc = result.register_rc;
>+			goto out;
>+		}
>+		if (deregister) {
>+			ret = listener_cmd(fd, result.portid, family, cpu, false);
>+			if (ret) {
>+				result.rc = ret;
>+					goto out;
>+				}
>+			}
>+		}
>+	result.rc = 1;
>+	if (write_full(result_fd, &result, sizeof(result)))
>+		goto done;
>+	ret = read_full_timeout(go_fd, &go, 1, PIPE_TIMEOUT_MS);
>+	if (ret) {
>+		result.rc = ret;
>+		goto final;
>+	}
>+	ret = receive_victim(fd, family, victim, &result, RECV_TIMEOUT_MS);
>+	result.rc = ret < 0 ? ret : 0;
>+	if (!attacker && !deregister)
>+		(void)listener_cmd(fd, result.portid, family, cpu, false);
>+final:
>+	(void)write_full(result_fd, &result, sizeof(result));
>+done:
>+	if (fd >= 0)
>+		close(fd);
>+	close(result_fd);
>+	close(go_fd);
>+	_exit(result.rc ? KSFT_FAIL : KSFT_PASS);
>+out:
>+	result.err = result.rc < 0 ? -result.rc : 0;
>+	fill_identity(&result);
>+	(void)write_full(result_fd, &result, sizeof(result));
>+	goto done;
>+}
>+
>+static pid_t spawn_victim(int cpu, int go_fd)
>+{
>+	pid_t pid = fork();
>+
>+	if (pid)
>+		return pid;
>+	if (pid < 0)
>+		return pid;
>+	{
>+		cpu_set_t set;
>+		char go;
>+
>+		CPU_ZERO(&set);
>+		CPU_SET(cpu, &set);
>+		if (sched_setaffinity(0, sizeof(set), &set) ||
>+		    prctl(PR_SET_NAME, VICTIM_COMM) ||
>+		    drop_privileges(TEST_UID, TEST_GID) ||
>+		    read_full_timeout(go_fd, &go, 1, PIPE_TIMEOUT_MS))
>+			_exit(125);
>+	}
>+	_exit(VICTIM_EXIT_CODE);
>+}
>+
>+static bool valid_record(const struct wire_result *result, pid_t victim)
>+{
>+	return result->found && result->id == (uint32_t)victim &&
>+	       result->ac_pid == (uint32_t)victim &&
>+	       result->ac_tgid == (uint32_t)victim &&
>+	       result->ac_uid == TEST_UID && result->ac_gid == TEST_GID &&
>+	       result->exit_code == VICTIM_EXIT_CODE &&
>+	       !strcmp(result->comm, VICTIM_COMM);
>+}
>+
>+static void log_result(const char *role, const struct wire_result *result)
>+{
>+	ksft_print_msg("%s: rc=%d errno=%d uid=%u gid=%u CapEff=%016llx\n",
>+		       role, result->rc, result->err, result->uid, result->gid,
>+		       result->cap_eff);
>+	ksft_print_msg("%s: netns=%llu portid=%u query=%d register=%d found=%d\n",
>+		       role, result->netns_ino, result->portid, result->query_rc,
>+		       result->register_rc, result->found);
>+	ksft_print_msg("%s: id=%u pid=%u tgid=%u uid=%u gid=%u comm=%s exit=%u\n",
>+		       role, result->id, result->ac_pid, result->ac_tgid,
>+		       result->ac_uid, result->ac_gid, result->comm,
>+		       result->exit_code);
>+}
>+
>+static int reap(pid_t pid)
>+{
>+	int status;
>+
>+	while (waitpid(pid, &status, 0) < 0) {
>+		if (errno != EINTR)
>+			return -errno;
>+	}
>+	return status;
>+}
>+
>+struct scenario_result {
>+	struct wire_result registrar;
>+	struct wire_result attacker;
>+	struct wire_result admin;
>+	struct wire_result deregistered;
>+	struct wire_result unrelated;
>+	pid_t victim;
>+	int victim_status;
>+};
>+
>+static int run_scenario(int cpu, uint16_t family,
>+			struct scenario_result *result)
>+{
>+	int reg_res[2], reg_go[2], atk_res[2], atk_go[2];
>+	int adm_res[2], adm_go[2], der_res[2], der_go[2], victim_go[2];
>+	pid_t reg_pid = -1, atk_pid = -1, adm_pid = -1, der_pid = -1;
>+	uint32_t other_port;
>+	int other_fd = -1;
>+	char go = 'G';
>+	int ret;
>+
>+	if (pipe2(reg_res, O_CLOEXEC) || pipe2(reg_go, O_CLOEXEC) ||
>+	    pipe2(atk_res, O_CLOEXEC) || pipe2(atk_go, O_CLOEXEC) ||
>+	    pipe2(adm_res, O_CLOEXEC) || pipe2(adm_go, O_CLOEXEC) ||
>+	    pipe2(der_res, O_CLOEXEC) || pipe2(der_go, O_CLOEXEC) ||
>+	    pipe2(victim_go, O_CLOEXEC))
>+		return -errno;
>+	result->victim = spawn_victim(cpu, victim_go[0]);
>+	if (result->victim < 0) {
>+		ret = -errno;
>+		goto fail;
>+	}
>+	reg_pid = fork();
>+	if (!reg_pid)
>+		listener_child(reg_res[1], reg_go[0], cpu, true, false, 0, 0,
>+			       result->victim, false);
>+	if (reg_pid < 0) {
>+		ret = -errno;
>+		goto fail;
>+	}
>+	close(reg_res[1]);
>+	close(reg_go[0]);
>+	ret = read_full_timeout(reg_res[0], &result->registrar,
>+				sizeof(result->registrar), PIPE_TIMEOUT_MS);
>+	if (ret || result->registrar.rc != 1)
>+		goto fail;
>+	atk_pid = fork();
>+	if (!atk_pid)
>+		listener_child(atk_res[1], atk_go[0], cpu, false, true,
>+			       result->registrar.portid, reg_pid, result->victim,
>+			       false);
>+	if (atk_pid < 0) {
>+		ret = -errno;
>+		goto fail;
>+	}
>+	close(atk_res[1]);
>+	close(atk_go[0]);
>+	ret = read_full_timeout(atk_res[0], &result->attacker,
>+				sizeof(result->attacker), PIPE_TIMEOUT_MS);
>+	if (ret || result->attacker.rc != 1)
>+		goto fail;
>+
>+	adm_pid = fork();
>+	if (!adm_pid)
>+		listener_child(adm_res[1], adm_go[0], cpu, false, false, 0, 0,
>+			       result->victim, false);
>+	if (adm_pid < 0) {
>+		ret = -errno;
>+		goto fail;
>+	}
>+	close(adm_res[1]);
>+	close(adm_go[0]);
>+	ret = read_full_timeout(adm_res[0], &result->admin,
>+				sizeof(result->admin), PIPE_TIMEOUT_MS);
>+	if (ret || result->admin.rc != 1)
>+		goto fail;
>+
>+	der_pid = fork();
>+	if (!der_pid)
>+		listener_child(der_res[1], der_go[0], cpu, false, false, 0, 0,
>+			       result->victim, true);
>+	if (der_pid < 0) {
>+		ret = -errno;
>+		goto fail;
>+	}
>+	close(der_res[1]);
>+	close(der_go[0]);
>+	ret = read_full_timeout(der_res[0], &result->deregistered,
>+				sizeof(result->deregistered), PIPE_TIMEOUT_MS);
>+	if (ret || result->deregistered.rc != 1)
>+		goto fail;
>+
>+	other_fd = open_netlink(0, &other_port);
>+	if (other_fd < 0) {
>+		ret = other_fd;
>+		goto fail;
>+	}
>+	fill_identity(&result->unrelated);
>+	result->unrelated.portid = other_port;
>+	if (write_full(reg_go[1], &go, 1) ||
>+	    write_full(atk_go[1], &go, 1) ||
>+	    write_full(adm_go[1], &go, 1) ||
>+	    write_full(der_go[1], &go, 1) ||
>+	    write_full(victim_go[1], &go, 1)) {
>+		ret = -EPIPE;
>+		goto fail;
>+	}
>+	ret = read_full_timeout(reg_res[0], &result->registrar,
>+				sizeof(result->registrar), PIPE_TIMEOUT_MS);
>+	if (ret)
>+		goto fail;
>+	ret = read_full_timeout(atk_res[0], &result->attacker,
>+				sizeof(result->attacker), PIPE_TIMEOUT_MS);
>+	if (ret)
>+		goto fail;
>+	ret = read_full_timeout(adm_res[0], &result->admin,
>+				sizeof(result->admin), PIPE_TIMEOUT_MS);
>+	if (ret)
>+		goto fail;
>+	ret = read_full_timeout(der_res[0], &result->deregistered,
>+				sizeof(result->deregistered), PIPE_TIMEOUT_MS);
>+	if (ret)
>+		goto fail;
>+	ret = receive_victim(other_fd, family, result->victim,
>+			     &result->unrelated, 250);
>+	if (ret < 0 && ret != -ETIMEDOUT)
>+		goto fail;
>+	ret = 0;
>+	(void)reap(reg_pid);
>+	(void)reap(atk_pid);
>+	(void)reap(adm_pid);
>+	(void)reap(der_pid);
>+	result->victim_status = reap(result->victim);
>+	log_result("registrar", &result->registrar);
>+	log_result("colliding receiver", &result->attacker);
>+	log_result("init_net listener", &result->admin);
>+	log_result("deregistered listener", &result->deregistered);
>+	log_result("unrelated port", &result->unrelated);
>+	ksft_print_msg("victim: pid=%d tgid=%d uid=%u gid=%u comm=%s exit=%d\n",
>+		       result->victim, result->victim, TEST_UID, TEST_GID,
>+		       VICTIM_COMM, VICTIM_EXIT_CODE);
>+	ksft_print_msg("victim: wait_status=%d cpu=%d\n",
>+		       result->victim_status, cpu);
>+	close(other_fd);
>+	return 0;
>+fail:
>+	if (reg_pid > 0)
>+		kill(reg_pid, SIGKILL);
>+	if (atk_pid > 0)
>+		kill(atk_pid, SIGKILL);
>+	if (adm_pid > 0)
>+		kill(adm_pid, SIGKILL);
>+	if (der_pid > 0)
>+		kill(der_pid, SIGKILL);
>+	if (result->victim > 0)
>+		kill(result->victim, SIGKILL);
>+	if (reg_pid > 0)
>+		(void)reap(reg_pid);
>+	if (atk_pid > 0)
>+		(void)reap(atk_pid);
>+	if (adm_pid > 0)
>+		(void)reap(adm_pid);
>+	if (der_pid > 0)
>+		(void)reap(der_pid);
>+	if (result->victim > 0)
>+		(void)reap(result->victim);
>+	if (other_fd >= 0)
>+		close(other_fd);
>+	log_result("registrar setup", &result->registrar);
>+	log_result("colliding receiver setup", &result->attacker);
>+	log_result("init_net listener setup", &result->admin);
>+	log_result("deregistered listener setup", &result->deregistered);
>+	return ret ? ret : -EINVAL;
>+}
>+
>+static int choose_cpu(void)
>+{
>+	cpu_set_t set;
>+	int cpu;
>+
>+	if (sched_getaffinity(0, sizeof(set), &set))
>+		return -errno;
>+	for (cpu = 0; cpu < CPU_SETSIZE; cpu++)
>+		if (CPU_ISSET(cpu, &set))
>+			return cpu;
>+	return -ENODEV;
>+}
>+
>+static int netns_preflight(void)
>+{
>+	int pipefd[2];
>+	pid_t pid;
>+	int err = 0;
>+	int ret;
>+
>+	if (pipe2(pipefd, O_CLOEXEC))
>+		return -errno;
>+	pid = fork();
>+	if (!pid) {
>+		close(pipefd[0]);
>+		if (unshare(CLONE_NEWNET))
>+			err = -errno;
>+		(void)write_full(pipefd[1], &err, sizeof(err));
>+		_exit(0);
>+	}
>+	close(pipefd[1]);
>+	if (pid < 0) {
>+		close(pipefd[0]);
>+		return -errno;
>+	}
>+	ret = read_full_timeout(pipefd[0], &err, sizeof(err), PIPE_TIMEOUT_MS);
>+	close(pipefd[0]);
>+	(void)reap(pid);
>+	return ret ? ret : err;
>+}
>+
>+int main(void)
>+{
>+	struct scenario_result scenario = {};
>+	unsigned long long init_netns;
>+	uint32_t portid;
>+	uint16_t family;
>+	int fd;
>+	int cpu;
>+	int ret;
>+
>+	ksft_print_header();
>+	if (geteuid())
>+		ksft_exit_skip("root or CAP_NET_ADMIN in the initial user namespace is required\n");
>+	if (!(read_cap_eff() & (1ULL << CAP_NET_ADMIN)))
>+		ksft_exit_skip("CAP_NET_ADMIN in the initial user namespace is required\n");
>+	cpu = choose_cpu();
>+	if (cpu < 0)
>+		ksft_exit_skip("no usable CPU: %s\n", strerror(-cpu));
>+	ret = netns_preflight();
>+	if (ret)
>+		ksft_exit_skip("network namespace creation unavailable: %s\n",
>+			       strerror(-ret));
>+	fd = open_netlink(0, &portid);
>+	if (fd < 0)
>+		ksft_exit_skip("Generic Netlink unavailable: %s\n", strerror(-fd));
>+	ret = resolve_family(fd, portid, &family);
>+	if (ret) {
>+		close(fd);
>+		ksft_exit_skip("taskstats family unavailable: %s\n", strerror(-ret));
>+	}
>+	init_netns = netns_inode();
>+	ksft_print_msg("environment: pid=%d uid=%d gid=%d CapEff=%016llx\n",
>+		       getpid(), getuid(), getgid(), read_cap_eff());
>+	ksft_print_msg("environment: netns=%llu cpu=%d family=%u portid=%u\n",
>+		       init_netns, cpu, family, portid);
>+	close(fd);
>+
>+	ksft_set_plan(4);
>+	ret = run_scenario(cpu, family, &scenario);
>+	ksft_test_result(!ret && valid_record(&scenario.admin, scenario.victim),
>+			 "init_net administrator listener receives exit record\n");
>+	ksft_test_result(!ret && !scenario.unrelated.found &&
>+			 scenario.unrelated.portid != scenario.admin.portid &&
>+			 scenario.unrelated.portid != scenario.attacker.portid,
>+			 "unregistered different port ID receives no exit record\n");
>+	ksft_test_result(!ret && !scenario.deregistered.found,
>+			 "explicitly deregistered listener receives no exit record\n");
>+	ksft_test_result(!ret &&
>+			 scenario.registrar.portid == scenario.attacker.portid &&
>+			 scenario.registrar.netns_ino != init_netns &&
>+			 scenario.attacker.netns_ino == init_netns &&
>+			 scenario.attacker.uid == ATTACKER_UID &&
>+			 scenario.attacker.gid == ATTACKER_GID &&
>+			 !scenario.attacker.cap_eff &&
>+			 scenario.attacker.query_rc == -EPERM &&
>+			 scenario.attacker.register_rc == -EPERM &&
>+			 WIFEXITED(scenario.victim_status) &&
>+			 WEXITSTATUS(scenario.victim_status) == VICTIM_EXIT_CODE &&
>+			 valid_record(&scenario.registrar, scenario.victim) &&
>+			 !scenario.attacker.found,
>+			 "exit record follows listener netns despite numeric port-ID collision\n");
>+
>+	ksft_finished();
>+	return ksft_get_fail_cnt() ? KSFT_FAIL : KSFT_PASS;
>+}
>

--- Thanks!
"I'm not a very positive person" - Linus torvalds

  reply	other threads:[~2026-10-02 16:44 UTC|newest]

Thread overview: 7+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-10-02 10:56 [PATCH v3 0/2] taskstats: fix cross-netns exit listener delivery tjdqudcks0424
2026-10-02 10:56 ` [PATCH v3 1/2] taskstats: route exit listener records through their netns tjdqudcks0424
2026-10-02 16:40   ` Bradley Morgan
2026-10-02 10:56 ` [PATCH v3 2/2] selftests: acct: test taskstats listener netns routing tjdqudcks0424
2026-10-02 16:43   ` Bradley Morgan [this message]
2026-10-03  2:24     ` tjdqudcks0424
2026-10-02 11:06 ` [PATCH v3 0/2] taskstats: fix cross-netns exit listener delivery Greg KH

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=98826F7B-9448-4585-89CA-55590AE29280@mainlining.org \
    --to=brads@mainlining.org \
    --cc=akpm@linux-foundation.org \
    --cc=bsingharora@gmail.com \
    --cc=cyyzero16@gmail.com \
    --cc=gregkh@linuxfoundation.org \
    --cc=linux-kernel@vger.kernel.org \
    --cc=linux-kselftest@vger.kernel.org \
    --cc=shuah@kernel.org \
    --cc=tjdqudcks0424@naver.com \
    --cc=xu.xin@linux.dev \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
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®