| /* 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 <stdio.h> |
| #include <stdlib.h> |
| #include <string.h> |
| #include <unistd.h> |
| #include <errno.h> |
| #include <sys/socket.h> |
| |
| #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; |
| } |