[PATCH io_uring] io_uring/msg_ring: reject CQE32 flag pass-through to normal rings
From: Melbin K Mathew
Date: Wed Jul 01 2026 - 04:14:30 EST
IORING_OP_MSG_RING with IORING_MSG_RING_FLAGS_PASS allows a sender to
pass completion flags through sqe->file_index. If the sender sets
IORING_CQE_F_32 in file_index, the target-side completion path treats
it as a 32-byte CQE and writes big_cqe[0] and big_cqe[1] into the CQ
ring regardless of whether the target ring was created with
IORING_SETUP_CQE32 or IORING_SETUP_CQE_MIXED.
On a normal 16-byte CQE ring, this writes 16 extra bytes (two u64
big_cqe fields) into the next CQE slot in the ring buffer. This was
confirmed by poisoning the adjacent slot with known values and observing
them being overwritten after the pass-through, while the CQ tail
advanced by only one slot.
Add a helper io_msg_ring_cqe_flags() that validates the flags before
they are forwarded, and use it in both the local and remote data paths.
Reject with -EINVAL when IORING_CQE_F_32 is passed to a ring that does
not support CQE32 or mixed CQE modes. In the remote path, this guard
runs before the target request allocation to avoid a leak on error.
Fixes: cbeb47a7b5f0 ("io_uring/msg_ring: Pass custom flags to the cqe")
Signed-off-by: Melbin K Mathew <mlbnkm1@xxxxxxxxx>
---
Notes:
Testing on Linux 7.2-rc1 with KASAN and UBSAN enabled:
Unpatched kernel: MSG_RING with IORING_MSG_RING_FLAGS_PASS and
IORING_CQE_F_32 posts CQE_F_32 to a normal target ring. Target CQ
tail advances by one while the adjacent unpublished CQ slot is
overwritten (confirmed by poisoning the adjacent slot with known
values and observing them zeroed after the pass-through).
Patched kernel: sender receives -EINVAL. Target CQ tail does not
advance. Adjacent poisoned slot remains intact.
git apply --check: OK
checkpatch --strict: 0 errors, 0 warnings, 0 checks
make W=1 io_uring/msg_ring.o: clean
io_uring/msg_ring.c | 34 +++++++++++++++++++++++++++-------
1 file changed, 27 insertions(+), 7 deletions(-)
diff --git a/io_uring/msg_ring.c b/io_uring/msg_ring.c
index 3ff9098573..3067c93439 100644
--- a/io_uring/msg_ring.c
+++ b/io_uring/msg_ring.c
@@ -93,19 +93,38 @@ static void io_msg_remote_post(struct io_ring_ctx *ctx, struct io_kiocb *req,
io_req_task_work_add_remote(req, IOU_F_TWQ_LAZY_WAKE);
}
+static int io_msg_ring_cqe_flags(struct io_ring_ctx *target_ctx,
+ const struct io_msg *msg, u32 *flags)
+{
+ *flags = 0;
+
+ if (!(msg->flags & IORING_MSG_RING_FLAGS_PASS))
+ return 0;
+
+ *flags = msg->cqe_flags;
+ if ((*flags & IORING_CQE_F_32) &&
+ !(target_ctx->flags & (IORING_SETUP_CQE32 |
+ IORING_SETUP_CQE_MIXED)))
+ return -EINVAL;
+
+ return 0;
+}
+
static int io_msg_data_remote(struct io_ring_ctx *target_ctx,
struct io_msg *msg)
{
struct io_kiocb *target;
- u32 flags = 0;
+ u32 flags;
+ int ret;
- target = kmem_cache_alloc(req_cachep, GFP_KERNEL | __GFP_NOWARN | __GFP_ZERO) ;
+ ret = io_msg_ring_cqe_flags(target_ctx, msg, &flags);
+ if (ret)
+ return ret;
+
+ target = kmem_cache_alloc(req_cachep, GFP_KERNEL | __GFP_NOWARN | __GFP_ZERO);
if (unlikely(!target))
return -ENOMEM;
- if (msg->flags & IORING_MSG_RING_FLAGS_PASS)
- flags = msg->cqe_flags;
-
io_msg_remote_post(target_ctx, target, msg->len, flags, msg->user_data);
return 0;
}
@@ -130,8 +149,9 @@ static int __io_msg_ring_data(struct io_ring_ctx *target_ctx,
if (io_msg_need_remote(target_ctx))
return io_msg_data_remote(target_ctx, msg);
- if (msg->flags & IORING_MSG_RING_FLAGS_PASS)
- flags = msg->cqe_flags;
+ ret = io_msg_ring_cqe_flags(target_ctx, msg, &flags);
+ if (ret)
+ return ret;
ret = -EOVERFLOW;
if (target_ctx->flags & IORING_SETUP_IOPOLL) {
--
2.39.5