From mboxrd@z Thu Jan 1 00:00:00 1970 Received: from mail.mainlining.org (mail.mainlining.org [5.75.144.95]) (using TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits)) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id 8BB12344D91; Fri, 2 Oct 2026 16:44:04 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=5.75.144.95 ARC-Seal:i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1790959451; cv=none; b=Dz/YEcW0pT1WTtYzXvPzFK0stpOQen6Dx3bLD91Y7i+8y3i7f3NU3QASsJy34XIA1/Sw2lApcIwuooHR80yeTBlcmNu2W4X29QPC0Wt5g0plsVdcvgG8MBKIP1boLz70/ADBFCQ08mfXfdvW5zC7sf8FKR9GbeJIZHLy6cemZ/w= ARC-Message-Signature:i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1790959451; c=relaxed/simple; bh=+UQ47d7J+VPRjYVJd9jC6CaHnQvZ+ZzxMu0VNidn6rs=; h=Date:From:To:CC:Subject:In-Reply-To:Message-ID:MIME-Version: Content-Type; b=gBoQsrySy56l9CXCaKDw4fGLF9BVRt/aV8gYr8We9bGxx0KmgbFdkvi5qAvMea8TSJ/FwZHXgNWgqSH2oYtp3mTj3mEVMzbEeSSL/a4/ZORyNZrwVfVsiYou3vDd7DnOaytexNGjxlQUIThVJYsRN375cBmCT6XFVQjNlhtWals= ARC-Authentication-Results:i=1; smtp.subspace.kernel.org; dmarc=pass (p=reject dis=none) header.from=mainlining.org; spf=pass smtp.mailfrom=mainlining.org; dkim=pass (2048-bit key) header.d=mainlining.org header.i=@mainlining.org header.b=G1D0teKj; dkim=permerror (0-bit key) header.d=mainlining.org header.i=@mainlining.org header.b=DJRcTvwv; arc=none smtp.client-ip=5.75.144.95 Authentication-Results: smtp.subspace.kernel.org; dmarc=pass (p=reject dis=none) header.from=mainlining.org Authentication-Results: smtp.subspace.kernel.org; spf=pass smtp.mailfrom=mainlining.org Authentication-Results: smtp.subspace.kernel.org; dkim=pass (2048-bit key) header.d=mainlining.org header.i=@mainlining.org header.b="G1D0teKj"; dkim=permerror (0-bit key) header.d=mainlining.org header.i=@mainlining.org header.b="DJRcTvwv" DKIM-Signature: v=1; a=rsa-sha256; s=202507r; d=mainlining.org; c=relaxed/relaxed; h=Message-ID:Subject:To:From:Date; t=1790959437; bh=etetle437uDPJFtXf6Crsac BGu54oPpp/fVF7uAvzs4=; b=G1D0teKjubcxOI9oO90JeW+bhDsjPILYOF+Y1VF/4XFBh06yGg HY+IBKFWar7SttVdNa/pfpH0yfjfQG/EsRcnuiuUSsM5jivQPVX5rp/l/teXvvNGL33A+hF5pQU CgJA1LUtUwTv/Xg4mf5OiC0Z+0vR7xV5w28ElINBMX7BflzdUCH94cFh/psJZXsUwDIX17pkJUF oHl0J25M7tOxCxvWiPvyTSYX1DHD9p52NvaGAhzgXixnp45Fiw2le/kKwBaUeBcvPEDgkGhoEcu Soa27Vw3dK1oNTpyT9mmagFmkurk8uqZ3usiGxylY4Fl62QiSxlWdXkQIoDzTof/XMg==; DKIM-Signature: v=1; a=ed25519-sha256; s=202507e; d=mainlining.org; c=relaxed/relaxed; h=Message-ID:Subject:To:From:Date; t=1790959437; bh=etetle437uDPJFtXf6Crsac BGu54oPpp/fVF7uAvzs4=; b=DJRcTvwv5gmjynblYNcI8sW/RKx24XmFBkS6CKjmjEi021LzUY i63afjFdDRN6osvg4kl2RrbX5GT+k0eyIkAw==; Date: Fri, 02 Oct 2026 17:43:57 +0100 From: Bradley Morgan 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: =?US-ASCII?Q?Re=3A_=5BPATCH_v3_2/2=5D_selftests=3A_acct=3A_?= =?US-ASCII?Q?test_taskstats_listener_netns_routing?= In-Reply-To: <20261002105614.195370-3-tjdqudcks0424@naver.com> Message-ID: <98826F7B-9448-4585-89CA-55590AE29280@mainlining.org> Precedence: bulk X-Mailing-List: linux-kernel@vger.kernel.org List-Id: List-Subscribe: List-Unsubscribe: MIME-Version: 1.0 Content-Type: text/plain; charset=utf-8 Content-Transfer-Encoding: 8bit On 2 October 2026 11:56:14 BST, tjdqudcks0424@naver.com wrote: >From: 성병찬 > >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: 성병찬 >--- > 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 >+#include >+#include >+#include >+#include >+#include >+#include >+#include >+#include >+#include >+#include >+#include >+#include >+#include >+#include >+#include >+#include >+#include >+#include >+#include >+#include >+#include >+#include >+ >+#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