[PATCH liburing] test: add rw-restart-ret regression test
Yitang Yang <[email protected]> Thu, 23 Jul 2026 20:46:28 +0800
| Newsgroups | org.kernel.vger.io-uring |
|---|---|
| Message-ID | <[email protected]> |
The filesystem layer may return internal restart codes to io_uring, which should be converted to -EINTR before being delivered to userspace. Add a test using userfaultfd with read multishot to expose the issue and prevent regressions. Link: https://lore.kernel.org/io-uring/[email protected]/ Signed-off-by: Yitang Yang <[email protected]> --- test/Makefile | 1 + test/rw-restart-ret.c | 304 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 305 insertions(+) create mode 100644 test/rw-restart-ret.c diff --git a/test/Makefile b/test/Makefile index d88a4287..e8cdda75 100644 --- a/test/Makefile +++ b/test/Makefile @@ -244,6 +244,7 @@ test_srcs := \ ring-leak.c \ ring-query.c \ rsrc_tags.c \ + rw-restart-ret.c \ rw_merge_test.c \ self.c \ recvsend_bundle.c \ diff --git a/test/rw-restart-ret.c b/test/rw-restart-ret.c new file mode 100644 index 00000000..e041c365 --- /dev/null +++ b/test/rw-restart-ret.c @@ -0,0 +1,304 @@ +/* SPDX-License-Identifier: MIT */ +/* + * Description: test that io_uring correctly converts internal -ERESTARTSYS + * to -EINTR on file read paths. Using userfaultfd to expose the issue. + */ +#include <errno.h> +#include <fcntl.h> +#include <linux/userfaultfd.h> +#include <pthread.h> +#include <stdint.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <sys/eventfd.h> +#include <sys/ioctl.h> +#include <sys/mman.h> +#include <sys/syscall.h> +#include <unistd.h> + +#include "liburing.h" +#include "helpers.h" + +#define BUF_RING_SIZE 32 +#define BGID 0 +#define EXIT_DATA 1 + +static size_t num_threads = 16; +static size_t pages_per_thread = 10000; +static size_t page_size; + +typedef struct { + void *data; + size_t size; + int uffd; + int stop_fd; + struct io_uring ring; + struct io_uring_buf_ring *buf_ring; +} handler_state; + +static int handle_page_fault(handler_state *h, struct uffd_msg *msg) +{ + void *fault_addr = (void *)msg->arg.pagefault.address; + struct uffdio_zeropage zeropage = { + .range = { + .start = (unsigned long)fault_addr, + .len = page_size, + }, + }; + + return ioctl(h->uffd, UFFDIO_ZEROPAGE, &zeropage); +} + +static int init_handler(handler_state *h, void *data, size_t size) +{ + int r; + + memset(h, 0, sizeof(*h)); + h->data = data; + h->size = size; + + h->uffd = syscall(__NR_userfaultfd, O_CLOEXEC | O_NONBLOCK); + if (h->uffd == -1) { + if (errno == ENOSYS || errno == EPERM) + return T_EXIT_SKIP; + return -1; + } + + struct uffdio_api api = { + .api = UFFD_API, + .features = 0, + }; + if (ioctl(h->uffd, UFFDIO_API, &api) == -1) + goto err_close_uffd; + + struct uffdio_register reg = { + .range = { + .start = (unsigned long)h->data, + .len = h->size, + }, + .mode = UFFDIO_REGISTER_MODE_MISSING, + }; + if (ioctl(h->uffd, UFFDIO_REGISTER, ®) == -1) + goto err_close_uffd; + + h->stop_fd = eventfd(0, EFD_NONBLOCK); + if (h->stop_fd == -1) + goto err_close_uffd; + + r = io_uring_queue_init(128, &h->ring, 0); + if (r < 0) + goto err_close_stopfd; + + h->buf_ring = io_uring_setup_buf_ring(&h->ring, BUF_RING_SIZE, + BGID, 0, &r); + if (!h->buf_ring) { + if (r == -EINVAL || r == -ENOENT) { + io_uring_queue_exit(&h->ring); + close(h->stop_fd); + close(h->uffd); + return T_EXIT_SKIP; + } + goto err_cleanup_ring; + } + + return 0; + +err_cleanup_ring: + io_uring_queue_exit(&h->ring); +err_close_stopfd: + close(h->stop_fd); +err_close_uffd: + close(h->uffd); + return -1; +} + +static void destroy_handler(handler_state *h) +{ + io_uring_free_buf_ring(&h->ring, h->buf_ring, BUF_RING_SIZE, BGID); + io_uring_queue_exit(&h->ring); + close(h->stop_fd); + close(h->uffd); +} + +static int handler_run(handler_state *h) +{ + struct io_uring_sqe *sqe; + struct io_uring_cqe *cqe; + uint64_t stop_signal = 0; + + struct uffd_msg msgs[BUF_RING_SIZE]; + int i; + + for (i = 0; i < BUF_RING_SIZE; i++) { + io_uring_buf_ring_add(h->buf_ring, msgs + i, + sizeof(struct uffd_msg), i, + BUF_RING_SIZE - 1, i); + } + io_uring_buf_ring_advance(h->buf_ring, BUF_RING_SIZE); + + sqe = io_uring_get_sqe(&h->ring); + io_uring_prep_read_multishot(sqe, h->uffd, 0, 0, BGID); + + sqe = io_uring_get_sqe(&h->ring); + io_uring_prep_read(sqe, h->stop_fd, &stop_signal, + sizeof(stop_signal), 0); + io_uring_sqe_set_data64(sqe, EXIT_DATA); + + io_uring_submit(&h->ring); + + while (1) { + int ret = io_uring_wait_cqe(&h->ring, &cqe); + if (ret < 0) + exit(T_EXIT_FAIL); + + unsigned head; + size_t count = 0; + + io_uring_for_each_cqe(&h->ring, head, cqe) { + if (cqe->user_data == EXIT_DATA) + return 0; + + /* + * The only acceptable errors for a multishot + * read on userfaultfd are ENOBUFS (buffers + * temporarily exhausted) and EINTR. + */ + if (cqe->res < 0 && + cqe->res != -ENOBUFS && + cqe->res != -EINTR) + exit(T_EXIT_FAIL); + + if (cqe->res >= 0) { + int bid = cqe->flags >> IORING_CQE_BUFFER_SHIFT; + struct uffd_msg *this_msg = &msgs[bid]; + + if (handle_page_fault(h, this_msg)) + exit(T_EXIT_FAIL); + + io_uring_buf_ring_add(h->buf_ring, this_msg, + sizeof(struct uffd_msg), + bid, BUF_RING_SIZE - 1, 0); + io_uring_buf_ring_advance(h->buf_ring, 1); + } + + if (!(cqe->flags & IORING_CQE_F_MORE)) { + sqe = io_uring_get_sqe(&h->ring); + io_uring_prep_read_multishot(sqe, h->uffd, + 0, 0, BGID); + io_uring_submit(&h->ring); + } + + count++; + } + io_uring_cq_advance(&h->ring, count); + } + + return 0; +} + +static ssize_t handler_stop(handler_state *h) +{ + uint64_t u = 1; + return write(h->stop_fd, &u, sizeof(u)); +} + +static void *thread_function(void *arg) +{ + char *data = (char *)arg; + size_t i; + + for (i = 0; i < pages_per_thread; i++) + data[i * page_size] = (char)i; + return NULL; +} + +static void *fault_thread_func(void *arg) +{ + handler_run((handler_state *)arg); + return NULL; +} + +static void print_usage(const char *prog) +{ + fprintf(stderr, + "Usage: %s [-n threads] [-p pages] [-h]\n" + " -n NUM Number of threads\n" + " -p NUM Pages per thread\n" + " -h Show this help\n", + prog); +} + +int main(int argc, char **argv) +{ + handler_state handler; + pthread_t fault_thread; + pthread_t *threads; + size_t total_size; + void *data; + int ret, opt; + size_t i; + + while ((opt = getopt(argc, argv, "n:p:h")) != -1) { + switch (opt) { + case 'n': + num_threads = strtoul(optarg, NULL, 10); + break; + case 'p': + pages_per_thread = strtoul(optarg, NULL, 10); + break; + case 'h': + default: + print_usage(argv[0]); + return opt == 'h' ? T_EXIT_SKIP : T_EXIT_FAIL; + } + } + + page_size = sysconf(_SC_PAGESIZE); + total_size = num_threads * pages_per_thread * page_size; + + data = mmap(NULL, total_size, PROT_READ | PROT_WRITE, + MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + if (data == MAP_FAILED) + return T_EXIT_FAIL; + + ret = init_handler(&handler, data, total_size); + if (ret < 0) + goto err_munmap; + if (ret == T_EXIT_SKIP) { + munmap(data, total_size); + return T_EXIT_SKIP; + } + + pthread_create(&fault_thread, NULL, fault_thread_func, &handler); + + threads = malloc(num_threads * sizeof(pthread_t)); + if (!threads) { + handler_stop(&handler); + pthread_join(fault_thread, NULL); + destroy_handler(&handler); + munmap(data, total_size); + return T_EXIT_FAIL; + } + + for (i = 0; i < num_threads; i++) { + char *ptr = (char *)data + i * pages_per_thread * page_size; + pthread_create(&threads[i], NULL, thread_function, ptr); + } + + for (i = 0; i < num_threads; i++) + pthread_join(threads[i], NULL); + + handler_stop(&handler); + pthread_join(fault_thread, NULL); + + free(threads); + destroy_handler(&handler); + munmap(data, total_size); + + return T_EXIT_PASS; + +err_munmap: + munmap(data, total_size); + return T_EXIT_FAIL; +} -- 2.43.0