[PATCH RESEND v2 13/13] selftests/proc: check that removing a polled entry wakes its pollers

Christian Brauner brauner at kernel.org
Wed Nov 4 13:14:20 PST 2026


Poll the channel file of a sunrpc cache in /proc/net of a network
namespace of its own and let the namespace die while the poller sleeps:

- the poller wakes up and gets POLLERR | POLLPRI

- so does an epoll with an item on the entry

- a poll of the removed entry keeps reporting POLLERR, and a read of it
  fails with -EIO

The namespace is torn down from a workqueue, so a regression stalls the
test for ten seconds before it fails.

Signed-off-by: Christian Brauner (Amutable) <brauner at kernel.org>
---
 tools/testing/selftests/proc/.gitignore           |   1 +
 tools/testing/selftests/proc/Makefile             |   1 +
 tools/testing/selftests/proc/config               |   2 +
 tools/testing/selftests/proc/proc-remove-polled.c | 189 ++++++++++++++++++++++
 4 files changed, 193 insertions(+)

diff --git a/tools/testing/selftests/proc/.gitignore b/tools/testing/selftests/proc/.gitignore
index ed863db1688f..d06bb704d29d 100644
--- a/tools/testing/selftests/proc/.gitignore
+++ b/tools/testing/selftests/proc/.gitignore
@@ -31,5 +31,6 @@
 /self
 /setns-dcache
 /setns-sysvipc
+/proc-remove-polled
 /proc-sysctl-readdir-churn
 /thread-self
diff --git a/tools/testing/selftests/proc/Makefile b/tools/testing/selftests/proc/Makefile
index 95c9212fb670..e4f5142da47d 100644
--- a/tools/testing/selftests/proc/Makefile
+++ b/tools/testing/selftests/proc/Makefile
@@ -30,6 +30,7 @@ TEST_GEN_PROGS += setns-sysvipc
 TEST_GEN_PROGS += thread-self
 TEST_GEN_PROGS += proc-multiple-procfs
 TEST_GEN_PROGS += proc-fsconfig-hidepid
+TEST_GEN_PROGS += proc-remove-polled
 TEST_GEN_PROGS += proc-sysctl-readdir-churn
 TEST_GEN_PROGS += proc-pidns
 TEST_GEN_PROGS += proc-remove-slow-sysctl
diff --git a/tools/testing/selftests/proc/config b/tools/testing/selftests/proc/config
index 15b307189392..3cc12e91e5a7 100644
--- a/tools/testing/selftests/proc/config
+++ b/tools/testing/selftests/proc/config
@@ -6,6 +6,8 @@ CONFIG_NET_NS=y
 CONFIG_IPV6=y
 CONFIG_TUN=y
 CONFIG_USERFAULTFD=y
+CONFIG_USER_NS=y
+CONFIG_SUNRPC=y
 CONFIG_SOUND=y
 CONFIG_SND=y
 CONFIG_SND_DUMMY=m
diff --git a/tools/testing/selftests/proc/proc-remove-polled.c b/tools/testing/selftests/proc/proc-remove-polled.c
new file mode 100644
index 000000000000..e313d22be43f
--- /dev/null
+++ b/tools/testing/selftests/proc/proc-remove-polled.c
@@ -0,0 +1,189 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * A poll() or epoll on a /proc entry must wake up when the entry is removed,
+ * and report the removed entry as an error. The channel file of a sunrpc
+ * cache lives in /proc/net of a network namespace and goes away with it.
+ */
+#define _GNU_SOURCE
+#include <errno.h>
+#include <fcntl.h>
+#include <poll.h>
+#include <pthread.h>
+#include <sched.h>
+#include <stdarg.h>
+#include <stdio.h>
+#include <string.h>
+#include <sys/epoll.h>
+#include <sys/wait.h>
+#include <unistd.h>
+
+#include "../kselftest_harness.h"
+
+#define CHANNEL_FMT	"/proc/%d/net/rpc/auth.unix.ip/channel"
+#define TIMEOUT_MS	10000
+
+struct netns_child {
+	pid_t pid;
+	int go;		/* closing it lets the child exit, and its netns die */
+};
+
+struct poller {
+	int fd;
+	int done;
+	int ret;
+	short revents;
+};
+
+static int write_file(const char *path, const char *fmt, ...)
+{
+	char buf[64];
+	va_list ap;
+	int fd, len, ret;
+
+	va_start(ap, fmt);
+	len = vsnprintf(buf, sizeof(buf), fmt, ap);
+	va_end(ap);
+	fd = open(path, O_WRONLY | O_CLOEXEC);
+	if (fd < 0)
+		return -1;
+	ret = write(fd, buf, len) == len ? 0 : -1;
+	close(fd);
+	return ret;
+}
+
+/*
+ * A child in a network namespace of its own, owned by a user namespace that
+ * maps us to root, so that we may open the files in its /proc/net. The child
+ * is the only thing that keeps the namespace alive.
+ */
+static int start_netns_child(struct netns_child *c)
+{
+	uid_t uid = getuid();
+	gid_t gid = getgid();
+	int ready[2], go[2];
+	char ch = 'f';
+
+	if (pipe2(ready, O_CLOEXEC) || pipe2(go, O_CLOEXEC))
+		return -1;
+	c->pid = fork();
+	if (c->pid < 0)
+		return -1;
+	if (c->pid == 0) {
+		close(ready[0]);
+		close(go[1]);
+		if (!unshare(CLONE_NEWUSER) &&
+		    !write_file("/proc/self/setgroups", "deny") &&
+		    !write_file("/proc/self/uid_map", "0 %d 1", uid) &&
+		    !write_file("/proc/self/gid_map", "0 %d 1", gid) &&
+		    !unshare(CLONE_NEWNET))
+			ch = 'r';
+		if (write(ready[1], &ch, 1) == 1 && ch == 'r')
+			(void)!read(go[0], &ch, 1);
+		_exit(0);
+	}
+	close(ready[1]);
+	close(go[0]);
+	c->go = go[1];
+	if (read(ready[0], &ch, 1) != 1 || ch != 'r') {
+		close(ready[0]);
+		close(c->go);
+		waitpid(c->pid, NULL, 0);
+		errno = EPERM;
+		return -1;
+	}
+	close(ready[0]);
+	return 0;
+}
+
+static void end_netns_child(struct netns_child *c)
+{
+	close(c->go);
+	waitpid(c->pid, NULL, 0);
+}
+
+static int open_channel(struct netns_child *c, char *path, size_t len)
+{
+	snprintf(path, len, CHANNEL_FMT, c->pid);
+	return open(path, O_RDONLY | O_CLOEXEC);
+}
+
+static void *poll_thread(void *arg)
+{
+	struct poller *p = arg;
+	struct pollfd pfd = { .fd = p->fd, .events = POLLPRI };
+	ssize_t ret;
+
+	p->ret = poll(&pfd, 1, TIMEOUT_MS);
+	p->revents = pfd.revents;
+	ret = write(p->done, "x", 1);
+	return (void *)ret;
+}
+
+TEST(poll_wakes_on_removal)
+{
+	struct netns_child c;
+	struct poller p = {};
+	struct pollfd pfd;
+	char path[64], buf[16];
+	pthread_t pt;
+	int done[2];
+
+	if (start_netns_child(&c))
+		SKIP(return, "network namespace: %s", strerror(errno));
+	p.fd = open_channel(&c, path, sizeof(path));
+	if (p.fd < 0)
+		SKIP(return, "%s: %s", path, strerror(errno));
+
+	/* A fresh channel has nothing to read and no error. */
+	pfd = (struct pollfd){ .fd = p.fd, .events = POLLIN | POLLPRI };
+	ASSERT_EQ(poll(&pfd, 1, 0), 0);
+
+	ASSERT_EQ(pipe2(done, O_CLOEXEC), 0);
+	p.done = done[1];
+	ASSERT_EQ(pthread_create(&pt, NULL, poll_thread, &p), 0);
+
+	/* The poller sleeps on the entry, now its namespace goes away. */
+	end_netns_child(&c);
+	pfd = (struct pollfd){ .fd = done[0], .events = POLLIN };
+	EXPECT_EQ(poll(&pfd, 1, TIMEOUT_MS), 1) {
+		TH_LOG("removing %s didn't wake its poller", path);
+	}
+	ASSERT_EQ(pthread_join(pt, NULL), 0);
+	EXPECT_EQ(p.ret, 1);
+	EXPECT_EQ(p.revents & (POLLERR | POLLPRI), POLLERR | POLLPRI);
+
+	/* The removed entry stays an error, and there is nothing to read. */
+	pfd = (struct pollfd){ .fd = p.fd, .events = POLLIN };
+	EXPECT_EQ(poll(&pfd, 1, 0), 1);
+	EXPECT_TRUE(pfd.revents & POLLERR);
+	EXPECT_EQ(read(p.fd, buf, sizeof(buf)), -1);
+	EXPECT_EQ(errno, EIO);
+}
+
+TEST(epoll_wakes_on_removal)
+{
+	struct epoll_event ev = { .events = EPOLLPRI };
+	struct netns_child c;
+	char path[64];
+	int fd, epfd;
+
+	if (start_netns_child(&c))
+		SKIP(return, "network namespace: %s", strerror(errno));
+	fd = open_channel(&c, path, sizeof(path));
+	if (fd < 0)
+		SKIP(return, "%s: %s", path, strerror(errno));
+
+	epfd = epoll_create1(EPOLL_CLOEXEC);
+	ASSERT_GE(epfd, 0);
+	ASSERT_EQ(epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &ev), 0);
+	ASSERT_EQ(epoll_wait(epfd, &ev, 1, 0), 0);
+
+	/* The item sits on the entry's queue, now its namespace goes away. */
+	end_netns_child(&c);
+	EXPECT_EQ(epoll_wait(epfd, &ev, 1, TIMEOUT_MS), 1) {
+		TH_LOG("removing %s didn't wake epoll", path);
+	}
+	EXPECT_EQ(ev.events & (EPOLLERR | EPOLLPRI), EPOLLERR | EPOLLPRI);
+}
+
+TEST_HARNESS_MAIN

-- 
2.53.0




More information about the linux-um mailing list