Skip to content

Commit 609e804

Browse files
fdmananakdave
authored andcommitted
Btrfs: fix file corruption after snapshotting due to mix of buffered/DIO writes
When we are mixing buffered writes with direct IO writes against the same file and snapshotting is happening concurrently, we can end up with a corrupt file content in the snapshot. Example: 1) Inode/file is empty. 2) Snapshotting starts. 2) Buffered write at offset 0 length 256Kb. This updates the i_size of the inode to 256Kb, disk_i_size remains zero. This happens after the task doing the snapshot flushes all existing delalloc. 3) DIO write at offset 256Kb length 768Kb. Once the ordered extent completes it sets the inode's disk_i_size to 1Mb (256Kb + 768Kb) and updates the inode item in the fs tree with a size of 1Mb (which is the value of disk_i_size). 4) The dealloc for the range [0, 256Kb[ did not start yet. 5) The transaction used in the DIO ordered extent completion, which updated the inode item, is committed by the snapshotting task. 6) Snapshot creation completes. 7) Dealloc for the range [0, 256Kb[ is flushed. After that when reading the file from the snapshot we always get zeroes for the range [0, 256Kb[, the file has a size of 1Mb and the data written by the direct IO write is found. From an application's point of view this is a corruption, since in the source subvolume it could never read a version of the file that included the data from the direct IO write without the data from the buffered write included as well. In the snapshot's tree, file extent items are missing for the range [0, 256Kb[. The issue, obviously, does not happen when using the -o flushoncommit mount option. Fix this by flushing delalloc for all the roots that are about to be snapshotted when committing a transaction. This guarantees total ordering when updating the disk_i_size of an inode since the flush for dealloc is done when a transaction is in the TRANS_STATE_COMMIT_START state and wait is done once no more external writers exist. This is similar to what we do when using the flushoncommit mount option, but we do it only if the transaction has snapshots to create and only for the roots of the subvolumes to be snapshotted. The bulk of the dealloc is flushed in the snapshot creation ioctl, so the flush work we do inside the transaction is minimized. This issue, involving buffered and direct IO writes with snapshotting, is often triggered by fstest btrfs/078, and got reported by fsck when not using the NO_HOLES features, for example: $ cat results/btrfs/078.full (...) _check_btrfs_filesystem: filesystem on /dev/sdc is inconsistent *** fsck.btrfs output *** [1/7] checking root items [2/7] checking extents [3/7] checking free space cache [4/7] checking fs roots root 258 inode 264 errors 100, file extent discount Found file extent holes: start: 524288, len: 65536 ERROR: errors found in fs roots Signed-off-by: Filipe Manana <[email protected]> Signed-off-by: David Sterba <[email protected]>
1 parent 2cc8334 commit 609e804

File tree

1 file changed

+43
-6
lines changed

1 file changed

+43
-6
lines changed

fs/btrfs/transaction.c

Lines changed: 43 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1886,8 +1886,10 @@ static void btrfs_cleanup_pending_block_groups(struct btrfs_trans_handle *trans)
18861886
}
18871887
}
18881888

1889-
static inline int btrfs_start_delalloc_flush(struct btrfs_fs_info *fs_info)
1889+
static inline int btrfs_start_delalloc_flush(struct btrfs_trans_handle *trans)
18901890
{
1891+
struct btrfs_fs_info *fs_info = trans->fs_info;
1892+
18911893
/*
18921894
* We use writeback_inodes_sb here because if we used
18931895
* btrfs_start_delalloc_roots we would deadlock with fs freeze.
@@ -1897,15 +1899,50 @@ static inline int btrfs_start_delalloc_flush(struct btrfs_fs_info *fs_info)
18971899
* from already being in a transaction and our join_transaction doesn't
18981900
* have to re-take the fs freeze lock.
18991901
*/
1900-
if (btrfs_test_opt(fs_info, FLUSHONCOMMIT))
1902+
if (btrfs_test_opt(fs_info, FLUSHONCOMMIT)) {
19011903
writeback_inodes_sb(fs_info->sb, WB_REASON_SYNC);
1904+
} else {
1905+
struct btrfs_pending_snapshot *pending;
1906+
struct list_head *head = &trans->transaction->pending_snapshots;
1907+
1908+
/*
1909+
* Flush dellaloc for any root that is going to be snapshotted.
1910+
* This is done to avoid a corrupted version of files, in the
1911+
* snapshots, that had both buffered and direct IO writes (even
1912+
* if they were done sequentially) due to an unordered update of
1913+
* the inode's size on disk.
1914+
*/
1915+
list_for_each_entry(pending, head, list) {
1916+
int ret;
1917+
1918+
ret = btrfs_start_delalloc_snapshot(pending->root);
1919+
if (ret)
1920+
return ret;
1921+
}
1922+
}
19021923
return 0;
19031924
}
19041925

1905-
static inline void btrfs_wait_delalloc_flush(struct btrfs_fs_info *fs_info)
1926+
static inline void btrfs_wait_delalloc_flush(struct btrfs_trans_handle *trans)
19061927
{
1907-
if (btrfs_test_opt(fs_info, FLUSHONCOMMIT))
1928+
struct btrfs_fs_info *fs_info = trans->fs_info;
1929+
1930+
if (btrfs_test_opt(fs_info, FLUSHONCOMMIT)) {
19081931
btrfs_wait_ordered_roots(fs_info, U64_MAX, 0, (u64)-1);
1932+
} else {
1933+
struct btrfs_pending_snapshot *pending;
1934+
struct list_head *head = &trans->transaction->pending_snapshots;
1935+
1936+
/*
1937+
* Wait for any dellaloc that we started previously for the roots
1938+
* that are going to be snapshotted. This is to avoid a corrupted
1939+
* version of files in the snapshots that had both buffered and
1940+
* direct IO writes (even if they were done sequentially).
1941+
*/
1942+
list_for_each_entry(pending, head, list)
1943+
btrfs_wait_ordered_extents(pending->root,
1944+
U64_MAX, 0, U64_MAX);
1945+
}
19091946
}
19101947

19111948
int btrfs_commit_transaction(struct btrfs_trans_handle *trans)
@@ -2023,7 +2060,7 @@ int btrfs_commit_transaction(struct btrfs_trans_handle *trans)
20232060

20242061
extwriter_counter_dec(cur_trans, trans->type);
20252062

2026-
ret = btrfs_start_delalloc_flush(fs_info);
2063+
ret = btrfs_start_delalloc_flush(trans);
20272064
if (ret)
20282065
goto cleanup_transaction;
20292066

@@ -2039,7 +2076,7 @@ int btrfs_commit_transaction(struct btrfs_trans_handle *trans)
20392076
if (ret)
20402077
goto cleanup_transaction;
20412078

2042-
btrfs_wait_delalloc_flush(fs_info);
2079+
btrfs_wait_delalloc_flush(trans);
20432080

20442081
btrfs_scrub_pause(fs_info);
20452082
/*

0 commit comments

Comments
 (0)