A device replace can be left SUSPENDED while the filesystem stays mounted and writable. btrfs_resume_dev_replace_async() leaves the replace SUSPENDED with the target device still present when it cannot claim the exclusive operation (e.g. a recovered paused balance owns it), and while SUSPENDED btrfs_dev_replace_is_ongoing() stays true, so writes to the source device are still duplicated to the target device (see btrfs_map_block() and handle_ops_on_dev_replace()), accounted by dev_replace->bio_counter. If the replace is then canceled, btrfs_dev_replace_cancel() takes the SUSPENDED case and frees the target with btrfs_destroy_dev_replace_tgtdev(). That helper does a synchronize_rcu() (to fence readers of the device list) but does not wait for the duplicated write bios to drain. A bio that completes after the free dereferences the freed tgt_device (e.g. btrfs_log_dev_io_error() -> btrfs_dev_stat_inc_and_print()), and btrfs_close_bdev() tears the block device down while I/O is still in flight against it -- a use-after-free. btrfs_dev_replace_finishing() handles this correctly on its own error path: it calls btrfs_rm_dev_replace_blocked() -- which blocks new bios and waits for bio_counter to reach zero -- before btrfs_destroy_dev_replace_tgtdev(), then calls btrfs_rm_dev_replace_unblocked(). The cancel path omitted the drain. Mirror the finishing error path: drain the in-flight bios before destroying the target. Fixes: d189dd70e255 ("btrfs: fix use-after-free due to race between replace start and cancel") Cc: stable@vger.kernel.org Signed-off-by: Christian Brauner (Amutable) --- fs/btrfs/dev-replace.c | 3 +++ 1 file changed, 3 insertions(+) diff --git a/fs/btrfs/dev-replace.c b/fs/btrfs/dev-replace.c index 51665ed09798..e5090fb7fc11 100644 --- a/fs/btrfs/dev-replace.c +++ b/fs/btrfs/dev-replace.c @@ -1161,8 +1161,11 @@ int btrfs_dev_replace_cancel(struct btrfs_fs_info *fs_info) btrfs_dev_name(src_device), src_device->devid, btrfs_dev_name(tgt_device)); + /* Drain writes still duplicated to tgtdev before freeing it. */ + btrfs_rm_dev_replace_blocked(fs_info); if (tgt_device) btrfs_destroy_dev_replace_tgtdev(tgt_device); + btrfs_rm_dev_replace_unblocked(fs_info); break; default: up_write(&dev_replace->rwsem); -- 2.47.3