[PATCH AUTOSEL 6.18] btrfs: balance: fix potential bg lookup failure in chunk_usage_range_filter()
From: Sasha Levin
Date: Mon Aug 31 2026 - 11:41:28 EST
From: ZhengYuan Huang <gality369@xxxxxxxxx>
[ Upstream commit 7a308f6d29cc689ceaf313b9ebdf68099f50e452 ]
[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 recent development branch.
[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 error pointer, return 0 if the chunk matches
the usage range, and 1 if it should be filtered out.
Signed-off-by: ZhengYuan Huang <gality369@xxxxxxxxx>
Reviewed-by: David Sterba <dsterba@xxxxxxxx>
Signed-off-by: David Sterba <dsterba@xxxxxxxx>
Signed-off-by: Sasha Levin <sashal@xxxxxxxxxx>
---
LLM Generated explanations, may be completely bogus:
# Stable Backport Analysis: `btrfs: balance: fix potential bg lookup
failure in chunk_usage_range_filter()`
**Local tree:** `linux-6.18.y` at `v6.18.44` (kernel 6.18.44)
---
## PHASE 1: COMMIT MESSAGE FORENSICS
### Step 1.1: Subject Line
**Record:** `[btrfs/balance]` **`fix`** — NULL block-group lookup in
`chunk_usage_range_filter()` during balance with usage-range filter
(`-dusage=min..max`).
### Step 1.2: Commit Message Tags
**Record:**
- **Fixes:** — not present (expected for manual review)
- **Reported-by:** — none
- **Tested-by:** — none
- **Reviewed-by:** David Sterba `<dsterba@xxxxxxxx>` (btrfs maintainer)
- **Acked-by:** — none
- **Link:** — none
- **Cc: stable:** — none (expected)
- **Signed-off-by:** ZhengYuan Huang `<gality369@xxxxxxxxx>`; David
Sterba (committer)
- **Notable:** Reviewed and committed by btrfs maintainer; KASAN stack
trace in body
### Step 1.3: Body Analysis
**Record:**
- **Bug:** NULL pointer dereference in `chunk_usage_range_filter()` when
running `btrfs balance` with `BTRFS_BALANCE_ARGS_USAGE_RANGE` on a
corrupted filesystem where a chunk exists in the chunk tree but has no
matching block group in the in-memory cache.
- **Symptom:** KASAN null-ptr-deref at `cache->used` (offset ~0x70 into
`struct btrfs_block_group`), reachable via `btrfs_ioctl_balance` →
`btrfs_balance` → `__btrfs_balance` → `should_balance_chunk`.
- **Root cause:** `btrfs_lookup_block_group()` can return NULL; caller
dereferences without checking.
- **Fix:** NULL check, `btrfs_err()` log, return `-EUCLEAN`; change
return type from `bool` to `int` for error propagation.
- **Version info:** Reproducible on recent development branch; no
specific kernel version cited.
### Step 1.4: Hidden Bug Fix Detection
**Record:** Not disguised — explicitly labeled `[BUG]` with KASAN trace.
Clear NULL-dereference fix.
---
## PHASE 2: DIFF ANALYSIS
### Step 2.1: Change Inventory
**Record:**
- **Files:** `fs/btrfs/volumes.c` only (+17 / -7 lines)
- **Functions modified:** `chunk_usage_range_filter()`,
`should_balance_chunk()` (usage-range branch only)
- **Scope:** Single-file surgical fix
### Step 2.2: Code Flow Changes
**Record:**
- **Hunk 1 (`chunk_usage_range_filter`):** Before: lookup block group,
unconditionally dereference `cache->used`. After: check
`unlikely(!cache)`, log error, return `-EUCLEAN`; otherwise same logic
with `int` return (0 = match filter, 1 = filter out).
- **Hunk 2 (`should_balance_chunk`):** Before: inline bool call, filter
out if true. After: call filter, propagate negative errors (`return
ret2`), filter out if positive return.
### Step 2.3: Bug Mechanism
**Record:** **Category:** NULL pointer dereference (memory safety).
**Mechanism:** Missing NULL check after `btrfs_lookup_block_group()` on
a corruption path where chunk-tree and block-group cache are
inconsistent.
### Step 2.4: Fix Quality
**Record:** Fix is obviously correct and minimal. Matches the pattern
already applied to `chunk_usage_filter()` in prerequisite commit
`6dde5221f608e`. Low regression risk — only affects error path on
corrupted metadata. `btrfs_put_block_group()` still called on success
path only.
---
## PHASE 3: GIT HISTORY INVESTIGATION
### Step 3.1: Blame
**Record:** Buggy NULL-deref line (`chunk_used = cache->used` without
check) introduced in `bc3094673f22d` (David Sterba, Oct 2015) — "btrfs:
extend balance filter usage to take minimum and maximum". Present in
this tree since 2015.
### Step 3.2: Fixes: Tag
**Record:** N/A — no `Fixes:` tag.
### Step 3.3: Related File History
**Record:** Part of a 3-commit series by ZhengYuan Huang (Mar 25, 2026):
1. `6dde5221f608e` — fix `chunk_usage_filter()` + change
`should_balance_chunk()` to `int` + add `ret < 0` handling in
`__btrfs_balance`
2. `7a308f6d29cc6` — **this commit** — fix `chunk_usage_range_filter()`
3. `18d32b0013efb` — fix `btrfs_may_alloc_data_chunk()`
None of these three are in `linux-6.18.y` yet.
### Step 3.4: Author Context
**Record:** ZhengYuan Huang is a btrfs contributor (other fixes in tree-
checker/root-item validation). David Sterba (maintainer) reviewed and
committed all three.
### Step 3.5: Dependencies
**Record:** **Prerequisite:** `6dde5221f608e` is required:
- Changes `should_balance_chunk()` from `bool` to `int` and adds `if
(ret < 0) goto error` in `__btrfs_balance`
- Without it, `-EUCLEAN` propagation from this commit is broken
- Verified: `6dde5221` applies cleanly to `v6.18.44`; `7a308f6` fails
alone but applies cleanly after `6dde5221`
---
## PHASE 4: MAILING LIST AND EXTERNAL RESEARCH
### Step 4.1: Original Discussion
**Record:** `b4 dig -c 7a308f6d29cc6` — **no match found** on
lore.kernel.org. Lore web search blocked by bot protection. Cannot
verify mailing-list discussion or stable nominations.
### Step 4.2: Reviewers
**Record:** David Sterba (btrfs maintainer) — Reviewed-by and Signed-
off-by. Sufficient subsystem review.
### Step 4.3: Bug Report
**Record:** KASAN trace in commit message only. No syzbot, bugzilla, or
user reports. Author states reproducible on development branch.
### Step 4.4: Related Patches
**Record:** Sibling commits `6dde5221` and `18d32b0013efb` fix the same
class of bug in adjacent balance code paths. Ideally backported as a
series; this commit is not standalone for clean apply.
### Step 4.5: Stable List History
**Record:** Not searched successfully (lore inaccessible). No evidence
found of prior stable discussion.
---
## PHASE 5: CODE SEMANTIC ANALYSIS
### Step 5.1: Key Functions
**Record:** `chunk_usage_range_filter()`, `should_balance_chunk()`,
callers: `__btrfs_balance()`, `btrfs_balance()`,
`btrfs_ioctl_balance()`.
### Step 5.2: Callers
**Record:** `should_balance_chunk()` called from `__btrfs_balance()`
chunk-tree iteration loop (every balance operation per chunk).
`btrfs_ioctl_balance()` requires `CAP_SYS_ADMIN`.
### Step 5.3: Callees
**Record:** `btrfs_lookup_block_group()` →
`block_group_cache_tree_search()` — returns NULL when no cached block
group covers the bytenr. `btrfs_put_block_group()`, `mult_perc()`,
`btrfs_err()`.
### Step 5.4: Reachability
**Record:** Trigger: admin runs `btrfs balance` with usage-range filter
(`BTRFS_BALANCE_ARGS_USAGE_RANGE`) on filesystem with chunk/block-group
metadata inconsistency. Reachable from `ioctl()` syscall path. Requires
corruption + specific filter flag; not everyday path but real and
reproducible.
### Step 5.5: Similar Patterns
**Record:** Same missing-NULL-check pattern exists in:
- `chunk_usage_filter()` (fixed by `6dde5221`)
- `btrfs_may_alloc_data_chunk()` with `ASSERT(cache)` only (fixed by
`18d32b0013efb`)
---
## PHASE 6: CROSS-REFERENCE WITH LOCAL TREE (6.18.44)
### Step 6.1: Buggy Code Present?
**Record:** **YES.** At lines 3968–3969 in `fs/btrfs/volumes.c`:
```3968:3969:fs/btrfs/volumes.c
cache = btrfs_lookup_block_group(fs_info, chunk_offset);
chunk_used = cache->used;
```
No NULL check. `BTRFS_BALANCE_ARGS_USAGE_RANGE` support present since
2015 (`bc3094673f22d` is ancestor).
### Step 6.2: Backport Complications
**Record:** Does not apply cleanly alone (`git apply --check` fails at
line 4158). Applies cleanly after prerequisite `6dde5221`. Minor
adaptation needed only if backported without prerequisite (not
recommended).
### Step 6.3: Related Fixes Already Present?
**Record:** **NO.** String `"has no corresponding block group"` not in
tree. `6dde5221` and `18d32b0013efb` also absent.
---
## PHASE 7: SUBSYSTEM CONTEXT
### Step 7.1: Subsystem Criticality
**Record:** **fs/btrfs** — IMPORTANT. Btrfs is widely deployed; balance
is an admin maintenance operation on live filesystems.
### Step 7.2: Subsystem Activity
**Record:** Actively maintained. Recent balance-related work includes
`f963e0128b180` (bool conversion, Apr 2025) and `c19830db30a09` (BUG() →
error handling in `__btrfs_balance`).
---
## PHASE 8: IMPACT AND RISK ASSESSMENT
### Step 8.1: Who Is Affected
**Record:** Btrfs users running balance with usage-range filter on
corrupted or inconsistently-metadata filesystems. Admin-only trigger
(`CAP_SYS_ADMIN`).
### Step 8.2: Trigger Conditions
**Record:** Corrupted chunk tree / extent tree inconsistency + balance
with `-dusage=min..max` range syntax. Uncommon but plausible during
recovery operations on damaged filesystems — exactly when robust error
handling matters most.
### Step 8.3: Failure Mode Severity
**Record:** **CRITICAL** — kernel NULL pointer dereference / oops.
System crash during admin maintenance on a filesystem that may already
be in distress.
### Step 8.4: Risk-Benefit
**Record:**
- **Benefit:** HIGH — prevents kernel crash; returns `-EUCLEAN` for
corruption (correct btrfs behavior)
- **Risk:** LOW — ~17 lines, well-reviewed, mirrors already-accepted
pattern from sibling commit
- **Ratio:** Strongly favorable, provided prerequisite `6dde5221` is
included
---
## PHASE 9: FINAL SYNTHESIS
### Step 9.1: Evidence Summary
**FOR backport:**
- Real, reproducible NULL pointer dereference with KASAN trace
- Bug present in 6.18.44 since 2015
- Small, surgical, maintainer-reviewed fix
- Prevents kernel oops during balance on corrupted filesystems
- Matches established btrfs corruption-handling pattern (`-EUCLEAN`)
**AGAINST backport:**
- Requires prerequisite commit `6dde5221` for clean apply and correct
error propagation
- Trigger needs metadata corruption + usage-range filter (less common
than simple `-dusage=N`)
- No syzbot or user bug reports
- Admin-only path (not unprivileged attack surface)
**Unresolved:**
- No lore discussion found via b4
- Third sibling commit `18d32b0013efb` addresses another balance NULL-
deref path; ideally included in same backport series
### Step 9.2: Stable Rules Checklist
1. Obviously correct and tested? **PASS** — NULL check after documented-
NULL-returning function; maintainer reviewed; KASAN-reproduced
2. Fixes real bug affecting users? **PASS** — kernel crash on balance
ioctl
3. Important issue? **PASS** — CRITICAL (kernel oops)
4. Small and contained? **PASS** — 17 lines, one file
5. No new features/APIs? **PASS** — bug fix only
6. Can apply to local tree? **PASS** (with prerequisite `6dde5221`;
verified via `git apply`)
### Step 9.3: Exception Categories
**Record:** N/A — standard bug fix, not device ID/quirk/docs/build fix.
### Step 9.4: Decision Rationale
This commit fixes a long-standing NULL pointer dereference in btrfs
balance code that exists in the 6.18.44 tree. The fix is minimal,
obviously correct, and reviewed by the btrfs maintainer. While the
trigger requires filesystem corruption and the usage-range filter,
crashing the kernel during recovery/maintenance on a damaged btrfs
filesystem is exactly the kind of issue stable trees should fix.
The prerequisite commit `6dde5221f608e` must accompany this backport (it
applies cleanly to 6.18.44 and is required for proper `-EUCLEAN`
propagation). Commit `18d32b0013efb` fixes the same bug class in
`btrfs_may_alloc_data_chunk()` and should also be considered for a
complete fix.
---
## Verification
- **[Phase 1]** Parsed commit message: BUG/CAUSE/FIX sections, Reviewed-
by David Sterba, no Fixes:/Reported-by:/Link: tags
- **[Phase 2]** Diff analysis: 17 lines in `fs/btrfs/volumes.c`; NULL
check + int return type + error propagation in `should_balance_chunk`
- **[Phase 3]** `git blame`: buggy code from `bc3094673f22d` (Oct 2015)
- **[Phase 3]** `git log -S`: found series `6dde5221`, `7a308f6`,
`18d32b0013efb` on master; none in 6.18.44
- **[Phase 3]** `git merge-base --is-ancestor`: `bc3094673f22d` present;
`6dde5221` NOT present
- **[Phase 3]** `git apply --check 6dde5221`: applies cleanly to
v6.18.44
- **[Phase 3]** `git apply --check 7a308f6` alone: **FAILS** at line
4158
- **[Phase 3]** `git apply 6dde5221` then `git apply --check 7a308f6`:
**SUCCEEDS**
- **[Phase 4]** `b4 dig -c 7a308f6d29cc6`: no lore match found
- **[Phase 4]** lore.kernel.org fetch: blocked by bot protection
- **[Phase 5]** Call chain verified: `btrfs_ioctl_balance`
(CAP_SYS_ADMIN) → `btrfs_balance` → `__btrfs_balance` →
`should_balance_chunk` → `chunk_usage_range_filter`
- **[Phase 5]** `btrfs_lookup_block_group()` in `block-group.c:279-283`
returns NULL via `block_group_cache_tree_search`
- **[Phase 6]** `git describe HEAD`: v6.18.44 on `stable/linux-6.18.y`
- **[Phase 6]** Grep confirmed buggy code at lines 3968-3969; no `"has
no corresponding block group"` string in tree
- **[Phase 6]** `BTRFS_BALANCE_ARGS_USAGE_RANGE` present in
`include/uapi/linux/btrfs.h`
- **[Phase 8]** Failure mode: KASAN null-ptr-deref → kernel oops,
severity CRITICAL
---
**YES**The background checks finished and match what the analysis
already used:
- **`b4 dig`** on HEAD found no lore.kernel.org thread for this commit.
- **`git log --grep="fix potential bg lookup failure"`** returned
nothing — the fix isn’t in the local `linux-6.18.y` tree yet.
That supports the earlier **YES** verdict: the bug is still present in
6.18.44, and this commit should be backported together with prerequisite
`6dde5221f608e` (`chunk_usage_filter()` fix).The `git log -S "has no
corresponding block group"` search finished with no matches in the
current `linux-6.18.y` tree — that error string only appears in the
unfixed commits on `master` (`6dde5221`, `7a308f6`, `18d32b0013efb`),
which confirms the fix series hasn’t landed in 6.18.44 yet.
fs/btrfs/volumes.c | 24 +++++++++++++++++-------
1 file changed, 17 insertions(+), 7 deletions(-)
diff --git a/fs/btrfs/volumes.c b/fs/btrfs/volumes.c
index a17fcf8bd8a99..298e85a69e61c 100644
--- a/fs/btrfs/volumes.c
+++ b/fs/btrfs/volumes.c
@@ -3960,16 +3960,21 @@ static bool chunk_profiles_filter(u64 chunk_type, struct btrfs_balance_args *bar
return 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 (unlikely(!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)
@@ -3985,7 +3990,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;
@@ -4155,9 +4160,14 @@ 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;
+
+ ret2 = chunk_usage_range_filter(fs_info, chunk_offset, bargs);
+ if (ret2 < 0)
+ return ret2;
+ if (ret2)
+ return false;
}
/* devid filter */
--
2.53.0