Re: [PATCH v2] bcache: fix UAF in cached_dev_free and safely flush/destroy
liequan che <[email protected]> Wed, 19 Nov 2025 16:12:43 +0800
| Newsgroups | org.kernel.vger.linux-bcache |
|---|---|
| Message-ID | <CAAsfc_p4KWYgkfny0BqS8G7jOiXf_r1QCb0x0Sh56je=GBdUXw@mail.gmail.com> |
Hi Coly and Kent,
Sorry for the earlier omissions. Here is a concise correction and completion.
1) Could you please point out exactly which reference is still held?
From the crash we analyzed, there isn’t a leaked struct cached_dev
reference causing the panic. The long-lived references during teardown
behave as expected:
dc->disk.cl (closure) is taken/released around detach/free paths.
dc->count (refcount) gates device lifetime.
The actual failure is a stop-ownership race on dc->writeback_thread:
one path stops the kthread and it exits (freeing the task), while
another racing path still calls kthread_stop() on a stale pointer,
triggering the refcount_t warning and subsequent fault. In short, it’s
not a missing cached_dev_put(), but a second kthread_stop() against an
already exited thread.
2) Could you please point out all the locations where writeback_wq is stopped?
If by “stopped” you mean flush/destroy of the per-device writeback
workqueue (dc->writeback_write_wq), there is only one place:
At the tail of bch_writeback_thread():
if (dc->writeback_write_wq) {
flush_workqueue(dc->writeback_write_wq);
destroy_workqueue(dc->writeback_write_wq);
}
Other sites stop the thread (see next paragraph), but do not
flush/destroy the workqueue.
For completeness, there are exactly three call sites that may call
kthread_stop(dc->writeback_thread):
cached_dev_detach_finish()
bch_cached_dev_attach() error path when bch_cached_dev_run() fails
(and error != -EBUSY)
cached_dev_free()
Note: bch_writeback_thread() does not call kthread_stop() on itself;
it only tears down its WQ, then cached_dev_put(dc) and
wait_for_kthread_stop().
The per-device writeback workqueue is flushed/destroyed at one place:
1、bch_writeback_thread() tail — it does not call kthread_stop(); it
only tears down dc->writeback_write_wq (flush + destroy), then
cached_dev_put(dc) and
wait_for_kthread_stop().
3)how the panic comes in code logic
I/O errors -> bch_cache_set_error()
-> set_bit(CACHE_SET_IO_DISABLE)
-> conditional_stop_bcache_device()
-> bcache_device_stop() -> cached_dev_flush()
-> continue_at(..., cached_dev_free, system_wq)
-> cached_dev_free():
kthread_stop(writeback_thread) [Thread stop site #3]
Meanwhile writeback thread observes IO_DISABLE:
bch_writeback_thread():
exit path:
- flush/destroy dc->writeback_write_wq (single owner)
- cached_dev_put(dc)
- wait_for_kthread_stop()
If last ref:
queue detach once:
cached_dev_detach_finish():
- cancel writeback-rate dwork
- kthread_stop(writeback_thread) if still present [Thread stop site #1]
- bcache_device_detach(), list moves, clear flags, closure_put()
Attach error path:
bch_cached_dev_attach():
if bch_cached_dev_run() fails && err != -EBUSY:
- kthread_stop(writeback_thread) [Thread stop site #2]
- cancel writeback-rate dwork
I/O errors (e.g., NVMe remove/journal error)
|
v
bch_cache_set_error()
|-- set_bit(CACHE_SET_IO_DISABLE, &c->flags)
|-- conditional_stop_bcache_device()
|
v
bcache_device_stop()
|
v
cached_dev_flush()
|
v
(system "events" WQ) -> cached_dev_free(dc)
|
+-- kthread_stop(dc->writeback_thread) [A]
+-- kthread_stop(dc->status_update_thread)
'-- cancel writeback-rate dwork, unlink, free...
Meanwhile:
bch_writeback_thread(dc) sees IO_DISABLE or should_stop and exits:
|
+-- flush/destroy dc->writeback_write_wq [sole WQ teardown site]
+-- cached_dev_put(dc)
'-- wait_for_kthread_stop()
Race:
- One path already ended the thread and freed its task_struct.
- A second path still calls kthread_stop(dc->writeback_thread) on the
stale pointer
-> refcount splat / GPF (what we observe).
Thanks again, and apologies for the earlier incomplete answer.
Best regards,
cheliequan
liequan che <[email protected]> 于2025年11月13日周四 19:38写道:
>
> stop writeback thread and rate-update work exactly once across teardown paths,
> - Add STOP_THREAD_ONCE() and use it at all three places that stop
> dc->writeback_thread: cached_dev_detach_finish(), cached_dev_free(),
> and the bch_cached_dev_attach() error path.
> - In cached_dev_detach_finish(), also clear WB_RUNNING and cancel the
> periodic writeback-rate delayed work to avoid a UAF window after
> detach is initiated.
> - Keep the per-dc writeback workqueue flush/destroy in the writeback
> thread exit tail, avoiding double-destroy.
> Signed-off-by: cheliequan <[email protected]>
> ---
> drivers/md/bcache/bcache.h | 11 +++++++++++
> drivers/md/bcache/super.c | 14 ++++++--------
> drivers/md/bcache/writeback.c | 7 +++++--
> 3 files changed, 22 insertions(+), 10 deletions(-)
> diff --git a/drivers/md/bcache/bcache.h b/drivers/md/bcache/bcache.h
> index 1d33e40d26ea..66dc5dca5c20 100644
> --- a/drivers/md/bcache/bcache.h
> +++ b/drivers/md/bcache/bcache.h
> @@ -961,6 +961,17 @@ static inline void wait_for_kthread_stop(void)
> }
> }
>
> +/*
> + * Stop a kthread exactly once by taking ownership of the pointer.
> + * Safe against concurrent callers and against already-stopped threads.
> + */
> +#define STOP_THREAD_ONCE(dc, member) \
> + do { \
> + struct task_struct *t__ = xchg(&(dc)->member, NULL); \
> + if (t__ && !IS_ERR(t__)) \
> + kthread_stop(t__); \
> + } while (0)
> +
> /* Forward declarations */
>
> void bch_count_backing_io_errors(struct cached_dev *dc, struct bio *bio);
> diff --git a/drivers/md/bcache/super.c b/drivers/md/bcache/super.c
> index 1492c8552255..b4da0a505d4a 100644
> --- a/drivers/md/bcache/super.c
> +++ b/drivers/md/bcache/super.c
> @@ -1143,8 +1143,7 @@ static void cached_dev_detach_finish(struct
> work_struct *w)
> cancel_writeback_rate_update_dwork(dc);
>
> if (!IS_ERR_OR_NULL(dc->writeback_thread)) {
> - kthread_stop(dc->writeback_thread);
> - dc->writeback_thread = NULL;
> + STOP_THREAD_ONCE(dc, writeback_thread);
> }
>
> mutex_lock(&bch_register_lock);
> @@ -1308,8 +1307,9 @@ int bch_cached_dev_attach(struct cached_dev *dc,
> struct cache_set *c,
> * created previously in bch_cached_dev_writeback_start()
> * have to be stopped manually here.
> */
> - kthread_stop(dc->writeback_thread);
> - cancel_writeback_rate_update_dwork(dc);
> + if (test_and_clear_bit(BCACHE_DEV_WB_RUNNING, &dc->disk.flags))
> + cancel_writeback_rate_update_dwork(dc);
> + STOP_THREAD_ONCE(dc, writeback_thread);
> pr_err("Couldn't run cached device %pg\n", dc->bdev);
> return ret;
> }
> @@ -1349,10 +1349,8 @@ static CLOSURE_CALLBACK(cached_dev_free)
> if (test_and_clear_bit(BCACHE_DEV_WB_RUNNING, &dc->disk.flags))
> cancel_writeback_rate_update_dwork(dc);
>
> - if (!IS_ERR_OR_NULL(dc->writeback_thread))
> - kthread_stop(dc->writeback_thread);
> - if (!IS_ERR_OR_NULL(dc->status_update_thread))
> - kthread_stop(dc->status_update_thread);
> + STOP_THREAD_ONCE(dc, writeback_thread);
> + STOP_THREAD_ONCE(dc, status_update_thread);
>
> mutex_lock(&bch_register_lock);
>
> diff --git a/drivers/md/bcache/writeback.c b/drivers/md/bcache/writeback.c
> index 302e75f1fc4b..50e67a784acd 100644
> --- a/drivers/md/bcache/writeback.c
> +++ b/drivers/md/bcache/writeback.c
> @@ -741,6 +741,7 @@ static int bch_writeback_thread(void *arg)
> struct cached_dev *dc = arg;
> struct cache_set *c = dc->disk.c;
> bool searched_full_index;
> + struct workqueue_struct *wq = NULL;
>
> bch_ratelimit_reset(&dc->writeback_rate);
>
> @@ -832,8 +833,10 @@ static int bch_writeback_thread(void *arg)
> }
> }
>
> - if (dc->writeback_write_wq)
> - destroy_workqueue(dc->writeback_write_wq);
> + wq = xchg(&dc->writeback_write_wq, NULL);
> + if (wq) {
> + destroy_workqueue(wq);
> + }
>
> cached_dev_put(dc);
> wait_for_kthread_stop();
> --
> 2.25.1