* [PATCH 1/7] io_uring/uring_cmd: only cancel requests of the given task
2026-09-09 14:06 [PATCHSET] Cancel requests at ring close time Jens Axboe
@ 2026-09-09 14:06 ` Jens Axboe
2026-09-09 14:06 ` [PATCH 2/7] io_uring/notif: count pending zerocopy notifications per ring Jens Axboe
` (5 subsequent siblings)
6 siblings, 0 replies; 8+ messages in thread
From: Jens Axboe @ 2026-09-09 14:06 UTC (permalink / raw)
To: io-uring; +Cc: juanlu, Jens Axboe
io_uring_try_cancel_uring_cmd() ignores the task it is asked to cancel
on behalf of as soon as cancel_all is true, and cancels every cancelable
command on the ring instead. That differs from how other requests are
matched, where a valid task always restricts matching to that task's
requests and cancel_all simply lifts the REQ_F_INFLIGHT restriction.
So far the only caller passing both a task and cancel_all is exec, where
tearing down another task's commands on a shared ring is somewhat
impolite. The exit path is about to do the same for every exiting
process though, so bring it in line. Match on the task if one is given,
and cancel everything only for ring teardown, which passes NULL. This
leaves cancel_all as useless, hence drop it.
Signed-off-by: Jens Axboe <axboe@kernel.dk>
---
io_uring/cancel.c | 2 +-
io_uring/uring_cmd.c | 4 ++--
io_uring/uring_cmd.h | 2 +-
3 files changed, 4 insertions(+), 4 deletions(-)
diff --git a/io_uring/cancel.c b/io_uring/cancel.c
index 7d7820eab878..5ee94246c43c 100644
--- a/io_uring/cancel.c
+++ b/io_uring/cancel.c
@@ -560,7 +560,7 @@ __cold bool io_uring_try_cancel_requests(struct io_ring_ctx *ctx,
ret |= io_poll_remove_all(ctx, tctx, cancel_all);
ret |= io_waitid_remove_all(ctx, tctx, cancel_all);
ret |= io_futex_remove_all(ctx, tctx, cancel_all);
- ret |= io_uring_try_cancel_uring_cmd(ctx, tctx, cancel_all);
+ ret |= io_uring_try_cancel_uring_cmd(ctx, tctx);
ret |= io_kill_timeouts(ctx, tctx, cancel_all);
mutex_unlock(&ctx->uring_lock);
if (tctx)
diff --git a/io_uring/uring_cmd.c b/io_uring/uring_cmd.c
index 726a659f38c3..28a87480626c 100644
--- a/io_uring/uring_cmd.c
+++ b/io_uring/uring_cmd.c
@@ -49,7 +49,7 @@ void io_uring_cmd_cleanup(struct io_kiocb *req)
}
bool io_uring_try_cancel_uring_cmd(struct io_ring_ctx *ctx,
- struct io_uring_task *tctx, bool cancel_all)
+ struct io_uring_task *tctx)
{
struct hlist_node *tmp;
struct io_kiocb *req;
@@ -63,7 +63,7 @@ bool io_uring_try_cancel_uring_cmd(struct io_ring_ctx *ctx,
struct io_uring_cmd);
struct file *file = req->file;
- if (!cancel_all && req->tctx != tctx)
+ if (tctx && req->tctx != tctx)
continue;
if (cmd->flags & IORING_URING_CMD_CANCELABLE) {
diff --git a/io_uring/uring_cmd.h b/io_uring/uring_cmd.h
index 041aef8a8aa3..a5cb3f2ee1c5 100644
--- a/io_uring/uring_cmd.h
+++ b/io_uring/uring_cmd.h
@@ -14,7 +14,7 @@ void io_uring_cmd_sqe_copy(struct io_kiocb *req);
void io_uring_cmd_cleanup(struct io_kiocb *req);
bool io_uring_try_cancel_uring_cmd(struct io_ring_ctx *ctx,
- struct io_uring_task *tctx, bool cancel_all);
+ struct io_uring_task *tctx);
bool io_uring_cmd_post_mshot_cqe32(struct io_uring_cmd *cmd,
unsigned int issue_flags,
--
2.55.0
^ permalink raw reply related [flat|nested] 8+ messages in thread* [PATCH 2/7] io_uring/notif: count pending zerocopy notifications per ring
2026-09-09 14:06 [PATCHSET] Cancel requests at ring close time Jens Axboe
2026-09-09 14:06 ` [PATCH 1/7] io_uring/uring_cmd: only cancel requests of the given task Jens Axboe
@ 2026-09-09 14:06 ` Jens Axboe
2026-09-09 14:06 ` [PATCH 3/7] io_uring/cancel: cancel and wait for all requests on process exit Jens Axboe
` (4 subsequent siblings)
6 siblings, 0 replies; 8+ messages in thread
From: Jens Axboe @ 2026-09-09 14:06 UTC (permalink / raw)
To: io-uring; +Cc: juanlu, Jens Axboe
SEND_ZC notifications are io_kiocbs like any other as far as request
and task accounting goes, but they are special in that they cannot be
canceled and only complete once the network stack is done with the
data. With an unresponsive peer, that can take a while.
The exit and release paths are about to start waiting for requests to
finish so that the files they pin get released in a timely manner, and
notifications neither pin files nor finish in a timely manner. Keep a
per-ring count of them so those paths can leave them out.
Signed-off-by: Jens Axboe <axboe@kernel.dk>
---
include/linux/io_uring_types.h | 2 ++
io_uring/notif.c | 2 ++
2 files changed, 4 insertions(+)
diff --git a/include/linux/io_uring_types.h b/include/linux/io_uring_types.h
index 39629ee77b91..90fea94ad202 100644
--- a/include/linux/io_uring_types.h
+++ b/include/linux/io_uring_types.h
@@ -522,6 +522,8 @@ struct io_ring_ctx {
/* protected by ->completion_lock */
unsigned nr_req_allocated;
+ /* pending SEND_ZC notifications, protected by ->uring_lock */
+ unsigned nr_notifs;
#ifdef CONFIG_NET_RX_BUSY_POLL
struct list_head napi_list; /* track busy poll napi_id */
diff --git a/io_uring/notif.c b/io_uring/notif.c
index efce8ae12eaa..6c49aff515df 100644
--- a/io_uring/notif.c
+++ b/io_uring/notif.c
@@ -36,6 +36,7 @@ static void io_notif_tw_complete(struct io_tw_req tw_req, io_tw_token_t tw)
}
nd = nd->next;
+ ctx->nr_notifs--;
io_req_task_complete((struct io_tw_req){notif}, tw);
} while (nd);
}
@@ -119,6 +120,7 @@ struct io_kiocb *io_alloc_notif(struct io_ring_ctx *ctx)
if (unlikely(!io_alloc_req(ctx, ¬if)))
return NULL;
+ ctx->nr_notifs++;
notif->ctx = ctx;
notif->opcode = IORING_OP_NOP;
notif->flags = 0;
--
2.55.0
^ permalink raw reply related [flat|nested] 8+ messages in thread* [PATCH 3/7] io_uring/cancel: cancel and wait for all requests on process exit
2026-09-09 14:06 [PATCHSET] Cancel requests at ring close time Jens Axboe
2026-09-09 14:06 ` [PATCH 1/7] io_uring/uring_cmd: only cancel requests of the given task Jens Axboe
2026-09-09 14:06 ` [PATCH 2/7] io_uring/notif: count pending zerocopy notifications per ring Jens Axboe
@ 2026-09-09 14:06 ` Jens Axboe
2026-09-09 14:06 ` [PATCH 4/7] io_uring: run cancelations synchronously on ring release Jens Axboe
` (3 subsequent siblings)
6 siblings, 0 replies; 8+ messages in thread
From: Jens Axboe @ 2026-09-09 14:06 UTC (permalink / raw)
To: io-uring; +Cc: juanlu, Jens Axboe
When a task exits, io_uring only cancels and waits for the few request
types that must not outlive it, and leaves everything else to be torn
down whenever the ring itself goes away. This happens from a kernel
workqueue, and may be some time after the final fput of the ring has
been completed. This sometimes causes application issues, where a task
that was doing IO on a file in /mnt, for example, will leave /mnt busy
for a brief period of time after close(ring_fd) is done.
If the whole thread group is exiting, no thread is left to reap
completions or care about those requests. For this case, tear everything
down. This is basically what exec does today.
Two types of requests are ignored, as they never pin any files and will
always complete on their own. One is armed timeouts, which may still be
required to trigger if a ring is being shared, and the other is SEND_ZC
notifications, which can take almost an unbounded time to complete.
io_uring_try_cancel_requests() now takes flags to manage that behavior.
While in there, fix up the wait loop for the exit case. A task that is
exiting because of a fatal signal still has that signal pending, which
turns the interruptible sleep into a busy loop. Use short
uninterruptible sleeps in that case instead, task_work still gets run in
between.
Signed-off-by: Jens Axboe <axboe@kernel.dk>
---
io_uring/cancel.c | 87 ++++++++++++++++++++++++++++++++++++++-------
io_uring/cancel.h | 13 ++++++-
io_uring/io_uring.c | 2 +-
io_uring/timeout.c | 18 ++++++++++
io_uring/timeout.h | 2 ++
5 files changed, 108 insertions(+), 14 deletions(-)
diff --git a/io_uring/cancel.c b/io_uring/cancel.c
index 5ee94246c43c..911a47075064 100644
--- a/io_uring/cancel.c
+++ b/io_uring/cancel.c
@@ -514,8 +514,9 @@ static __cold bool io_uring_try_cancel_iowq(struct io_ring_ctx *ctx)
__cold bool io_uring_try_cancel_requests(struct io_ring_ctx *ctx,
struct io_uring_task *tctx,
- bool cancel_all, bool is_sqpoll_thread)
+ unsigned int flags)
{
+ bool cancel_all = flags & IO_CANCEL_ALL;
struct io_task_cancel cancel = { .tctx = tctx, .all = cancel_all, };
enum io_wq_cancel cret;
bool ret = false;
@@ -544,7 +545,7 @@ __cold bool io_uring_try_cancel_requests(struct io_ring_ctx *ctx,
/* SQPOLL thread does its own polling */
if ((!(ctx->flags & IORING_SETUP_SQPOLL) && cancel_all) ||
- is_sqpoll_thread) {
+ (flags & IO_CANCEL_SQPOLL)) {
while (!list_empty(&ctx->iopoll_list)) {
io_iopoll_try_reap_events(ctx);
ret = true;
@@ -561,6 +562,8 @@ __cold bool io_uring_try_cancel_requests(struct io_ring_ctx *ctx,
ret |= io_waitid_remove_all(ctx, tctx, cancel_all);
ret |= io_futex_remove_all(ctx, tctx, cancel_all);
ret |= io_uring_try_cancel_uring_cmd(ctx, tctx);
+ if (flags & IO_CANCEL_KEEP_TIMEOUTS)
+ cancel_all = false;
ret |= io_kill_timeouts(ctx, tctx, cancel_all);
mutex_unlock(&ctx->uring_lock);
if (tctx)
@@ -575,6 +578,44 @@ static s64 tctx_inflight(struct io_uring_task *tctx, bool tracked)
return percpu_counter_sum(&tctx->inflight);
}
+/*
+ * If true, whole thread group is exiting, at which point no task is left that
+ * can reap completions and care about requests in-flight.
+ */
+static bool io_task_group_exiting(void)
+{
+ return current->signal->flags & SIGNAL_GROUP_EXIT;
+}
+
+/*
+ * Return a count of requests an exiting task should wait for.
+ */
+static s64 tctx_inflight_exit(struct io_uring_task *tctx)
+{
+ struct io_tctx_node *node;
+ unsigned long index;
+ s64 inflight;
+
+ inflight = tctx_inflight(tctx, false);
+ xa_for_each(&tctx->xa, index, node) {
+ /* unlocked read is fine, the caller re-evaluates until done */
+ inflight -= data_race(node->ctx->nr_notifs);
+ /* takes ->uring_lock, we hold nothing on the group exit path */
+ inflight -= io_timeouts_armed(node->ctx, tctx);
+ }
+ return inflight;
+}
+
+static bool io_tctx_cancel_done(struct io_uring_task *tctx, bool cancel_all,
+ bool group_exit)
+{
+ if (cancel_all)
+ return !tctx_inflight(tctx, false);
+ if (tctx_inflight(tctx, true))
+ return false;
+ return !group_exit || tctx_inflight_exit(tctx) <= 0;
+}
+
/*
* Find any io_uring ctx that this task has registered or done IO on, and cancel
* requests. @sqd should be not-null IFF it's an SQPOLL thread cancellation.
@@ -584,7 +625,9 @@ __cold void io_uring_cancel_generic(bool cancel_all, struct io_sq_data *sqd)
struct io_uring_task *tctx = current->io_uring;
struct io_ring_ctx *ctx;
struct io_tctx_node *node;
+ unsigned int flags = 0;
unsigned long index;
+ bool group_exit;
s64 inflight;
DEFINE_WAIT(wait);
@@ -595,18 +638,30 @@ __cold void io_uring_cancel_generic(bool cancel_all, struct io_sq_data *sqd)
if (tctx->io_wq)
io_wq_exit_start(tctx->io_wq);
+ /*
+ * If a whole thread group is exiting, nobody will look at completions.
+ * If a single thread is exiting, cancel only those that belong to that
+ * thread.
+ */
+ group_exit = !cancel_all && io_task_group_exiting();
+ if (cancel_all)
+ flags = IO_CANCEL_ALL;
+ else if (group_exit)
+ flags = IO_CANCEL_ALL | IO_CANCEL_KEEP_TIMEOUTS;
+ if (sqd)
+ flags |= IO_CANCEL_SQPOLL;
+
atomic_inc(&tctx->in_cancel);
do {
bool loop = false;
+ unsigned int state;
io_uring_drop_tctx_refs(current);
- if (!tctx_inflight(tctx, !cancel_all))
+ if (io_tctx_cancel_done(tctx, cancel_all, group_exit))
break;
/* read completions before cancelations */
inflight = tctx_inflight(tctx, false);
- if (!inflight)
- break;
if (!sqd) {
xa_for_each(&tctx->xa, index, node) {
@@ -615,15 +670,13 @@ __cold void io_uring_cancel_generic(bool cancel_all, struct io_sq_data *sqd)
continue;
loop |= io_uring_try_cancel_requests(node->ctx,
current->io_uring,
- cancel_all,
- false);
+ flags);
}
} else {
list_for_each_entry(ctx, &sqd->ctx_list, sqd_list)
loop |= io_uring_try_cancel_requests(ctx,
current->io_uring,
- cancel_all,
- true);
+ flags);
}
if (loop) {
@@ -631,7 +684,12 @@ __cold void io_uring_cancel_generic(bool cancel_all, struct io_sq_data *sqd)
continue;
}
- prepare_to_wait(&tctx->wait, &wait, TASK_INTERRUPTIBLE);
+ state = TASK_INTERRUPTIBLE;
+ if (task_sigpending(current))
+ state = TASK_UNINTERRUPTIBLE;
+ if (!cancel_all)
+ state |= TASK_FREEZABLE;
+ prepare_to_wait(&tctx->wait, &wait, state);
io_run_task_work();
io_uring_drop_tctx_refs(current);
xa_for_each(&tctx->xa, index, node) {
@@ -646,8 +704,13 @@ __cold void io_uring_cancel_generic(bool cancel_all, struct io_sq_data *sqd)
* avoids a race where a completion comes in before we did
* prepare_to_wait().
*/
- if (inflight == tctx_inflight(tctx, !cancel_all))
- schedule();
+ if (inflight == tctx_inflight(tctx, false)) {
+ unsigned long timeout = 1;
+
+ if (state & TASK_INTERRUPTIBLE)
+ timeout = MAX_SCHEDULE_TIMEOUT;
+ schedule_timeout(timeout);
+ }
end_wait:
finish_wait(&tctx->wait, &wait);
} while (1);
diff --git a/io_uring/cancel.h b/io_uring/cancel.h
index 1b201a094303..e49713a1bc66 100644
--- a/io_uring/cancel.h
+++ b/io_uring/cancel.h
@@ -30,9 +30,20 @@ bool io_cancel_remove_all(struct io_ring_ctx *ctx, struct io_uring_task *tctx,
int io_cancel_remove(struct io_ring_ctx *ctx, struct io_cancel_data *cd,
unsigned int issue_flags, struct hlist_head *list,
bool (*cancel)(struct io_kiocb *));
+
+/* io_uring_try_cancel_requests() flags */
+enum {
+ /* match all requests, not just REQ_F_INFLIGHT */
+ IO_CANCEL_ALL = 1,
+ /* ignore timeouts */
+ IO_CANCEL_KEEP_TIMEOUTS = 2,
+ /* called by the SQPOLL thread */
+ IO_CANCEL_SQPOLL = 4,
+};
+
__cold bool io_uring_try_cancel_requests(struct io_ring_ctx *ctx,
struct io_uring_task *tctx,
- bool cancel_all, bool is_sqpoll_thread);
+ unsigned int flags);
__cold void io_uring_cancel_generic(bool cancel_all, struct io_sq_data *sqd);
__cold bool io_cancel_ctx_cb(struct io_wq_work *work, void *data);
diff --git a/io_uring/io_uring.c b/io_uring/io_uring.c
index 61053421d809..3fc07d9f3eec 100644
--- a/io_uring/io_uring.c
+++ b/io_uring/io_uring.c
@@ -2337,7 +2337,7 @@ static __cold void io_ring_exit_work(struct work_struct *work)
if (ctx->flags & IORING_SETUP_DEFER_TASKRUN)
io_cancel_local_task_work(ctx);
cond_resched();
- } while (io_uring_try_cancel_requests(ctx, NULL, true, false));
+ } while (io_uring_try_cancel_requests(ctx, NULL, IO_CANCEL_ALL));
if (ctx->sq_data) {
struct io_sq_data *sqd = ctx->sq_data;
diff --git a/io_uring/timeout.c b/io_uring/timeout.c
index c4dd26cf342d..9c239c0a1715 100644
--- a/io_uring/timeout.c
+++ b/io_uring/timeout.c
@@ -729,6 +729,24 @@ static bool io_match_task(struct io_kiocb *head, struct io_uring_task *tctx,
return false;
}
+__cold unsigned int io_timeouts_armed(struct io_ring_ctx *ctx,
+ struct io_uring_task *tctx)
+{
+ struct io_timeout *timeout;
+ unsigned int nr = 0;
+
+ guard(mutex)(&ctx->uring_lock);
+ raw_spin_lock_irq(&ctx->timeout_lock);
+ list_for_each_entry(timeout, &ctx->timeout_list, list) {
+ struct io_kiocb *req = cmd_to_io_kiocb(timeout);
+
+ if (req->tctx == tctx)
+ nr += io_linked_nr(req);
+ }
+ raw_spin_unlock_irq(&ctx->timeout_lock);
+ return nr;
+}
+
/* Returns true if we found and killed one or more timeouts */
__cold bool io_kill_timeouts(struct io_ring_ctx *ctx, struct io_uring_task *tctx,
bool cancel_all)
diff --git a/io_uring/timeout.h b/io_uring/timeout.h
index 1620f94dd45a..b6cd608fb667 100644
--- a/io_uring/timeout.h
+++ b/io_uring/timeout.h
@@ -11,6 +11,8 @@ struct io_timeout_data {
__cold void io_flush_timeouts(struct io_ring_ctx *ctx);
struct io_cancel_data;
int io_timeout_cancel(struct io_ring_ctx *ctx, struct io_cancel_data *cd);
+__cold unsigned int io_timeouts_armed(struct io_ring_ctx *ctx,
+ struct io_uring_task *tctx);
__cold bool io_kill_timeouts(struct io_ring_ctx *ctx, struct io_uring_task *tctx,
bool cancel_all);
void io_queue_linked_timeout(struct io_kiocb *req);
--
2.55.0
^ permalink raw reply related [flat|nested] 8+ messages in thread* [PATCH 4/7] io_uring: run cancelations synchronously on ring release
2026-09-09 14:06 [PATCHSET] Cancel requests at ring close time Jens Axboe
` (2 preceding siblings ...)
2026-09-09 14:06 ` [PATCH 3/7] io_uring/cancel: cancel and wait for all requests on process exit Jens Axboe
@ 2026-09-09 14:06 ` Jens Axboe
2026-09-09 14:06 ` [PATCH 5/7] io_uring: drop registered files and buffers at release time Jens Axboe
` (2 subsequent siblings)
6 siblings, 0 replies; 8+ messages in thread
From: Jens Axboe @ 2026-09-09 14:06 UTC (permalink / raw)
To: io-uring; +Cc: juanlu, Jens Axboe
io_uring_release() just marks the ring as dying and punts everything
else to exit_work, including the cancelation of requests that are easily
cancelable right away Until that work has run, and the task_work it
generates has as well, those requests keep their files pinned. An
application that closes its ring and then expects the files it had been
using to be closed as well may be confused by this currently not being
the case.
Re-factor the cancelation pass out of io_ring_exit_work() and run it
directly from release, before punting the rest.
Signed-off-by: Jens Axboe <axboe@kernel.dk>
---
io_uring/io_uring.c | 82 ++++++++++++++++++++++++++-------------------
1 file changed, 48 insertions(+), 34 deletions(-)
diff --git a/io_uring/io_uring.c b/io_uring/io_uring.c
index 3fc07d9f3eec..e98b6f1d4495 100644
--- a/io_uring/io_uring.c
+++ b/io_uring/io_uring.c
@@ -2306,18 +2306,18 @@ static __cold void io_tctx_exit_cb(struct callback_head *cb)
complete(&work->completion);
}
-static __cold void io_ring_exit_work(struct work_struct *work)
+/*
+ * Cancel what can be canceled on a dying ring and reap what has completed.
+ * Only waits on polled I/O, never anything else.
+ */
+static __cold void io_ring_ctx_cancel(struct io_ring_ctx *ctx)
{
- struct io_ring_ctx *ctx = container_of(work, struct io_ring_ctx, exit_work);
- unsigned long timeout = jiffies + IO_URING_EXIT_WAIT_MAX;
- unsigned long interval = HZ / 20;
- struct io_tctx_exit exit;
- struct io_tctx_node *node;
- int ret;
+ struct io_sq_data *sqd = ctx->sq_data;
- mutex_lock(&ctx->uring_lock);
- io_terminate_zcrx(ctx);
- mutex_unlock(&ctx->uring_lock);
+ if (test_bit(IO_CHECK_CQ_OVERFLOW_BIT, &ctx->check_cq)) {
+ scoped_guard(mutex, &ctx->uring_lock)
+ io_cqring_overflow_kill(ctx);
+ }
/*
* If we're doing polled IO and end up having requests being
@@ -2326,32 +2326,36 @@ static __cold void io_ring_exit_work(struct work_struct *work)
* as nobody else will be looking for them.
*/
do {
- if (test_bit(IO_CHECK_CQ_OVERFLOW_BIT, &ctx->check_cq)) {
- mutex_lock(&ctx->uring_lock);
- io_cqring_overflow_kill(ctx);
- mutex_unlock(&ctx->uring_lock);
- }
+ if (ctx->flags & IORING_SETUP_DEFER_TASKRUN)
+ io_cancel_local_task_work(ctx);
+ cond_resched();
+ } while (io_uring_try_cancel_requests(ctx, NULL, IO_CANCEL_ALL));
- /* The SQPOLL thread never reaches this path */
- do {
- if (ctx->flags & IORING_SETUP_DEFER_TASKRUN)
- io_cancel_local_task_work(ctx);
- cond_resched();
- } while (io_uring_try_cancel_requests(ctx, NULL, IO_CANCEL_ALL));
-
- if (ctx->sq_data) {
- struct io_sq_data *sqd = ctx->sq_data;
- struct task_struct *tsk;
-
- io_sq_thread_park(sqd);
- tsk = sqpoll_task_locked(sqd);
- if (tsk && tsk->io_uring && tsk->io_uring->io_wq)
- io_wq_cancel_cb(tsk->io_uring->io_wq,
- io_cancel_ctx_cb, ctx, true);
- io_sq_thread_unpark(sqd);
- }
+ if (sqd) {
+ struct task_struct *tsk;
+
+ io_sq_thread_park(sqd);
+ tsk = sqpoll_task_locked(sqd);
+ if (tsk && tsk->io_uring && tsk->io_uring->io_wq)
+ io_wq_cancel_cb(tsk->io_uring->io_wq,
+ io_cancel_ctx_cb, ctx, true);
+ io_sq_thread_unpark(sqd);
+ }
+
+ io_req_caches_free(ctx);
+}
+
+static __cold void io_ring_exit_work(struct work_struct *work)
+{
+ struct io_ring_ctx *ctx = container_of(work, struct io_ring_ctx, exit_work);
+ unsigned long timeout = jiffies + IO_URING_EXIT_WAIT_MAX;
+ unsigned long interval = HZ / 20;
+ struct io_tctx_exit exit;
+ struct io_tctx_node *node;
+ int ret;
- io_req_caches_free(ctx);
+ do {
+ io_ring_ctx_cancel(ctx);
if (WARN_ON_ONCE(time_after(jiffies, timeout))) {
/* there is little hope left, don't run it too often */
@@ -2419,8 +2423,18 @@ static __cold void io_ring_ctx_wait_and_kill(struct io_ring_ctx *ctx)
percpu_ref_kill(&ctx->refs);
xa_for_each(&ctx->personalities, index, creds)
io_unregister_personality(ctx, index);
+ io_terminate_zcrx(ctx);
mutex_unlock(&ctx->uring_lock);
+ /*
+ * Do the first round of cancelations upfront rather than leaving it
+ * to to exit_work. Anything cancelable is then already on its way
+ * out, and for requests owned by the task closing the ring, this
+ * ensures any held files are put before close(2) returns.
+ */
+ if (!(current->flags & PF_IO_WORKER))
+ io_ring_ctx_cancel(ctx);
+
INIT_WORK(&ctx->exit_work, io_ring_exit_work);
/*
* Use system_dfl_wq to avoid spawning tons of event kworkers
--
2.55.0
^ permalink raw reply related [flat|nested] 8+ messages in thread* [PATCH 5/7] io_uring: drop registered files and buffers at release time
2026-09-09 14:06 [PATCHSET] Cancel requests at ring close time Jens Axboe
` (3 preceding siblings ...)
2026-09-09 14:06 ` [PATCH 4/7] io_uring: run cancelations synchronously on ring release Jens Axboe
@ 2026-09-09 14:06 ` Jens Axboe
2026-09-09 14:06 ` [PATCH 6/7] io_uring: wait for in-flight requests on ring release Jens Axboe
2026-09-09 14:06 ` [PATCH 7/7] io_uring/io-wq: put the request file before posting a completion Jens Axboe
6 siblings, 0 replies; 8+ messages in thread
From: Jens Axboe @ 2026-09-09 14:06 UTC (permalink / raw)
To: io-uring; +Cc: juanlu, Jens Axboe
Registered file and buffer tables are only torn down from
io_ring_ctx_free(), which only runs when all requests have completed and
the ctx refs have hit zero. Before this, every registered file remains
open.
The resource nodes are reference counted and any request using a
registered file or buffer holds its own reference to the node, so this
only releases what is actually unused. The rest follow as those requests
complete or get canceled.
Signed-off-by: Jens Axboe <axboe@kernel.dk>
---
io_uring/io_uring.c | 5 +++--
1 file changed, 3 insertions(+), 2 deletions(-)
diff --git a/io_uring/io_uring.c b/io_uring/io_uring.c
index e98b6f1d4495..0acdece3c196 100644
--- a/io_uring/io_uring.c
+++ b/io_uring/io_uring.c
@@ -2157,8 +2157,6 @@ static __cold void io_ring_ctx_free(struct io_ring_ctx *ctx)
io_sq_thread_finish(ctx);
mutex_lock(&ctx->uring_lock);
- io_sqe_buffers_unregister(ctx);
- io_sqe_files_unregister(ctx);
io_unregister_zcrx(ctx);
io_cqring_overflow_kill(ctx);
io_eventfd_unregister(ctx);
@@ -2424,6 +2422,9 @@ static __cold void io_ring_ctx_wait_and_kill(struct io_ring_ctx *ctx)
xa_for_each(&ctx->personalities, index, creds)
io_unregister_personality(ctx, index);
io_terminate_zcrx(ctx);
+ /* Drop these now, rather than async, to unpin the files */
+ io_sqe_buffers_unregister(ctx);
+ io_sqe_files_unregister(ctx);
mutex_unlock(&ctx->uring_lock);
/*
--
2.55.0
^ permalink raw reply related [flat|nested] 8+ messages in thread* [PATCH 6/7] io_uring: wait for in-flight requests on ring release
2026-09-09 14:06 [PATCHSET] Cancel requests at ring close time Jens Axboe
` (4 preceding siblings ...)
2026-09-09 14:06 ` [PATCH 5/7] io_uring: drop registered files and buffers at release time Jens Axboe
@ 2026-09-09 14:06 ` Jens Axboe
2026-09-09 14:06 ` [PATCH 7/7] io_uring/io-wq: put the request file before posting a completion Jens Axboe
6 siblings, 0 replies; 8+ messages in thread
From: Jens Axboe @ 2026-09-09 14:06 UTC (permalink / raw)
To: io-uring; +Cc: juanlu, Jens Axboe
With cancelations now run at release time, what's left in-flight on
the ring afterwards is mostly I/O that has already been issued to a
device and just needs to finish. Until that happens, the files from
those requests pin the files they were using. Wait for those.
Signed-off-by: Jens Axboe <axboe@kernel.dk>
---
io_uring/io_uring.c | 74 ++++++++++++++++++++++++++++++++++++++++++++-
1 file changed, 73 insertions(+), 1 deletion(-)
diff --git a/io_uring/io_uring.c b/io_uring/io_uring.c
index 0acdece3c196..32adb2d26d17 100644
--- a/io_uring/io_uring.c
+++ b/io_uring/io_uring.c
@@ -2343,6 +2343,76 @@ static __cold void io_ring_ctx_cancel(struct io_ring_ctx *ctx)
io_req_caches_free(ctx);
}
+/* Number of requests that should be waited for */
+static __cold unsigned int io_ring_ctx_inflight(struct io_ring_ctx *ctx)
+{
+ guard(mutex)(&ctx->uring_lock);
+ __io_req_caches_free(ctx);
+ return ctx->nr_req_allocated - ctx->nr_notifs;
+}
+
+/*
+ * Run task_work completions for current. Only do so if the io_uring callback
+ * itself can get pruned first, otherwise we risk recursing.
+ */
+static __cold bool io_ring_run_own_completions(struct io_uring_task *tctx)
+{
+ unsigned int count = 0;
+
+ if (!tctx || mpscq_empty(&tctx->task_list))
+ return true;
+ if (!task_work_cancel(current, &tctx->task_work))
+ return false;
+ tctx_task_work_run(tctx, UINT_MAX, &count);
+ return true;
+}
+
+/*
+ * Requests may remain after cancelations have been run, as not all requests
+ * are cancelable. Storage I/O is an example. Wait for those so that once
+ * close(2) returns, files pinned by these requests have been released.
+ */
+static __cold void io_ring_ctx_wait_inflight(struct io_ring_ctx *ctx)
+{
+ struct io_uring_task *tctx = current->io_uring;
+ bool ran_own = true;
+
+ if (current->flags & (PF_KTHREAD | PF_EXITING))
+ return;
+ if (tctx && atomic_read(&tctx->in_cancel))
+ return;
+
+ while (io_ring_ctx_inflight(ctx) && !fatal_signal_pending(current)) {
+ unsigned int state;
+
+ if (test_thread_flag(TIF_NOTIFY_SIGNAL)) {
+ clear_notify_signal();
+ if (task_work_pending(current))
+ set_notify_resume(current);
+ }
+ state = TASK_INTERRUPTIBLE;
+ if (signal_pending(current))
+ state = TASK_KILLABLE;
+ set_current_state(state | TASK_FREEZABLE);
+ /* don't sleep on work that's already there and that we can run */
+ if (ran_own && ((tctx && !mpscq_empty(&tctx->task_list)) ||
+ io_local_work_pending(ctx)))
+ __set_current_state(TASK_RUNNING);
+ else
+ schedule_timeout(1);
+
+ /* completions may be queued behind us */
+ if (!io_ring_run_own_completions(tctx)) {
+ if (!ran_own)
+ break;
+ ran_own = false;
+ } else {
+ ran_own = true;
+ }
+ io_ring_ctx_cancel(ctx);
+ }
+}
+
static __cold void io_ring_exit_work(struct work_struct *work)
{
struct io_ring_ctx *ctx = container_of(work, struct io_ring_ctx, exit_work);
@@ -2433,8 +2503,10 @@ static __cold void io_ring_ctx_wait_and_kill(struct io_ring_ctx *ctx)
* out, and for requests owned by the task closing the ring, this
* ensures any held files are put before close(2) returns.
*/
- if (!(current->flags & PF_IO_WORKER))
+ if (!(current->flags & PF_IO_WORKER)) {
io_ring_ctx_cancel(ctx);
+ io_ring_ctx_wait_inflight(ctx);
+ }
INIT_WORK(&ctx->exit_work, io_ring_exit_work);
/*
--
2.55.0
^ permalink raw reply related [flat|nested] 8+ messages in thread* [PATCH 7/7] io_uring/io-wq: put the request file before posting a completion
2026-09-09 14:06 [PATCHSET] Cancel requests at ring close time Jens Axboe
` (5 preceding siblings ...)
2026-09-09 14:06 ` [PATCH 6/7] io_uring: wait for in-flight requests on ring release Jens Axboe
@ 2026-09-09 14:06 ` Jens Axboe
6 siblings, 0 replies; 8+ messages in thread
From: Jens Axboe @ 2026-09-09 14:06 UTC (permalink / raw)
To: io-uring; +Cc: juanlu, Jens Axboe
Once io-wq is done with the request, put any potential file that request
had pinned upfront. This ensures any file references are fully put by
the time io-wq posts a completion. For registered files we don't need to
worry about this, as these are canceled/dropped earlier.
Signed-off-by: Jens Axboe <axboe@kernel.dk>
---
io_uring/io_uring.c | 25 ++++++++++++++++++++++++-
1 file changed, 24 insertions(+), 1 deletion(-)
diff --git a/io_uring/io_uring.c b/io_uring/io_uring.c
index 32adb2d26d17..b5dc55bba195 100644
--- a/io_uring/io_uring.c
+++ b/io_uring/io_uring.c
@@ -907,6 +907,24 @@ bool io_req_post_cqe32(struct io_kiocb *req, struct io_uring_cqe cqe[2])
return posted;
}
+/*
+ * Drop any io-wq request with a file upfront, otherwise it gets deferred to
+ * much later post CQE posting.
+ */
+static void io_req_put_file_iowq(struct io_kiocb *req, bool sync)
+{
+ struct file *file = req->file;
+
+ if (!file || (req->flags & (REQ_F_FIXED_FILE | REQ_F_REISSUE)))
+ return;
+
+ WRITE_ONCE(req->file, NULL);
+ if (sync)
+ __fput_sync(file);
+ else
+ fput(file);
+}
+
static void io_req_complete_post(struct io_kiocb *req, unsigned issue_flags)
{
struct io_ring_ctx *ctx = req->ctx;
@@ -919,6 +937,8 @@ static void io_req_complete_post(struct io_kiocb *req, unsigned issue_flags)
if (WARN_ON_ONCE(!(issue_flags & IO_URING_F_IOWQ)))
return;
+ io_req_put_file_iowq(req, true);
+
/*
* Handle special CQ sync cases via task_work. DEFER_TASKRUN requires
* the submitter task context, IOPOLL protects with uring_lock.
@@ -1480,6 +1500,7 @@ void io_wq_submit_work(struct io_wq_work *work)
/* either cancelled or io-wq is dying, so don't touch tctx->iowq */
if (atomic_read(&work->flags) & IO_WQ_WORK_CANCEL) {
fail:
+ io_req_put_file_iowq(req, false);
io_req_task_queue_fail(req, err);
return;
}
@@ -1555,8 +1576,10 @@ void io_wq_submit_work(struct io_wq_work *work)
} while (1);
/* avoid locking problems by failing it from a clean context */
- if (ret)
+ if (ret) {
+ io_req_put_file_iowq(req, true);
io_req_task_queue_fail(req, ret);
+ }
}
inline struct file *io_file_get_fixed(struct io_kiocb *req, int fd,
--
2.55.0
^ permalink raw reply related [flat|nested] 8+ messages in thread