#include "../config-host.h" /* SPDX-License-Identifier: MIT */ /* * Description: stress test for buffer ring drain and refill. Creates a * small buffer ring, submits many reads that consume all * buffers, verifies correct behavior when buffers are * exhausted, then refills and continues. Tests both read * and recv paths with buffer rings under pressure. */ #include #include #include #include #include #include #include #include #include #include "liburing.h" #include "helpers.h" #define BGID 1 #define BUF_SIZE 256 #define NR_BUFS 8 #define NR_ROUNDS 10000 #define FILE_SIZE (BUF_SIZE * NR_BUFS * NR_ROUNDS) static void provide_buffers(struct io_uring_buf_ring *br, void *base, int nr_bufs, int buf_size) { int i; for (i = 0; i < nr_bufs; i++) { void *addr = base + i * buf_size; io_uring_buf_ring_add(br, addr, buf_size, i, io_uring_buf_ring_mask(nr_bufs), i); } io_uring_buf_ring_advance(br, nr_bufs); } /* * Test: submit reads consuming all buffers, refill, repeat. * Verifies data integrity through the whole process. */ static int test_read_drain_refill(void) { struct io_uring ring; struct io_uring_buf_ring *br; struct io_uring_sqe *sqe; struct io_uring_cqe *cqe; void *buf_base; char *file_data; const char *fname = ".buf-ring-stress-tmp"; int fd, ret, round; int total_read = 0; ret = io_uring_queue_init(32, &ring, 0); if (ret) { fprintf(stderr, "ring setup: %d\n", ret); return T_EXIT_FAIL; } /* Setup buffer ring */ br = io_uring_setup_buf_ring(&ring, NR_BUFS, BGID, 0, &ret); if (!br) { if (ret == -EINVAL || ret == -ENOENT) { io_uring_queue_exit(&ring); return T_EXIT_SKIP; } fprintf(stderr, "buf ring setup: %d\n", ret); io_uring_queue_exit(&ring); return T_EXIT_FAIL; } buf_base = malloc(NR_BUFS * BUF_SIZE); if (!buf_base) goto err; /* Create test file with known pattern */ file_data = malloc(FILE_SIZE); if (!file_data) goto err; for (int i = 0; i < FILE_SIZE; i++) file_data[i] = (char)(i & 0xff); fd = open(fname, O_CREAT | O_WRONLY | O_TRUNC, 0644); if (fd < 0) { perror("open write"); free(file_data); goto err; } if (write(fd, file_data, FILE_SIZE) != FILE_SIZE) { perror("write"); close(fd); free(file_data); goto err; } close(fd); fd = open(fname, O_RDONLY); if (fd < 0) { perror("open read"); free(file_data); goto err; } for (round = 0; round < NR_ROUNDS; round++) { int reads_submitted = 0; int reads_completed = 0; /* Provide fresh buffers for this round */ provide_buffers(br, buf_base, NR_BUFS, BUF_SIZE); /* Submit NR_BUFS reads using buffer selection */ for (int i = 0; i < NR_BUFS; i++) { sqe = io_uring_get_sqe(&ring); if (!sqe) { fprintf(stderr, "get sqe round %d/%d\n", round, i); goto err_file; } io_uring_prep_read(sqe, fd, NULL, BUF_SIZE, total_read + i * BUF_SIZE); sqe->flags |= IOSQE_BUFFER_SELECT; sqe->buf_group = BGID; sqe->user_data = (round << 16) | i; reads_submitted++; } ret = io_uring_submit(&ring); if (ret != reads_submitted) { fprintf(stderr, "submit round %d: %d\n", round, ret); goto err_file; } /* Reap all completions */ while (reads_completed < reads_submitted) { ret = io_uring_wait_cqe(&ring, &cqe); if (ret) { fprintf(stderr, "wait round %d: %d\n", round, ret); goto err_file; } if (cqe->res < 0) { fprintf(stderr, "read error round %d: %d\n", round, cqe->res); io_uring_cqe_seen(&ring, cqe); goto err_file; } if (!(cqe->flags & IORING_CQE_F_BUFFER)) { fprintf(stderr, "no buffer flag round %d\n", round); io_uring_cqe_seen(&ring, cqe); goto err_file; } int bid = cqe->flags >> IORING_CQE_BUFFER_SHIFT; if (bid >= NR_BUFS) { fprintf(stderr, "bad bid %d round %d\n", bid, round); io_uring_cqe_seen(&ring, cqe); goto err_file; } /* Verify data integrity */ void *got = buf_base + bid * BUF_SIZE; int idx = cqe->user_data & 0xffff; int off = total_read + idx * BUF_SIZE; if (cqe->res > 0 && memcmp(got, file_data + off, cqe->res) != 0) { fprintf(stderr, "data mismatch round %d bid %d\n", round, bid); io_uring_cqe_seen(&ring, cqe); goto err_file; } io_uring_cqe_seen(&ring, cqe); reads_completed++; } total_read += NR_BUFS * BUF_SIZE; } if (total_read != FILE_SIZE) { fprintf(stderr, "total read %d, expected %d\n", total_read, FILE_SIZE); goto err_file; } close(fd); unlink(fname); free(file_data); free(buf_base); io_uring_free_buf_ring(&ring, br, NR_BUFS, BGID); io_uring_queue_exit(&ring); return T_EXIT_PASS; err_file: close(fd); unlink(fname); free(file_data); err: free(buf_base); io_uring_queue_exit(&ring); return T_EXIT_FAIL; } /* * Test: submit more reads than available buffers. Excess reads should * fail with -ENOBUFS. Then refill and retry. */ static int test_read_overflow_refill(void) { struct io_uring ring; struct io_uring_buf_ring *br; struct io_uring_sqe *sqe; struct io_uring_cqe *cqe; void *buf_base; const char *fname = ".buf-ring-overflow-tmp"; int fd, ret; int nr_success = 0, nr_enobufs = 0; ret = io_uring_queue_init(32, &ring, 0); if (ret) { fprintf(stderr, "ring setup: %d\n", ret); return T_EXIT_FAIL; } br = io_uring_setup_buf_ring(&ring, NR_BUFS, BGID, 0, &ret); if (!br) { if (ret == -EINVAL || ret == -ENOENT) { io_uring_queue_exit(&ring); return T_EXIT_SKIP; } fprintf(stderr, "buf ring setup: %d\n", ret); io_uring_queue_exit(&ring); return T_EXIT_FAIL; } buf_base = malloc(NR_BUFS * BUF_SIZE); if (!buf_base) goto err; /* Create test file */ t_create_file(fname, NR_BUFS * BUF_SIZE * 4); fd = open(fname, O_RDONLY); if (fd < 0) { perror("open"); goto err; } /* Provide only NR_BUFS buffers */ provide_buffers(br, buf_base, NR_BUFS, BUF_SIZE); /* Submit more reads than we have buffers */ for (int i = 0; i < NR_BUFS * 2; i++) { sqe = io_uring_get_sqe(&ring); io_uring_prep_read(sqe, fd, NULL, BUF_SIZE, i * BUF_SIZE); sqe->flags |= IOSQE_BUFFER_SELECT; sqe->buf_group = BGID; sqe->user_data = i; } ret = io_uring_submit(&ring); if (ret != NR_BUFS * 2) { fprintf(stderr, "submit: %d\n", ret); goto err_file; } /* Collect all completions */ for (int i = 0; i < NR_BUFS * 2; i++) { ret = io_uring_wait_cqe(&ring, &cqe); if (ret) { fprintf(stderr, "wait: %d\n", ret); goto err_file; } if (cqe->res == -ENOBUFS) { nr_enobufs++; } else if (cqe->res > 0) { nr_success++; } else { fprintf(stderr, "unexpected res %d for ud %llu\n", cqe->res, (unsigned long long)cqe->user_data); io_uring_cqe_seen(&ring, cqe); goto err_file; } io_uring_cqe_seen(&ring, cqe); } /* We should have gotten exactly NR_BUFS successes */ if (nr_success != NR_BUFS) { fprintf(stderr, "expected %d success, got %d\n", NR_BUFS, nr_success); goto err_file; } if (nr_enobufs != NR_BUFS) { fprintf(stderr, "expected %d ENOBUFS, got %d\n", NR_BUFS, nr_enobufs); goto err_file; } /* Now refill and do another round - should work fine */ provide_buffers(br, buf_base, NR_BUFS, BUF_SIZE); nr_success = 0; for (int i = 0; i < NR_BUFS; i++) { sqe = io_uring_get_sqe(&ring); io_uring_prep_read(sqe, fd, NULL, BUF_SIZE, i * BUF_SIZE); sqe->flags |= IOSQE_BUFFER_SELECT; sqe->buf_group = BGID; sqe->user_data = 100 + i; } ret = io_uring_submit(&ring); if (ret != NR_BUFS) { fprintf(stderr, "refill submit: %d\n", ret); goto err_file; } for (int i = 0; i < NR_BUFS; i++) { ret = io_uring_wait_cqe(&ring, &cqe); if (ret) { fprintf(stderr, "refill wait: %d\n", ret); goto err_file; } if (cqe->res <= 0) { fprintf(stderr, "refill read res: %d\n", cqe->res); io_uring_cqe_seen(&ring, cqe); goto err_file; } nr_success++; io_uring_cqe_seen(&ring, cqe); } if (nr_success != NR_BUFS) { fprintf(stderr, "refill success: %d\n", nr_success); goto err_file; } close(fd); unlink(fname); free(buf_base); io_uring_free_buf_ring(&ring, br, NR_BUFS, BGID); io_uring_queue_exit(&ring); return T_EXIT_PASS; err_file: close(fd); unlink(fname); err: free(buf_base); io_uring_queue_exit(&ring); return T_EXIT_FAIL; } /* * Test: rapid drain/refill cycles with concurrent reads */ static int test_rapid_drain_refill(void) { struct io_uring ring; struct io_uring_buf_ring *br; struct io_uring_sqe *sqe; struct io_uring_cqe *cqe; void *buf_base; const char *fname = ".buf-ring-rapid-tmp"; int fd, ret; int total_success = 0; ret = io_uring_queue_init(64, &ring, 0); if (ret) { fprintf(stderr, "ring setup: %d\n", ret); return T_EXIT_FAIL; } br = io_uring_setup_buf_ring(&ring, NR_BUFS, BGID, 0, &ret); if (!br) { if (ret == -EINVAL || ret == -ENOENT) { io_uring_queue_exit(&ring); return T_EXIT_SKIP; } fprintf(stderr, "buf ring setup: %d\n", ret); io_uring_queue_exit(&ring); return T_EXIT_FAIL; } buf_base = malloc(NR_BUFS * BUF_SIZE); if (!buf_base) goto err; t_create_file(fname, FILE_SIZE); fd = open(fname, O_RDONLY); if (fd < 0) { perror("open"); goto err; } /* * Do many rapid cycles: provide 1 buffer, submit 1 read, * reap, repeat. This tests the fast path of single-buffer * provide/consume cycles. */ for (int i = 0; i < NR_ROUNDS * NR_BUFS; i++) { /* Provide exactly 1 buffer */ void *addr = buf_base + (i % NR_BUFS) * BUF_SIZE; io_uring_buf_ring_add(br, addr, BUF_SIZE, i % NR_BUFS, io_uring_buf_ring_mask(NR_BUFS), 0); io_uring_buf_ring_advance(br, 1); sqe = io_uring_get_sqe(&ring); io_uring_prep_read(sqe, fd, NULL, BUF_SIZE, (i * BUF_SIZE) % FILE_SIZE); sqe->flags |= IOSQE_BUFFER_SELECT; sqe->buf_group = BGID; sqe->user_data = i; ret = io_uring_submit(&ring); if (ret != 1) { fprintf(stderr, "rapid submit %d: %d\n", i, ret); goto err_file; } ret = io_uring_wait_cqe(&ring, &cqe); if (ret) { fprintf(stderr, "rapid wait %d: %d\n", i, ret); goto err_file; } if (cqe->res <= 0) { fprintf(stderr, "rapid read %d res: %d\n", i, cqe->res); io_uring_cqe_seen(&ring, cqe); goto err_file; } if (!(cqe->flags & IORING_CQE_F_BUFFER)) { fprintf(stderr, "rapid read %d: no buffer flag\n", i); io_uring_cqe_seen(&ring, cqe); goto err_file; } total_success++; io_uring_cqe_seen(&ring, cqe); } if (total_success != NR_ROUNDS * NR_BUFS) { fprintf(stderr, "rapid: %d success, expected %d\n", total_success, NR_ROUNDS * NR_BUFS); goto err_file; } close(fd); unlink(fname); free(buf_base); io_uring_free_buf_ring(&ring, br, NR_BUFS, BGID); io_uring_queue_exit(&ring); return T_EXIT_PASS; err_file: close(fd); unlink(fname); err: free(buf_base); io_uring_queue_exit(&ring); return T_EXIT_FAIL; } int main(int argc, char *argv[]) { int ret; if (argc > 1) return T_EXIT_SKIP; ret = test_read_drain_refill(); if (ret == T_EXIT_SKIP) { printf("Buffer rings not supported, skipping\n"); return T_EXIT_SKIP; } if (ret == T_EXIT_FAIL) { fprintf(stderr, "test_read_drain_refill failed\n"); return T_EXIT_FAIL; } ret = test_read_overflow_refill(); if (ret == T_EXIT_FAIL) { fprintf(stderr, "test_read_overflow_refill failed\n"); return T_EXIT_FAIL; } ret = test_rapid_drain_refill(); if (ret == T_EXIT_FAIL) { fprintf(stderr, "test_rapid_drain_refill failed\n"); return T_EXIT_FAIL; } return T_EXIT_PASS; }