io_uring/io-wq: no new workers once the exit has begun

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: 685fe7feed ("io-wq: eliminate the need for a manager thread")
Signed-off-by: Josef Bacik <josef@toxicpanda.com>
[axboe: the barrier is already upstream, see 0e5663856b, only the
 cancel is new]
Signed-off-by: Jens Axboe <axboe@kernel.dk>
This commit is contained in:
Josef Bacik 2026-10-08 02:48:45 +00:00 committed by Jens Axboe
parent 2295613cd3
commit 47c68f9250

View File

@ -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)