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) <[email protected]>
---
 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

Reply via email to