#include "../config-host.h" /* SPDX-License-Identifier: MIT */ /* * Description: Test that a provided buffer ring descriptor's len is not * persistently corrupted by a non-incremental bundle operation * that truncates the head buffer. * * 1) A bundle recv that fails with -EAGAIN (no data available) * must NOT leave the head buffer's len shrunk to the * requested size. A later operation using the same buffer * group must still see the full buffer length. * * 2) A bundle recv that succeeds while truncating the buffer * (sqe->len < buf->len) must post a completion that matches * the actual transfer length, not the original buffer len, * and must consume the buffer correctly. * * Based on the reproducer from Federico Brasili: * https://lore.kernel.org/io-uring/CAAEr8jbY60noGj1fw_k91UJRBkyiRVoS6=nLhZ7Svwidjn4CAA@mail.gmail.com/ */ #include #include #include #include #include #include #include "liburing.h" #include "helpers.h" #define BGID 1 #define NR_BUFS 2 #define BUF_SIZE 4096 #define SHORT_LEN 32 static int no_buf_ring, no_bundle; struct test_ctx { struct io_uring ring; struct io_uring_buf_ring *br; void *buf_mem; }; static void *buf_addr(struct test_ctx *t, int bid) { return (char *) t->buf_mem + bid * BUF_SIZE; } /* * Set up a ring + a non-incremental provided buffer ring with NR_BUFS * buffers of BUF_SIZE each, fully published to the kernel. */ static int setup(struct test_ctx *t) { struct io_uring_params p = { }; int ret, i, mask; ret = io_uring_queue_init_params(8, &t->ring, &p); if (ret < 0) { fprintf(stderr, "queue init: %d\n", ret); return T_EXIT_FAIL; } if (!(p.features & IORING_FEAT_RECVSEND_BUNDLE)) { no_bundle = 1; io_uring_queue_exit(&t->ring); return T_EXIT_SKIP; } if (posix_memalign(&t->buf_mem, 4096, NR_BUFS * BUF_SIZE)) { perror("posix_memalign"); io_uring_queue_exit(&t->ring); return T_EXIT_FAIL; } t->br = io_uring_setup_buf_ring(&t->ring, NR_BUFS, BGID, 0, &ret); if (!t->br) { if (ret == -EINVAL) { no_buf_ring = 1; free(t->buf_mem); io_uring_queue_exit(&t->ring); return T_EXIT_SKIP; } fprintf(stderr, "buf ring setup: %d\n", ret); free(t->buf_mem); io_uring_queue_exit(&t->ring); return T_EXIT_FAIL; } mask = io_uring_buf_ring_mask(NR_BUFS); for (i = 0; i < NR_BUFS; i++) io_uring_buf_ring_add(t->br, buf_addr(t, i), BUF_SIZE, i, mask, i); io_uring_buf_ring_advance(t->br, NR_BUFS); return T_EXIT_PASS; } static void teardown(struct test_ctx *t) { io_uring_free_buf_ring(&t->ring, t->br, NR_BUFS, BGID); io_uring_queue_exit(&t->ring); free(t->buf_mem); } static int submit_wait(struct test_ctx *t, struct io_uring_cqe **cqe) { int ret; ret = io_uring_submit(&t->ring); if (ret != 1) { fprintf(stderr, "submit: %d\n", ret); return -1; } ret = io_uring_wait_cqe(&t->ring, cqe); if (ret < 0) { fprintf(stderr, "wait_cqe: %d\n", ret); return -1; } return 0; } /* * Case 1: a failed (-EAGAIN) bundle recv on an empty socket must not shrink * the head buffer's len. We verify both the raw descriptor and that a later * unrelated READ using the same group sees the full buffer. */ static int test_eagain_no_corrupt(void) { struct io_uring_cqe *cqe; struct io_uring_sqe *sqe; struct test_ctx t; int ret, sv[2], pfd[2], bid; char *pipe_buf; ret = setup(&t); if (ret != T_EXIT_PASS) return ret; if (socketpair(AF_UNIX, SOCK_DGRAM, 0, sv) < 0) { perror("socketpair"); teardown(&t); return T_EXIT_FAIL; } /* empty socket, MSG_DONTWAIT -> -EAGAIN, bundle, len=1 */ sqe = io_uring_get_sqe(&t.ring); io_uring_prep_recv(sqe, sv[0], NULL, 1, MSG_DONTWAIT); sqe->ioprio |= IORING_RECVSEND_BUNDLE; sqe->flags |= IOSQE_BUFFER_SELECT; sqe->buf_group = BGID; if (submit_wait(&t, &cqe)) goto fail; if (cqe->res == -EINVAL) { no_bundle = 1; io_uring_cqe_seen(&t.ring, cqe); goto skip; } if (cqe->res != -EAGAIN) { fprintf(stderr, "case1: expected -EAGAIN, got %d\n", cqe->res); io_uring_cqe_seen(&t.ring, cqe); goto fail; } io_uring_cqe_seen(&t.ring, cqe); /* the failed recv must not have consumed or shrunk buffer 0 */ if (t.br->bufs[0].len != BUF_SIZE) { fprintf(stderr, "case1: head buf len corrupted: %u (want %u)\n", t.br->bufs[0].len, BUF_SIZE); goto fail; } /* * User-visible impact: an unrelated READ from a pipe using the same * buffer group must consume the full buffer, not the poisoned len. */ if (pipe(pfd) < 0) { perror("pipe"); goto fail; } pipe_buf = malloc(BUF_SIZE); memset(pipe_buf, 0x5a, BUF_SIZE); if (write(pfd[1], pipe_buf, BUF_SIZE) != BUF_SIZE) { perror("write pipe"); free(pipe_buf); close(pfd[0]); close(pfd[1]); goto fail; } free(pipe_buf); sqe = io_uring_get_sqe(&t.ring); io_uring_prep_read(sqe, pfd[0], NULL, BUF_SIZE, 0); sqe->flags |= IOSQE_BUFFER_SELECT; sqe->buf_group = BGID; ret = submit_wait(&t, &cqe); close(pfd[0]); close(pfd[1]); if (ret) goto fail; if (cqe->res != BUF_SIZE) { fprintf(stderr, "case1: read consumed poisoned len: res=%d " "(want %d)\n", cqe->res, BUF_SIZE); io_uring_cqe_seen(&t.ring, cqe); goto fail; } if (!(cqe->flags & IORING_CQE_F_BUFFER)) { fprintf(stderr, "case1: read did not select a buffer\n"); io_uring_cqe_seen(&t.ring, cqe); goto fail; } bid = cqe->flags >> IORING_CQE_BUFFER_SHIFT; if (bid != 0) { fprintf(stderr, "case1: read used wrong bid %d\n", bid); io_uring_cqe_seen(&t.ring, cqe); goto fail; } io_uring_cqe_seen(&t.ring, cqe); close(sv[0]); close(sv[1]); teardown(&t); return T_EXIT_PASS; skip: close(sv[0]); close(sv[1]); teardown(&t); return T_EXIT_SKIP; fail: close(sv[0]); close(sv[1]); teardown(&t); return T_EXIT_FAIL; } /* * Case 2: a successful bundle recv that truncates the buffer (len=32 on a * 4096 buffer) must post a completion matching the actual transfer length * (32), not the buffer's original len, and must read only the truncated * amount even though more data is queued. */ static int test_success_trim(void) { struct io_uring_cqe *cqe; struct io_uring_sqe *sqe; struct test_ctx t; char snd[64], *got; int ret, sv[2], bid, i; ret = setup(&t); if (ret != T_EXIT_PASS) return ret; if (socketpair(AF_UNIX, SOCK_STREAM, 0, sv) < 0) { perror("socketpair"); teardown(&t); return T_EXIT_FAIL; } for (i = 0; i < (int) sizeof(snd); i++) snd[i] = 'A' + (i % 26); if (write(sv[1], snd, sizeof(snd)) != sizeof(snd)) { perror("write sock"); goto fail; } /* 64 bytes available, ask for 32: trim to 32, expect res == 32 */ sqe = io_uring_get_sqe(&t.ring); io_uring_prep_recv(sqe, sv[0], NULL, SHORT_LEN, MSG_DONTWAIT); sqe->ioprio |= IORING_RECVSEND_BUNDLE; sqe->flags |= IOSQE_BUFFER_SELECT; sqe->buf_group = BGID; if (submit_wait(&t, &cqe)) goto fail; if (cqe->res == -EINVAL) { no_bundle = 1; io_uring_cqe_seen(&t.ring, cqe); goto skip; } if (cqe->res != SHORT_LEN) { fprintf(stderr, "case2: expected res=%d, got %d\n", SHORT_LEN, cqe->res); io_uring_cqe_seen(&t.ring, cqe); goto fail; } if (!(cqe->flags & IORING_CQE_F_BUFFER)) { fprintf(stderr, "case2: recv did not select a buffer\n"); io_uring_cqe_seen(&t.ring, cqe); goto fail; } bid = cqe->flags >> IORING_CQE_BUFFER_SHIFT; if (bid != 0) { fprintf(stderr, "case2: recv used wrong bid %d\n", bid); io_uring_cqe_seen(&t.ring, cqe); goto fail; } io_uring_cqe_seen(&t.ring, cqe); /* the 32 bytes landed in buffer 0 and match the head of the stream */ got = buf_addr(&t, 0); if (memcmp(got, snd, SHORT_LEN)) { fprintf(stderr, "case2: received data mismatch\n"); goto fail; } close(sv[0]); close(sv[1]); teardown(&t); return T_EXIT_PASS; skip: close(sv[0]); close(sv[1]); teardown(&t); return T_EXIT_SKIP; fail: close(sv[0]); close(sv[1]); teardown(&t); return T_EXIT_FAIL; } int main(int argc, char *argv[]) { int ret; if (argc > 1) return T_EXIT_SKIP; ret = test_eagain_no_corrupt(); if (ret == T_EXIT_FAIL) { fprintf(stderr, "test_eagain_no_corrupt failed\n"); return T_EXIT_FAIL; } if (no_buf_ring || no_bundle) return T_EXIT_SKIP; ret = test_success_trim(); if (ret == T_EXIT_FAIL) { fprintf(stderr, "test_success_trim failed\n"); return T_EXIT_FAIL; } return T_EXIT_PASS; }