test/recv-bundle-pbuf-len-poison.c | 146 +++++++++++++++++++++++++++++ 1 file changed, 146 insertions(+) create mode 100644 test/recv-bundle-pbuf-len-poison.c
A failed IORING_RECVSEND_BUNDLE receive on a non-INC provided-buffer
ring can persistently corrupt the buffer descriptor length. When the
receive fails with -EAGAIN, the kernel writes the requested length into
buf->len during buffer selection but never restores it on failure.
A later unrelated IORING_OP_READ using the same buffer group then
consumes the corrupted length, returning fewer bytes than expected.
This test reproduces the issue as reported by Federico Brasili.
Reported-by: Federico Brasili <federico.brasili@gmail.com>
Link: https://lore.kernel.org/io-uring/CAAEr8jbY60noGj1fw_k91UJRBkyiRVoS6=nLhZ7Svwidjn4CAA@mail.gmail.com/
Signed-off-by: Nyakundi Emmanuel <nyariboemmanuel8@gmail.com>
---
test/recv-bundle-pbuf-len-poison.c | 146 +++++++++++++++++++++++++++++
1 file changed, 146 insertions(+)
create mode 100644 test/recv-bundle-pbuf-len-poison.c
diff --git a/test/recv-bundle-pbuf-len-poison.c b/test/recv-bundle-pbuf-len-poison.c
new file mode 100644
index 00000000..90fafff4
--- /dev/null
+++ b/test/recv-bundle-pbuf-len-poison.c
@@ -0,0 +1,146 @@
+/* SPDX-License-Identifier: MIT */
+/*
+ * Regression test for io_uring provided-buffer ring length corruption.
+ *
+ * A failed IORING_RECVSEND_BUNDLE receive on a non-INC provided-buffer
+ * ring can persistently shrink the user-visible buffer descriptor length.
+ * The modified length is not rolled back when the receive fails with
+ * -EAGAIN, and a later unrelated IORING_OP_READ from a pipe consumes
+ * the corrupted length.
+ *
+ * Reported-by: Federico Brasili <federico.brasili@gmail.com>
+ */
+#include <errno.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+#include <sys/socket.h>
+
+#include "liburing.h"
+#include "helpers.h"
+
+#define BGID 8
+#define BUF_SIZE 4096
+#define NR_BUFS 2
+
+static int test(void)
+{
+ struct io_uring_buf_ring *br;
+ struct io_uring_cqe *cqe;
+ struct io_uring_sqe *sqe;
+ struct io_uring ring;
+ struct io_uring_buf *buf_entry;
+ int sockfd, pipefds[2], ret;
+ void *buf;
+ char pipe_data[BUF_SIZE];
+
+ ret = io_uring_queue_init(8, &ring, 0);
+ if (ret) {
+ fprintf(stderr, "queue init failed: %d\n", ret);
+ return T_EXIT_FAIL;
+ }
+
+ if (posix_memalign(&buf, 4096, BUF_SIZE * NR_BUFS))
+ return T_EXIT_FAIL;
+
+ /* set up non-INC provided buffer ring with 2 buffers of BUF_SIZE */
+ br = io_uring_setup_buf_ring(&ring, NR_BUFS, BGID, 0, &ret);
+ if (!br) {
+ if (ret == -EINVAL)
+ return T_EXIT_SKIP;
+ fprintf(stderr, "buf ring setup failed: %d\n", ret);
+ return T_EXIT_FAIL;
+ }
+
+ io_uring_buf_ring_add(br, buf, BUF_SIZE, 0, NR_BUFS - 1, 0);
+ io_uring_buf_ring_add(br, buf + BUF_SIZE, BUF_SIZE, 1, NR_BUFS - 1, 1);
+ io_uring_buf_ring_advance(br, NR_BUFS);
+
+ /* create an empty SOCK_DGRAM socket to trigger -EAGAIN */
+ sockfd = socket(AF_UNIX, SOCK_DGRAM, 0);
+ if (sockfd < 0) {
+ perror("socket");
+ return T_EXIT_FAIL;
+ }
+
+ /* submit RECV_BUNDLE on empty socket — expects -EAGAIN */
+ sqe = io_uring_get_sqe(&ring);
+ io_uring_prep_recv(sqe, sockfd, NULL, 1, MSG_DONTWAIT);
+ sqe->ioprio |= IORING_RECVSEND_BUNDLE;
+ sqe->flags |= IOSQE_BUFFER_SELECT;
+ sqe->buf_group = BGID;
+ sqe->user_data = 0x1111;
+ io_uring_submit(&ring);
+
+ ret = io_uring_wait_cqe(&ring, &cqe);
+ if (ret) {
+ fprintf(stderr, "wait cqe failed: %d\n", ret);
+ return T_EXIT_FAIL;
+ }
+ if (cqe->res != -EAGAIN) {
+ fprintf(stderr, "expected -EAGAIN, got %d\n", cqe->res);
+ io_uring_cqe_seen(&ring, cqe);
+ return T_EXIT_FAIL;
+ }
+ io_uring_cqe_seen(&ring, cqe);
+
+ /* check entry0.len — must still be BUF_SIZE after failed RECV */
+ buf_entry = &br->bufs[0];
+ if (buf_entry->len != BUF_SIZE) {
+ fprintf(stderr,
+ "FAIL: entry0.len corrupted after -EAGAIN RECV_BUNDLE: "
+ "got %u, expected %u\n",
+ buf_entry->len, BUF_SIZE);
+ return T_EXIT_FAIL;
+ }
+
+ /* now do a pipe READ using the same buffer group */
+ if (pipe(pipefds)) {
+ perror("pipe");
+ return T_EXIT_FAIL;
+ }
+
+ memset(pipe_data, 'A', BUF_SIZE);
+ if (write(pipefds[1], pipe_data, BUF_SIZE) != BUF_SIZE) {
+ fprintf(stderr, "pipe write failed\n");
+ return T_EXIT_FAIL;
+ }
+
+ sqe = io_uring_get_sqe(&ring);
+ io_uring_prep_read(sqe, pipefds[0], NULL, BUF_SIZE, 0);
+ sqe->flags |= IOSQE_BUFFER_SELECT;
+ sqe->buf_group = BGID;
+ sqe->user_data = 0x6666;
+ io_uring_submit(&ring);
+
+ ret = io_uring_wait_cqe(&ring, &cqe);
+ if (ret) {
+ fprintf(stderr, "wait read cqe failed: %d\n", ret);
+ return T_EXIT_FAIL;
+ }
+ if (cqe->res != BUF_SIZE) {
+ fprintf(stderr,
+ "FAIL: READ got %d bytes, expected %d — "
+ "pbuf len was poisoned by failed RECV_BUNDLE\n",
+ cqe->res, BUF_SIZE);
+ io_uring_cqe_seen(&ring, cqe);
+ return T_EXIT_FAIL;
+ }
+ io_uring_cqe_seen(&ring, cqe);
+
+ close(sockfd);
+ close(pipefds[0]);
+ close(pipefds[1]);
+ io_uring_queue_exit(&ring);
+ free(buf);
+ return T_EXIT_PASS;
+}
+
+int main(int argc, char *argv[])
+{
+ if (argc > 1)
+ return T_EXIT_SKIP;
+
+ return test();
+}
--
2.54.0
On 6/7/26 4:10 PM, Nyakundi Emmanuel wrote: > A failed IORING_RECVSEND_BUNDLE receive on a non-INC provided-buffer > ring can persistently corrupt the buffer descriptor length. When the > receive fails with -EAGAIN, the kernel writes the requested length into > buf->len during buffer selection but never restores it on failure. > > A later unrelated IORING_OP_READ using the same buffer group then > consumes the corrupted length, returning fewer bytes than expected. > > This test reproduces the issue as reported by Federico Brasili. Thanks, but I already wrote one, which also tests the much more important aspect of the kernel change - that the reported CQE completion reports the right amount without truncating the buffer length when no bytes have been transferred. And once again, it's not _corrupting_ the buffer length. It's shrinking it, which is unexpected and should not happen, but there's no corruption taking place. I'm dubious on how much AI koolaid was used in reproducing the test case and report? That said, it is something we should fix, as the kernel should not be changing the buffer length for this case. -- Jens Axboe
© 2016 - 2026 Red Hat, Inc.