mirror of https://lore.kernel.org/lkml/
 help / color / mirror / Atom feed
* [PATCH v3 0/2] taskstats: fix cross-netns exit listener delivery
@ 2026-10-02 10:56 tjdqudcks0424
  2026-10-02 10:56 ` [PATCH v3 1/2] taskstats: route exit listener records through their netns tjdqudcks0424
                   ` (2 more replies)
  0 siblings, 3 replies; 7+ messages in thread
From: tjdqudcks0424 @ 2026-10-02 10:56 UTC (permalink / raw)
  To: bsingharora, akpm
  Cc: xu.xin, gregkh, shuah, cyyzero16, linux-kernel, linux-kselftest,
	성병찬

From: 성병찬 <tjdqudcks0424@naver.com>

CPU-mask taskstats listeners are currently identified only by a numeric
Generic Netlink port ID and exit records are sent through init_net. Since
port IDs are namespace-local, an unprivileged init_net socket can bind the
same number as a listener in another network namespace and receive that
listener's cross-UID exit record.

Patch 1 associates listener entries with the registering Generic Netlink
socket's namespace and port ID and cleans them up when that socket closes.
Patch 2 adds an acct kselftest which constructs the collision and checks the
normal listener, explicit deregistration, and unrelated-port controls.

The test was built with GCC using -Wall -Wextra and run in disposable QEMU
guests with the same userspace binary. On the unmodified baseline it reports
3 passes and 1 failure: the child-netns listener misses the victim record and
the colliding unprivileged init_net socket receives it. On the fixed kernel,
three independent clean boots each report 4 passes and no failures. No
KASAN, UBSAN, BUG, WARNING, Oops, panic, lockdep, refcount, use-after-free,
out-of-bounds, or kmemleak diagnostic was emitted during the final runs.

The exact Message-ID and lore URL of the previous public posting were not
present in the available local sent-mail or raw-mail artifacts:

Link: https://lore.kernel.org/r/20261002075842.146026-1-tjdqudcks0424@naver.com/

Changes in v3:
- remove wording that called a private draft "v1"
- add an in-tree taskstats CPU-listener network-namespace regression test
- send the fix and selftest as a two-patch series

성병찬 (2):
  taskstats: route exit listener records through their netns
  selftests: acct: test taskstats listener netns routing

 kernel/taskstats.c                            | 135 ++-
 tools/testing/selftests/acct/Makefile         |   1 +
 .../selftests/acct/taskstats_netns_listener.c | 945 ++++++++++++++++++
 3 files changed, 1037 insertions(+), 44 deletions(-)
 create mode 100644 tools/testing/selftests/acct/taskstats_netns_listener.c


base-commit: ce1e0223d8ad4211275c82a17ed6d43ab81e13d9
-- 
2.43.0

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

* [PATCH v3 1/2] taskstats: route exit listener records through their netns
  2026-10-02 10:56 [PATCH v3 0/2] taskstats: fix cross-netns exit listener delivery tjdqudcks0424
@ 2026-10-02 10:56 ` 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 11:06 ` [PATCH v3 0/2] taskstats: fix cross-netns exit listener delivery Greg KH
  2 siblings, 1 reply; 7+ messages in thread
From: tjdqudcks0424 @ 2026-10-02 10:56 UTC (permalink / raw)
  To: bsingharora, akpm
  Cc: xu.xin, gregkh, shuah, cyyzero16, linux-kernel, linux-kselftest,
	성병찬,
	stable

From: 성병찬 <tjdqudcks0424@naver.com>

Commit edc73c7261ca ("kernel: make taskstats available from all net
namespaces") made the taskstats Generic Netlink family available from all
network namespaces. CPU-mask listener registrations, however, still store
only a bare netlink port ID in a global per-CPU list and send exit records
through init_net.

Netlink port IDs are namespace-local. An administrator can register an exit
listener after unsharing only the network namespace, while an unprivileged
init_net socket binds the same numeric port ID. The latter then receives
taskstats for exiting tasks of other UIDs. It cannot register a listener or
issue a direct taskstats query.

Associate each listener with taskstats family-private storage for the exact
Generic Netlink socket. Record that socket's network namespace and port ID
and use both for unicast. On socket release, the family-private destructor
removes every listener owned by that socket. The socket pins its namespace
until the destructor returns, so no additional net reference is needed.

The per-CPU rwsem protects the listener-to-owner pointer from registration
through unicast and removal. It also makes explicit deregistration,
failed-send cleanup, and socket destruction mutually safe. Allocate a
complete multi-CPU registration batch before publishing it so an allocation
failure neither leaves a partial registration nor removes an older one.

An in-tree regression test creates the cross-network-namespace port-ID
collision. In a disposable QEMU guest with the unmodified kernel, the test
fails because the child-netns listener misses the victim's exit record and
the colliding unprivileged init_net socket receives it. The same test passes
in three independent boots with this fix: the child-netns listener receives
the record while the colliding socket does not. It also verifies the victim
identity, EPERM for the unprivileged socket's direct query and registration,
normal init_net delivery, explicit deregistration, and an unrelated port ID.

Fixes: edc73c7261ca ("kernel: make taskstats available from all net namespaces")
Cc: stable@vger.kernel.org
Link: https://lore.kernel.org/all/20110630120831.GB7707@albatros/
Link: https://lore.kernel.org/all/87v8x678ph.fsf@email.froward.int.ebiederm.org/
Assisted-by: OpenAI Codex
Signed-off-by: 성병찬 <tjdqudcks0424@naver.com>
---
 kernel/taskstats.c | 135 ++++++++++++++++++++++++++++++---------------
 1 file changed, 91 insertions(+), 44 deletions(-)

diff --git a/kernel/taskstats.c b/kernel/taskstats.c
index f31df72f0e9df..fb8cbbdd1c345 100644
--- a/kernel/taskstats.c
+++ b/kernel/taskstats.c
@@ -45,9 +45,15 @@ static const struct nla_policy cgroupstats_cmd_get_policy[] = {
 	[CGROUPSTATS_CMD_ATTR_FD] = { .type = NLA_U32 },
 };
 
+struct taskstats_sock_priv {
+	struct net *net;
+	u32 portid;
+};
+
 struct listener {
 	struct list_head list;
-	pid_t pid;
+	struct taskstats_sock_priv *owner;
+	unsigned int cpu;
 	char valid;
 };
 
@@ -128,9 +134,10 @@ static void send_cpu_listeners(struct sk_buff *skb,
 			if (!skb_next)
 				break;
 		}
-		rc = genlmsg_unicast(&init_net, skb_cur, s->pid);
+		rc = genlmsg_unicast(s->owner->net, skb_cur,
+				     s->owner->portid);
 		if (rc == -ECONNREFUSED) {
-			s->valid = 0;
+			WRITE_ONCE(s->valid, 0);
 			delcount++;
 		}
 		skb_cur = skb_next;
@@ -146,7 +153,7 @@ static void send_cpu_listeners(struct sk_buff *skb,
 	/* Delete invalidated entries */
 	down_write(&listeners->sem);
 	list_for_each_entry_safe(s, tmp, &listeners->list, list) {
-		if (!s->valid) {
+		if (!READ_ONCE(s->valid)) {
 			list_del(&s->list);
 			kfree(s);
 		}
@@ -296,63 +303,77 @@ static void fill_tgid_exit(struct task_struct *tsk)
 	return;
 }
 
-static int add_del_listener(pid_t pid, const struct cpumask *mask, int isadd)
+static int add_listener(struct taskstats_sock_priv *owner,
+			const struct cpumask *mask)
 {
+	LIST_HEAD(new_listeners);
 	struct listener_list *listeners;
 	struct listener *s, *tmp, *s2;
 	unsigned int cpu;
-	int ret = 0;
-
-	if (!cpumask_subset(mask, cpu_possible_mask))
-		return -EINVAL;
 
-	if (current_user_ns() != &init_user_ns)
-		return -EINVAL;
+	for_each_cpu(cpu, mask) {
+		s = kmalloc_node(sizeof(*s), GFP_KERNEL, cpu_to_node(cpu));
+		if (!s)
+			goto free_new;
+		s->owner = owner;
+		s->cpu = cpu;
+		s->valid = 1;
+		list_add_tail(&s->list, &new_listeners);
+	}
 
-	if (task_active_pid_ns(current) != &init_pid_ns)
-		return -EINVAL;
+	list_for_each_entry_safe(s, tmp, &new_listeners, list) {
+		bool exists = false;
 
-	if (isadd == REGISTER) {
-		for_each_cpu(cpu, mask) {
-			s = kmalloc_node(sizeof(struct listener),
-					GFP_KERNEL, cpu_to_node(cpu));
-			if (!s) {
-				ret = -ENOMEM;
-				goto cleanup;
-			}
-			s->pid = pid;
-			s->valid = 1;
-
-			listeners = &per_cpu(listener_array, cpu);
-			down_write(&listeners->sem);
-			list_for_each_entry(s2, &listeners->list, list) {
-				if (s2->pid == pid && s2->valid)
-					goto exists;
+		list_del(&s->list);
+		listeners = &per_cpu(listener_array, s->cpu);
+		down_write(&listeners->sem);
+		list_for_each_entry(s2, &listeners->list, list) {
+			if (s2->owner == owner && READ_ONCE(s2->valid)) {
+				exists = true;
+				break;
 			}
-			list_add(&s->list, &listeners->list);
-			s = NULL;
-exists:
-			up_write(&listeners->sem);
-			kfree(s); /* nop if NULL */
 		}
-		return 0;
+		if (!exists)
+			list_add(&s->list, &listeners->list);
+		up_write(&listeners->sem);
+		if (exists)
+			kfree(s);
 	}
+	return 0;
+
+free_new:
+	list_for_each_entry_safe(s, tmp, &new_listeners, list) {
+		list_del(&s->list);
+		kfree(s);
+	}
+	return -ENOMEM;
+}
+
+static void remove_listener(struct taskstats_sock_priv *owner,
+			    const struct cpumask *mask)
+{
+	struct listener_list *listeners;
+	struct listener *s, *tmp;
+	unsigned int cpu;
 
-	/* Deregister or cleanup */
-cleanup:
 	for_each_cpu(cpu, mask) {
 		listeners = &per_cpu(listener_array, cpu);
 		down_write(&listeners->sem);
 		list_for_each_entry_safe(s, tmp, &listeners->list, list) {
-			if (s->pid == pid) {
+			if (s->owner == owner) {
 				list_del(&s->list);
 				kfree(s);
-				break;
 			}
 		}
 		up_write(&listeners->sem);
 	}
-	return ret;
+}
+
+static void taskstats_sock_priv_destroy(void *data)
+{
+	struct taskstats_sock_priv *owner = data;
+
+	remove_listener(owner, cpu_possible_mask);
 }
 
 static int parse(struct nlattr *na, struct cpumask *mask)
@@ -448,10 +469,12 @@ static int cgroupstats_user_cmd(struct sk_buff *skb, struct genl_info *info)
 	return send_reply(rep_skb, info);
 }
 
-static int cmd_attr_cpumask(struct genl_info *info, int attr,
+static int cmd_attr_cpumask(struct sk_buff *skb, struct genl_info *info,
+			    int attr,
 			    enum actions action)
 {
 	cpumask_var_t mask __free(free_cpumask_var) = CPUMASK_VAR_NULL;
+	struct taskstats_sock_priv *owner;
 	int rc;
 
 	if (!alloc_cpumask_var(&mask, GFP_KERNEL))
@@ -459,7 +482,29 @@ static int cmd_attr_cpumask(struct genl_info *info, int attr,
 	rc = parse(info->attrs[attr], mask);
 	if (rc < 0)
 		return rc;
-	return add_del_listener(info->snd_portid, mask, action);
+	if (!cpumask_subset(mask, cpu_possible_mask))
+		return -EINVAL;
+	if (current_user_ns() != &init_user_ns)
+		return -EINVAL;
+	if (task_active_pid_ns(current) != &init_pid_ns)
+		return -EINVAL;
+
+	owner = genl_sk_priv_get(&family, NETLINK_CB(skb).sk);
+	if (IS_ERR(owner))
+		return PTR_ERR(owner);
+	if (!owner->net) {
+		owner->net = genl_info_net(info);
+		owner->portid = info->snd_portid;
+	} else if (WARN_ON_ONCE(!net_eq(owner->net, genl_info_net(info)) ||
+				owner->portid != info->snd_portid)) {
+		return -EINVAL;
+	}
+
+	if (action == REGISTER)
+		return add_listener(owner, mask);
+
+	remove_listener(owner, mask);
+	return 0;
 }
 
 static size_t taskstats_packet_size(void)
@@ -534,11 +579,11 @@ static int cmd_attr_tgid(struct genl_info *info)
 static int taskstats_user_cmd(struct sk_buff *skb, struct genl_info *info)
 {
 	if (info->attrs[TASKSTATS_CMD_ATTR_REGISTER_CPUMASK])
-		return cmd_attr_cpumask(info,
+		return cmd_attr_cpumask(skb, info,
 					TASKSTATS_CMD_ATTR_REGISTER_CPUMASK,
 					REGISTER);
 	else if (info->attrs[TASKSTATS_CMD_ATTR_DEREGISTER_CPUMASK])
-		return cmd_attr_cpumask(info,
+		return cmd_attr_cpumask(skb, info,
 					TASKSTATS_CMD_ATTR_DEREGISTER_CPUMASK,
 					DEREGISTER);
 	else if (info->attrs[TASKSTATS_CMD_ATTR_PID])
@@ -671,6 +716,8 @@ static struct genl_family family __ro_after_init = {
 	.n_ops		= ARRAY_SIZE(taskstats_ops),
 	.resv_start_op	= CGROUPSTATS_CMD_GET + 1,
 	.netnsok	= true,
+	.sock_priv_size	= sizeof(struct taskstats_sock_priv),
+	.sock_priv_destroy = taskstats_sock_priv_destroy,
 };
 
 /* Needed early in initialization */
-- 
2.43.0


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

* [PATCH v3 2/2] selftests: acct: test taskstats listener netns routing
  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 10:56 ` tjdqudcks0424
  2026-10-02 16:43   ` Bradley Morgan
  2026-10-02 11:06 ` [PATCH v3 0/2] taskstats: fix cross-netns exit listener delivery Greg KH
  2 siblings, 1 reply; 7+ messages in thread
From: tjdqudcks0424 @ 2026-10-02 10:56 UTC (permalink / raw)
  To: bsingharora, akpm
  Cc: xu.xin, gregkh, shuah, cyyzero16, linux-kernel, linux-kselftest,
	성병찬

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.

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;
+}
-- 
2.43.0


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

* Re: [PATCH v3 0/2] taskstats: fix cross-netns exit listener delivery
  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 10:56 ` [PATCH v3 2/2] selftests: acct: test taskstats listener netns routing tjdqudcks0424
@ 2026-10-02 11:06 ` Greg KH
  2 siblings, 0 replies; 7+ messages in thread
From: Greg KH @ 2026-10-02 11:06 UTC (permalink / raw)
  To: tjdqudcks0424
  Cc: bsingharora, akpm, xu.xin, shuah, cyyzero16, linux-kernel,
	linux-kselftest

On Fri, Oct 02, 2026 at 07:56:12PM +0900, tjdqudcks0424@naver.com wrote:
> From: 성병찬 <tjdqudcks0424@naver.com>
> 
> CPU-mask taskstats listeners are currently identified only by a numeric
> Generic Netlink port ID and exit records are sent through init_net. Since
> port IDs are namespace-local, an unprivileged init_net socket can bind the
> same number as a listener in another network namespace and receive that
> listener's cross-UID exit record.
> 
> Patch 1 associates listener entries with the registering Generic Netlink
> socket's namespace and port ID and cleans them up when that socket closes.
> Patch 2 adds an acct kselftest which constructs the collision and checks the
> normal listener, explicit deregistration, and unrelated-port controls.
> 
> The test was built with GCC using -Wall -Wextra and run in disposable QEMU
> guests with the same userspace binary. On the unmodified baseline it reports
> 3 passes and 1 failure: the child-netns listener misses the victim record and
> the colliding unprivileged init_net socket receives it. On the fixed kernel,
> three independent clean boots each report 4 passes and no failures. No
> KASAN, UBSAN, BUG, WARNING, Oops, panic, lockdep, refcount, use-after-free,
> out-of-bounds, or kmemleak diagnostic was emitted during the final runs.
> 
> The exact Message-ID and lore URL of the previous public posting were not
> present in the available local sent-mail or raw-mail artifacts:
> 
> Link: https://lore.kernel.org/r/20261002075842.146026-1-tjdqudcks0424@naver.com/

Please slow down, there's no rush here and you aren't giving people a
chance to test or review any of this.

thanks,

greg k-h

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

* Re: [PATCH v3 1/2] taskstats: route exit listener records through their netns
  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
  0 siblings, 0 replies; 7+ messages in thread
From: Bradley Morgan @ 2026-10-02 16:40 UTC (permalink / raw)
  To: tjdqudcks0424
  Cc: akpm, bsingharora, cyyzero16, gregkh, linux-kernel,
	linux-kselftest, shuah, stable, xu.xin

On 2 October 2026 11:56:13 BST, tjdqudcks0424@naver.com wrote:
>From: 성병찬 <tjdqudcks0424@naver.com>
>
>Commit edc73c7261ca ("kernel: make taskstats available from all net
>namespaces") made the taskstats Generic Netlink family available from all
>network namespaces. CPU-mask listener registrations, however, still store
>only a bare netlink port ID in a global per-CPU list and send exit records
>through init_net.
>
>Netlink port IDs are namespace-local. An administrator can register an
>exit
>listener after unsharing only the network namespace, while an unprivileged
>init_net socket binds the same numeric port ID. The latter then receives
>taskstats for exiting tasks of other UIDs. It cannot register a listener
>or
>issue a direct taskstats query.
>
>Associate each listener with taskstats family-private storage for the
>exact
>Generic Netlink socket. Record that socket's network namespace and port ID
>and use both for unicast. On socket release, the family-private destructor
>removes every listener owned by that socket. The socket pins its namespace
>until the destructor returns, so no additional net reference is needed.
>
>The per-CPU rwsem protects the listener-to-owner pointer from registration
>through unicast and removal. It also makes explicit deregistration,
>failed-send cleanup, and socket destruction mutually safe. Allocate a
>complete multi-CPU registration batch before publishing it so an
>allocation
>failure neither leaves a partial registration nor removes an older one.
>
>An in-tree regression test creates the cross-network-namespace port-ID
>collision. In a disposable QEMU guest with the unmodified kernel, the test
>fails because the child-netns listener misses the victim's exit record and
>the colliding unprivileged init_net socket receives it. The same test
>passes
>in three independent boots with this fix: the child-netns listener
>receives
>the record while the colliding socket does not. It also verifies the
>victim
>identity, EPERM for the unprivileged socket's direct query and
>registration,
>normal init_net delivery, explicit deregistration, and an unrelated port
>ID.
>
>Fixes: edc73c7261ca ("kernel: make taskstats available from all net namespaces")
>Cc: stable@vger.kernel.org
>Link: https://lore.kernel.org/all/20110630120831.GB7707@albatros/
>Link: https://lore.kernel.org/all/87v8x678ph.fsf@email.froward.int.ebiederm.org/
>Assisted-by: OpenAI Codex
>Signed-off-by: 성병찬 <tjdqudcks0424@naver.com>
>---
> kernel/taskstats.c | 135 ++++++++++++++++++++++++++++++---------------
> 1 file changed, 91 insertions(+), 44 deletions(-)
>
>diff --git a/kernel/taskstats.c b/kernel/taskstats.c
>index f31df72f0e9df..fb8cbbdd1c345 100644
>--- a/kernel/taskstats.c
>+++ b/kernel/taskstats.c
>@@ -45,9 +45,15 @@ static const struct nla_policy cgroupstats_cmd_get_policy[] = {
> 	[CGROUPSTATS_CMD_ATTR_FD] = { .type = NLA_U32 },
> };
> 
>+struct taskstats_sock_priv {
>+	struct net *net;
>+	u32 portid;
>+};
>+

Comment? ^


> struct listener {
> 	struct list_head list;
>-	pid_t pid;
>+	struct taskstats_sock_priv *owner;
>+	unsigned int cpu;
> 	char valid;
> };
> 
>@@ -128,9 +134,10 @@ static void send_cpu_listeners(struct sk_buff *skb,
> 			if (!skb_next)
> 				break;
> 		}
>-		rc = genlmsg_unicast(&init_net, skb_cur, s->pid);
>+		rc = genlmsg_unicast(s->owner->net, skb_cur,
>+				     s->owner->portid);
> 		if (rc == -ECONNREFUSED) {
>-			s->valid = 0;
>+			WRITE_ONCE(s->valid, 0);
> 			delcount++;
> 		}
> 		skb_cur = skb_next;
>@@ -146,7 +153,7 @@ static void send_cpu_listeners(struct sk_buff *skb,
> 	/* Delete invalidated entries */
> 	down_write(&listeners->sem);
> 	list_for_each_entry_safe(s, tmp, &listeners->list, list) {
>-		if (!s->valid) {
>+		if (!READ_ONCE(s->valid)) {
> 			list_del(&s->list);
> 			kfree(s);
> 		}
>@@ -296,63 +303,77 @@ static void fill_tgid_exit(struct task_struct *tsk)
> 	return;
> }
> 
>-static int add_del_listener(pid_t pid, const struct cpumask *mask, int isadd)
>+static int add_listener(struct taskstats_sock_priv *owner,
>+			const struct cpumask *mask)
> {
>+	LIST_HEAD(new_listeners);
> 	struct listener_list *listeners;
> 	struct listener *s, *tmp, *s2;
> 	unsigned int cpu;
>-	int ret = 0;
>-
>-	if (!cpumask_subset(mask, cpu_possible_mask))
>-		return -EINVAL;
> 
>-	if (current_user_ns() != &init_user_ns)
>-		return -EINVAL;
>+	for_each_cpu(cpu, mask) {
>+		s = kmalloc_node(sizeof(*s), GFP_KERNEL, cpu_to_node(cpu));
>+		if (!s)
>+			goto free_new;
>+		s->owner = owner;
>+		s->cpu = cpu;
>+		s->valid = 1;
>+		list_add_tail(&s->list, &new_listeners);
>+	}
> 
>-	if (task_active_pid_ns(current) != &init_pid_ns)
>-		return -EINVAL;
>+	list_for_each_entry_safe(s, tmp, &new_listeners, list) {
>+		bool exists = false;
> 
>-	if (isadd == REGISTER) {
>-		for_each_cpu(cpu, mask) {
>-			s = kmalloc_node(sizeof(struct listener),
>-					GFP_KERNEL, cpu_to_node(cpu));
>-			if (!s) {
>-				ret = -ENOMEM;
>-				goto cleanup;
>-			}
>-			s->pid = pid;
>-			s->valid = 1;
>-
>-			listeners = &per_cpu(listener_array, cpu);
>-			down_write(&listeners->sem);
>-			list_for_each_entry(s2, &listeners->list, list) {
>-				if (s2->pid == pid && s2->valid)
>-					goto exists;
>+		list_del(&s->list);
>+		listeners = &per_cpu(listener_array, s->cpu);
>+		down_write(&listeners->sem);
>+		list_for_each_entry(s2, &listeners->list, list) {
>+			if (s2->owner == owner && READ_ONCE(s2->valid)) {
>+				exists = true;
>+				break;
> 			}
>-			list_add(&s->list, &listeners->list);
>-			s = NULL;
>-exists:
>-			up_write(&listeners->sem);
>-			kfree(s); /* nop if NULL */
> 		}
>-		return 0;
>+		if (!exists)
>+			list_add(&s->list, &listeners->list);
>+		up_write(&listeners->sem);
>+		if (exists)
>+			kfree(s);
> 	}
>+	return 0;
>+
>+free_new:
>+	list_for_each_entry_safe(s, tmp, &new_listeners, list) {
>+		list_del(&s->list);
>+		kfree(s);
>+	}
>+	return -ENOMEM;
>+}
>+
>+static void remove_listener(struct taskstats_sock_priv *owner,
>+			    const struct cpumask *mask)
>+{
>+	struct listener_list *listeners;
>+	struct listener *s, *tmp;
>+	unsigned int cpu;
> 
>-	/* Deregister or cleanup */
>-cleanup:
> 	for_each_cpu(cpu, mask) {
> 		listeners = &per_cpu(listener_array, cpu);
> 		down_write(&listeners->sem);
> 		list_for_each_entry_safe(s, tmp, &listeners->list, list) {
>-			if (s->pid == pid) {
>+			if (s->owner == owner) {
> 				list_del(&s->list);
> 				kfree(s);
>-				break;
> 			}
> 		}
> 		up_write(&listeners->sem);
> 	}
>-	return ret;
>+}
>+

Nit: this is an O(nr_cpu_ids) write lock sweep
per socket close. Fine for taskstats, listeners are rare, but maybe
worth a comment saying you know?????? ↓

>+static void taskstats_sock_priv_destroy(void *data)
>+{
>+	struct taskstats_sock_priv *owner = data;
>+
>+	remove_listener(owner, cpu_possible_mask);
> }
> 
> static int parse(struct nlattr *na, struct cpumask *mask)
>@@ -448,10 +469,12 @@ static int cgroupstats_user_cmd(struct sk_buff *skb, struct genl_info *info)
> 	return send_reply(rep_skb, info);
> }
> 
>-static int cmd_attr_cpumask(struct genl_info *info, int attr,
>+static int cmd_attr_cpumask(struct sk_buff *skb, struct genl_info *info,
>+			    int attr,
> 			    enum actions action)
> {
> 	cpumask_var_t mask __free(free_cpumask_var) = CPUMASK_VAR_NULL;
>+	struct taskstats_sock_priv *owner;
> 	int rc;
> 
> 	if (!alloc_cpumask_var(&mask, GFP_KERNEL))
>@@ -459,7 +482,29 @@ static int cmd_attr_cpumask(struct genl_info *info, int attr,
> 	rc = parse(info->attrs[attr], mask);
> 	if (rc < 0)
> 		return rc;
>-	return add_del_listener(info->snd_portid, mask, action);
>+	if (!cpumask_subset(mask, cpu_possible_mask))
>+		return -EINVAL;
>+	if (current_user_ns() != &init_user_ns)
>+		return -EINVAL;
>+	if (task_active_pid_ns(current) != &init_pid_ns)
>+		return -EINVAL;
>+
>+	owner = genl_sk_priv_get(&family, NETLINK_CB(skb).sk);
>+	if (IS_ERR(owner))
>+		return PTR_ERR(owner);
>+	if (!owner->net) {
>+		owner->net = genl_info_net(info);
>+		owner->portid = info->snd_portid;
>+	} else if (WARN_ON_ONCE(!net_eq(owner->net, genl_info_net(info)) ||
>+				owner->portid != info->snd_portid)) {
>+		return -EINVAL;
>+	}
>+
>+	if (action == REGISTER)
>+		return add_listener(owner, mask);
>+
>+	remove_listener(owner, mask);
>+	return 0;
> }
> 
> static size_t taskstats_packet_size(void)
>@@ -534,11 +579,11 @@ static int cmd_attr_tgid(struct genl_info *info)
> static int taskstats_user_cmd(struct sk_buff *skb, struct genl_info
> *info)
> {
> 	if (info->attrs[TASKSTATS_CMD_ATTR_REGISTER_CPUMASK])
>-		return cmd_attr_cpumask(info,
>+		return cmd_attr_cpumask(skb, info,
> 					TASKSTATS_CMD_ATTR_REGISTER_CPUMASK,
> 					REGISTER);
> 	else if (info->attrs[TASKSTATS_CMD_ATTR_DEREGISTER_CPUMASK])
>-		return cmd_attr_cpumask(info,
>+		return cmd_attr_cpumask(skb, info,
> 					TASKSTATS_CMD_ATTR_DEREGISTER_CPUMASK,
> 					DEREGISTER);
> 	else if (info->attrs[TASKSTATS_CMD_ATTR_PID])
>@@ -671,6 +716,8 @@ static struct genl_family family __ro_after_init = {
> 	.n_ops		= ARRAY_SIZE(taskstats_ops),
> 	.resv_start_op	= CGROUPSTATS_CMD_GET + 1,
> 	.netnsok	= true,
>+	.sock_priv_size	= sizeof(struct taskstats_sock_priv),
>+	.sock_priv_destroy = taskstats_sock_priv_destroy,

Fine.

> };
> 
> /* Needed early in initialization */
>

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

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

* Re: [PATCH v3 2/2] selftests: acct: test taskstats listener netns routing
  2026-10-02 10:56 ` [PATCH v3 2/2] selftests: acct: test taskstats listener netns routing tjdqudcks0424
@ 2026-10-02 16:43   ` Bradley Morgan
  2026-10-03  2:24     ` tjdqudcks0424
  0 siblings, 1 reply; 7+ messages in thread
From: Bradley Morgan @ 2026-10-02 16:43 UTC (permalink / raw)
  To: tjdqudcks0424
  Cc: akpm, bsingharora, cyyzero16, gregkh, linux-kernel,
	linux-kselftest, shuah, xu.xin

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

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

* Re: [PATCH v3 2/2] selftests: acct: test taskstats listener netns routing
  2026-10-02 16:43   ` Bradley Morgan
@ 2026-10-03  2:24     ` tjdqudcks0424
  0 siblings, 0 replies; 7+ messages in thread
From: tjdqudcks0424 @ 2026-10-03  2:24 UTC (permalink / raw)
  To: brads
  Cc: akpm, bsingharora, cyyzero16, gregkh, linux-kernel,
	linux-kselftest, shuah, xu.xin

From: 성병찬 <tjdqudcks0424@naver.com>

Hi Bradley,

Thank you for testing the series and for the detailed feedback.

I reproduced the repeated-run problem and found that the test was rejecting
valid unsolicited exit notifications after the first one.  The kernel uses
an increasing per-CPU nlmsg_seq for these notifications; run 1 used sequence
zero, while later runs used nonzero sequences that receive_victim() silently
discarded.  The normal init_net listener therefore appeared silent for the
same userspace reason even though unicast succeeded.

The test now distinguishes matched, unmatched, expected-no-record, poll
timeout, deadline expiry, malformed, receive-error, socket-error, and
suspected-drop outcomes.  It reports receive counters and per-socket
/proc/net/netlink diagnostics, including Drops.  I also made registration,
victim exit, deregistration, close, and reap ordering explicit, added bounded
cooperative cleanup, and made closed-pipe cleanup SIGPIPE-safe.

I ran the same test binary 20 times back-to-back on one vulnerable boot.
Every run delivered the exact victim record to the colliding unprivileged
init_net socket instead of the child-netns registrar, while the normal
administrator and negative controls behaved as expected.  I then ran it 20
times back-to-back on each of two fixed boots; all 40 runs passed.  After
four induced failure modes, the post-failure recovery test passed 10/10.

I also repeated the validation on current mainline: the vulnerable kernel
reproduced 10/10, two fixed boots passed 20/20 each, and post-failure
recovery passed 10/10.

I added comments documenting the socket-private namespace lifetime and the
intentional all-CPU listener sweep during socket destruction.

I will send v4 as a separate series after allowing time for review.

Thanks,
Sung Byeongchan

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

end of thread, other threads:[~2026-10-03  2:24 UTC | newest]

Thread overview: 7+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
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
2026-10-03  2:24     ` tjdqudcks0424
2026-10-02 11:06 ` [PATCH v3 0/2] taskstats: fix cross-netns exit listener delivery Greg KH

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®