Skip to content

Commit 4c8f353

Browse files
fdmananakdave
authored andcommitted
btrfs: fix filesystem corruption after a device replace
We use a device's allocation state tree to track ranges in a device used for allocated chunks, and we set ranges in this tree when allocating a new chunk. However after a device replace operation, we were not setting the allocated ranges in the new device's allocation state tree, so that tree is empty after a device replace. This means that a fitrim operation after a device replace will trim the device ranges that have allocated chunks and extents, as we trim every range for which there is not a range marked in the device's allocation state tree. It is also important during chunk allocation, since the device's allocation state is used to determine if a range is already allocated when allocating a new chunk. This is trivial to reproduce and the following script triggers the bug: $ cat reproducer.sh #!/bin/bash DEV1="/dev/sdg" DEV2="/dev/sdh" DEV3="/dev/sdi" wipefs -a $DEV1 $DEV2 $DEV3 &> /dev/null # Create a raid1 test fs on 2 devices. mkfs.btrfs -f -m raid1 -d raid1 $DEV1 $DEV2 > /dev/null mount $DEV1 /mnt/btrfs xfs_io -f -c "pwrite -S 0xab 0 10M" /mnt/btrfs/foo echo "Starting to replace $DEV1 with $DEV3" btrfs replace start -B $DEV1 $DEV3 /mnt/btrfs echo echo "Running fstrim" fstrim /mnt/btrfs echo echo "Unmounting filesystem" umount /mnt/btrfs echo "Mounting filesystem in degraded mode using $DEV3 only" wipefs -a $DEV1 $DEV2 &> /dev/null mount -o degraded $DEV3 /mnt/btrfs if [ $? -ne 0 ]; then dmesg | tail echo echo "Failed to mount in degraded mode" exit 1 fi echo echo "File foo data (expected all bytes = 0xab):" od -A d -t x1 /mnt/btrfs/foo umount /mnt/btrfs When running the reproducer: $ ./replace-test.sh wrote 10485760/10485760 bytes at offset 0 10 MiB, 2560 ops; 0.0901 sec (110.877 MiB/sec and 28384.5216 ops/sec) Starting to replace /dev/sdg with /dev/sdi Running fstrim Unmounting filesystem Mounting filesystem in degraded mode using /dev/sdi only mount: /mnt/btrfs: wrong fs type, bad option, bad superblock on /dev/sdi, missing codepage or helper program, or other error. [19581.748641] BTRFS info (device sdg): dev_replace from /dev/sdg (devid 1) to /dev/sdi started [19581.803842] BTRFS info (device sdg): dev_replace from /dev/sdg (devid 1) to /dev/sdi finished [19582.208293] BTRFS info (device sdi): allowing degraded mounts [19582.208298] BTRFS info (device sdi): disk space caching is enabled [19582.208301] BTRFS info (device sdi): has skinny extents [19582.212853] BTRFS warning (device sdi): devid 2 uuid 1f731f47-e1bb-4f00-bfbb-9e5a0cb4ba9f is missing [19582.213904] btree_readpage_end_io_hook: 25839 callbacks suppressed [19582.213907] BTRFS error (device sdi): bad tree block start, want 30490624 have 0 [19582.214780] BTRFS warning (device sdi): failed to read root (objectid=7): -5 [19582.231576] BTRFS error (device sdi): open_ctree failed Failed to mount in degraded mode So fix by setting all allocated ranges in the replace target device when the replace operation is finishing, when we are holding the chunk mutex and we can not race with new chunk allocations. A test case for fstests follows soon. Fixes: 1c11b63 ("btrfs: replace pending/pinned chunks lists with io tree") CC: [email protected] # 5.2+ Reviewed-by: Nikolay Borisov <[email protected]> Reviewed-by: Johannes Thumshirn <[email protected]> Signed-off-by: Filipe Manana <[email protected]> Signed-off-by: David Sterba <[email protected]>
1 parent a466c85 commit 4c8f353

File tree

1 file changed

+39
-1
lines changed

1 file changed

+39
-1
lines changed

fs/btrfs/dev-replace.c

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -599,6 +599,37 @@ static void btrfs_rm_dev_replace_unblocked(struct btrfs_fs_info *fs_info)
599599
wake_up(&fs_info->dev_replace.replace_wait);
600600
}
601601

602+
/*
603+
* When finishing the device replace, before swapping the source device with the
604+
* target device we must update the chunk allocation state in the target device,
605+
* as it is empty because replace works by directly copying the chunks and not
606+
* through the normal chunk allocation path.
607+
*/
608+
static int btrfs_set_target_alloc_state(struct btrfs_device *srcdev,
609+
struct btrfs_device *tgtdev)
610+
{
611+
struct extent_state *cached_state = NULL;
612+
u64 start = 0;
613+
u64 found_start;
614+
u64 found_end;
615+
int ret = 0;
616+
617+
lockdep_assert_held(&srcdev->fs_info->chunk_mutex);
618+
619+
while (!find_first_extent_bit(&srcdev->alloc_state, start,
620+
&found_start, &found_end,
621+
CHUNK_ALLOCATED, &cached_state)) {
622+
ret = set_extent_bits(&tgtdev->alloc_state, found_start,
623+
found_end, CHUNK_ALLOCATED);
624+
if (ret)
625+
break;
626+
start = found_end + 1;
627+
}
628+
629+
free_extent_state(cached_state);
630+
return ret;
631+
}
632+
602633
static int btrfs_dev_replace_finishing(struct btrfs_fs_info *fs_info,
603634
int scrub_ret)
604635
{
@@ -673,8 +704,14 @@ static int btrfs_dev_replace_finishing(struct btrfs_fs_info *fs_info,
673704
dev_replace->time_stopped = ktime_get_real_seconds();
674705
dev_replace->item_needs_writeback = 1;
675706

676-
/* replace old device with new one in mapping tree */
707+
/*
708+
* Update allocation state in the new device and replace the old device
709+
* with the new one in the mapping tree.
710+
*/
677711
if (!scrub_ret) {
712+
scrub_ret = btrfs_set_target_alloc_state(src_device, tgt_device);
713+
if (scrub_ret)
714+
goto error;
678715
btrfs_dev_replace_update_device_in_mapping_tree(fs_info,
679716
src_device,
680717
tgt_device);
@@ -685,6 +722,7 @@ static int btrfs_dev_replace_finishing(struct btrfs_fs_info *fs_info,
685722
btrfs_dev_name(src_device),
686723
src_device->devid,
687724
rcu_str_deref(tgt_device->name), scrub_ret);
725+
error:
688726
up_write(&dev_replace->rwsem);
689727
mutex_unlock(&fs_info->chunk_mutex);
690728
mutex_unlock(&fs_info->fs_devices->device_list_mutex);

0 commit comments

Comments
 (0)