btrfs: fix balance NULL derefs and chunk/bg mapping verification
Subsystems (2)
v2 cover btrfs: fix balance NULL derefs and chunk/bg mapping verification
This series fixes two NULL dereferences in btrfs balance usage filters 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, the usage filters call btrfs_lookup_block_group() and dereference the returned pointer without checking for NULL.
The first two patches add the missing NULL checks and propagate -EUCLEAN back to userspace instead of crashing. They are split because the usage and usage-range filters were introduced by different commits, which should also make backporting easier, as suggested by Qu Wenruo.
The third patch fixes the root cause on the mount-time verification side. 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 filters so the corruption is reported as -EUCLEAN instead of triggering a NULL dereference.
Changes since v1:
- split the two balance filter fixes into separate patches
- reworked the third patch to fix the case where check_chunk_block_group_mappings() does not actually check all chunk mappings
[NOTE]
Some of the changelogs may repeat parts of the bug analysis, which can make the series somewhat verbose. I did that intentionally because I was trying to follow the usual expectation that each patch should be able to stand on its own and explain the specific issue it fixes. In particular, I wanted each patch to describe its own immediate cause clearly, even where the overall trigger path overlaps with the others. If that is not the preferred style here, I would be happy to rework the changelogs and resend the series in a different form.
Also, in a previous reply, Qu Wenruo suggested adding a separate chunk/block-group consistency check. After looking into that, I found that btrfs already has a function intended for this purpose, check_chunk_block_group_mappings(). Patch 3 is based on the observation that this check exists, but due to its current iteration logic it can exit without checking any chunk mappings at all.
Since I am not very familiar with all the details of btrfs internals, if my analysis of patch 3 is flawed, or if the fix is not the right one, I would greatly appreciate any correction or guidance, and I will revise and resend the patch accordingly.
ZhengYuan Huang (3):
btrfs: balance: handle missing block groups in usage filter
btrfs: balance: handle missing block groups in usage range filter
btrfs: fix check_chunk_block_group_mappings() to iterate all chunk maps
fs/btrfs/block-group.c | 21 ++++++------------ fs/btrfs/volumes.c | 48 +++++++++++++++++++++++++++++++----------- 2 files changed, 42 insertions(+), 27 deletions(-)
Signature · 1 line
2.43.0
PATCH v2 1/3 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 with our dynamic metadata fuzzing tool, which corrupts btrfs metadata at runtime.
[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 the filter function now has an error return path, change its return type from bool to int (negative = error, 0 = do not balance, positive = balance). Update should_balance_chunk() accordingly (bool -> int, with the same convention) and add error propagation for the usage filter path. 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 | 28 +++++++++++++++++++++------- 1 file changed, 21 insertions(+), 7 deletions(-)
fs/btrfs/volumes.c +21 -7
@@ -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; 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 )
@@ -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 filter_ret =chunk_usage_filter (fs_info, chunk_offset, bargs); + +if (filter_ret <0 ) +return filter_ret; +if (filter_ret) +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
Re: [PATCH v2 1/3] btrfs: balance: fix null-ptr-deref in chunk_usage_filter
Quoted 17 lines · ZhengYuan Huang
[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] ...
Quoted 2 lines
The bug is reproducible on next-20260312 with our dynamic metadata fuzzing tool, which corrupts btrfs metadata at runtime.
So, for example you let a filesystem create some structures, let it continue, damage/destroy the structures and then let it access again?
If this is supposed to emulate a corruption, either on media or in the IO path then OK.
Quoted 57 lines
[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 bybtrfs_read_block_groups()and holds a structbtrfs_block_groupfor 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()callschunk_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 coverschunk_offset.chunk_usage_filter()does not check the return value, so the immediately following dereference ofcache->usedtriggers the crash.[FIX]
Add a NULL check afterbtrfs_lookup_block_group()inchunk_usage_filter(). When the lookup fails, emit abtrfs_err()message identifying the affected bytenr and return-EUCLEANto indicate filesystem corruption.Since the filter function now has an error return path, change its return type from bool to int (negative = error, 0 = do not balance, positive = balance). Update
should_balance_chunk()accordingly (bool -> int, with the same convention) and add error propagation for the usage filter path. 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.
Fixes 5ce5b3c0916b ("Btrfs: usage filter")Signed-off-by ZhengYuan Huang <[email protected]>--- fs/btrfs/volumes.c | 28 +++++++++++++++++++++------- 1 file changed, 21 insertions(+), 7 deletions(-)fs/btrfs/volumes.c +0
-0@@ -3863,14 +3863,20 @@ static bool chunk_usage_range_filter(struct btrfs_fs_info *fs_info, u64 chunk_ofreturn ret;
fs/btrfs/volumes.c static bool chunk_usage_range_filter(struct btrfs_fs_info *fs_info, u64 chunk_of · line 3871
Quoted 10 lines
fs/btrfs/volumes.c +2
-2} -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;
As this is bool it does not match the changed return type anymore
Quoted 23 lines
fs/btrfs/volumes.c +8
-2cache =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 )@@ -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;
fs/btrfs/volumes.c static bool should_balance_chunk(struct extent_buffer *leaf, struct btrfs_chunk · line 4024
Quoted 8 lines
fs/btrfs/volumes.c +2
-3@@ -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 filter_ret =chunk_usage_filter (fs_info, chunk_offset, bargs);
Same problem here. Also please use ret2 for nested return values.
Quoted 8 lines
fs/btrfs/volumes.c +5
-0+ +if (filter_ret <0 ) +return filter_ret; +if (filter_ret) +return false; }else if ((bargs->flags &BTRFS_BALANCE_ARGS_USAGE_RANGE ) &&chunk_usage_range_filter (fs_info, chunk_offset, bargs)) {return false;
Re: [PATCH v2 1/3] btrfs: balance: fix null-ptr-deref in chunk_usage_filter
Quoted 5 lines · David Sterba
So, for example you let a filesystem create some structures, let it continue, damage/destroy the structures and then let it access again?
If this is supposed to emulate a corruption, either on media or in the IO path then OK.
Yes, this is one of the fuzzing strategies we use, where metadata is intentionally corrupted at runtime to emulate possible media corruption or I/O errors.
Quoted 44 lines
Quoted 6 lines
fs/btrfs/volumes.c +0
-0@@ -3863,14 +3863,20 @@ static bool chunk_usage_range_filter(struct btrfs_fs_info *fs_info, u64 chunk_ofreturn ret;fs/btrfs/volumes.c static bool chunk_usage_range_filter(struct btrfs_fs_info *fs_info, u64 chunk_of · line 3871
Quoted 10 lines
fs/btrfs/volumes.c +2
-2} -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;As this is bool it does not match the changed return type anymore
Quoted 25 lines
fs/btrfs/volumes.c +8
-2cache =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 )@@ -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 }
Quoted 8 lines
fs/btrfs/volumes.c static bool should_balance_chunk(struct extent_buffer *leaf, struct btrfs_chunk · line 4024
Quoted 6 lines
fs/btrfs/volumes.c +2
-3/* 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 filter_ret =chunk_usage_filter (fs_info, chunk_offset, bargs);Same problem here. Also please use ret2 for nested return values.
Thanks for the note, I’ll fix the return type issue and send a v3.
Thanks,
ZhengYuan Huang
PATCH v2 2/3 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 with our dynamic metadata fuzzing tool, which corrupts btrfs metadata at runtime.
[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 return path, change its return type from bool to int (negative = error, 0 = do not balance, positive = balance). Update the BTRFS_BALANCE_ARGS_USAGE_RANGE branch in should_balance_chunk() to propagate negative errors 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 | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-)
fs/btrfs/volumes.c +15 -5
@@ -3832,8 +3832,8 @@ 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;
@@ -3842,6 +3842,12 @@ static bool chunk_usage_range_filter(struct btrfs_fs_info *fs_info, u64 chunk_ofbool ret = true; 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 )
@@ -4027,9 +4033,13 @@ static int should_balance_chunk(struct extent_buffer *leaf, struct btrfs_chunk *return filter_ret;if (filter_ret)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 filter_ret =chunk_usage_range_filter (fs_info, chunk_offset, bargs); + +if (filter_ret <0 ) +return filter_ret; +if (filter_ret) +return false; }/* devid filter */
Signature · 1 line
2.43.0
PATCH v2 3/3 btrfs: fix check_chunk_block_group_mappings() to actually iterate all chunks
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:4604The 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.
[CAUSE]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(), protected by mapping_tree_lock. This guarantees that every chunk map in the tree is visited regardless of the logical addresses involved. Since the mapping_tree itself is accessed under read_lock, no refcount manipulation of each map entry is needed inside the loop, so the btrfs_free_chunk_map() calls on the map are also removed.
--- fs/btrfs/block-group.c | 21 ++++++--------------- 1 file changed, 6 insertions(+), 15 deletions(-)
fs/btrfs/block-group.c +6 -15
@@ -2319,29 +2319,22 @@ 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 ) { +read_lock (&fs_info->mapping_tree_lock); +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,14 +2347,12 @@ 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); } +read_unlock (&fs_info->mapping_tree_lock);return ret; }
Signature · 1 line
2.43.0
Re: [PATCH v2 3/3] btrfs: fix check_chunk_block_group_mappings() to actually iterate all chunks
Quoted 53 lines · 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 thoughcheck_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:4604The crash occurs because
__btrfs_balance()iterates the on-disk chunk tree, finds the orphaned chunk, callschunk_usage_filter()(orchunk_usage_range_filter()), which queries the in-memory block group cache viabtrfs_lookup_block_group(). Since no block group was ever inserted for this chunk, the lookup returns NULL, and the subsequent dereference ofcache->usedcrashes.[CAUSE]
check_chunk_block_group_mappings()usesbtrfs_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 thebtrfs_find_chunk_map()based loop with a direct in-order walk offs_info->mapping_treeusingrb_first_cached()+rb_next(), protected bymapping_tree_lock. This guarantees that every chunk map in the tree is visited regardless of the logical addresses involved. Since themapping_treeitself is accessed underread_lock, no refcount manipulation of each map entry is needed inside the loop, so thebtrfs_free_chunk_map()calls on the map are also removed.Signed-off-by ZhengYuan Huang <[email protected]>--- fs/btrfs/block-group.c | 21 ++++++--------------- 1 file changed, 6 insertions(+), 15 deletions(-)fs/btrfs/block-group.c +0
-0
fs/btrfs/block-group.c static struct btrfs_block_group *btrfs_create_block_group_cache( · line 2325
Quoted 9 lines
fs/btrfs/block-group.c +2
-2@@ -2319,29 +2319,22 @@ 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 ) { +read_lock (&fs_info->mapping_tree_lock);
This is called during mount indirectly from open_ctree() and this is single threaded (partially), so the lock may not be needed. It would be needed if there's eg. caching thread possibly accessing the same structures, I haven't looked closely.
Quoted 6 lines
fs/btrfs/block-group.c +2
-1+for (node =rb_first_cached (&fs_info->mapping_tree); node; + node =rb_next (node)) {struct btrfs_chunk_map *map;struct btrfs_block_group *bg; -/*
fs/btrfs/block-group.c static struct btrfs_block_group *btrfs_create_block_group_cache( · line 2332
Quoted 10 lines
fs/btrfs/block-group.c +1
-8-* 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);
What concerns me is this lookup. Previously the references avoided taking the big lock. The time the lock is held may add up significanly for all block groups but as said before it might not be necessary due to the mount context.
Quoted 28 lines
fs/btrfs/block-group.c +1
-4if (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,14 +2347,12 @@ 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); } +read_unlock (&fs_info->mapping_tree_lock);return ret; }Signature · 1 line
2.43.0
Re: [PATCH v2 3/3] btrfs: fix check_chunk_block_group_mappings() to actually iterate all chunks
Quoted 22 lines · David Sterba
This is called during mount indirectly from
open_ctree()and this is single threaded (partially), so the lock may not be needed. It would be needed if there's eg. caching thread possibly accessing the same structures, I haven't looked closely.Quoted 6 lines
fs/btrfs/block-group.c +2
-1+for (node =rb_first_cached (&fs_info->mapping_tree); node; + node =rb_next (node)) {struct btrfs_chunk_map *map;struct btrfs_block_group *bg; -/* fs/btrfs/block-group.c static struct btrfs_block_group *btrfs_create_block_group_cache( · line 2332
Quoted 10 lines
fs/btrfs/block-group.c +1
-8-* 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);
Quoted 4 lines
What concerns me is this lookup. Previously the references avoided taking the big lock. The time the lock is held may add up significanly for all block groups but as said before it might not be necessary due to the mount context.
Thanks for the suggestion, I’ll take a closer look at the locking here. If the lock turns out to be unnecessary in this context, I’ll drop it and include the change in v3.
Thanks,
ZhengYuan Huang
Quoted 23 lines · ZhengYuan Huang
This series fixes two NULL dereferences in btrfs balance usage filters 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, the usage filters call
btrfs_lookup_block_group()and dereference the returned pointer without checking for NULL.The first two patches add the missing NULL checks and propagate
-EUCLEANback to userspace instead of crashing. They are split because the usage and usage-range filters were introduced by different commits, which should also make backporting easier, as suggested by Qu Wenruo.The third patch fixes the root cause on the mount-time verification side.
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.
Quoted 3 lines
This series makes btrfs reject the inconsistency earlier at mount time, and also hardens the balance filters so the corruption is reported as
-EUCLEANinstead of triggering a NULL dereference.
As I understand it you're using some advanced fuzzing tool (patch 1 mentions runtime fuzzing), so the errors would not normally happen. With fuzzing it depends on the capabilities, at runtime it is possible to confuse the filesystem so much that sipmle checks can't detect it.
Here checking if block group lookups are ok makes sense in general. There are existing checks that seem to be following the same logic like in unpin_extent_range().
Quoted 7 lines
Changes since v1:
- split the two balance filter fixes into separate patches
- reworked the third patch to fix the case wherecheck_chunk_block_group_mappings()does not actually check all chunk mappings
Quoted 5 lines
[NOTE]
Some of the changelogs may repeat parts of the bug analysis, which can make the series somewhat verbose. I did that intentionally because I was trying to follow the usual expectation that each patch should be able to stand on its own and explain the specific issue it fixes.
This is good, thanks. For simple fixes or cleanups it's fine to make a vague reference to the main patch or a "in the previous/followup patches".
Quoted 8 lines · David Sterba
As I understand it you're using some advanced fuzzing tool (patch 1 mentions runtime fuzzing), so the errors would not normally happen. With fuzzing it depends on the capabilities, at runtime it is possible to confuse the filesystem so much that sipmle checks can't detect it.
Here checking if block group lookups are ok makes sense in general. There are existing checks that seem to be following the same logic like in
unpin_extent_range().
Thanks for your review.
Yes, we are using an in-house runtime fuzzing tool.
However, after further investigation of this bug, we found that it is not limited to fuzzing-only scenarios. The issue can be reliably triggered by using a crafted filesystem image together with normal syscalls.
So this may not be purely a fuzzing artifact, but rather a potential robustness issue that could be hit in practice.
Quoted 3 lines
This is good, thanks. For simple fixes or cleanups it's fine to make a vague reference to the main patch or a "in the previous/followup patches".
Thanks for the guidance, I’ll continue to follow this convention for changelogs.
Thanks,
ZhengYuan Huang