[PATCH] btrfs: validate free space bitmap size before testing bits

ZhengYuan Huang posted 1 patch 1 month ago
fs/btrfs/extent_io.c       | 10 ++++++++++
fs/btrfs/free-space-tree.c | 19 +++++++++++++++++++
2 files changed, 29 insertions(+)
[PATCH] btrfs: validate free space bitmap size before testing bits
Posted by ZhengYuan Huang 1 month ago
A corrupt free-space-tree leaf can contain a FREE_SPACE_BITMAP item
whose on-disk item size does not match the bitmap size implied by
key.offset.

The free-space-tree loading path currently uses key.offset to iterate
bitmap coverage, but does not verify that the item size matches
free_space_bitmap_size(fs_info, key.offset). This allows a zero-sized
or otherwise truncated bitmap item to be consumed as if it contained
valid bitmap data.

Once bit access runs past the valid extent buffer range, the computed
folio index can reach an unpopulated eb->folios[] slot and trigger a
NULL dereference in assert_eb_folio_uptodate().

Fix this by validating FREE_SPACE_BITMAP item sizes in
load_free_space_bitmaps() before testing any bits. If the on-disk item
size does not match the expected bitmap size, treat the free-space-tree
leaf as corrupt and fail loading it with -EUCLEAN.

Also add a defensive range check in extent_buffer_test_bit() so that
corrupt metadata cannot drive bitmap bit access beyond the extent
buffer even if a bad caller reaches that helper.

The bug is reproducible on 7.0.0-rc2-next-20260306 with a dynamic
metadata fuzzing tool that injects single-bit corruptions into btrfs
leaf blocks at runtime. After this change, the corrupt bitmap item is
rejected and the filesystem reports corruption instead of crashing.

Fixes: a5ed91828518 ("Btrfs: implement the free space B-tree")
Cc: stable@vger.kernel.org # 4.5+
Signed-off-by: ZhengYuan Huang <gality369@gmail.com>
---
Root cause
==========
The direct fault is a NULL dereference in assert_eb_folio_uptodate(),
reached from the free-space-tree bitmap loading path:

  caching_thread()
    -> btrfs_load_free_space_tree()
    -> load_free_space_bitmaps()
    -> btrfs_free_space_test_bit()
    -> extent_buffer_test_bit()
    -> assert_eb_folio_uptodate()

The corrupted metadata pattern is a FREE_SPACE_BITMAP item whose
item_size is smaller than the bitmap size described by key.offset. In the
reproducer, multiple bitmap items had item_size == 0 while key.offset
still described non-empty bitmap ranges.

For one failing item, the instrumented run showed:

  leaf_len = 16384
  ptr = 16312
  item_size = 0
  expected = 402
  key.type = BTRFS_FREE_SPACE_BITMAP_KEY

So only 72 bytes remained in the leaf data area, while the bitmap range
described by key.offset required 402 bytes of bitmap data. The existing
code did not validate that mismatch before iterating over bitmap bits.

btrfs_free_space_test_bit() uses btrfs_item_ptr_offset() as the bitmap
start, and extent_buffer_test_bit() then translates the bit access into
a folio index. Without a range check, once start + BIT_BYTE(nr) goes past
eb->len, the computed folio index can exceed the populated folio range of
the extent buffer.

extent_buffer objects are zero-initialized and only the first
num_extent_folios(eb) entries in eb->folios[] are populated. An access
past that range can therefore hit a NULL eb->folios[] slot, which is then
dereferenced by assert_eb_folio_uptodate() via folio_test_uptodate().

Reproduction (v6.18, x86_64, KASAN)
===================================
The PoC is relatively large, so it is provided separately through google drive:
https://drive.google.com/drive/folders/1eB6QzkGViZhlq8xouE5WSVRU0fovu0qw

To reproduce the issue:
  1. Build the PoC program: gcc poc.c -o poc
  2. Build the ublk helper program from the ublk codebase, which is
	 used to provide the runtime corruption capability:
	  g++ -std=c++20 -fcoroutines -O2 -o standalone_replay \
      standalone_replay_btrfs.cpp targets/ublksrv_tgt.cpp \
      -I. -Iinclude -Itargets/include \
      -L./lib/.libs -lublksrv -luring -lpthread
  3. Attach the crafted image through ublk:
      ./standalone_replay add -t loop -f /path/to/image
  4. Run the PoC: ./poc
This reliably reproduces the bug.

Test notes
==========
The reproducer was verified on 7.0.0-rc2-next-20260306 with runtime
single-bit corruption injected into btrfs leaf blocks. I have not yet
retested it on the latest stable kernel, but I can do so if needed.

Fix
===
Two complementary defences are added:

1. In load_free_space_bitmaps() (free-space-tree.c), validate that the
   on-disk item_size equals free_space_bitmap_size(fs_info, key.offset)
   before entering the per-sector bit-reading loop.  A mismatch is a
   clear sign of on-disk corruption; log a specific error message and
   return -EUCLEAN so the caller can handle it gracefully instead of
   walking off the end of the leaf.

2. In extent_buffer_test_bit() (extent_io.c), call check_eb_range()
   before eb_bitmap_offset(), mirroring the pattern already used by
   read_extent_buffer() and extent_buffer_get_byte().  This makes the
   function safe against any caller that passes an out-of-range (start,
   nr) pair, regardless of how the corruption reached this point.

Defence (1) catches the specific free-space-tree path at the semantic
layer and produces a meaningful log entry. Defence (2) is a generic
safety net for the low-level helper that prevents the NULL-folio crash
for any future caller that might bypass the upper-layer check.

KASAN reports
=============
BUG: KASAN: null-ptr-deref in instrument_atomic_read include/linux/instrumented.h:68 [inline]
BUG: KASAN: null-ptr-deref in _test_bit include/asm-generic/bitops/instrumented-non-atomic.h:141 [inline]
BUG: KASAN: null-ptr-deref in folio_test_uptodate include/linux/page-flags.h:787 [inline]
BUG: KASAN: null-ptr-deref in assert_eb_folio_uptodate+0x198/0x2b0 fs/btrfs/extent_io.c:4071
Read of size 8 at addr 0000000000000000 by task kworker/u8:0/12

CPU: 1 UID: 0 PID: 12 Comm: kworker/u8:0 Not tainted 6.18.0+ #12 PREEMPT(voluntary) 
Hardware name: QEMU Ubuntu 24.04 PC v2 (i440FX + PIIX, arch_caps fix, 1996), BIOS 1.16.3-debian-1.16.3-2 04/01/2014
Workqueue: btrfs-cache btrfs_work_helper
Call Trace:
<TASK>
dump_stack_lvl+0xbe/0x130
print_report+0x437/0x650
? srso_alias_return_thunk+0x5/0xfbef5 arch/x86/lib/retpoline.S:220
? early_section include/linux/mmzone.h:2184 [inline]
? pfn_valid include/linux/mmzone.h:2196 [inline]
? __virt_addr_valid+0xca/0x4c0 arch/x86/mm/physaddr.c:65
? srso_alias_return_thunk+0x5/0xfbef5 arch/x86/lib/retpoline.S:220
? kasan_addr_to_slab+0xd/0xb0 mm/kasan/common.c:46
kasan_report+0xfb/0x140
? instrument_atomic_read include/linux/instrumented.h:68 [inline]
? _test_bit include/asm-generic/bitops/instrumented-non-atomic.h:141 [inline]
? folio_test_uptodate include/linux/page-flags.h:787 [inline]
? assert_eb_folio_uptodate+0x198/0x2b0 fs/btrfs/extent_io.c:4071
? instrument_atomic_read include/linux/instrumented.h:68 [inline]
? _test_bit include/asm-generic/bitops/instrumented-non-atomic.h:141 [inline]
? folio_test_uptodate include/linux/page-flags.h:787 [inline]
? assert_eb_folio_uptodate+0x198/0x2b0 fs/btrfs/extent_io.c:4071
kasan_check_range+0x11c/0x200
__kasan_check_read+0x11/0x20
assert_eb_folio_uptodate+0x198/0x2b0
extent_buffer_test_bit+0xce/0x200
btrfs_free_space_test_bit+0x1b3/0x270
? __pfx_btrfs_free_space_test_bit+0x10/0x10 include/linux/sched/mm.h:332
? __asan_memmove+0x30/0x80 mm/kasan/shadow.c:95
? srso_alias_return_thunk+0x5/0xfbef5 arch/x86/lib/retpoline.S:220
? read_extent_buffer+0x114/0x3d0 fs/btrfs/extent_io.c:3946
btrfs_load_free_space_tree+0x57a/0xe40
? __pfx_btrfs_load_free_space_tree+0x10/0x10 fs/btrfs/free-space-tree.c:1492
? __entry_text_end+0x1025b9/0x1025bd
? __kasan_check_write+0x14/0x30 mm/kasan/shadow.c:37
? srso_alias_return_thunk+0x5/0xfbef5 arch/x86/lib/retpoline.S:220
? instrument_atomic_write include/linux/instrumented.h:82 [inline]
? atomic_long_set include/linux/atomic/atomic-instrumented.h:3223 [inline]
? __rwsem_set_reader_owned kernel/locking/rwsem.c:177 [inline]
? rwsem_set_reader_owned kernel/locking/rwsem.c:182 [inline]
? rwsem_read_trylock kernel/locking/rwsem.c:257 [inline]
? rwsem_read_trylock kernel/locking/rwsem.c:249 [inline]
? __down_read_common kernel/locking/rwsem.c:1260 [inline]
? __down_read kernel/locking/rwsem.c:1274 [inline]
? down_read+0x1c5/0x4a0 kernel/locking/rwsem.c:1539
? hung_task_set_blocker include/linux/hung_task.h:55 [inline]
? rwsem_down_read_slowpath+0xbd0/0xca0 kernel/locking/rwsem.c:1070
? srso_alias_return_thunk+0x5/0xfbef5 arch/x86/lib/retpoline.S:220
? trace_hardirqs_on+0x53/0x60 kernel/trace/trace_preemptirq.c:79
caching_thread+0x3d5/0x1f20
? srso_alias_return_thunk+0x5/0xfbef5 arch/x86/lib/retpoline.S:220
? save_trace+0x54/0x390 kernel/locking/lockdep.c:587
? __pfx_caching_thread+0x10/0x10 fs/btrfs/block-group.c:533
? __entry_text_end+0x1025b9/0x1025bd
? instrument_atomic_read_write include/linux/instrumented.h:96 [inline]
? atomic_try_cmpxchg_acquire include/linux/atomic/atomic-instrumented.h:1301 [inline]
? queued_spin_lock include/asm-generic/qspinlock.h:111 [inline]
? do_raw_spin_lock+0x133/0x290 kernel/locking/spinlock_debug.c:116
? srso_alias_return_thunk+0x5/0xfbef5 arch/x86/lib/retpoline.S:220
? find_held_lock+0x31/0x90 kernel/locking/lockdep.c:5350
? spin_unlock include/linux/spinlock.h:391 [inline]
? thresh_exec_hook fs/btrfs/async-thread.c:203 [inline]
? btrfs_work_helper+0x1a2/0xa50 fs/btrfs/async-thread.c:311
? srso_alias_return_thunk+0x5/0xfbef5 arch/x86/lib/retpoline.S:220
? srso_alias_return_thunk+0x5/0xfbef5 arch/x86/lib/retpoline.S:220
? pv_queued_spin_unlock arch/x86/include/asm/paravirt.h:562 [inline]
? queued_spin_unlock arch/x86/include/asm/qspinlock.h:57 [inline]
? do_raw_spin_unlock+0x14b/0x200 kernel/locking/spinlock_debug.c:142
btrfs_work_helper+0x1d4/0xa50
? srso_alias_return_thunk+0x5/0xfbef5 arch/x86/lib/retpoline.S:220
process_one_work+0x8e0/0x1980
? __pfx_process_one_work+0x10/0x10 include/linux/list.h:226
? move_linked_works+0x1a8/0x2c0 kernel/workqueue.c:1165
? srso_alias_return_thunk+0x5/0xfbef5 arch/x86/lib/retpoline.S:220
? assign_work+0x19d/0x240 kernel/workqueue.c:1206
? srso_alias_return_thunk+0x5/0xfbef5 arch/x86/lib/retpoline.S:220
? __lock_is_held kernel/locking/lockdep.c:5601 [inline]
? lock_is_held_type+0xa3/0x130 kernel/locking/lockdep.c:5940
worker_thread+0x683/0xf80
? __pfx_worker_thread+0x10/0x10 kernel/workqueue.c:3570
kthread+0x3f0/0x850
? srso_alias_return_thunk+0x5/0xfbef5 arch/x86/lib/retpoline.S:220
? __pfx_kthread+0x10/0x10 arch/x86/include/asm/bitops.h:202
? srso_alias_return_thunk+0x5/0xfbef5 arch/x86/lib/retpoline.S:220
? trace_hardirqs_on+0x53/0x60 kernel/trace/trace_preemptirq.c:79
? srso_alias_return_thunk+0x5/0xfbef5 arch/x86/lib/retpoline.S:220
? __raw_spin_unlock_irq include/linux/spinlock_api_smp.h:159 [inline]
? _raw_spin_unlock_irq+0x27/0x70 kernel/locking/spinlock.c:202
? srso_alias_return_thunk+0x5/0xfbef5 arch/x86/lib/retpoline.S:220
? spin_unlock_irq include/linux/spinlock.h:401 [inline]
? calculate_sigpending+0x7c/0xb0 kernel/signal.c:194
? __pfx_kthread+0x10/0x10 arch/x86/include/asm/bitops.h:202
ret_from_fork+0x50f/0x610
? __pfx_kthread+0x10/0x10 arch/x86/include/asm/bitops.h:202
ret_from_fork_asm+0x1a/0x30
</TASK>
---
 fs/btrfs/extent_io.c       | 10 ++++++++++
 fs/btrfs/free-space-tree.c | 19 +++++++++++++++++++
 2 files changed, 29 insertions(+)

diff --git a/fs/btrfs/extent_io.c b/fs/btrfs/extent_io.c
index 23273d0e6f22..14da72a9a950 100644
--- a/fs/btrfs/extent_io.c
+++ b/fs/btrfs/extent_io.c
@@ -4254,6 +4254,16 @@ bool extent_buffer_test_bit(const struct extent_buffer *eb, unsigned long start,
 	size_t offset;
 	u8 *kaddr;
 
+	/*
+	 * Defend against a corrupt bitmap item whose item_size is smaller
+	 * than what key.offset implies: if start + BIT_BYTE(nr) would fall
+	 * outside this extent buffer, eb_bitmap_offset() would compute an
+	 * out-of-bounds folio index, and assert_eb_folio_uptodate() would
+	 * then dereference a NULL eb->folios[] slot.
+	 */
+	if (check_eb_range(eb, start, BIT_BYTE(nr) + 1))
+		return false;
+
 	eb_bitmap_offset(eb, start, nr, &i, &offset);
 	assert_eb_folio_uptodate(eb, i);
 	kaddr = folio_address(eb->folios[i]);
diff --git a/fs/btrfs/free-space-tree.c b/fs/btrfs/free-space-tree.c
index d86541073d42..04fde74c35e5 100644
--- a/fs/btrfs/free-space-tree.c
+++ b/fs/btrfs/free-space-tree.c
@@ -1555,6 +1555,8 @@ static int load_free_space_bitmaps(struct btrfs_caching_control *caching_ctl,
 	u64 end, offset;
 	u64 total_found = 0;
 	u32 extent_count = 0;
+	u32 expected_bitmap_size;
+	u32 actual_bitmap_size;
 	int ret;
 
 	block_group = caching_ctl->block_group;
@@ -1578,6 +1580,23 @@ static int load_free_space_bitmaps(struct btrfs_caching_control *caching_ctl,
 		ASSERT(key.type == BTRFS_FREE_SPACE_BITMAP_KEY);
 		ASSERT(key.objectid < end && key.objectid + key.offset <= end);
 
+		/*
+		 * Validate the on-disk item size matches what we compute
+		 * from key.offset.  A zero-sized (or otherwise wrong-sized)
+		 * bitmap item would cause extent_buffer_test_bit() to walk
+		 * past the end of the leaf, ultimately dereferencing a NULL
+		 * folio pointer in assert_eb_folio_uptodate().
+		 */
+		expected_bitmap_size = free_space_bitmap_size(fs_info, key.offset);
+		actual_bitmap_size = btrfs_item_size(path->nodes[0], path->slots[0]);
+		if (unlikely(actual_bitmap_size != expected_bitmap_size)) {
+			btrfs_err(fs_info,
+				  "corrupt free space bitmap for block group %llu: objectid=%llu expected item size %u got %u",
+				  block_group->start, key.objectid,
+				  expected_bitmap_size, actual_bitmap_size);
+			return -EUCLEAN;
+		}
+
 		offset = key.objectid;
 		while (offset < key.objectid + key.offset) {
 			bool bit_set;
-- 
2.43.0
Re: [PATCH] btrfs: validate free space bitmap size before testing bits
Posted by Qu Wenruo 1 month ago

在 2026/3/9 21:06, ZhengYuan Huang 写道:
> A corrupt free-space-tree leaf can contain a FREE_SPACE_BITMAP item
> whose on-disk item size does not match the bitmap size implied by
> key.offset.
> 
> The free-space-tree loading path currently uses key.offset to iterate
> bitmap coverage, but does not verify that the item size matches
> free_space_bitmap_size(fs_info, key.offset). This allows a zero-sized
> or otherwise truncated bitmap item to be consumed as if it contained
> valid bitmap data.

Such check should be put into tree-checker, as that where we do all the 
proper on-disk metadata checks.

> 
> Once bit access runs past the valid extent buffer range, the computed
> folio index can reach an unpopulated eb->folios[] slot and trigger a
> NULL dereference in assert_eb_folio_uptodate().
> 
> Fix this by validating FREE_SPACE_BITMAP item sizes in
> load_free_space_bitmaps() before testing any bits. If the on-disk item
> size does not match the expected bitmap size, treat the free-space-tree
> leaf as corrupt and fail loading it with -EUCLEAN.
> 
> Also add a defensive range check in extent_buffer_test_bit() so that
> corrupt metadata cannot drive bitmap bit access beyond the extent
> buffer even if a bad caller reaches that helper.
> 
> The bug is reproducible on 7.0.0-rc2-next-20260306 with a dynamic
> metadata fuzzing tool that injects single-bit corruptions into btrfs
> leaf blocks at runtime.

The "injects single-bit corruption into btrfs at runtime" part is confusing.

As far as the fix goes, it's really just extra checks for on-disk metadata.

The sentense gives an impression that it's injecting error into the 
memory, which is not fixable as such memory corruption can easily 
corrupt any critical kernel structure.

Just says it's a fuzzed image.

> After this change, the corrupt bitmap item is
> rejected and the filesystem reports corruption instead of crashing.
> 
> Fixes: a5ed91828518 ("Btrfs: implement the free space B-tree")
> Cc: stable@vger.kernel.org # 4.5+
> Signed-off-by: ZhengYuan Huang <gality369@gmail.com>
> ---
> Root cause
> ==========

Put the proper analyze into the commit message.

In fact, put a btrfs ins dump-tree output for that item (the content of 
that item may not be properly displayed though) would be more than 
enough to explain the situation.

> The direct fault is a NULL dereference in assert_eb_folio_uptodate(),
> reached from the free-space-tree bitmap loading path:
> 
>    caching_thread()
>      -> btrfs_load_free_space_tree()
>      -> load_free_space_bitmaps()
>      -> btrfs_free_space_test_bit()
>      -> extent_buffer_test_bit()
>      -> assert_eb_folio_uptodate()
> 
> The corrupted metadata pattern is a FREE_SPACE_BITMAP item whose
> item_size is smaller than the bitmap size described by key.offset. In the
> reproducer, multiple bitmap items had item_size == 0 while key.offset
> still described non-empty bitmap ranges.
> 
> For one failing item, the instrumented run showed:
> 
>    leaf_len = 16384
>    ptr = 16312
>    item_size = 0
>    expected = 402
>    key.type = BTRFS_FREE_SPACE_BITMAP_KEY
> 
> So only 72 bytes remained in the leaf data area, while the bitmap range
> described by key.offset required 402 bytes of bitmap data. The existing
> code did not validate that mismatch before iterating over bitmap bits.
> 
> btrfs_free_space_test_bit() uses btrfs_item_ptr_offset() as the bitmap
> start, and extent_buffer_test_bit() then translates the bit access into
> a folio index. Without a range check, once start + BIT_BYTE(nr) goes past
> eb->len, the computed folio index can exceed the populated folio range of
> the extent buffer.
> 
> extent_buffer objects are zero-initialized and only the first
> num_extent_folios(eb) entries in eb->folios[] are populated. An access
> past that range can therefore hit a NULL eb->folios[] slot, which is then
> dereferenced by assert_eb_folio_uptodate() via folio_test_uptodate().
> 
> Reproduction (v6.18, x86_64, KASAN)
> ===================================
> The PoC is relatively large, so it is provided separately through google drive:
> https://drive.google.com/drive/folders/1eB6QzkGViZhlq8xouE5WSVRU0fovu0qw

Since you know the root cause, you can easily craft the image with just 
corrupted item size for that FREESPACE_BITMAP item.

As we also put the minimal fuzzed image into btrfs-progs' 
tests/fuzzed-tests directory.

Thus a minimal image is always appreciated.

Thanks,
Qu

> 
> To reproduce the issue:
>    1. Build the PoC program: gcc poc.c -o poc
>    2. Build the ublk helper program from the ublk codebase, which is
> 	 used to provide the runtime corruption capability:
> 	  g++ -std=c++20 -fcoroutines -O2 -o standalone_replay \
>        standalone_replay_btrfs.cpp targets/ublksrv_tgt.cpp \
>        -I. -Iinclude -Itargets/include \
>        -L./lib/.libs -lublksrv -luring -lpthread
>    3. Attach the crafted image through ublk:
>        ./standalone_replay add -t loop -f /path/to/image
>    4. Run the PoC: ./poc
> This reliably reproduces the bug.
> 
> Test notes
> ==========
> The reproducer was verified on 7.0.0-rc2-next-20260306 with runtime
> single-bit corruption injected into btrfs leaf blocks. I have not yet
> retested it on the latest stable kernel, but I can do so if needed.
> 
> Fix
> ===
> Two complementary defences are added:
> 
> 1. In load_free_space_bitmaps() (free-space-tree.c), validate that the
>     on-disk item_size equals free_space_bitmap_size(fs_info, key.offset)
>     before entering the per-sector bit-reading loop.  A mismatch is a
>     clear sign of on-disk corruption; log a specific error message and
>     return -EUCLEAN so the caller can handle it gracefully instead of
>     walking off the end of the leaf.
> 
> 2. In extent_buffer_test_bit() (extent_io.c), call check_eb_range()
>     before eb_bitmap_offset(), mirroring the pattern already used by
>     read_extent_buffer() and extent_buffer_get_byte().  This makes the
>     function safe against any caller that passes an out-of-range (start,
>     nr) pair, regardless of how the corruption reached this point.
> 
> Defence (1) catches the specific free-space-tree path at the semantic
> layer and produces a meaningful log entry. Defence (2) is a generic
> safety net for the low-level helper that prevents the NULL-folio crash
> for any future caller that might bypass the upper-layer check.
> 
> KASAN reports
> =============
> BUG: KASAN: null-ptr-deref in instrument_atomic_read include/linux/instrumented.h:68 [inline]
> BUG: KASAN: null-ptr-deref in _test_bit include/asm-generic/bitops/instrumented-non-atomic.h:141 [inline]
> BUG: KASAN: null-ptr-deref in folio_test_uptodate include/linux/page-flags.h:787 [inline]
> BUG: KASAN: null-ptr-deref in assert_eb_folio_uptodate+0x198/0x2b0 fs/btrfs/extent_io.c:4071
> Read of size 8 at addr 0000000000000000 by task kworker/u8:0/12
> 
> CPU: 1 UID: 0 PID: 12 Comm: kworker/u8:0 Not tainted 6.18.0+ #12 PREEMPT(voluntary)
> Hardware name: QEMU Ubuntu 24.04 PC v2 (i440FX + PIIX, arch_caps fix, 1996), BIOS 1.16.3-debian-1.16.3-2 04/01/2014
> Workqueue: btrfs-cache btrfs_work_helper
> Call Trace:
> <TASK>
> dump_stack_lvl+0xbe/0x130
> print_report+0x437/0x650
> ? srso_alias_return_thunk+0x5/0xfbef5 arch/x86/lib/retpoline.S:220
> ? early_section include/linux/mmzone.h:2184 [inline]
> ? pfn_valid include/linux/mmzone.h:2196 [inline]
> ? __virt_addr_valid+0xca/0x4c0 arch/x86/mm/physaddr.c:65
> ? srso_alias_return_thunk+0x5/0xfbef5 arch/x86/lib/retpoline.S:220
> ? kasan_addr_to_slab+0xd/0xb0 mm/kasan/common.c:46
> kasan_report+0xfb/0x140
> ? instrument_atomic_read include/linux/instrumented.h:68 [inline]
> ? _test_bit include/asm-generic/bitops/instrumented-non-atomic.h:141 [inline]
> ? folio_test_uptodate include/linux/page-flags.h:787 [inline]
> ? assert_eb_folio_uptodate+0x198/0x2b0 fs/btrfs/extent_io.c:4071
> ? instrument_atomic_read include/linux/instrumented.h:68 [inline]
> ? _test_bit include/asm-generic/bitops/instrumented-non-atomic.h:141 [inline]
> ? folio_test_uptodate include/linux/page-flags.h:787 [inline]
> ? assert_eb_folio_uptodate+0x198/0x2b0 fs/btrfs/extent_io.c:4071
> kasan_check_range+0x11c/0x200
> __kasan_check_read+0x11/0x20
> assert_eb_folio_uptodate+0x198/0x2b0
> extent_buffer_test_bit+0xce/0x200
> btrfs_free_space_test_bit+0x1b3/0x270
> ? __pfx_btrfs_free_space_test_bit+0x10/0x10 include/linux/sched/mm.h:332
> ? __asan_memmove+0x30/0x80 mm/kasan/shadow.c:95
> ? srso_alias_return_thunk+0x5/0xfbef5 arch/x86/lib/retpoline.S:220
> ? read_extent_buffer+0x114/0x3d0 fs/btrfs/extent_io.c:3946
> btrfs_load_free_space_tree+0x57a/0xe40
> ? __pfx_btrfs_load_free_space_tree+0x10/0x10 fs/btrfs/free-space-tree.c:1492
> ? __entry_text_end+0x1025b9/0x1025bd
> ? __kasan_check_write+0x14/0x30 mm/kasan/shadow.c:37
> ? srso_alias_return_thunk+0x5/0xfbef5 arch/x86/lib/retpoline.S:220
> ? instrument_atomic_write include/linux/instrumented.h:82 [inline]
> ? atomic_long_set include/linux/atomic/atomic-instrumented.h:3223 [inline]
> ? __rwsem_set_reader_owned kernel/locking/rwsem.c:177 [inline]
> ? rwsem_set_reader_owned kernel/locking/rwsem.c:182 [inline]
> ? rwsem_read_trylock kernel/locking/rwsem.c:257 [inline]
> ? rwsem_read_trylock kernel/locking/rwsem.c:249 [inline]
> ? __down_read_common kernel/locking/rwsem.c:1260 [inline]
> ? __down_read kernel/locking/rwsem.c:1274 [inline]
> ? down_read+0x1c5/0x4a0 kernel/locking/rwsem.c:1539
> ? hung_task_set_blocker include/linux/hung_task.h:55 [inline]
> ? rwsem_down_read_slowpath+0xbd0/0xca0 kernel/locking/rwsem.c:1070
> ? srso_alias_return_thunk+0x5/0xfbef5 arch/x86/lib/retpoline.S:220
> ? trace_hardirqs_on+0x53/0x60 kernel/trace/trace_preemptirq.c:79
> caching_thread+0x3d5/0x1f20
> ? srso_alias_return_thunk+0x5/0xfbef5 arch/x86/lib/retpoline.S:220
> ? save_trace+0x54/0x390 kernel/locking/lockdep.c:587
> ? __pfx_caching_thread+0x10/0x10 fs/btrfs/block-group.c:533
> ? __entry_text_end+0x1025b9/0x1025bd
> ? instrument_atomic_read_write include/linux/instrumented.h:96 [inline]
> ? atomic_try_cmpxchg_acquire include/linux/atomic/atomic-instrumented.h:1301 [inline]
> ? queued_spin_lock include/asm-generic/qspinlock.h:111 [inline]
> ? do_raw_spin_lock+0x133/0x290 kernel/locking/spinlock_debug.c:116
> ? srso_alias_return_thunk+0x5/0xfbef5 arch/x86/lib/retpoline.S:220
> ? find_held_lock+0x31/0x90 kernel/locking/lockdep.c:5350
> ? spin_unlock include/linux/spinlock.h:391 [inline]
> ? thresh_exec_hook fs/btrfs/async-thread.c:203 [inline]
> ? btrfs_work_helper+0x1a2/0xa50 fs/btrfs/async-thread.c:311
> ? srso_alias_return_thunk+0x5/0xfbef5 arch/x86/lib/retpoline.S:220
> ? srso_alias_return_thunk+0x5/0xfbef5 arch/x86/lib/retpoline.S:220
> ? pv_queued_spin_unlock arch/x86/include/asm/paravirt.h:562 [inline]
> ? queued_spin_unlock arch/x86/include/asm/qspinlock.h:57 [inline]
> ? do_raw_spin_unlock+0x14b/0x200 kernel/locking/spinlock_debug.c:142
> btrfs_work_helper+0x1d4/0xa50
> ? srso_alias_return_thunk+0x5/0xfbef5 arch/x86/lib/retpoline.S:220
> process_one_work+0x8e0/0x1980
> ? __pfx_process_one_work+0x10/0x10 include/linux/list.h:226
> ? move_linked_works+0x1a8/0x2c0 kernel/workqueue.c:1165
> ? srso_alias_return_thunk+0x5/0xfbef5 arch/x86/lib/retpoline.S:220
> ? assign_work+0x19d/0x240 kernel/workqueue.c:1206
> ? srso_alias_return_thunk+0x5/0xfbef5 arch/x86/lib/retpoline.S:220
> ? __lock_is_held kernel/locking/lockdep.c:5601 [inline]
> ? lock_is_held_type+0xa3/0x130 kernel/locking/lockdep.c:5940
> worker_thread+0x683/0xf80
> ? __pfx_worker_thread+0x10/0x10 kernel/workqueue.c:3570
> kthread+0x3f0/0x850
> ? srso_alias_return_thunk+0x5/0xfbef5 arch/x86/lib/retpoline.S:220
> ? __pfx_kthread+0x10/0x10 arch/x86/include/asm/bitops.h:202
> ? srso_alias_return_thunk+0x5/0xfbef5 arch/x86/lib/retpoline.S:220
> ? trace_hardirqs_on+0x53/0x60 kernel/trace/trace_preemptirq.c:79
> ? srso_alias_return_thunk+0x5/0xfbef5 arch/x86/lib/retpoline.S:220
> ? __raw_spin_unlock_irq include/linux/spinlock_api_smp.h:159 [inline]
> ? _raw_spin_unlock_irq+0x27/0x70 kernel/locking/spinlock.c:202
> ? srso_alias_return_thunk+0x5/0xfbef5 arch/x86/lib/retpoline.S:220
> ? spin_unlock_irq include/linux/spinlock.h:401 [inline]
> ? calculate_sigpending+0x7c/0xb0 kernel/signal.c:194
> ? __pfx_kthread+0x10/0x10 arch/x86/include/asm/bitops.h:202
> ret_from_fork+0x50f/0x610
> ? __pfx_kthread+0x10/0x10 arch/x86/include/asm/bitops.h:202
> ret_from_fork_asm+0x1a/0x30
> </TASK>
> ---
>   fs/btrfs/extent_io.c       | 10 ++++++++++
>   fs/btrfs/free-space-tree.c | 19 +++++++++++++++++++
>   2 files changed, 29 insertions(+)
> 
> diff --git a/fs/btrfs/extent_io.c b/fs/btrfs/extent_io.c
> index 23273d0e6f22..14da72a9a950 100644
> --- a/fs/btrfs/extent_io.c
> +++ b/fs/btrfs/extent_io.c
> @@ -4254,6 +4254,16 @@ bool extent_buffer_test_bit(const struct extent_buffer *eb, unsigned long start,
>   	size_t offset;
>   	u8 *kaddr;
>   
> +	/*
> +	 * Defend against a corrupt bitmap item whose item_size is smaller
> +	 * than what key.offset implies: if start + BIT_BYTE(nr) would fall
> +	 * outside this extent buffer, eb_bitmap_offset() would compute an
> +	 * out-of-bounds folio index, and assert_eb_folio_uptodate() would
> +	 * then dereference a NULL eb->folios[] slot.
> +	 */
> +	if (check_eb_range(eb, start, BIT_BYTE(nr) + 1))
> +		return false;
> +
>   	eb_bitmap_offset(eb, start, nr, &i, &offset);
>   	assert_eb_folio_uptodate(eb, i);
>   	kaddr = folio_address(eb->folios[i]);
> diff --git a/fs/btrfs/free-space-tree.c b/fs/btrfs/free-space-tree.c
> index d86541073d42..04fde74c35e5 100644
> --- a/fs/btrfs/free-space-tree.c
> +++ b/fs/btrfs/free-space-tree.c
> @@ -1555,6 +1555,8 @@ static int load_free_space_bitmaps(struct btrfs_caching_control *caching_ctl,
>   	u64 end, offset;
>   	u64 total_found = 0;
>   	u32 extent_count = 0;
> +	u32 expected_bitmap_size;
> +	u32 actual_bitmap_size;
>   	int ret;
>   
>   	block_group = caching_ctl->block_group;
> @@ -1578,6 +1580,23 @@ static int load_free_space_bitmaps(struct btrfs_caching_control *caching_ctl,
>   		ASSERT(key.type == BTRFS_FREE_SPACE_BITMAP_KEY);
>   		ASSERT(key.objectid < end && key.objectid + key.offset <= end);
>   
> +		/*
> +		 * Validate the on-disk item size matches what we compute
> +		 * from key.offset.  A zero-sized (or otherwise wrong-sized)
> +		 * bitmap item would cause extent_buffer_test_bit() to walk
> +		 * past the end of the leaf, ultimately dereferencing a NULL
> +		 * folio pointer in assert_eb_folio_uptodate().
> +		 */
> +		expected_bitmap_size = free_space_bitmap_size(fs_info, key.offset);
> +		actual_bitmap_size = btrfs_item_size(path->nodes[0], path->slots[0]);
> +		if (unlikely(actual_bitmap_size != expected_bitmap_size)) {
> +			btrfs_err(fs_info,
> +				  "corrupt free space bitmap for block group %llu: objectid=%llu expected item size %u got %u",
> +				  block_group->start, key.objectid,
> +				  expected_bitmap_size, actual_bitmap_size);
> +			return -EUCLEAN;
> +		}
> +
>   		offset = key.objectid;
>   		while (offset < key.objectid + key.offset) {
>   			bool bit_set;