]> git.ipfire.org Git - thirdparty/kernel/stable.git/commitdiff
btrfs: fix a double release on reserved extents in cow_one_range()
authorQu Wenruo <wqu@suse.com>
Mon, 9 Feb 2026 03:31:45 +0000 (14:01 +1030)
committerDavid Sterba <dsterba@suse.com>
Thu, 26 Feb 2026 14:03:26 +0000 (15:03 +0100)
[BUG]
Commit c28214bde6da ("btrfs: refactor the main loop of
cow_file_range()") refactored the handling of COWing one range.

However it changed the error handling of the reserved extent.

The old cleanup looks like this:

out_drop_extent_cache:
btrfs_drop_extent_map_range(inode, start, start + cur_alloc_size - 1, false);
out_reserve:
btrfs_dec_block_group_reservations(fs_info, ins.objectid);
btrfs_free_reserved_extent(fs_info, ins.objectid, ins.offset, true);
[...]
clear_bits = EXTENT_LOCKED | EXTENT_DELALLOC | EXTENT_DELALLOC_NEW |
     EXTENT_DEFRAG | EXTENT_CLEAR_META_RESV;
page_ops = PAGE_UNLOCK | PAGE_START_WRITEBACK | PAGE_END_WRITEBACK;
/*
 * For the range (2). If we reserved an extent for our delalloc range
 * (or a subrange) and failed to create the respective ordered extent,
 * then it means that when we reserved the extent we decremented the
 * extent's size from the data space_info's bytes_may_use counter and
 * incremented the space_info's bytes_reserved counter by the same
 * amount. We must make sure extent_clear_unlock_delalloc() does not try
 * to decrement again the data space_info's bytes_may_use counter,
 * therefore we do not pass it the flag EXTENT_CLEAR_DATA_RESV.
 */
if (cur_alloc_size) {
        extent_clear_unlock_delalloc(inode, start,
                                     start + cur_alloc_size - 1,
                                     locked_folio, &cached, clear_bits,
                                     page_ops);
        btrfs_qgroup_free_data(inode, NULL, start, cur_alloc_size, NULL);
}

Which only calls EXTENT_CLEAR_META_RESV.
As the reserved extent is properly handled by
btrfs_free_reserved_extent().

However the new cleanup is:

extent_clear_unlock_delalloc(inode, file_offset, cur_end, locked_folio, cached,
     EXTENT_LOCKED | EXTENT_DELALLOC |
     EXTENT_DELALLOC_NEW |
     EXTENT_DEFRAG | EXTENT_DO_ACCOUNTING,
     PAGE_UNLOCK | PAGE_START_WRITEBACK |
     PAGE_END_WRITEBACK);
btrfs_qgroup_free_data(inode, NULL, file_offset, cur_len, NULL);
btrfs_dec_block_group_reservations(fs_info, ins->objectid);
btrfs_free_reserved_extent(fs_info, ins->objectid, ins->offset, true);

The flag EXTENT_DO_ACCOUNTING implies both EXTENT_CLEAR_META_RESV and
EXTENT_CLEAR_DATA_RESV, which will release the bytes_may_use, which
later btrfs_free_reserved_extent() will do again, causing incorrect
double release (and may underflow bytes_may_use).

[FIX]
Use EXTENT_CLEAR_META_RESV to replace EXTENT_DO_ACCOUNTING, and add back
the comments on why we only use EXTENT_CLEAR_META_RESV.

Fixes: c28214bde6da ("btrfs: refactor the main loop of cow_file_range()")
Reported-by: Chris Mason <clm@meta.com>
Link: https://lore.kernel.org/linux-btrfs/20260208184920.1102719-1-clm@meta.com/
Reviewed-by: Filipe Manana <fdmanana@suse.com>
Signed-off-by: Qu Wenruo <wqu@suse.com>
Signed-off-by: David Sterba <dsterba@suse.com>
fs/btrfs/inode.c

index b6c763a17406bd3e02a437ff1fa2d2635c49e8c9..ade5907077938b4dcc47a429f5cf0140fa9a0ee1 100644 (file)
@@ -1392,10 +1392,25 @@ static int cow_one_range(struct btrfs_inode *inode, struct folio *locked_folio,
        return ret;
 
 free_reserved:
+       /*
+        * If we have reserved an extent for the current range and failed to
+        * create the respective extent map or ordered extent, it means that
+        * when we reserved the extent we decremented the extent's size from
+        * the data space_info's bytes_may_use counter and
+        * incremented the space_info's bytes_reserved counter by the same
+        * amount.
+        *
+        * We must make sure extent_clear_unlock_delalloc() does not try
+        * to decrement again the data space_info's bytes_may_use counter, which
+        * will be handled by btrfs_free_reserved_extent().
+        *
+        * Therefore we do not pass it the flag EXTENT_CLEAR_DATA_RESV, but only
+        * EXTENT_CLEAR_META_RESV.
+        */
        extent_clear_unlock_delalloc(inode, file_offset, cur_end, locked_folio, cached,
                                     EXTENT_LOCKED | EXTENT_DELALLOC |
                                     EXTENT_DELALLOC_NEW |
-                                    EXTENT_DEFRAG | EXTENT_DO_ACCOUNTING,
+                                    EXTENT_DEFRAG | EXTENT_CLEAR_META_RESV,
                                     PAGE_UNLOCK | PAGE_START_WRITEBACK |
                                     PAGE_END_WRITEBACK);
        btrfs_qgroup_free_data(inode, NULL, file_offset, cur_len, NULL);