#include "../config-host.h" /* SPDX-License-Identifier: MIT */ /* * Description: stress test cancel racing with completion. Submits operations * and immediately cancels them, checking that we get consistent * results (either the op completed or was cancelled, never both * or neither). */ #include #include #include #include #include #include #include #include #include #include #include "liburing.h" #include "helpers.h" #define RING_SIZE 64 #define NR_ITERS 10000 /* user_data encoding: op type in low bits, sequence in upper bits */ #define UD_OP_POLL 1 #define UD_OP_CANCEL 2 #define UD_OP_READ 3 #define UD_SEQ_SHIFT 8 static int no_cancel; /* * Stress poll + cancel race: submit a poll and immediately cancel it. * We should see exactly one completion per pair (either the poll * completes or the cancel succeeds, or the cancel fails because the * poll already completed). */ static int test_poll_cancel_race(void) { struct io_uring ring; int ret, i; int pipe_fds[2]; if (pipe(pipe_fds) < 0) { perror("pipe"); return T_EXIT_FAIL; } ret = io_uring_queue_init(RING_SIZE, &ring, 0); if (ret) { fprintf(stderr, "ring setup: %d\n", ret); close(pipe_fds[0]); close(pipe_fds[1]); return T_EXIT_FAIL; } for (i = 0; i < NR_ITERS; i++) { struct io_uring_sqe *sqe; struct io_uring_cqe *cqe; __u64 poll_ud = UD_OP_POLL | ((__u64)i << UD_SEQ_SHIFT); __u64 cancel_ud = UD_OP_CANCEL | ((__u64)i << UD_SEQ_SHIFT); bool got_poll = false, got_cancel = false; /* Submit poll */ sqe = io_uring_get_sqe(&ring); io_uring_prep_poll_add(sqe, pipe_fds[0], POLLIN); sqe->user_data = poll_ud; /* Submit cancel immediately after */ sqe = io_uring_get_sqe(&ring); io_uring_prep_cancel64(sqe, poll_ud, 0); sqe->user_data = cancel_ud; ret = io_uring_submit(&ring); if (ret != 2) { fprintf(stderr, "submit iter %d: %d\n", i, ret); goto err; } /* Must get exactly 2 CQEs */ for (int j = 0; j < 2; j++) { ret = io_uring_wait_cqe(&ring, &cqe); if (ret) { fprintf(stderr, "wait iter %d/%d: %d\n", i, j, ret); goto err; } if ((cqe->user_data & 0xff) == UD_OP_POLL) { got_poll = true; /* * Poll result: either -ECANCELED (cancel won) * or 0/POLLIN (poll completed first, shouldn't * happen here since nothing writes to pipe) */ if (cqe->res != -ECANCELED && cqe->res != 0) { fprintf(stderr, "poll res: %d\n", cqe->res); io_uring_cqe_seen(&ring, cqe); goto err; } } else if ((cqe->user_data & 0xff) == UD_OP_CANCEL) { got_cancel = true; /* * Cancel result: 0 (cancelled successfully) or * -ENOENT (poll already completed) or * -EALREADY (in progress) */ if (cqe->res != 0 && cqe->res != -ENOENT && cqe->res != -EALREADY) { if (cqe->res == -EINVAL) { no_cancel = 1; io_uring_cqe_seen(&ring, cqe); goto out; } fprintf(stderr, "cancel res: %d\n", cqe->res); io_uring_cqe_seen(&ring, cqe); goto err; } } io_uring_cqe_seen(&ring, cqe); } if (!got_poll || !got_cancel) { fprintf(stderr, "missing CQE iter %d: poll=%d cancel=%d\n", i, got_poll, got_cancel); goto err; } } out: close(pipe_fds[0]); close(pipe_fds[1]); io_uring_queue_exit(&ring); if (no_cancel) return T_EXIT_SKIP; return T_EXIT_PASS; err: close(pipe_fds[0]); close(pipe_fds[1]); io_uring_queue_exit(&ring); return T_EXIT_FAIL; } /* * Stress read + cancel race: submit a read on a pipe (that won't complete * immediately) and immediately cancel it. */ static int test_read_cancel_race(void) { struct io_uring ring; char buf[32]; int ret, i; int pipe_fds[2]; if (no_cancel) return T_EXIT_SKIP; if (pipe(pipe_fds) < 0) { perror("pipe"); return T_EXIT_FAIL; } /* Make read end non-blocking so prep works, but reads will still * go async since there's no data */ t_set_nonblock(pipe_fds[0]); ret = io_uring_queue_init(RING_SIZE, &ring, 0); if (ret) { fprintf(stderr, "ring setup: %d\n", ret); close(pipe_fds[0]); close(pipe_fds[1]); return T_EXIT_FAIL; } for (i = 0; i < NR_ITERS; i++) { struct io_uring_sqe *sqe; struct io_uring_cqe *cqe; __u64 read_ud = UD_OP_READ | ((__u64)i << UD_SEQ_SHIFT); __u64 cancel_ud = UD_OP_CANCEL | ((__u64)i << UD_SEQ_SHIFT); bool got_read = false, got_cancel = false; int nr_cqes; /* Submit read that will block (no data in pipe) */ sqe = io_uring_get_sqe(&ring); io_uring_prep_read(sqe, pipe_fds[0], buf, sizeof(buf), 0); sqe->user_data = read_ud; /* Submit cancel */ sqe = io_uring_get_sqe(&ring); io_uring_prep_cancel64(sqe, read_ud, 0); sqe->user_data = cancel_ud; ret = io_uring_submit(&ring); if (ret != 2) { fprintf(stderr, "submit iter %d: %d\n", i, ret); goto err; } /* * We should get 2 CQEs, but the read may complete * with -EAGAIN immediately (non-blocking pipe with no data), * in which case cancel gets -ENOENT. */ nr_cqes = 0; while (nr_cqes < 2) { ret = io_uring_wait_cqe(&ring, &cqe); if (ret) { fprintf(stderr, "wait iter %d/%d: %d\n", i, nr_cqes, ret); goto err; } if ((cqe->user_data & 0xff) == UD_OP_READ) { got_read = true; /* -ECANCELED, -EAGAIN, or short read all OK */ if (cqe->res != -ECANCELED && cqe->res != -EAGAIN && cqe->res < 0 && cqe->res != -EINTR) { fprintf(stderr, "read res iter %d: %d\n", i, cqe->res); io_uring_cqe_seen(&ring, cqe); goto err; } } else if ((cqe->user_data & 0xff) == UD_OP_CANCEL) { got_cancel = true; if (cqe->res != 0 && cqe->res != -ENOENT && cqe->res != -EALREADY) { fprintf(stderr, "cancel res iter %d: %d\n", i, cqe->res); io_uring_cqe_seen(&ring, cqe); goto err; } } io_uring_cqe_seen(&ring, cqe); nr_cqes++; } if (!got_read || !got_cancel) { fprintf(stderr, "missing CQE iter %d\n", i); goto err; } } close(pipe_fds[0]); close(pipe_fds[1]); io_uring_queue_exit(&ring); return T_EXIT_PASS; err: close(pipe_fds[0]); close(pipe_fds[1]); io_uring_queue_exit(&ring); return T_EXIT_FAIL; } /* * Stress concurrent cancel from multiple threads: one thread submits * polls, another cancels them via IORING_ASYNC_CANCEL_ANY. */ struct cancel_thread_data { struct io_uring *ring; int cancel_count; volatile int stop; }; static void *cancel_thread_fn(void *arg) { struct cancel_thread_data *ctd = arg; struct io_uring_sqe *sqe; struct io_uring_cqe *cqe; int ret; while (!ctd->stop) { sqe = io_uring_get_sqe(ctd->ring); if (!sqe) { io_uring_submit(ctd->ring); continue; } io_uring_prep_cancel64(sqe, 0, IORING_ASYNC_CANCEL_ANY); sqe->user_data = 0xdead; ret = io_uring_submit(ctd->ring); if (ret < 0) continue; ret = io_uring_wait_cqe(ctd->ring, &cqe); if (ret) continue; if (cqe->res == 0) ctd->cancel_count++; io_uring_cqe_seen(ctd->ring, cqe); } return NULL; } static int test_concurrent_cancel(void) { struct io_uring submit_ring, cancel_ring; struct cancel_thread_data ctd; struct io_uring_sqe *sqe; struct io_uring_cqe *cqe; pthread_t thread; int ret, i; int pipe_fds[2]; int submitted = 0, completed = 0; if (no_cancel) return T_EXIT_SKIP; if (pipe(pipe_fds) < 0) { perror("pipe"); return T_EXIT_FAIL; } ret = io_uring_queue_init(RING_SIZE, &submit_ring, 0); if (ret) { fprintf(stderr, "submit ring setup: %d\n", ret); close(pipe_fds[0]); close(pipe_fds[1]); return T_EXIT_FAIL; } ret = io_uring_queue_init(RING_SIZE, &cancel_ring, 0); if (ret) { fprintf(stderr, "cancel ring setup: %d\n", ret); io_uring_queue_exit(&submit_ring); close(pipe_fds[0]); close(pipe_fds[1]); return T_EXIT_FAIL; } ctd.ring = &cancel_ring; ctd.cancel_count = 0; ctd.stop = 0; pthread_create(&thread, NULL, cancel_thread_fn, &ctd); /* Submit polls and let the other thread race cancels */ for (i = 0; i < NR_ITERS; i++) { sqe = io_uring_get_sqe(&submit_ring); if (!sqe) { /* Flush and drain */ io_uring_submit(&submit_ring); while (io_uring_peek_cqe(&submit_ring, &cqe) == 0) { completed++; io_uring_cqe_seen(&submit_ring, cqe); } sqe = io_uring_get_sqe(&submit_ring); if (!sqe) continue; } io_uring_prep_poll_add(sqe, pipe_fds[0], POLLIN); sqe->user_data = i + 1; submitted++; if (i % 16 == 15) { io_uring_submit(&submit_ring); /* Drain any completions */ while (io_uring_peek_cqe(&submit_ring, &cqe) == 0) { completed++; io_uring_cqe_seen(&submit_ring, cqe); } } } io_uring_submit(&submit_ring); ctd.stop = 1; pthread_join(thread, NULL); /* Drain remaining CQEs from submit ring */ while (completed < submitted) { /* Cancel anything remaining */ sqe = io_uring_get_sqe(&submit_ring); if (sqe) { io_uring_prep_cancel64(sqe, 0, IORING_ASYNC_CANCEL_ANY); sqe->user_data = 0xbeef; io_uring_submit(&submit_ring); } ret = io_uring_peek_cqe(&submit_ring, &cqe); if (ret == -EAGAIN) { struct __kernel_timespec ts = { .tv_sec = 1 }; ret = io_uring_wait_cqe_timeout(&submit_ring, &cqe, &ts); if (ret == -ETIME) break; if (ret) break; } if (cqe->user_data != 0xbeef) completed++; io_uring_cqe_seen(&submit_ring, cqe); } close(pipe_fds[0]); close(pipe_fds[1]); io_uring_queue_exit(&cancel_ring); io_uring_queue_exit(&submit_ring); return T_EXIT_PASS; } int main(int argc, char *argv[]) { int ret; if (argc > 1) return T_EXIT_SKIP; ret = test_poll_cancel_race(); if (ret == T_EXIT_SKIP) { printf("cancel not supported, skipping\n"); return T_EXIT_SKIP; } if (ret == T_EXIT_FAIL) { fprintf(stderr, "test_poll_cancel_race failed\n"); return T_EXIT_FAIL; } ret = test_read_cancel_race(); if (ret == T_EXIT_FAIL) { fprintf(stderr, "test_read_cancel_race failed\n"); return T_EXIT_FAIL; } ret = test_concurrent_cancel(); if (ret == T_EXIT_FAIL) { fprintf(stderr, "test_concurrent_cancel failed\n"); return T_EXIT_FAIL; } return T_EXIT_PASS; }