diff options
| author | Josef Bacik <josef@toxicpanda.com> | 2026-10-15 19:30:38 +0000 |
|---|---|---|
| committer | Jens Axboe <axboe@kernel.dk> | 2026-10-15 14:17:56 -0600 |
| commit | 6d2d4c2530e95ff6ddeb98195d49d99dbca14369 (patch) | |
| tree | 0746d9b378019c83b0d7a553d71b727d2a81a0d4 /tools/lib/python | |
| parent | e9efeb4fc97b0879512a3c3b995ed448f2487e12 (diff) | |
io_uring/poll: don't lose a cancel as io-wq arms poll
A request on a pollable file that was forced to io-wq is issued without
blocking there. If that ends in -EAGAIN, io_wq_submit_work() arms poll
for it and returns. It looks at IO_WQ_WORK_CANCEL once, before it issues
the request.
Somebody who wants to cancel the request in between finds it running:
io-wq worker io_try_cancel()
io_wq_submit_work()
IO_WQ_WORK_CANCEL isn't set
io_issue_sqe() = -EAGAIN
io_async_cancel_one()
sets IO_WQ_WORK_CANCEL
signals the worker
= -EALREADY
io_poll_cancel() = -ENOENT
...
= -ENOENT
io_arm_poll_handler() = IO_APOLL_OK
return
Nobody looks at the flag again, and the signal doesn't matter to a
worker that doesn't sleep. The request is armed as if nothing had
happened:
- a linked timeout completes with -ENOENT, and the request it was
meant to limit waits for as long as it takes.
- IORING_OP_ASYNC_CANCEL returns -EALREADY, and the request stays.
- IORING_REGISTER_SYNC_CANCEL waits for the request to complete if it
gets -EALREADY. Since commit 5623eb1ed035 ("io_uring/tctx: add
separate lock for list of tctx's in ctx") it doesn't sleep but spins,
so its next attempt finds the armed poll.
The same goes for a multishot poll, recv or accept that is armed from
io-wq.
With a read of an empty pipe, IOSQE_ASYNC, and a linked timeout of 5ms
or a cancel in the same submission, in a guest that is slow enough (TCG,
KASAN), the request is lost in 21 of 24 rounds of 40 or 100 attempts.
While it arms poll from io-wq, __io_arm_poll_handler() owns the request.
Look at the flag once the request is in the hash table. The table is
searched and the request is added with ->uring_lock held, and the flag is
set before the search. So either the search finds the request, or it
came first and the flag is seen. With the request marked as cancelled, the
task work that is queued, right away or because ownership can't be
released, fails it with -ECANCELED.
What the canceller is told doesn't change. In this window a linked timeout
still completes with -ENOENT rather than -ETIME, a cancel request with
-EALREADY, and one with IORING_ASYNC_CANCEL_ALL or _ANY with a count that
doesn't include the request. But the request is cancelled, so what is new
is a request that completes with -ECANCELED after such a result.
This doesn't help in two cases:
- IORING_ASYNC_CANCEL_ALL and _ANY, if the work is flagged after the
test added here, while io-wq still has it as the current work of the
worker. io_cancel_cb() stamps it with the sequence of the cancel, so the
search of the hash table on behalf of the same cancel takes it for
done and skips it.
- requests that are parked elsewhere by io-wq, like IORING_OP_FUTEX_WAIT
and IORING_OP_WAITID. They have the same problem.
Fixes: 90fa02883f06 ("io_uring: implement async hybrid mode for pollable requests")
Assisted-by: LLM
Signed-off-by: Josef Bacik <josef@toxicpanda.com>
Link: https://patch.msgid.link/20261015-b4-io-uring-fixes-v1-14-c1556f5d7bf5@toxicpanda.com
Signed-off-by: Jens Axboe <axboe@kernel.dk>
Diffstat (limited to 'tools/lib/python')
0 files changed, 0 insertions, 0 deletions
