mirror of
https://github.com/torvalds/linux.git
synced 2026-07-27 17:47:41 +02:00
writeback: fix race between cgroup_writeback_umount() and inode_switch_wbs()
When a container exits, the following BUG_ON() is occasionally triggered: ================================================================== VFS: Busy inodes after unmount of sdb (ext4) ------------[ cut here ]------------ kernel BUG at fs/super.c:695! CPU: 3 PID: 6 Comm: containerd-shim Tainted: G OE K 6.6 #1 pstate: 63400009 (nZCv daif +PAN -UAO +TCO +DIT -SSBS BTYPE=--) pc : generic_shutdown_super+0xf0/0x100 lr : generic_shutdown_super+0xf0/0x100 Call trace: generic_shutdown_super+0xf0/0x100 kill_block_super+0x20/0x48 ext4_kill_sb+0x28/0x60 deactivate_locked_super+0x54/0x130 deactivate_super+0x84/0xa0 cleanup_mnt+0xa4/0x140 __cleanup_mnt+0x18/0x28 task_work_run+0x78/0xe0 do_notify_resume+0x204/0x240 ================================================================== The root cause is a race between cgroup_writeback_umount() and inode_switch_wbs()/cleanup_offline_cgwb(). There is a window between inode_prepare_wbs_switch() returning true and the subsequent wb_queue_isw() call. Following is the process that triggers the issue: CPU A (umount) | CPU B (writeback) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ inode_switch_wbs/cleanup_offline_cgwb atomic_inc(&isw_nr_in_flight) inode_prepare_wbs_switch -> passes SB_ACTIVE check __iget(inode) generic_shutdown_super sb->s_flags &= ~SB_ACTIVE cgroup_writeback_umount(sb) smp_mb() atomic_read(&isw_nr_in_flight) rcu_barrier() -> no pending RCU callbacks flush_workqueue(isw_wq) -> nothing queued, returns evict_inodes(sb) -> Inode skipped as isw still holds a ref. sop->put_super(sb) /* destroys percpu counters */ -> VFS: Busy inodes after unmount! wb_queue_isw() queue_work(isw_wq, ...) /* later in work function */ inode_switch_wbs_work_fn process_inode_switch_wbs iput() -> evict percpu_counter_dec() // UAF! Fix this by extending the RCU read-side critical section in inode_switch_wbs() and cleanup_offline_cgwb() to cover from inode_prepare_wbs_switch() through wb_queue_isw(). Since there is no sleep in this window, rcu_read_lock() can be used. Then add a synchronize_rcu() in cgroup_writeback_umount() before the existing rcu_barrier(), so that all in-flight switchers that have passed the SB_ACTIVE check have completed queue_work() before flush_workqueue() is called. The existing rcu_barrier() is intentionally retained so this fix can be backported unchanged to stable kernels (5.10.y, 6.6.y, ...) that still queue switches via queue_rcu_work(). It is a no-op on current mainline (since commite1b849cfa6("writeback: Avoid contention on wb->list_lock when switching inodes")) and is removed in a follow-up patch. Fixes:a1a0e23e49("writeback: flush inode cgroup wb switches instead of pinning super_block") Cc: stable@vger.kernel.org Suggested-by: Jan Kara <jack@suse.cz> Link: https://lore.kernel.org/all/mxnjq2l6guusfchvauxr3v7c4bwjasybxlleqbbh4efloeqspz@iqylk76ohufz Reviewed-by: Jan Kara <jack@suse.cz> Signed-off-by: Baokun Li <libaokun@linux.alibaba.com> Link: https://patch.msgid.link/20260521095016.2791354-2-libaokun@linux.alibaba.com Acked-by: Tejun Heo <tj@kernel.org> Signed-off-by: Christian Brauner (Amutable) <brauner@kernel.org>
This commit is contained in:
parent
254f49634e
commit
cba38ec4cb
|
|
@ -660,12 +660,19 @@ static void inode_switch_wbs(struct inode *inode, int new_wb_id)
|
|||
|
||||
atomic_inc(&isw_nr_in_flight);
|
||||
|
||||
/* find and pin the new wb */
|
||||
/*
|
||||
* Paired with synchronize_rcu() in cgroup_writeback_umount():
|
||||
* holding rcu_read_lock across inode_prepare_wbs_switch()
|
||||
* (covering the SB_ACTIVE check and the inode grab) and
|
||||
* wb_queue_isw() ensures synchronize_rcu() cannot return until
|
||||
* the work is queued, so the subsequent flush_workqueue() will
|
||||
* wait for the switch.
|
||||
*/
|
||||
rcu_read_lock();
|
||||
/* find and pin the new wb */
|
||||
memcg_css = css_from_id(new_wb_id, &memory_cgrp_subsys);
|
||||
if (memcg_css && !css_tryget(memcg_css))
|
||||
memcg_css = NULL;
|
||||
rcu_read_unlock();
|
||||
if (!memcg_css)
|
||||
goto out_free;
|
||||
|
||||
|
|
@ -681,9 +688,11 @@ static void inode_switch_wbs(struct inode *inode, int new_wb_id)
|
|||
|
||||
trace_inode_switch_wbs_queue(inode->i_wb, new_wb, 1);
|
||||
wb_queue_isw(new_wb, isw);
|
||||
rcu_read_unlock();
|
||||
return;
|
||||
|
||||
out_free:
|
||||
rcu_read_unlock();
|
||||
atomic_dec(&isw_nr_in_flight);
|
||||
if (new_wb)
|
||||
wb_put(new_wb);
|
||||
|
|
@ -741,6 +750,14 @@ bool cleanup_offline_cgwb(struct bdi_writeback *wb)
|
|||
new_wb = &wb->bdi->wb; /* wb_get() is noop for bdi's wb */
|
||||
|
||||
nr = 0;
|
||||
/*
|
||||
* Paired with synchronize_rcu() in cgroup_writeback_umount().
|
||||
* Holding rcu_read_lock across the SB_ACTIVE check, the inode grab
|
||||
* and wb_queue_isw() ensures synchronize_rcu() cannot return until
|
||||
* the work is queued, so the subsequent flush_workqueue() will wait
|
||||
* for the switch.
|
||||
*/
|
||||
rcu_read_lock();
|
||||
spin_lock(&wb->list_lock);
|
||||
/*
|
||||
* In addition to the inodes that have completed writeback, also switch
|
||||
|
|
@ -758,6 +775,7 @@ bool cleanup_offline_cgwb(struct bdi_writeback *wb)
|
|||
|
||||
/* no attached inodes? bail out */
|
||||
if (nr == 0) {
|
||||
rcu_read_unlock();
|
||||
atomic_dec(&isw_nr_in_flight);
|
||||
wb_put(new_wb);
|
||||
kfree(isw);
|
||||
|
|
@ -766,6 +784,7 @@ bool cleanup_offline_cgwb(struct bdi_writeback *wb)
|
|||
|
||||
trace_inode_switch_wbs_queue(wb, new_wb, nr);
|
||||
wb_queue_isw(new_wb, isw);
|
||||
rcu_read_unlock();
|
||||
|
||||
return restart;
|
||||
}
|
||||
|
|
@ -1221,6 +1240,14 @@ void cgroup_writeback_umount(struct super_block *sb)
|
|||
smp_mb();
|
||||
|
||||
if (atomic_read(&isw_nr_in_flight)) {
|
||||
/*
|
||||
* Paired with rcu_read_lock() in inode_switch_wbs() and
|
||||
* cleanup_offline_cgwb(). synchronize_rcu() waits for any
|
||||
* in-flight switcher that already passed the SB_ACTIVE check
|
||||
* to finish queueing its work, so flush_workqueue() below
|
||||
* will then drain it.
|
||||
*/
|
||||
synchronize_rcu();
|
||||
/*
|
||||
* Use rcu_barrier() to wait for all pending callbacks to
|
||||
* ensure that all in-flight wb switches are in the workqueue.
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user