All of lore.kernel.org
 help / color / mirror / Atom feed
* [PATCHSET 0/10] Various io_uring fixes
@ 2026-10-28 16:46 Jens Axboe
  2026-10-28 16:46 ` [PATCH 01/10] io_uring/io-wq: no new workers once the exit has begun Jens Axboe
                   ` (9 more replies)
  0 siblings, 10 replies; 11+ messages in thread
From: Jens Axboe @ 2026-10-28 16:46 UTC (permalink / raw)
  To: io-uring

Hi,

No particular topic here, just a bunch of bug fixes. Please take a
look, thanks!

-- 
Jens Axboe


^ permalink raw reply	[flat|nested] 11+ messages in thread

* [PATCH 01/10] io_uring/io-wq: no new workers once the exit has begun
  2026-10-28 16:46 [PATCHSET 0/10] Various io_uring fixes Jens Axboe
@ 2026-10-28 16:46 ` Jens Axboe
  2026-10-28 16:46 ` [PATCH 02/10] io_uring/rsrc: charge buffer arrays and nodes to memcg Jens Axboe
                   ` (8 subsequent siblings)
  9 siblings, 0 replies; 11+ messages in thread
From: Jens Axboe @ 2026-10-28 16:46 UTC (permalink / raw)
  To: io-uring; +Cc: Josef Bacik, Jens Axboe

From: Josef Bacik <josef@toxicpanda.com>

io_wq_exit_start() sets IO_WQ_BIT_EXIT, but worker creation task_work
queued before that stays queued until io_wq_exit_workers() cancels it.
In between the task runs its task_work while canceling requests, and a
worker can still get created.

That's a problem for exec. begin_new_exec() calls io_uring_task_cancel()
after de_thread(), when it's supposedly the only thread left, and
io_wq_exit_workers() only waits for ->worker_done, which a worker
completes before do_exit(). So the exiting worker is still in the thread
group, using the mm, files and sighand, while exec replaces them. Easy
to hit with a signal pending when create_io_thread() fails and the retry
gets queued right before execve().

Cancel the queued creation task_work as soon as the exit bit is set.
Pending work that isn't otherwise canceled now ends with -ECANCELED when
the io-wq is destroyed, rather than depending on whether the task
happened to run its task_work first.

Fixes: 685fe7feedb9 ("io-wq: eliminate the need for a manager thread")
Signed-off-by: Josef Bacik <josef@toxicpanda.com>
[axboe: the barrier is already upstream, see 0e5663856b69, only the
 cancel is new]
Signed-off-by: Jens Axboe <axboe@kernel.dk>
---
 io_uring/io-wq.c | 6 ++++++
 1 file changed, 6 insertions(+)

diff --git a/io_uring/io-wq.c b/io_uring/io-wq.c
index e93cfa82b850..28533174d0da 100644
--- a/io_uring/io-wq.c
+++ b/io_uring/io-wq.c
@@ -1420,6 +1420,12 @@ void io_wq_exit_start(struct io_wq *wq)
 	set_bit(IO_WQ_BIT_EXIT, &wq->state);
 	/* Pairs with task_work_add() in io_queue_worker_create(). */
 	smp_mb__after_atomic();
+	/*
+	 * No more workers from here on. The caller is going to run its task
+	 * work, and on exec a thread that is created now would still be
+	 * around, exiting, by the time that the caller must be on its own.
+	 */
+	io_wq_cancel_tw_create(wq);
 }
 
 static void io_wq_cancel_tw_create(struct io_wq *wq)
-- 
2.56.0


^ permalink raw reply related	[flat|nested] 11+ messages in thread

* [PATCH 02/10] io_uring/rsrc: charge buffer arrays and nodes to memcg
  2026-10-28 16:46 [PATCHSET 0/10] Various io_uring fixes Jens Axboe
  2026-10-28 16:46 ` [PATCH 01/10] io_uring/io-wq: no new workers once the exit has begun Jens Axboe
@ 2026-10-28 16:46 ` Jens Axboe
  2026-10-28 16:46 ` [PATCH 03/10] io_uring: charge tctx nodes and personalities Jens Axboe
                   ` (7 subsequent siblings)
  9 siblings, 0 replies; 11+ messages in thread
From: Jens Axboe @ 2026-10-28 16:46 UTC (permalink / raw)
  To: io-uring; +Cc: Chris Mason, Jens Axboe

From: Chris Mason <mason@kernel.org>

The io_mapped_ubuf of a registered buffer has one bio_vec per page,
unless the pages coalesce into huge pages. Huge pages only count against
RLIMIT_MEMLOCK once per ring, so a buffer that maps the same 2MB folio
over and over in a way that defeats coalescing gets a full bvec array
per registration while only the one folio counts as locked. 1GB of that
registered in 100 slots took several times a 64MB memory.max, with 2.6MB
charged.

Allocate the io_mapped_ubuf and the io_rsrc_node with
GFP_KERNEL_ACCOUNT. An object reused from the ring caches stays charged
to whoever allocated it first, same as the io_kiocb cache.

Fixes: de2939388be5 ("io_uring: improve registered buffer accounting for huge pages")
Signed-off-by: Chris Mason <mason@kernel.org>
Signed-off-by: Jens Axboe <axboe@kernel.dk>
---
 io_uring/rsrc.c | 7 ++++---
 1 file changed, 4 insertions(+), 3 deletions(-)

diff --git a/io_uring/rsrc.c b/io_uring/rsrc.c
index 1efaf29514e8..d569cea12df9 100644
--- a/io_uring/rsrc.c
+++ b/io_uring/rsrc.c
@@ -162,8 +162,9 @@ static struct io_mapped_ubuf *io_alloc_imu(struct io_ring_ctx *ctx,
 					   int nr_bvecs)
 {
 	if (nr_bvecs <= IO_CACHED_BVECS_SEGS)
-		return io_cache_alloc(&ctx->imu_cache, GFP_KERNEL);
-	return kvmalloc_flex(struct io_mapped_ubuf, bvec, nr_bvecs);
+		return io_cache_alloc(&ctx->imu_cache, GFP_KERNEL_ACCOUNT);
+	return kvmalloc_flex(struct io_mapped_ubuf, bvec, nr_bvecs,
+			     GFP_KERNEL_ACCOUNT);
 }
 
 static void io_free_imu(struct io_ring_ctx *ctx, struct io_mapped_ubuf *imu)
@@ -246,7 +247,7 @@ struct io_rsrc_node *io_rsrc_node_alloc(struct io_ring_ctx *ctx, int type)
 {
 	struct io_rsrc_node *node;
 
-	node = io_cache_alloc(&ctx->node_cache, GFP_KERNEL);
+	node = io_cache_alloc(&ctx->node_cache, GFP_KERNEL_ACCOUNT);
 	if (node) {
 		node->type = type;
 		node->refs = 1;
-- 
2.56.0


^ permalink raw reply related	[flat|nested] 11+ messages in thread

* [PATCH 03/10] io_uring: charge tctx nodes and personalities
  2026-10-28 16:46 [PATCHSET 0/10] Various io_uring fixes Jens Axboe
  2026-10-28 16:46 ` [PATCH 01/10] io_uring/io-wq: no new workers once the exit has begun Jens Axboe
  2026-10-28 16:46 ` [PATCH 02/10] io_uring/rsrc: charge buffer arrays and nodes to memcg Jens Axboe
@ 2026-10-28 16:46 ` Jens Axboe
  2026-10-28 16:46 ` [PATCH 04/10] io_uring: free XArray nodes left by a failed store Jens Axboe
                   ` (6 subsequent siblings)
  9 siblings, 0 replies; 11+ messages in thread
From: Jens Axboe @ 2026-10-28 16:46 UTC (permalink / raw)
  To: io-uring; +Cc: Chris Mason, Jens Axboe

From: Chris Mason <mason@kernel.org>

The io_tctx_node allocated the first time a task touches a ring, and
the XArray nodes behind tctx->xa and ctx->personalities, are plain
GFP_KERNEL. tctx->xa is indexed by the ring pointer, so nearly every
entry needs nodes of its own, and a task gets one entry per ring it has
used. 642 threads on 4000 rings held 2.3GB with 59MB charged against a
64MB memory.max.

Allocate the node with GFP_KERNEL_ACCOUNT and set XA_FLAGS_ACCOUNT on
both XArrays.

Fixes: 0f2122045b94 ("io_uring: don't rely on weak ->files references")
Signed-off-by: Chris Mason <mason@kernel.org>
Signed-off-by: Jens Axboe <axboe@kernel.dk>
---
 io_uring/io_uring.c | 2 +-
 io_uring/tctx.c     | 4 ++--
 2 files changed, 3 insertions(+), 3 deletions(-)

diff --git a/io_uring/io_uring.c b/io_uring/io_uring.c
index e9c65cf0cc51..76de44a91a7b 100644
--- a/io_uring/io_uring.c
+++ b/io_uring/io_uring.c
@@ -271,7 +271,7 @@ static __cold struct io_ring_ctx *io_ring_ctx_alloc(struct io_uring_params *p)
 	if (ret)
 		goto free_ref;
 	init_completion(&ctx->ref_comp);
-	xa_init_flags(&ctx->personalities, XA_FLAGS_ALLOC1);
+	xa_init_flags(&ctx->personalities, XA_FLAGS_ALLOC1 | XA_FLAGS_ACCOUNT);
 	mutex_init(&ctx->uring_lock);
 	init_waitqueue_head(&ctx->cq_wait);
 	init_waitqueue_head(&ctx->poll_wq);
diff --git a/io_uring/tctx.c b/io_uring/tctx.c
index 466b7300e208..4dedcdc44780 100644
--- a/io_uring/tctx.c
+++ b/io_uring/tctx.c
@@ -104,7 +104,7 @@ __cold struct io_uring_task *io_uring_alloc_task_context(struct task_struct *tas
 	}
 
 	tctx->task = task;
-	xa_init(&tctx->xa);
+	xa_init_flags(&tctx->xa, XA_FLAGS_ACCOUNT);
 	init_waitqueue_head(&tctx->wait);
 	atomic_set(&tctx->in_cancel, 0);
 	atomic_set(&tctx->inflight_tracked, 0);
@@ -123,7 +123,7 @@ static int io_tctx_install_node(struct io_ring_ctx *ctx,
 	if (xa_load(&tctx->xa, (unsigned long)ctx))
 		return 0;
 
-	node = kmalloc_obj(*node);
+	node = kmalloc_obj(*node, GFP_KERNEL_ACCOUNT);
 	if (!node)
 		return -ENOMEM;
 	node->ctx = ctx;
-- 
2.56.0


^ permalink raw reply related	[flat|nested] 11+ messages in thread

* [PATCH 04/10] io_uring: free XArray nodes left by a failed store
  2026-10-28 16:46 [PATCHSET 0/10] Various io_uring fixes Jens Axboe
                   ` (2 preceding siblings ...)
  2026-10-28 16:46 ` [PATCH 03/10] io_uring: charge tctx nodes and personalities Jens Axboe
@ 2026-10-28 16:46 ` Jens Axboe
  2026-10-28 16:46 ` [PATCH 05/10] io_uring/bpf-ops: clear bpf_ctx while submitting SQEs Jens Axboe
                   ` (5 subsequent siblings)
  9 siblings, 0 replies; 11+ messages in thread
From: Jens Axboe @ 2026-10-28 16:46 UTC (permalink / raw)
  To: io-uring; +Cc: Chris Mason, Jens Axboe

From: Chris Mason <mason@kernel.org>

A failed xa_store() can leave nodes without entries in the tree, and
erasing every entry doesn't free those. An XArray needs xa_destroy()
before it goes away, even an empty one. That was dropped for tctx->xa in
commit ca6484cd308a ("io_uring: no need to call xa_destroy() on empty
xarray") over a lockdep report from freeing the tctx in softirq context,
and ctx->personalities never had it.

Now that the nodes are charged, a store can fail at memory.max and the
leaked nodes stay charged. Call xa_destroy() in io_uring_clean_tctx(),
which the task runs itself in process context before the tctx is freed,
in the error path of __io_uring_add_tctx_node(), and for
ctx->personalities in io_ring_ctx_free().

Fixes: ca6484cd308a ("io_uring: no need to call xa_destroy() on empty xarray")
Signed-off-by: Chris Mason <mason@kernel.org>
Signed-off-by: Jens Axboe <axboe@kernel.dk>
---
 io_uring/io_uring.c | 1 +
 io_uring/tctx.c     | 3 +++
 2 files changed, 4 insertions(+)

diff --git a/io_uring/io_uring.c b/io_uring/io_uring.c
index 76de44a91a7b..b09859c6e8ab 100644
--- a/io_uring/io_uring.c
+++ b/io_uring/io_uring.c
@@ -2342,6 +2342,7 @@ static __cold void io_ring_ctx_free(struct io_ring_ctx *ctx)
 	kvfree(ctx->cancel_table.hbs);
 	xa_destroy(&ctx->io_bl_xa);
 	xa_destroy(&ctx->hpage_acct);
+	xa_destroy(&ctx->personalities);
 	kfree(ctx);
 }
 
diff --git a/io_uring/tctx.c b/io_uring/tctx.c
index 4dedcdc44780..291e30e00e45 100644
--- a/io_uring/tctx.c
+++ b/io_uring/tctx.c
@@ -189,6 +189,7 @@ int __io_uring_add_tctx_node(struct io_ring_ctx *ctx)
 			io_wq_exit_start(tctx->io_wq);
 			io_wq_put_and_exit(tctx->io_wq);
 		}
+		xa_destroy(&tctx->xa);
 		percpu_counter_destroy(&tctx->inflight);
 		kfree(tctx);
 	}
@@ -250,6 +251,8 @@ __cold void io_uring_clean_tctx(struct io_uring_task *tctx)
 		io_uring_del_tctx_node(index);
 		cond_resched();
 	}
+	/* A failed xa_store() leaves nodes with no entry behind. */
+	xa_destroy(&tctx->xa);
 	if (wq) {
 		/*
 		 * Must be after io_uring_del_tctx_node() (removes nodes under
-- 
2.56.0


^ permalink raw reply related	[flat|nested] 11+ messages in thread

* [PATCH 05/10] io_uring/bpf-ops: clear bpf_ctx while submitting SQEs
  2026-10-28 16:46 [PATCHSET 0/10] Various io_uring fixes Jens Axboe
                   ` (3 preceding siblings ...)
  2026-10-28 16:46 ` [PATCH 04/10] io_uring: free XArray nodes left by a failed store Jens Axboe
@ 2026-10-28 16:46 ` Jens Axboe
  2026-10-28 16:46 ` [PATCH 06/10] io_uring: free the task context after a grace period Jens Axboe
                   ` (4 subsequent siblings)
  9 siblings, 0 replies; 11+ messages in thread
From: Jens Axboe @ 2026-10-28 16:46 UTC (permalink / raw)
  To: io-uring; +Cc: Chris Mason, Jens Axboe

From: Chris Mason <mason@kernel.org>

bpf_io_uring_submit_sqes() calls io_submit_sqes() from inside the loop
program, with current->bpf_ctx set, and requests issued inline run with
it set too. The socket code uses has_current_bpf_ctx() to recognize
calls from bpf_setsockopt() and friends, where the caller holds the
socket lock and has already been vetted, and skips both.

So a SOCKET_URING_OP_SETSOCKOPT from the SQ passes sockopt_capable()
without the capability, and runs without lock_sock(). A task with no
capability can set SO_MARK, IP_TRANSPARENT or TCP_REPAIR, and two
threads changing IP_OPTIONS on one socket, one through the ring, double
free in no time. SOCK_DESTROY via IORING_OP_SENDMSG ends up in
tcp_abort() without the lock the same way.

Clear the run context around io_submit_sqes(). The SQEs come from
userspace and must be issued like those of any other ring. They still
run inside the program's RCU tasks trace read section, so a request
that sleeps there holds up that grace period, but other request types
do that already.

Fixes: 890819248a86 ("io_uring/bpf-ops: add kfunc helpers")
Signed-off-by: Chris Mason <mason@kernel.org>
Signed-off-by: Jens Axboe <axboe@kernel.dk>
---
 io_uring/bpf-ops.c | 16 +++++++++++++++-
 1 file changed, 15 insertions(+), 1 deletion(-)

diff --git a/io_uring/bpf-ops.c b/io_uring/bpf-ops.c
index cf2bd068e331..6c2a587fb1bf 100644
--- a/io_uring/bpf-ops.c
+++ b/io_uring/bpf-ops.c
@@ -17,8 +17,22 @@ __bpf_kfunc_start_defs();
 __bpf_kfunc int bpf_io_uring_submit_sqes(struct iou_ctx *loop_ctx, u32 nr)
 {
 	struct io_ring_ctx *ctx = io_loop_demangle_ctx(loop_ctx);
+	struct bpf_run_ctx *old_run_ctx;
+	int ret;
 
-	return io_submit_sqes(ctx, nr);
+	/*
+	 * The requests come from the SQ, which user space can write, so they
+	 * are issued for the task and not for the BPF program. Hide the run
+	 * context of the program from them. The users of has_current_bpf_ctx(),
+	 * such as sockopt_lock_sock() and sockopt_capable(), take a context for
+	 * a call from a BPF helper, whose caller holds the socket lock already
+	 * and needs no capability.
+	 */
+	old_run_ctx = bpf_set_run_ctx(NULL);
+	ret = io_submit_sqes(ctx, nr);
+	bpf_reset_run_ctx(old_run_ctx);
+
+	return ret;
 }
 
 __bpf_kfunc
-- 
2.56.0


^ permalink raw reply related	[flat|nested] 11+ messages in thread

* [PATCH 06/10] io_uring: free the task context after a grace period
  2026-10-28 16:46 [PATCHSET 0/10] Various io_uring fixes Jens Axboe
                   ` (4 preceding siblings ...)
  2026-10-28 16:46 ` [PATCH 05/10] io_uring/bpf-ops: clear bpf_ctx while submitting SQEs Jens Axboe
@ 2026-10-28 16:46 ` Jens Axboe
  2026-10-28 16:46 ` [PATCH 07/10] io_uring/sqpoll: don't wait for the thread to exit Jens Axboe
                   ` (3 subsequent siblings)
  9 siblings, 0 replies; 11+ messages in thread
From: Jens Axboe @ 2026-10-28 16:46 UTC (permalink / raw)
  To: io-uring; +Cc: Chris Mason, Jens Axboe

From: Chris Mason <mason@kernel.org>

A task other than the owner can free a request, io_ring_exit_work()
running the local task_work of a DEFER_TASKRUN ring for example, or
io_do_iopoll() reaping completions. io_put_task() then takes the else
branch:

    percpu_counter_sub(&tctx->inflight, 1);
    if (unlikely(atomic_read(&tctx->in_cancel)))
            wake_up(&tctx->wait);
    put_task_struct(tctx->task);

The first line is what lets the owner leave io_uring_cancel_generic(),
and on exec it frees the tctx right after. Nothing holds that off for
the remaining three lines, the task reference only covers a tctx freed
along with the task_struct. It used to work out because
io_uring_del_tctx_node() took the uring_lock that the freeing side holds
across io_put_task(), until that moved to tctx_lock.

Free the tctx with kfree_rcu(), and enter an RCU read section in
io_put_task() before the count is dropped.

Fixes: 5623eb1ed035 ("io_uring/tctx: add separate lock for list of tctx's in ctx")
Signed-off-by: Chris Mason <mason@kernel.org>
Signed-off-by: Jens Axboe <axboe@kernel.dk>
---
 include/linux/io_uring_types.h | 3 +++
 io_uring/io_uring.c            | 2 ++
 io_uring/tctx.c                | 2 +-
 3 files changed, 6 insertions(+), 1 deletion(-)

diff --git a/include/linux/io_uring_types.h b/include/linux/io_uring_types.h
index 2e5a786f2a2a..8c51ce903671 100644
--- a/include/linux/io_uring_types.h
+++ b/include/linux/io_uring_types.h
@@ -161,6 +161,9 @@ struct io_uring_task {
 	/* drains ->task_list once the task can no longer run task_work */
 	struct work_struct		fallback_work;
 
+	/* the tctx is freed after a grace period, see io_put_task() */
+	struct rcu_head			rcu;
+
 	struct { /* task_work */
 		struct mpscq		task_list;
 		struct callback_head	task_work;
diff --git a/io_uring/io_uring.c b/io_uring/io_uring.c
index b09859c6e8ab..615933a6e8ff 100644
--- a/io_uring/io_uring.c
+++ b/io_uring/io_uring.c
@@ -634,6 +634,8 @@ static inline void io_put_task(struct io_kiocb *req)
 	if (likely(tctx->task == current)) {
 		tctx->cached_refs++;
 	} else {
+		guard(rcu)();
+
 		percpu_counter_sub(&tctx->inflight, 1);
 		if (unlikely(atomic_read(&tctx->in_cancel)))
 			wake_up(&tctx->wait);
diff --git a/io_uring/tctx.c b/io_uring/tctx.c
index 291e30e00e45..b4fecea3ea47 100644
--- a/io_uring/tctx.c
+++ b/io_uring/tctx.c
@@ -64,7 +64,7 @@ void io_uring_free_tctx(struct task_struct *tsk)
 		WARN_ON_ONCE(tctx->cached_refs);
 
 		percpu_counter_destroy(&tctx->inflight);
-		kfree(tctx);
+		kfree_rcu(tctx, rcu);
 		tsk->io_uring = NULL;
 	}
 }
-- 
2.56.0


^ permalink raw reply related	[flat|nested] 11+ messages in thread

* [PATCH 07/10] io_uring/sqpoll: don't wait for the thread to exit
  2026-10-28 16:46 [PATCHSET 0/10] Various io_uring fixes Jens Axboe
                   ` (5 preceding siblings ...)
  2026-10-28 16:46 ` [PATCH 06/10] io_uring: free the task context after a grace period Jens Axboe
@ 2026-10-28 16:46 ` Jens Axboe
  2026-10-28 16:46 ` [PATCH 08/10] io_uring/cancel: drop sqd->lock while SQPOLL waits Jens Axboe
                   ` (2 subsequent siblings)
  9 siblings, 0 replies; 11+ messages in thread
From: Jens Axboe @ 2026-10-28 16:46 UTC (permalink / raw)
  To: io-uring; +Cc: Chris Mason, Jens Axboe

From: Chris Mason <mason@kernel.org>

io_sq_thread_stop() wakes the SQPOLL thread and waits for it to exit.
The thread belongs to the thread group that created the ring, so a
SIGSTOP or the cgroup freezer holds it in get_signal(), and the wake up
doesn't change that. io_ring_exit_work() then sleeps in that wait until
someone continues or kills the process. iou_exit has 64 slots, so 64
closed SQPOLL rings of a stopped process keep every other ring from
being freed, for everyone.

Don't wait. Set the stop bit, wake the thread, and let it exit when it
gets to run again. The sqd can now outlive its last ring, so give it a
second refcount with one reference for the rings and one for the thread,
and free it with the last one. The thread can't reach a freed ring,
rings leave ctx_list before dropping their reference, and the io-wq hash
is refcounted.

Fixes: 16efa4fce3b7 ("io_uring: allow IO worker threads to be frozen")
Signed-off-by: Chris Mason <mason@kernel.org>
Signed-off-by: Jens Axboe <axboe@kernel.dk>
---
 io_uring/sqpoll.c | 28 +++++++++++++++++++++++-----
 io_uring/sqpoll.h |  3 ++-
 2 files changed, 25 insertions(+), 6 deletions(-)

diff --git a/io_uring/sqpoll.c b/io_uring/sqpoll.c
index ee97808a065a..49c52284ecdd 100644
--- a/io_uring/sqpoll.c
+++ b/io_uring/sqpoll.c
@@ -63,6 +63,24 @@ void io_sq_thread_park(struct io_sq_data *sqd)
 	}
 }
 
+/*
+ * The sqd has two holders: its rings, as one, and its thread. Neither waits
+ * for the other to let go. The one that lets go last frees the sqd.
+ */
+static void io_sq_data_release(struct io_sq_data *sqd)
+{
+	if (refcount_dec_and_test(&sqd->free_refs))
+		kfree(sqd);
+}
+
+/*
+ * Ask the thread to exit, and don't wait for it. A thread that is stopped or
+ * frozen exits only when a user lets it run again, and the exit work of the
+ * ring would keep its place in the workqueue until then. All rings have left
+ * ctx_list by now and have no request left, so the thread can reach none of
+ * them. What it still uses is its own or counted: the sqd by free_refs, the
+ * hash of its io-wq by hash->refs.
+ */
 void io_sq_thread_stop(struct io_sq_data *sqd)
 {
 	struct task_struct *tsk;
@@ -77,7 +95,6 @@ void io_sq_thread_stop(struct io_sq_data *sqd)
 		wake_up_process(tsk);
 	}
 	mutex_unlock(&sqd->lock);
-	wait_for_completion(&sqd->exited);
 }
 
 void io_put_sq_data(struct io_sq_data *sqd)
@@ -86,7 +103,7 @@ void io_put_sq_data(struct io_sq_data *sqd)
 		WARN_ON_ONCE(atomic_read(&sqd->park_pending));
 
 		io_sq_thread_stop(sqd);
-		kfree(sqd);
+		io_sq_data_release(sqd);
 	}
 }
 
@@ -160,10 +177,10 @@ static struct io_sq_data *io_get_sq_data(struct io_uring_params *p,
 
 	atomic_set(&sqd->park_pending, 0);
 	refcount_set(&sqd->refs, 1);
+	refcount_set(&sqd->free_refs, 2);
 	INIT_LIST_HEAD(&sqd->ctx_list);
 	mutex_init(&sqd->lock);
 	init_waitqueue_head(&sqd->wait);
-	init_completion(&sqd->exited);
 	return sqd;
 }
 
@@ -424,7 +441,7 @@ static int io_sq_thread(void *data)
 	io_run_task_work();
 	mutex_unlock(&sqd->lock);
 err_out:
-	complete(&sqd->exited);
+	io_sq_data_release(sqd);
 	do_exit(0);
 }
 
@@ -544,7 +561,8 @@ __cold int io_sq_offload_create(struct io_ring_ctx *ctx,
 	}
 	return 0;
 err_sqpoll:
-	complete(&ctx->sq_data->exited);
+	/* no thread was made: let go of its share */
+	io_sq_data_release(ctx->sq_data);
 err:
 	io_sq_thread_finish(ctx);
 	return ret;
diff --git a/io_uring/sqpoll.h b/io_uring/sqpoll.h
index fd2f6f29b516..024a51dc3069 100644
--- a/io_uring/sqpoll.h
+++ b/io_uring/sqpoll.h
@@ -2,6 +2,8 @@
 
 struct io_sq_data {
 	refcount_t		refs;
+	/* one for all the rings, one for the thread: the last put frees */
+	refcount_t		free_refs;
 	atomic_t		park_pending;
 	struct mutex		lock;
 
@@ -18,7 +20,6 @@ struct io_sq_data {
 
 	u64			work_time;
 	unsigned long		state;
-	struct completion	exited;
 };
 
 int io_sq_offload_create(struct io_ring_ctx *ctx, struct io_uring_params *p);
-- 
2.56.0


^ permalink raw reply related	[flat|nested] 11+ messages in thread

* [PATCH 08/10] io_uring/cancel: drop sqd->lock while SQPOLL waits
  2026-10-28 16:46 [PATCHSET 0/10] Various io_uring fixes Jens Axboe
                   ` (6 preceding siblings ...)
  2026-10-28 16:46 ` [PATCH 07/10] io_uring/sqpoll: don't wait for the thread to exit Jens Axboe
@ 2026-10-28 16:46 ` Jens Axboe
  2026-10-28 16:46 ` [PATCH 09/10] io_uring/io-wq: save worker type before dropping its reference Jens Axboe
  2026-10-28 16:46 ` [PATCH 10/10] io_uring: defer foreign io-wq enqueues to task_work Jens Axboe
  9 siblings, 0 replies; 11+ messages in thread
From: Jens Axboe @ 2026-10-28 16:46 UTC (permalink / raw)
  To: io-uring; +Cc: Chris Mason, Jens Axboe

From: Chris Mason <mason@kernel.org>

An exiting SQPOLL thread calls io_uring_cancel_generic() with sqd->lock
held, and keeps it across cond_resched() and the sleep until nothing is
in flight anymore. A zero-copy send notification can't be canceled and
stays pending as long as the peer doesn't read, so the lock is held for
as long as a user cares to. io_ring_exit_work() needs it to park the
thread, and so do IORING_REGISTER_IOWQ_AFF and IOWQ_MAX_WORKERS. With
64 such rings sharing the thread, every iou_exit slot sleeps on the lock
and no ring gets freed for anyone.

Drop the lock around cond_resched() and the sleep, like the main loop of
the thread does. Each walk of ctx_list is still under one hold, and
nothing is kept across the unlock. The thread drops its io-wq, frees its
tctx and clears sqd->thread under one hold after the loop, so whoever
holds the lock and finds sqd->thread set can still use them.

Fixes: 63809137ebb5 ("io_uring: flush notifiers after sendzc")
Signed-off-by: Chris Mason <mason@kernel.org>
[axboe: adapted to the schedule_timeout() based loop]
Signed-off-by: Jens Axboe <axboe@kernel.dk>
---
 io_uring/cancel.c | 20 ++++++++++++++++++++
 1 file changed, 20 insertions(+)

diff --git a/io_uring/cancel.c b/io_uring/cancel.c
index 6f7a6cf356c2..79b5c133319b 100644
--- a/io_uring/cancel.c
+++ b/io_uring/cancel.c
@@ -703,7 +703,16 @@ __cold void io_uring_cancel_generic(bool cancel_all, struct io_sq_data *sqd)
 		}
 
 		if (loop) {
+			/*
+			 * A request that runs in io-wq and doesn't end makes
+			 * each round come here. Don't hold sqd->lock for that
+			 * long either.
+			 */
+			if (sqd)
+				mutex_unlock(&sqd->lock);
 			cond_resched();
+			if (sqd)
+				mutex_lock(&sqd->lock);
 			continue;
 		}
 
@@ -741,7 +750,18 @@ __cold void io_uring_cancel_generic(bool cancel_all, struct io_sq_data *sqd)
 
 			if (state & TASK_INTERRUPTIBLE)
 				timeout = MAX_SCHEDULE_TIMEOUT;
+			/*
+			 * A zero-copy notification can't be canceled, and a
+			 * peer that doesn't read keeps it pending. Don't hold
+			 * sqd->lock while waiting for it, or whoever wants the
+			 * lock sleeps as long. No ring is used across this,
+			 * the next round walks ->ctx_list anew.
+			 */
+			if (sqd)
+				mutex_unlock(&sqd->lock);
 			schedule_timeout(timeout);
+			if (sqd)
+				mutex_lock(&sqd->lock);
 		}
 end_wait:
 		finish_wait(&tctx->wait, &wait);
-- 
2.56.0


^ permalink raw reply related	[flat|nested] 11+ messages in thread

* [PATCH 09/10] io_uring/io-wq: save worker type before dropping its reference
  2026-10-28 16:46 [PATCHSET 0/10] Various io_uring fixes Jens Axboe
                   ` (7 preceding siblings ...)
  2026-10-28 16:46 ` [PATCH 08/10] io_uring/cancel: drop sqd->lock while SQPOLL waits Jens Axboe
@ 2026-10-28 16:46 ` Jens Axboe
  2026-10-28 16:46 ` [PATCH 10/10] io_uring: defer foreign io-wq enqueues to task_work Jens Axboe
  9 siblings, 0 replies; 11+ messages in thread
From: Jens Axboe @ 2026-10-28 16:46 UTC (permalink / raw)
  To: io-uring; +Cc: Kyle Zeng, stable, Jens Axboe

From: Kyle Zeng <kylebot@openai.com>

io_wq_cancel_tw_create() inspects cb->func after calling
io_worker_cancel_cb(). For an existing worker, that call can drop the
last worker reference and wake io_worker_exit(). If the canceling task
is preempted, the worker can be unlinked and freed by kfree_rcu() before
the callback function is inspected. KASAN reports a slab-use-after-free
read in this path.

Save whether the callback belongs to an unpublished continuation worker
while its reference is still held, then use the saved result for the
conditional free. This preserves the existing accounting and frees only
the worker allocated for the canceled continuation.

Fixes: e6db6f9398da ("io_uring/io-wq: only free worker if it was allocated for creation")
Cc: stable@vger.kernel.org
Signed-off-by: Kyle Zeng <kylebot@openai.com>
Signed-off-by: Jens Axboe <axboe@kernel.dk>
---
 io_uring/io-wq.c | 8 +++++---
 1 file changed, 5 insertions(+), 3 deletions(-)

diff --git a/io_uring/io-wq.c b/io_uring/io-wq.c
index 28533174d0da..92df54c6f50c 100644
--- a/io_uring/io-wq.c
+++ b/io_uring/io-wq.c
@@ -1434,14 +1434,16 @@ static void io_wq_cancel_tw_create(struct io_wq *wq)
 
 	while ((cb = task_work_cancel_match(wq->task, io_task_work_match, wq)) != NULL) {
 		struct io_worker *worker;
+		bool free_worker;
 
 		worker = container_of(cb, struct io_worker, create_work);
-		io_worker_cancel_cb(worker);
 		/*
 		 * Only the worker continuation helper has worker allocated and
-		 * hence needs freeing.
+		 * hence needs freeing. Save this before dropping its reference.
 		 */
-		if (cb->func == create_worker_cont)
+		free_worker = cb->func == create_worker_cont;
+		io_worker_cancel_cb(worker);
+		if (free_worker)
 			kfree(worker);
 	}
 }
-- 
2.56.0


^ permalink raw reply related	[flat|nested] 11+ messages in thread

* [PATCH 10/10] io_uring: defer foreign io-wq enqueues to task_work
  2026-10-28 16:46 [PATCHSET 0/10] Various io_uring fixes Jens Axboe
                   ` (8 preceding siblings ...)
  2026-10-28 16:46 ` [PATCH 09/10] io_uring/io-wq: save worker type before dropping its reference Jens Axboe
@ 2026-10-28 16:46 ` Jens Axboe
  9 siblings, 0 replies; 11+ messages in thread
From: Jens Axboe @ 2026-10-28 16:46 UTC (permalink / raw)
  To: io-uring; +Cc: Kyle Zeng, Jens Axboe

From: Kyle Zeng <kylebot@openai.com>

An IOPOLL ring without DEFER_TASKRUN can be reaped by a task other than
the one that submitted the I/O. If a polled request is marked
REQ_F_REISSUE, io_free_batch_list() calls io_queue_iowq() with the
original tctx. Ordinary task exit can tear down tctx->io_wq after
draining only the tracked requests. The live request pins the task, but
not its io-wq, so the foreign task can race io_wq_put_and_exit() and
enqueue through a freed or NULL queue.

Forward foreign userspace enqueues to the owning task before accessing
tctx->io_wq. Use a dedicated task_work callback so reissues still go
straight to io-wq, and fail them if task_work is running in cancellation
mode. Preserve the existing PF_KTHREAD cancellation path. This also
covers the other callers of io_queue_iowq() without changing the
worker-owned enqueue path.

Signed-off-by: Kyle Zeng <kylebot@openai.com>
Signed-off-by: Jens Axboe <axboe@kernel.dk>
---
 io_uring/io_uring.c | 25 +++++++++++++++++++++++--
 1 file changed, 23 insertions(+), 2 deletions(-)

diff --git a/io_uring/io_uring.c b/io_uring/io_uring.c
index 615933a6e8ff..03724419c4e1 100644
--- a/io_uring/io_uring.c
+++ b/io_uring/io_uring.c
@@ -423,6 +423,17 @@ static __always_inline bool io_assign_file(struct io_kiocb *req,
 	return !!req->file;
 }
 
+static void io_req_queue_iowq_tw(struct io_tw_req tw_req, io_tw_token_t tw)
+{
+	struct io_kiocb *req = tw_req.req;
+
+	io_tw_lock(req->ctx, tw);
+	if (unlikely(tw.cancel))
+		io_req_defer_failed(req, -ECANCELED);
+	else
+		io_queue_iowq(req);
+}
+
 void io_queue_iowq(struct io_kiocb *req)
 {
 	struct io_uring_task *tctx = req->tctx;
@@ -430,10 +441,17 @@ void io_queue_iowq(struct io_kiocb *req)
 	BUG_ON(!tctx);
 	lockdep_assert_held(&req->ctx->uring_lock);
 
-	if ((current->flags & PF_KTHREAD) || !tctx->io_wq) {
-		io_req_task_queue_fail(req, -ECANCELED);
+	if (current->flags & PF_KTHREAD)
+		goto cancel;
+
+	/* Only the submitter can safely access its io-wq during task exit. */
+	if (unlikely(current != tctx->task)) {
+		req->io_task_work.func = io_req_queue_iowq_tw;
+		io_req_task_work_add(req);
 		return;
 	}
+	if (!tctx->io_wq)
+		goto cancel;
 
 	/*
 	 * The file is looked up as late as possible, as it may be the result
@@ -462,6 +480,9 @@ void io_queue_iowq(struct io_kiocb *req)
 
 	trace_io_uring_queue_async_work(req, io_wq_is_hashed(&req->work));
 	io_wq_enqueue(tctx->io_wq, &req->work);
+	return;
+cancel:
+	io_req_task_queue_fail(req, -ECANCELED);
 }
 
 unsigned io_linked_nr(struct io_kiocb *req)
-- 
2.56.0


^ permalink raw reply related	[flat|nested] 11+ messages in thread

end of thread, other threads:[~2026-10-28 16:47 UTC | newest]

Thread overview: 11+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-10-28 16:46 [PATCHSET 0/10] Various io_uring fixes Jens Axboe
2026-10-28 16:46 ` [PATCH 01/10] io_uring/io-wq: no new workers once the exit has begun Jens Axboe
2026-10-28 16:46 ` [PATCH 02/10] io_uring/rsrc: charge buffer arrays and nodes to memcg Jens Axboe
2026-10-28 16:46 ` [PATCH 03/10] io_uring: charge tctx nodes and personalities Jens Axboe
2026-10-28 16:46 ` [PATCH 04/10] io_uring: free XArray nodes left by a failed store Jens Axboe
2026-10-28 16:46 ` [PATCH 05/10] io_uring/bpf-ops: clear bpf_ctx while submitting SQEs Jens Axboe
2026-10-28 16:46 ` [PATCH 06/10] io_uring: free the task context after a grace period Jens Axboe
2026-10-28 16:46 ` [PATCH 07/10] io_uring/sqpoll: don't wait for the thread to exit Jens Axboe
2026-10-28 16:46 ` [PATCH 08/10] io_uring/cancel: drop sqd->lock while SQPOLL waits Jens Axboe
2026-10-28 16:46 ` [PATCH 09/10] io_uring/io-wq: save worker type before dropping its reference Jens Axboe
2026-10-28 16:46 ` [PATCH 10/10] io_uring: defer foreign io-wq enqueues to task_work Jens Axboe

This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.