btrfs: fix a double release on reserved extents in cow_one_range()

[BUG]
Commit c28214bde6 ("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: c28214bde6 ("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>
This commit is contained in:
Qu Wenruo 2026-02-09 14:01:45 +10:30 committed by David Sterba
parent 2970525f78
commit a4fe134fc1

View 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);