From: sashiko-bot@kernel.org
To: "Alex Markuze" <amarkuze@redhat.com>
Cc: bpf@vger.kernel.org
Subject: Re: [RFC PATCH net-next 09/12] io_uring/opaque: add IORING_OP_OPAQUE_COLLECT
Date: Wed, 11 Nov 2026 13:20:46 +0000 [thread overview]
Message-ID: <sashiko-outbox-209066@kernel.org> (raw)
In-Reply-To: <20261110132006.3280986-10-amarkuze@redhat.com>
Thank you for your contribution! Sashiko AI review found 4 potential
issues to consider:
- [High] io_uring/opaque: infinite io-wq/task_work loop in
io_opaque_recv()
- [Medium] io_uring/opaque: wrong need_bytes in io_opaque_append() on
tail reuse
- [Medium] io_uring/opaque: stale hold_undecided in io_opaque_actor()
after decision
- [Medium] io_uring/opaque: POLL_FIRST collector ignores sock_rx_owned()
in poll
--
commit 8d32c303015f5762780ff1306cc30dacc6d593bb
Author: Alex Markuze <amarkuze@redhat.com>
io_uring/opaque: add IORING_OP_OPAQUE_COLLECT
This commit adds the IORING_OP_OPAQUE_COLLECT multishot operation to attach
a connected TCP socket to an opaque store and capture its receive stream
into decisions or undecided extents using native multishot socket polling.
> diff --git a/io_uring/opaque.c b/io_uring/opaque.c
> index 0c58900073f72..4610e212a8005 100644
> --- a/io_uring/opaque.c
> +++ b/io_uring/opaque.c
> @@ -49,6 +50,38 @@ static void io_opaque_vec_cleanup(struct io_opaque_req *op);
> static void io_opaque_stream_close(struct io_opaque_stream *stream);
> static void io_opaque_tx_put(struct io_opaque_req *op);
>
> +/*
> + * Publish before waking; native poll enrolls before reading collect_ready.
> + * This is not TCP readiness: only COLLECT and receive-owner-affected apoll
> + * recheck their predicates. Ordinary poll and output apoll ignore the key.
> + */
> +static void io_opaque_collect_wake(struct io_opaque_stream *stream)
> +{
> + struct socket *sock = sock_from_file(stream->file);
> +
> + atomic_set(&stream->collect_ready, 1);
> + wake_up_interruptible_poll(sk_sleep(sock->sk), POLL_RX_OWNER);
> +}
> +
> +static void io_opaque_data_ready(struct sock *sk, void *data)
> +{
> + struct io_opaque_stream *stream = data;
> +
> + /* sk_callback_lock pins stream; do not take stream or socket locks here. */
> + atomic_set(&stream->collect_ready, 1);
> + wake_up_interruptible_poll(sk_sleep(sk), POLL_RX_OWNER);
> +}
> +
> +__poll_t io_opaque_collect_poll(struct io_kiocb *req)
> +{
> + struct io_opaque_req *op = req->async_data;
> +
> + /* io-wq may poll before the first issue, including a trailing link. */
> + if (!op->stream)
> + return op->collect_flags & IORING_RECVSEND_POLL_FIRST ? 0 : EPOLLIN;
> + return atomic_read(&op->stream->collect_ready) ? EPOLLIN : 0;
> +}
[Severity: Medium]
When an unattached IORING_OP_OPAQUE_COLLECT request with
IORING_RECVSEND_POLL_FIRST is waiting in apoll, does
io_opaque_collect_poll() need to return EPOLLIN if sock_rx_owned(sock->sk)
is true?
In io_poll_mask(), req->opcode == IORING_OP_OPAQUE_COLLECT calls
io_opaque_collect_poll() instead of io_poll_rx_owned(). If another
collector is already attached or attaches to the same socket and wakes
sk_sleep(sock->sk) with POLL_RX_OWNER, io_opaque_collect_poll() returns 0
for the unattached POLL_FIRST collector, so io_poll_check_events() goes
back to sleep when the receive queue is empty instead of issuing
io_opaque_recv() -> io_opaque_attach() and failing with -EBUSY.
[ ... ]
> @@ -1158,6 +1188,269 @@ static void io_opaque_stream_process(struct io_opaque_stream *stream)
> }
> }
>
> +/* Only the earliest undecided range is consulted on the capture hot path. */
> +static struct io_opaque_req *io_opaque_next_decision(struct io_opaque_stream *stream)
> +{
> + struct rb_node *node = rb_first_cached(&stream->decisions);
> +
> + return node ? rb_entry(node, struct io_opaque_req, decision) : NULL;
> +}
> +
> +/* Undecided bytes are collected only when nothing is pending or a request needs later bytes. */
> +static bool io_opaque_collect_demand(struct io_opaque_stream *stream)
> +{
> + struct io_opaque_req *op = io_opaque_next_decision(stream);
> +
> + lockdep_assert_held(&stream->lock);
> + return !stream->undecided || !list_empty(&stream->reads) ||
> + (op && op->offset > stream->next);
> +}
[Severity: Medium]
Does setting stream->hold_undecided once per pass from
io_opaque_collect_demand() allow io_opaque_actor() to keep draining
trailing undecided bytes after a pending decision completes?
When a pass starts with a pending decision at op->offset > stream->next,
io_opaque_collect_demand() returns true and sets stream->hold_undecided =
false for the entire tcp_read_sock() call, even when stream->reads is empty.
Once io_opaque_actor() captures the gap and finishes that decision (leaving
op == NULL and stream->undecided > 0), the "if (stream->hold_undecided &&
!op) break;" check in io_opaque_actor() does not stop, so capture continues
pulling up to 256 KiB of undecided bytes off the socket instead of applying
TCP flow control.
> +
> +static struct io_opaque_req *io_opaque_capture_keep(struct io_opaque_stream *stream)
> +{
> + struct io_opaque_req *op = io_opaque_next_decision(stream);
> +
> + if (op && op->op == IORING_OPAQUE_RECV_OBJECT && stream->next >= op->offset &&
> + stream->next < op->offset + op->length)
> + return op;
> + return NULL;
> +}
> +
> +static struct io_opaque_extent *io_opaque_capture_tail(struct io_opaque_stream *stream)
> +{
> + struct io_opaque_req *op = io_opaque_capture_keep(stream);
> + struct list_head *list = op ? &op->data->extents : &stream->extents;
> +
> + return list_last_entry_or_null(list, struct io_opaque_extent, list);
> +}
> +
> +static int io_opaque_append(struct io_opaque_stream *stream, struct page *page,
> + u32 offset, u32 length, bool take_ref, bool copied)
> +{
> + struct io_opaque_req *op = io_opaque_capture_keep(stream);
> + struct list_head *list = &stream->extents;
> + struct io_opaque_extent *extent, *tail;
> + u64 start = stream->next;
> + bool new_backing = false;
> +
> + if (op) {
> + list = &op->data->extents;
> + start -= op->offset;
> + }
> + tail = list_last_entry_or_null(list, struct io_opaque_extent, list);
> + offset += (page - compound_head(page)) * PAGE_SIZE;
> + page = compound_head(page);
> + if (tail && tail->backing->page == page) {
> + if (tail->start + tail->length == start &&
> + tail->offset + tail->length == offset) {
> + tail->length += length;
> + extent = tail;
> + goto accounted;
> + }
> + }
> + stream->need_bytes = page_size(page);
> + if (atomic_read(&stream->store->extents) >= stream->store->config.max_extents - 2)
> + return -ENOBUFS;
> + if (tail && tail->backing->page == page) {
> + if (!io_opaque_extent_reserve(stream->store))
> + return -ENOBUFS;
[Severity: Medium]
Should stream->need_bytes be set to 0 when tail && tail->backing->page ==
page reuses an already-charged backing page and only needs a new extent
slot?
If a stream reuses tail->backing across a non-coalescing boundary (such as
after an intervening IORING_OPAQUE_DISCARD advances stream->next) and hits
the max_extents - 2 limit, io_opaque_append() sets stream->need_bytes =
page_size(page) even though no additional byte budget is needed. If the
store's byte usage is within page_size(page) of io_opaque_rx_limit(store),
io_opaque_budget_ready() will see op->need_bytes > limit - used and keep
the collector stalled in IO_OPAQUE_BUDGET_POLL even after extent slots are
freed; on the zero-copy path in io_opaque_capture(), -ENOBUFS from extent
exhaustion here also falls through to copy: and charges a redundant page.
[ ... ]
> +int io_opaque_recv(struct io_kiocb *req, unsigned int issue_flags)
> +{
> + struct io_opaque_req *op = req->async_data;
> + struct io_opaque_stream *stream;
> + struct socket *sock = sock_from_file(req->file);
> + read_descriptor_t desc = { .count = IO_OPAQUE_RX_BATCH };
> + struct mem_cgroup *old;
> + bool blocked;
> + int ret;
> +
> + io_ring_submit_lock(req->ctx, issue_flags);
> + if (!(req->flags & REQ_F_POLLED) &&
> + (op->collect_flags & IORING_RECVSEND_POLL_FIRST)) {
> + ret = -EAGAIN;
> + goto out;
> + }
> + if (!op->stream && (issue_flags & IO_URING_F_UNLOCKED)) {
> + io_req_task_queue(req);
> + ret = IOU_ISSUE_SKIP_COMPLETE;
> + goto out;
> + }
[Severity: High]
Can an unattached IORING_OP_OPAQUE_COLLECT request loop forever between
io-wq and task_work when REQ_F_FORCE_ASYNC is set on it after prep?
Although io_opaque_recv_prep() rejects REQ_F_FORCE_ASYNC, if
IORING_OP_OPAQUE_COLLECT is the head of a link chain and a later SQE in the
link sets IOSQE_IO_DRAIN, io_init_drain() sets REQ_F_FORCE_ASYNC on the
head after prep:
io_uring/io_uring.c:io_init_drain() {
if (head) {
head->flags |= REQ_F_IO_DRAIN | REQ_F_FORCE_ASYNC;
ctx->int_flags |= IO_RING_F_DRAIN_NEXT;
}
}
When that request runs on a pollable file without O_NONBLOCK or
FMODE_NOWAIT, io_wq_submit_work() calls io_opaque_recv() with
IO_URING_F_UNLOCKED, which calls io_req_task_queue(req) without clearing
REQ_F_FORCE_ASYNC, and io_req_task_submit() sees req->flags &
REQ_F_FORCE_ASYNC and queues the request right back to io-wq in an infinite
loop. In addition, because socket validation happens later in
io_opaque_attach(), submitting IORING_OP_OPAQUE_COLLECT with
IORING_RECVSEND_POLL_FIRST on a non-pollable non-socket file returns
-EAGAIN here and fails in io-wq with -EBADFD instead of -EOPNOTSUPP.
--
Sashiko AI review · https://sashiko.dev/#/patchset/20261110132006.3280986-1-amarkuze@redhat.com?part=9
next prev parent reply other threads:[~2026-11-11 13:20 UTC|newest]
Thread overview: 18+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-11-10 13:19 [RFC PATCH net-next 00/12] io_uring: opaque TCP payload objects Alex Markuze
2026-11-10 13:19 ` [RFC PATCH net-next 01/12] net: add an exclusive TCP receive claim Alex Markuze
2026-11-11 13:20 ` sashiko-bot
2026-11-10 13:19 ` [RFC PATCH net-next 02/12] io_uring/opaque: add data structures and the UAPI header Alex Markuze
2026-11-10 13:19 ` [RFC PATCH net-next 03/12] io_uring/opaque: add backing accounting and immutable versions Alex Markuze
2026-11-11 13:20 ` sashiko-bot
2026-11-10 13:19 ` [RFC PATCH net-next 04/12] io_uring/opaque: add owned-page helpers for dense copies Alex Markuze
2026-11-10 13:19 ` [RFC PATCH net-next 05/12] io_uring/opaque: add store registration and IORING_OP_OPAQUE_OBJ Alex Markuze
2026-11-10 13:20 ` [RFC PATCH net-next 06/12] io_uring/opaque: allow rings to share a store Alex Markuze
2026-11-10 13:20 ` [RFC PATCH net-next 07/12] io_uring/opaque: add IORING_OP_OPAQUE_OBJ_SEND Alex Markuze
2026-11-11 13:20 ` sashiko-bot
2026-11-10 13:20 ` [RFC PATCH net-next 08/12] io_uring/opaque: add vectored IORING_OP_OPAQUE_OBJ_SEND Alex Markuze
2026-11-10 13:20 ` [RFC PATCH net-next 09/12] io_uring/opaque: add IORING_OP_OPAQUE_COLLECT Alex Markuze
2026-11-11 13:20 ` sashiko-bot [this message]
2026-11-10 13:20 ` [RFC PATCH net-next 10/12] io_uring/opaque: add IORING_OPAQUE_CREATE Alex Markuze
2026-11-11 13:20 ` sashiko-bot
2026-11-10 13:20 ` [RFC PATCH net-next 11/12] io_uring/opaque: add manual and automatic compaction Alex Markuze
2026-11-10 13:20 ` [RFC PATCH net-next 12/12] io_uring/opaque: add basic KUnit tests Alex Markuze
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=sashiko-outbox-209066@kernel.org \
--to=sashiko-bot@kernel.org \
--cc=amarkuze@redhat.com \
--cc=bpf@vger.kernel.org \
--cc=sashiko-reviews@lists.linux.dev \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox