Read /proc/net/dev_snmp6/<dev> of a tun device into a buffer whose first page is backed by userfaultfd and remove the device while the reader sleeps in its copy to userspace:
- removing the entry doesn't wait for the reader - the reader still gets what was printed before the removal - and the next read fails with -EIO The entry is removed under RTNL, so a regression stalls the test for five seconds before it fails. Signed-off-by: Christian Brauner (Amutable) <[email protected]> --- .../selftests/proc/proc-remove-slow-reader.c | 133 +++++++++++++++++++++ 1 file changed, 133 insertions(+) diff --git a/tools/testing/selftests/proc/proc-remove-slow-reader.c b/tools/testing/selftests/proc/proc-remove-slow-reader.c new file mode 100644 index 000000000000..a201645beca9 --- /dev/null +++ b/tools/testing/selftests/proc/proc-remove-slow-reader.c @@ -0,0 +1,133 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * A read that faults while copying to userspace must not hold up removal of + * the /proc entry. /proc/net/dev_snmp6/<dev> of a tun device is removed under + * RTNL when the device's file is closed. + */ +#define _GNU_SOURCE +#include <errno.h> +#include <fcntl.h> +#include <linux/if.h> +#include <linux/if_tun.h> +#include <linux/userfaultfd.h> +#include <poll.h> +#include <pthread.h> +#include <sched.h> +#include <string.h> +#include <sys/ioctl.h> +#include <sys/mman.h> +#include <sys/syscall.h> +#include <unistd.h> + +#include "../kselftest_harness.h" + +#define TUN_NAME "procslow0" +#define SNMP6_PATH "/proc/net/dev_snmp6/" TUN_NAME +#define READ_PAGES 16 +#define TIMEOUT_MS 5000 + +struct reader { + int fd; + char *buf; + size_t len; + ssize_t ret; +}; + +struct closer { + int tun; + int done; +}; + +static void *read_thread(void *arg) +{ + struct reader *r = arg; + + r->ret = read(r->fd, r->buf, r->len); + return NULL; +} + +static void *close_thread(void *arg) +{ + struct closer *c = arg; + ssize_t ret; + + close(c->tun); + ret = write(c->done, "x", 1); + return (void *)ret; +} + +TEST(remove_while_reader_faults) +{ + struct uffdio_api api = { .api = UFFD_API }; + struct uffdio_register reg = {}; + struct uffdio_zeropage zero = {}; + long psz = sysconf(_SC_PAGESIZE); + struct ifreq ifr = {}; + struct reader r = {}; + struct closer c = {}; + struct uffd_msg msg; + struct pollfd pfd; + pthread_t rt, ct; + int uffd, done[2]; + char *buf; + + if (unshare(CLONE_NEWNET)) + SKIP(return, "unshare(CLONE_NEWNET): %s", strerror(errno)); + + c.tun = open("/dev/net/tun", O_RDWR | O_CLOEXEC); + if (c.tun < 0) + SKIP(return, "/dev/net/tun: %s", strerror(errno)); + strncpy(ifr.ifr_name, TUN_NAME, IFNAMSIZ - 1); + ifr.ifr_flags = IFF_TUN | IFF_NO_PI; + ASSERT_EQ(ioctl(c.tun, TUNSETIFF, &ifr), 0); + + r.fd = open(SNMP6_PATH, O_RDONLY | O_CLOEXEC); + if (r.fd < 0) + SKIP(return, SNMP6_PATH ": %s", strerror(errno)); + + uffd = syscall(__NR_userfaultfd, O_CLOEXEC | O_NONBLOCK); + if (uffd < 0) + SKIP(return, "userfaultfd: %s", strerror(errno)); + ASSERT_EQ(ioctl(uffd, UFFDIO_API, &api), 0); + + /* Only the first page of the buffer goes through userfaultfd. */ + buf = mmap(NULL, READ_PAGES * psz, PROT_READ | PROT_WRITE, + MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + ASSERT_NE(buf, MAP_FAILED); + reg.range.start = (unsigned long)buf; + reg.range.len = psz; + reg.mode = UFFDIO_REGISTER_MODE_MISSING; + ASSERT_EQ(ioctl(uffd, UFFDIO_REGISTER, ®), 0); + + r.buf = buf; + r.len = READ_PAGES * psz; + ASSERT_EQ(pthread_create(&rt, NULL, read_thread, &r), 0); + + /* The reader now sleeps in its copy to userspace. */ + pfd = (struct pollfd){ .fd = uffd, .events = POLLIN }; + ASSERT_EQ(poll(&pfd, 1, TIMEOUT_MS), 1); + ASSERT_EQ(read(uffd, &msg, sizeof(msg)), sizeof(msg)); + ASSERT_EQ(msg.event, UFFD_EVENT_PAGEFAULT); + + /* Closing the device removes the file that is being read. */ + ASSERT_EQ(pipe2(done, O_CLOEXEC), 0); + c.done = done[1]; + ASSERT_EQ(pthread_create(&ct, NULL, close_thread, &c), 0); + pfd = (struct pollfd){ .fd = done[0], .events = POLLIN }; + EXPECT_EQ(poll(&pfd, 1, TIMEOUT_MS), 1) { + TH_LOG("removing " SNMP6_PATH " waited for the reader"); + } + + zero.range.start = (unsigned long)buf; + zero.range.len = psz; + ASSERT_EQ(ioctl(uffd, UFFDIO_ZEROPAGE, &zero), 0); + ASSERT_EQ(pthread_join(rt, NULL), 0); + ASSERT_EQ(pthread_join(ct, NULL), 0); + + /* What was printed before the removal still arrives, then nothing. */ + EXPECT_GT(r.ret, 0); + EXPECT_EQ(read(r.fd, buf, psz), -1); + EXPECT_EQ(errno, EIO); +} + +TEST_HARNESS_MAIN -- 2.53.0
