btrfs: fix balance NULL derefs and chunk/bg mapping verification
v3 cover btrfs: fix balance NULL derefs and chunk/bg mapping verification
This series fixes three NULL dereferences in btrfs balance paths and the underlying mount-time verification bug that lets the corresponding chunk/block-group inconsistency go undetected.
The balance crashes happen when metadata corruption leaves a chunk present in the chunk tree but without a corresponding block group in the in-memory block group cache. In that case, balance reaches code paths that call btrfs_lookup_block_group() and dereference the returned pointer without checking for NULL.
The first three patches harden the affected balance paths:
- patch 1 fixes chunk_usage_filter()
- patch 2 fixes chunk_usage_range_filter()
- patch 3 fixes btrfs_may_alloc_data_chunk()
They are kept separate because the affected code was introduced by different commits, which should also make backporting easier, as suggested by Qu Wenruo.
The fourth patch fixes the mount-time verification side. Based on David Sterba's feedback, it now explicitly relies on the mount-time context and uses a lockless traversal of mapping_tree. check_chunk_block_group_mappings() is supposed to verify that every chunk has a matching block group, but its current iteration starts with btrfs_find_chunk_map(fs_info, 0, 1). If no chunk contains logical address 0, the lookup returns NULL immediately and the loop exits without checking any chunk at all. As a result, the corrupted mapping can survive mount and only crash later when balance reaches it.
This series makes btrfs reject the inconsistency earlier at mount time, and also hardens the balance paths so the corruption is reported as -EUCLEAN instead of triggering NULL dereferences.
[CHANGELOG]
v3:
- added a new patch to fix the same missing-block-group NULL dereference in btrfs_may_alloc_data_chunk()
- patch 1 and 2:
- changed the bool return flow to explicit int error propagation
- used ret2 for the nested filter return value in should_balance_chunk()
- patch 4:
- reworked the changelog based on David Sterba's feedback
- clarified the mount-time context for the lockless mapping_tree traversal
v2:
- split the two balance filter fixes into separate patches
- reworked the chunk/block-group verification fix so the last patch addresses the case where check_chunk_block_group_mappings() does not actually iterate all chunk mappings
ZhengYuan Huang (4):
btrfs: balance: fix null-ptr-deref in chunk_usage_filter
btrfs: balance: fix null-ptr-deref in chunk_usage_range_filter
btrfs: balance: fix null-ptr-deref in btrfs_may_alloc_data_chunk
btrfs: fix check_chunk_block_group_mappings() to iterate all chunk maps
fs/btrfs/block-group.c | 24 ++++++---------- fs/btrfs/volumes.c | 63 ++++++++++++++++++++++++++++++------------ 2 files changed, 55 insertions(+), 32 deletions(-)
Signature · 1 line
2.43.0
PATCH v3 1/4 btrfs: balance: fix null-ptr-deref in chunk_usage_filter
[BUG]
Running btrfs balance with a usage filter (-dusage=N) can trigger a null-ptr-deref when metadata corruption causes a chunk to have no corresponding block group in the in-memory cache:
KASAN: null-ptr-deref in range [0x0000000000000070-0x0000000000000077]
RIP: 0010:chunk_usage_filter fs/btrfs/volumes.c:3874 [inline]
RIP: 0010:should_balance_chunk fs/btrfs/volumes.c:4018 [inline]
RIP: 0010:__btrfs_balance fs/btrfs/volumes.c:4172 [inline]
RIP: 0010:btrfs_balance+0x2024/0x42b0 fs/btrfs/volumes.c:4604
...
Call Trace:
btrfs_ioctl_balance fs/btrfs/ioctl.c:3577 [inline]
btrfs_ioctl+0x25cf/0x5b90 fs/btrfs/ioctl.c:5313
vfs_ioctl fs/ioctl.c:51 [inline]
...The bug is reproducible on next-20260312.
[CAUSE]
Two separate data structures are involved:
1. The on-disk chunk tree, which records every chunk (logical address space region) and is iterated by __btrfs_balance().
2. The in-memory block group cache (fs_info->block_group_cache_tree), which is built at mount time by btrfs_read_block_groups() and holds a struct btrfs_block_group for each chunk. This cache is what the usage filter queries.
On a well-formed filesystem, these two are kept in 1:1 correspondence. However, btrfs_read_block_groups() builds the cache from block group items in the extent tree, not directly from the chunk tree. A corrupted image can therefore contain a chunk item in the chunk tree whose corresponding block group item is absent from the extent tree; that chunk's block group is then never inserted into the in-memory cache.
When balance iterates the chunk tree and reaches such an orphaned chunk, should_balance_chunk() calls chunk_usage_filter(), which queries the block group cache:
cache = btrfs_lookup_block_group(fs_info, chunk_offset); chunk_used = cache->used; /* cache may be NULL */
btrfs_lookup_block_group() returns NULL silently when no cached entry covers chunk_offset. chunk_usage_filter() does not check the return value, so the immediately following dereference of cache->used triggers the crash.
[FIX]
Add a NULL check after btrfs_lookup_block_group() in chunk_usage_filter(). When the lookup fails, emit a btrfs_err() message identifying the affected bytenr and return -EUCLEAN to indicate filesystem corruption.
Since chunk_usage_filter() now has an error path, change its return type from bool to int: negative errno on error, 0 if the chunk passes the usage filter, and 1 if it should be skipped. Update should_balance_chunk() accordingly to propagate negative errors from the usage filter path while still returning 0 for chunks that should not be balanced and 1 for chunks that should be balanced. Finally, handle the new negative return in __btrfs_balance() by jumping to the existing error path, which aborts the balance operation and reports the error to userspace.
After the fix, the same corruption is correctly detected and reported by the filter, and the null-ptr-deref is no longer triggered.
--- fs/btrfs/volumes.c | 32 +++++++++++++++++++++++--------- 1 file changed, 23 insertions(+), 9 deletions(-)
fs/btrfs/volumes.c +23 -9
@@ -3863,14 +3863,20 @@ static bool chunk_usage_range_filter(struct btrfs_fs_info *fs_info, u64 chunk_ofreturn ret; } -static bool chunk_usage_filter (struct btrfs_fs_info *fs_info,u64 chunk_offset, -struct btrfs_balance_args *bargs) +static int chunk_usage_filter (struct btrfs_fs_info *fs_info,u64 chunk_offset, +struct btrfs_balance_args *bargs) {struct btrfs_block_group *cache;u64 chunk_used, user_thresh; -bool ret = true; +int ret =1 ; cache =btrfs_lookup_block_group (fs_info, chunk_offset); +if (!cache) { +btrfs_err (fs_info, +"balance: chunk at bytenr %llu has no corresponding block group" , + chunk_offset); +return -EUCLEAN ; + } chunk_used = cache->used;if (bargs->usage_min ==0 )
@@ -3881,7 +3887,7 @@ static bool chunk_usage_filter(struct btrfs_fs_info *fs_info, u64 chunk_offset, user_thresh =mult_perc (cache->length, bargs->usage);if (chunk_used < user_thresh) - ret = false; + ret =0 ;btrfs_put_block_group (cache);return ret;
@@ -3986,8 +3992,8 @@ static bool chunk_soft_convert_filter(u64 chunk_type, struct btrfs_balance_argsreturn false; } -static bool should_balance_chunk (struct extent_buffer *leaf,struct btrfs_chunk *chunk, -u64 chunk_offset) +static int should_balance_chunk (struct extent_buffer *leaf,struct btrfs_chunk *chunk, +u64 chunk_offset) {struct btrfs_fs_info *fs_info = leaf->fs_info;struct btrfs_balance_control *bctl = fs_info->balance_ctl;
@@ -4014,9 +4020,13 @@ static bool should_balance_chunk(struct extent_buffer *leaf, struct btrfs_chunk }/* usage filter */ -if ((bargs->flags &BTRFS_BALANCE_ARGS_USAGE ) && -chunk_usage_filter (fs_info, chunk_offset, bargs)) { -return false; +if (bargs->flags &BTRFS_BALANCE_ARGS_USAGE ) { +int ret2 =chunk_usage_filter (fs_info, chunk_offset, bargs); + +if (ret2 <0 ) +return ret2; +if (ret2) +return false; }else if ((bargs->flags &BTRFS_BALANCE_ARGS_USAGE_RANGE ) &&chunk_usage_range_filter (fs_info, chunk_offset, bargs)) {return false;
@@ -4172,6 +4182,10 @@ static int __btrfs_balance(struct btrfs_fs_info *fs_info) ret =should_balance_chunk (leaf, chunk, found_key.offset);btrfs_release_path (path); +if (ret <0 ) { +mutex_unlock (&fs_info->reclaim_bgs_lock); + goto error; + }if (!ret) {mutex_unlock (&fs_info->reclaim_bgs_lock); goto loop;
Signature · 1 line
2.43.0
PATCH v3 2/4 btrfs: balance: fix null-ptr-deref in chunk_usage_range_filter
In reply to ZhengYuan Huang[BUG]
Running btrfs balance with a usage range filter (-dusage=min..max) can trigger a null-ptr-deref when metadata corruption causes a chunk to have no corresponding block group in the in-memory cache:
KASAN: null-ptr-deref in range [0x0000000000000070-0x0000000000000077]
RIP: 0010:chunk_usage_range_filter fs/btrfs/volumes.c:3845 [inline]
RIP: 0010:should_balance_chunk fs/btrfs/volumes.c:4031 [inline]
RIP: 0010:__btrfs_balance fs/btrfs/volumes.c:4182 [inline]
RIP: 0010:btrfs_balance+0x249e/0x4320 fs/btrfs/volumes.c:4618
...
Call Trace:
btrfs_ioctl_balance fs/btrfs/ioctl.c:3577 [inline]
btrfs_ioctl+0x25cf/0x5b90 fs/btrfs/ioctl.c:5313
vfs_ioctl fs/ioctl.c:51 [inline]
...The bug is reproducible on next-20260312.
[CAUSE]
Two separate data structures are involved:
1. The on-disk chunk tree, which records every chunk (logical address space region) and is iterated by __btrfs_balance().
2. The in-memory block group cache (fs_info->block_group_cache_tree), which is built at mount time by btrfs_read_block_groups() and holds a struct btrfs_block_group for each chunk. This cache is what the usage range filter queries.
On a well-formed filesystem, these two are kept in 1:1 correspondence. However, btrfs_read_block_groups() builds the cache from block group items in the extent tree, not directly from the chunk tree. A corrupted image can therefore contain a chunk item in the chunk tree whose corresponding block group item is absent from the extent tree; that chunk's block group is then never inserted into the in-memory cache.
When balance iterates the chunk tree and reaches such an orphaned chunk, should_balance_chunk() calls chunk_usage_range_filter(), which queries the block group cache:
cache = btrfs_lookup_block_group(fs_info, chunk_offset); chunk_used = cache->used; /* cache may be NULL */
btrfs_lookup_block_group() returns NULL silently when no cached entry covers chunk_offset. chunk_usage_range_filter() does not check the return value, so the immediately following dereference of cache->used triggers the crash.
[FIX]
Add a NULL check after btrfs_lookup_block_group() in chunk_usage_range_filter(). When the lookup fails, emit a btrfs_err() message identifying the affected bytenr and return -EUCLEAN to indicate filesystem corruption.
Since chunk_usage_range_filter() now has an error path, change its return type from bool to int: negative errno on error, 0 if the chunk matches the usage range, and 1 if it should be filtered out. Update the BTRFS_BALANCE_ARGS_USAGE_RANGE branch in should_balance_chunk() to propagate negative errors from chunk_usage_range_filter() instead of treating them as a normal filter result.
After the fix, the same corruption is correctly detected and reported by the filter, and the null-ptr-deref is no longer triggered.
--- fs/btrfs/volumes.c | 24 +++++++++++++++++------- 1 file changed, 17 insertions(+), 7 deletions(-)
fs/btrfs/volumes.c +17 -7
@@ -3832,16 +3832,22 @@ static bool chunk_profiles_filter(u64 chunk_type, struct btrfs_balance_args *barreturn true; } -static bool chunk_usage_range_filter (struct btrfs_fs_info *fs_info,u64 chunk_offset, -struct btrfs_balance_args *bargs) +static int chunk_usage_range_filter (struct btrfs_fs_info *fs_info,u64 chunk_offset, +struct btrfs_balance_args *bargs) {struct btrfs_block_group *cache;u64 chunk_used;u64 user_thresh_min;u64 user_thresh_max; -bool ret = true; +int ret =1 ; cache =btrfs_lookup_block_group (fs_info, chunk_offset); +if (!cache) { +btrfs_err (fs_info, +"balance: chunk at bytenr %llu has no corresponding block group" , + chunk_offset); +return -EUCLEAN ; + } chunk_used = cache->used;if (bargs->usage_min ==0 )
@@ -3857,7 +3863,7 @@ static bool chunk_usage_range_filter(struct btrfs_fs_info *fs_info, u64 chunk_of user_thresh_max =mult_perc (cache->length, bargs->usage_max);if (user_thresh_min <= chunk_used && chunk_used < user_thresh_max) - ret = false; + ret =0 ;btrfs_put_block_group (cache);return ret;
@@ -4027,9 +4033,13 @@ static int should_balance_chunk(struct extent_buffer *leaf, struct btrfs_chunk *return ret2;if (ret2)return false; - }else if ((bargs->flags &BTRFS_BALANCE_ARGS_USAGE_RANGE ) && -chunk_usage_range_filter (fs_info, chunk_offset, bargs)) { -return false; + }else if (bargs->flags &BTRFS_BALANCE_ARGS_USAGE_RANGE ) { +int ret2 =chunk_usage_range_filter (fs_info, chunk_offset, bargs); + +if (ret2 <0 ) +return ret2; +if (ret2) +return false; }/* devid filter */
Signature · 1 line
2.43.0
PATCH v3 3/4 btrfs: balance: fix null-ptr-deref in btrfs_may_alloc_data_chunk
In reply to ZhengYuan Huang[BUG]
Running btrfs balance can trigger a null-ptr-deref before relocating a data chunk when metadata corruption leaves a chunk in the chunk tree without a corresponding block group in the in-memory cache:
KASAN: null-ptr-deref in range [0x0000000000000088-0x000000000000008f]
RIP: 0010:btrfs_may_alloc_data_chunk+0x40/0x1c0 fs/btrfs/volumes.c:3601
Call Trace:
__btrfs_balance fs/btrfs/volumes.c:4217 [inline]
btrfs_balance+0x2516/0x42b0 fs/btrfs/volumes.c:4604
btrfs_ioctl_balance fs/btrfs/ioctl.c:3577 [inline]
btrfs_ioctl+0x25cf/0x5b90 fs/btrfs/ioctl.c:5313
...[CAUSE]__btrfs_balance() iterates the on-disk chunk tree and passes the chunk logical bytenr to btrfs_may_alloc_data_chunk() before relocating a data chunk. That helper then queries the in-memory block group cache:
cache = btrfs_lookup_block_group(fs_info, chunk_offset); chunk_type = cache->flags; /* cache may be NULL */
A corrupt image can contain a chunk item whose matching block group item is missing, so no block group is ever inserted into the cache. In that case btrfs_lookup_block_group() returns NULL.
The code only guards this with ASSERT(cache), which becomes a no-op when CONFIG_BTRFS_ASSERT is disabled. The subsequent dereference of cache->flags therefore crashes the kernel.
[FIX]
Add a NULL check after btrfs_lookup_block_group() in btrfs_may_alloc_data_chunk(). If the lookup fails, emit a btrfs_err() message identifying the affected bytenr and return -EUCLEAN to report filesystem corruption instead of dereferencing NULL.
The caller already treats negative returns from btrfs_may_alloc_data_chunk() as fatal errors, so balance aborts cleanly and reports the corruption to userspace.
--- fs/btrfs/volumes.c | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-)
fs/btrfs/volumes.c +6 -1
@@ -3597,7 +3597,12 @@ static int btrfs_may_alloc_data_chunk(struct btrfs_fs_info *fs_info,u64 chunk_type; cache =btrfs_lookup_block_group (fs_info, chunk_offset); -ASSERT (cache); +if (!cache) { +btrfs_err (fs_info, +"balance: chunk at bytenr %llu has no corresponding block group" , + chunk_offset); +return -EUCLEAN ; + } chunk_type = cache->flags;btrfs_put_block_group (cache);
Signature · 1 line
2.43.0
PATCH v3 4/4 btrfs: fix check_chunk_block_group_mappings() to iterate all chunk maps
In reply to ZhengYuan Huang[BUG]
A corrupted image with a chunk present in the chunk tree but whose corresponding block group item is missing from the extent tree can be mounted successfully, even though check_chunk_block_group_mappings() is supposed to catch exactly this corruption at mount time. Once mounted, running btrfs balance with a usage filter (-dusage=N or -dusage=min..max) triggers a null-ptr-deref:
KASAN: null-ptr-deref in range [0x0000000000000070-0x0000000000000077]
RIP: 0010:chunk_usage_filter fs/btrfs/volumes.c:3874 [inline]
RIP: 0010:should_balance_chunk fs/btrfs/volumes.c:4018 [inline]
RIP: 0010:__btrfs_balance fs/btrfs/volumes.c:4172 [inline]
RIP: 0010:btrfs_balance+0x2024/0x42b0 fs/btrfs/volumes.c:4604[CAUSE]
The crash occurs because __btrfs_balance() iterates the on-disk chunk tree, finds the orphaned chunk, calls chunk_usage_filter() (or chunk_usage_range_filter()), which queries the in-memory block group cache via btrfs_lookup_block_group(). Since no block group was ever inserted for this chunk, the lookup returns NULL, and the subsequent dereference of cache->used crashes.
check_chunk_block_group_mappings() uses btrfs_find_chunk_map() to iterate the in-memory chunk map (fs_info->mapping_tree):
map = btrfs_find_chunk_map(fs_info, start, 1);
With @start = 0 and @length = 1, btrfs_find_chunk_map() looks for a chunk map that *contains* the logical address 0. If no chunk contains logical address 0, btrfs_find_chunk_map(fs_info, 0, 1) returns NULL immediately and the loop breaks after the very first iteration, having checked zero chunks. The entire verification function is therefore a no-op, and the corrupted image passes the mount-time check undetected.
[FIX]
Replace the btrfs_find_chunk_map() based loop with a direct in-order walk of fs_info->mapping_tree using rb_first_cached() + rb_next(). This guarantees that every chunk map in the tree is visited regardless of the logical addresses involved.
No lock is taken around the traversal. This function is called during mount from btrfs_read_block_groups(), which is invoked from open_ctree() before any background threads (cleaner, transaction kthread, etc.) are started. There are therefore no concurrent writers that could modify mapping_tree at this point. An analogous lockless direct traversal of mapping_tree already exists in fill_dummy_bgs() in the same file.
Since we walk the RB-tree directly via rb_entry() without going through btrfs_find_chunk_map(), no reference is taken on each map entry, so the btrfs_free_chunk_map() calls are also removed.
--- fs/btrfs/block-group.c | 24 +++++++++--------------- 1 file changed, 9 insertions(+), 15 deletions(-)
fs/btrfs/block-group.c +9 -15
@@ -2319,29 +2319,26 @@ static struct btrfs_block_group *btrfs_create_block_group_cache(*/ static int check_chunk_block_group_mappings (struct btrfs_fs_info *fs_info) { -u64 start =0 ; +struct rb_node *node;int ret =0 ; -while (1 ) { +/* +* This is called during mount from btrfs_read_block_groups(), before +* any background threads are started, so no concurrent writers can +* modify the mapping_tree. No lock is needed here. +*/ +for (node =rb_first_cached (&fs_info->mapping_tree); node; + node =rb_next (node)) {struct btrfs_chunk_map *map;struct btrfs_block_group *bg; -/* -* btrfs_find_chunk_map() will return the first chunk map -* intersecting the range, so setting @length to 1 is enough to -* get the first chunk. -*/ - map =btrfs_find_chunk_map (fs_info, start,1 ); -if (!map) -break ; - + map = rb_entry(node, struct btrfs_chunk_map, rb_node); bg =btrfs_lookup_block_group (fs_info, map->start);if (unlikely (!bg)) {btrfs_err (fs_info,"chunk start=%llu len=%llu doesn't have corresponding block group" , map->start, map->chunk_len); ret = -EUCLEAN ; -btrfs_free_chunk_map (map);break ; } if (unlikely(bg->start != map->start || bg->length != map->chunk_len ||
@@ -2354,12 +2351,9 @@ static int check_chunk_block_group_mappings(struct btrfs_fs_info *fs_info) bg->start, bg->length, bg->flags & BTRFS_BLOCK_GROUP_TYPE_MASK); ret = -EUCLEAN ; -btrfs_free_chunk_map (map);btrfs_put_block_group (bg);break ; } - start = map->start + map->chunk_len; -btrfs_free_chunk_map (map);btrfs_put_block_group (bg); }return ret;
Signature · 1 line
2.43.0
Quoted 33 lines · ZhengYuan Huang
This series fixes three NULL dereferences in btrfs balance paths and the underlying mount-time verification bug that lets the corresponding chunk/block-group inconsistency go undetected.
The balance crashes happen when metadata corruption leaves a chunk present in the chunk tree but without a corresponding block group in the in-memory block group cache. In that case, balance reaches code paths that call
btrfs_lookup_block_group()and dereference the returned pointer without checking for NULL.The first three patches harden the affected balance paths:
- patch 1 fixeschunk_usage_filter()
- patch 2 fixeschunk_usage_range_filter()
- patch 3 fixesbtrfs_may_alloc_data_chunk()They are kept separate because the affected code was introduced by different commits, which should also make backporting easier, as suggested by Qu Wenruo.
The fourth patch fixes the mount-time verification side. Based on David Sterba's feedback, it now explicitly relies on the mount-time context and uses a lockless traversal of
mapping_tree.check_chunk_block_group_mappings()is supposed to verify that every chunk has a matching block group, but its current iteration starts withbtrfs_find_chunk_map(fs_info, 0, 1). If no chunk contains logical address 0, the lookup returns NULL immediately and the loop exits without checking any chunk at all. As a result, the corrupted mapping can survive mount and only crash later when balance reaches it.This series makes btrfs reject the inconsistency earlier at mount time, and also hardens the balance paths so the corruption is reported as
-EUCLEANinstead of triggering NULL dereferences.
Quoted 10 lines
[CHANGELOG]
v3:
- added a new patch to fix the same missing-block-group NULL dereference inbtrfs_may_alloc_data_chunk()
- patch 1 and 2:
- changed the bool return flow to explicit int error propagation
- used ret2 for the nested filter return value inshould_balance_chunk()
- patch 4:
- reworked the changelog based on David Sterba's feedback
- clarified the mount-time context for the locklessmapping_treetraversal
Thanks for the v3 update, I've added the fixes to for-next. I've edited the changelogs a bit in places where the explanations felt stating the obvious, but otherwise the problem descrtiptions were good.