[PATCH] test/recv-bundle-pbuf-len-poison: add regression test for pbuf len corruption

From: Nyakundi Emmanuel

Date: Sun Jun 07 2026 - 18:14:23 EST


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@xxxxxxxxx>
Link: https://lore.kernel.org/io-uring/CAAEr8jbY60noGj1fw_k91UJRBkyiRVoS6=nLhZ7Svwidjn4CAA@xxxxxxxxxxxxxx/
Signed-off-by: Nyakundi Emmanuel <nyariboemmanuel8@xxxxxxxxx>
---
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@xxxxxxxxx>
+ */
+#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