Skip to content

Commit 49a0b34

Browse files
robbieko-aifdmanana
authored andcommitted
btrfs: fix incorrect i_size after remount caused by KEEP_SIZE prealloc gap
When fallocate() with FALLOC_FL_KEEP_SIZE preallocates an extent past the current i_size, the file_extent_tree of the inode is updated to cover that range. However, on the next mount, btrfs_read_locked_inode() only re-populates file_extent_tree with [0, round_up(i_size, sectorsize)), losing the marks that belonged to the KEEP_SIZE prealloc extent beyond i_size. Later, when a non-KEEP_SIZE fallocate() extends i_size into / past that old prealloc extent, the reservation loop in btrfs_fallocate() skips already-prealloc segments and does not call into the path that marks the file_extent_tree, so a gap remains inside the file_extent_tree across [old_aligned_i_size, start_of_new_alloc). Then __btrfs_prealloc_file_range() calls btrfs_inode_safe_disk_i_size_write(), which uses find_contiguous_extent_bit() starting at offset 0 to derive disk_i_size. The walk stops at the gap, so disk_i_size ends up smaller than i_size and gets persisted. After the next mount, the file shows the wrong (smaller) size. The following reproducer triggers the problem: $ cat test.sh MNT=/mnt/sdi DEV=/dev/sdi mkdir -p $MNT mkfs.btrfs -f -O ^no-holes $DEV mount $DEV $MNT touch $MNT/file1 # KEEP_SIZE prealloc beyond i_size (i_size stays 0) fallocate -n -o 4M -l 4M $MNT/file1 umount $MNT mount $DEV $MNT # non-KEEP_SIZE fallocate that overlaps the previous prealloc tail # and extends past it fallocate -o 7M -l 2M $MNT/file1 ls -lh $MNT/file1 umount $MNT mount $DEV $MNT ls -lh $MNT/file1 umount $MNT Running the reproducer gives the following result: $ ./test.sh (...) -rw-rw-r-- 1 root root 9.0M May 4 16:35 /mnt/sdi/file1 -rw-rw-r-- 1 root root 7.0M May 4 16:35 /mnt/sdi/file1 The size before the second mount is correct (9M), but after the remount it drops to 7M, i.e. the start of the gap inside file_extent_tree. Fix this in __btrfs_prealloc_file_range() by marking the entire range [round_down(old_i_size, sectorsize), round_up(new_i_size, sectorsize)) in file_extent_tree before updating i_size and calling btrfs_inode_safe_disk_i_size_write(). This ensures the contiguous bit search starting from 0 is not truncated by a stale gap left behind by a previous KEEP_SIZE prealloc that was not restored on inode load. The fix has no effect when the NO_HOLES feature is enabled because btrfs_inode_safe_disk_i_size_write() and btrfs_inode_set_file_extent_range() both take the fast path that directly tracks disk_i_size without consulting file_extent_tree. Fixes: 9ddc959 ("btrfs: use the file extent tree infrastructure") Reviewed-by: Filipe Manana <fdmanana@suse.com> Signed-off-by: Robbie Ko <robbieko@synology.com> [ Minor updates to the change log ] Signed-off-by: Filipe Manana <fdmanana@suse.com>
1 parent 5db7ccf commit 49a0b34

1 file changed

Lines changed: 28 additions & 0 deletions

File tree

fs/btrfs/inode.c

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9140,10 +9140,38 @@ static int __btrfs_prealloc_file_range(struct inode *inode, int mode,
91409140
if (!(mode & FALLOC_FL_KEEP_SIZE) &&
91419141
(actual_len > inode->i_size) &&
91429142
(cur_offset > inode->i_size)) {
9143+
u64 range_start;
9144+
u64 range_end;
9145+
91439146
if (cur_offset > actual_len)
91449147
i_size = actual_len;
91459148
else
91469149
i_size = cur_offset;
9150+
9151+
/*
9152+
* Make sure the file_extent_tree covers the entire
9153+
* range [old_i_size, new_i_size) before we update
9154+
* disk_i_size. Without this, a previous KEEP_SIZE
9155+
* prealloc that extended past i_size (and was lost
9156+
* across umount/mount because file_extent_tree is
9157+
* only populated up to round_up(i_size) on inode
9158+
* load) can leave a gap inside this range. That gap
9159+
* would cause btrfs_inode_safe_disk_i_size_write()
9160+
* (via find_contiguous_extent_bit() starting at 0)
9161+
* to truncate disk_i_size to the start of the gap,
9162+
* making the persisted size smaller than i_size.
9163+
*/
9164+
range_start = round_down(inode->i_size, fs_info->sectorsize);
9165+
range_end = round_up(i_size, fs_info->sectorsize);
9166+
ret = btrfs_inode_set_file_extent_range(BTRFS_I(inode),
9167+
range_start, range_end - range_start);
9168+
if (ret) {
9169+
btrfs_abort_transaction(trans, ret);
9170+
if (own_trans)
9171+
btrfs_end_transaction(trans);
9172+
break;
9173+
}
9174+
91479175
i_size_write(inode, i_size);
91489176
btrfs_inode_safe_disk_i_size_write(BTRFS_I(inode), 0);
91499177
}

0 commit comments

Comments
 (0)