[PATCH v2] btrfs: free-space-tree: reject mismatched extent and bitmap items

Zhang Cen posted 1 patch 1 month ago
There is a newer version of this series
fs/btrfs/free-space-tree.c | 37 ++++++++++++++++++++++++++++++++-----
fs/btrfs/tree-checker.c    |  4 ++++
2 files changed, 36 insertions(+), 5 deletions(-)
[PATCH v2] btrfs: free-space-tree: reject mismatched extent and bitmap items
Posted by Zhang Cen 1 month ago
btrfs_load_free_space_tree() reads FREE_SPACE_INFO once and then chooses
the bitmap or extent loader for all following free-space records until the
next FREE_SPACE_INFO item. Those loaders currently enforce the selected
record type only with ASSERT().

On production builds without CONFIG_BTRFS_ASSERT, a malformed free-space
tree can therefore be decoded in the wrong mode. An EXTENT item can reach
btrfs_free_space_test_bit() as bitmap data, while a BITMAP item can be
added as a full free extent. The latter corrupts the in-memory free-space
cache and the former can read beyond the item payload.

Validate every post-info key before decoding it. Reject keys whose type
does not match the mode selected by FREE_SPACE_INFO, and reject keys
whose range extends past the block group, returning -EUCLEAN instead of
feeding the wrong record type to the bitmap or extent decoder.

Also reject zero-length FREE_SPACE_EXTENT items in tree-checker, matching
the existing FREE_SPACE_BITMAP zero-length check. This keeps the loader
range check simple and prevents a zero-length extent item from being a
valid on-disk free-space record.

Sanitizer validation reported:
Oops: general protection fault, probably for non-canonical address 0xdffffc0000000001: 0000 [#1] SMP KASAN NOPTI
Call trace:
  assert_eb_folio_uptodate() (fs/btrfs/extent_io.c:4134)
  extent_buffer_test_bit() (?:?)
  btrfs_free_space_test_bit() (fs/btrfs/free-space-tree.c:518)
  srso_alias_return_thunk() (arch/x86/include/asm/nospec-branch.h:375)
  __entry_text_end() (?:?)
  __asan_memcpy() (mm/kasan/shadow.c:103)
  read_extent_buffer() (?:?)
  load_free_space_bitmaps() (fs/btrfs/free-space-tree.c:1548)
  btrfs_get_32() (fs/btrfs/free-space-tree.c:?)
  btrfs_set_16() (fs/btrfs/free-space-tree.c:?)
  kmem_cache_alloc_noprof() (?:?)
  btrfs_load_free_space_tree() (fs/btrfs/free-space-tree.c:1685)
  load_free_space_tree_for_test() (?:?)
  rcu_disable_urgency_upon_qs() (kernel/rcu/tree.c:721)
  vprintk_emit() (?:?)
  __up_write() (kernel/locking/rwsem.c:1401)
  clone_commit_root_for_test() (?:?)
  test_extent_as_bitmap_mode_mismatch() (?:?)
  kmem_cache_free() (?:?)
  btrfs_free_path() (fs/btrfs/free-space-tree.c:1449)
  __add_block_group_free_space() (fs/btrfs/free-space-tree.c:20)
  run_test() (?:?)
  do_raw_spin_unlock() (?:?)
  btrfs_test_free_space_tree() (fs/btrfs/tests/free-space-tree-tests.c:547)
  btrfs_test_qgroups() (fs/btrfs/tests/qgroup-tests.c:462)
  btrfs_run_sanity_tests() (fs/btrfs/free-space-tree.c:?)
  init_btrfs_fs() (fs/btrfs/super.c:2690)
  do_one_initcall() (init/main.c:1382)
  __kasan_kmalloc() (?:?)
  rcu_is_watching() (?:?)
  do_initcalls() (init/main.c:1457)
  kernel_init_freeable() (init/main.c:1674)
  kernel_init() (init/main.c:1584)
  ret_from_fork() (?:?)
  __switch_to() (?:?)
  ret_from_fork_asm() (?:?)

Signed-off-by: Zhang Cen <rollkingzzc@gmail.com>

---
From ccb16e1f1b9490dc3d76e219cc2d9fe8cdd3afa7 Mon Sep 17 00:00:00 2001
Message-ID: <ccb16e1f1b9490dc3d76e219cc2d9fe8cdd3afa7.1778403060.git.rollkingzzc@gmail.com>
In-Reply-To: <20260510074943.2644334-1-rollkingzzc@gmail.com>
References: <20260510074943.2644334-1-rollkingzzc@gmail.com>
From: Zhang Cen <rollkingzzc@gmail.com>
Date: Sun, 10 May 2026 16:51:00 +0800
Subject: [PATCH v2] btrfs: free-space-tree: reject mismatched extent and
 bitmap items
To: Chris Mason <clm@fb.com>,
    David Sterba <dsterba@suse.com>
Cc: linux-btrfs@vger.kernel.org,
    linux-kernel@vger.kernel.org,
    Qu Wenruo <wqu@suse.com>,
    zerocling0077@gmail.com,
    2045gemini@gmail.com,
    Zhang Cen <rollkingzzc@gmail.com>

btrfs_load_free_space_tree() reads FREE_SPACE_INFO once and then chooses
the bitmap or extent loader for all following free-space records until the
next FREE_SPACE_INFO item. Those loaders currently enforce the selected
record type only with ASSERT().

On production builds without CONFIG_BTRFS_ASSERT, a malformed free-space
tree can therefore be decoded in the wrong mode. An EXTENT item can reach
btrfs_free_space_test_bit() as bitmap data, while a BITMAP item can be
added as a full free extent. The latter corrupts the in-memory free-space
cache and the former can read beyond the item payload.

Validate every post-info key before decoding it. Reject keys whose type
does not match the mode selected by FREE_SPACE_INFO, and reject keys
whose range extends past the block group, returning -EUCLEAN instead of
feeding the wrong record type to the bitmap or extent decoder.

Also reject zero-length FREE_SPACE_EXTENT items in tree-checker, matching
the existing FREE_SPACE_BITMAP zero-length check. This keeps the loader
range check simple and prevents a zero-length extent item from being a
valid on-disk free-space record.

A malformed extent-as-bitmap record was observed as a KASAN fault in
extent_buffer_test_bit() (fs/btrfs/extent_io.c:4313), reached through
btrfs_free_space_test_bit() (fs/btrfs/free-space-tree.c:518) from
load_free_space_bitmaps() (fs/btrfs/free-space-tree.c:1603).

Signed-off-by: Zhang Cen <rollkingzzc@gmail.com>
---
Changes since v1:
- Use unlikely() on validate_free_space_key() failure checks.
- Replace the loader range check with key->objectid + key->offset > end.
- Reject zero-length free-space extent items in tree-checker.

 fs/btrfs/free-space-tree.c | 37 ++++++++++++++++++++++++++++++++-----
 fs/btrfs/tree-checker.c    |  4 ++++
 2 files changed, 36 insertions(+), 5 deletions(-)

diff --git a/fs/btrfs/free-space-tree.c b/fs/btrfs/free-space-tree.c
index 472b3060e5ac..c1af70761919 100644
--- a/fs/btrfs/free-space-tree.c
+++ b/fs/btrfs/free-space-tree.c
@@ -1545,6 +1545,30 @@ int btrfs_remove_block_group_free_space(struct btrfs_trans_handle *trans,
 	return 0;
 }
 
+static int validate_free_space_key(struct btrfs_block_group *block_group,
+				   const struct btrfs_key *key,
+				   u8 expected_type)
+{
+	const u64 end = btrfs_block_group_end(block_group);
+
+	if (unlikely(key->type != expected_type)) {
+		btrfs_err(block_group->fs_info,
+			  "block group %llu has unexpected free space key type %u, expected %u",
+			  block_group->start, key->type, expected_type);
+		return -EUCLEAN;
+	}
+
+	if (unlikely(key->objectid + key->offset > end)) {
+		btrfs_err(block_group->fs_info,
+			  "block group %llu has invalid free space key (%llu %u %llu)",
+			  block_group->start, key->objectid, key->type,
+			  key->offset);
+		return -EUCLEAN;
+	}
+
+	return 0;
+}
+
 static int load_free_space_bitmaps(struct btrfs_caching_control *caching_ctl,
 				   struct btrfs_path *path,
 				   u32 expected_extent_count)
@@ -1576,8 +1600,10 @@ static int load_free_space_bitmaps(struct btrfs_caching_control *caching_ctl,
 		if (key.type == BTRFS_FREE_SPACE_INFO_KEY)
 			break;
 
-		ASSERT(key.type == BTRFS_FREE_SPACE_BITMAP_KEY);
-		ASSERT(key.objectid < end && key.objectid + key.offset <= end);
+		ret = validate_free_space_key(block_group, &key,
+					      BTRFS_FREE_SPACE_BITMAP_KEY);
+		if (unlikely(ret))
+			return ret;
 
 		offset = key.objectid;
 		while (offset < key.objectid + key.offset) {
@@ -1633,7 +1659,6 @@ static int load_free_space_extents(struct btrfs_caching_control *caching_ctl,
 	struct btrfs_fs_info *fs_info = block_group->fs_info;
 	struct btrfs_root *root;
 	struct btrfs_key key;
-	const u64 end = btrfs_block_group_end(block_group);
 	u64 total_found = 0;
 	u32 extent_count = 0;
 	int ret;
@@ -1654,8 +1679,10 @@ static int load_free_space_extents(struct btrfs_caching_control *caching_ctl,
 		if (key.type == BTRFS_FREE_SPACE_INFO_KEY)
 			break;
 
-		ASSERT(key.type == BTRFS_FREE_SPACE_EXTENT_KEY);
-		ASSERT(key.objectid < end && key.objectid + key.offset <= end);
+		ret = validate_free_space_key(block_group, &key,
+					      BTRFS_FREE_SPACE_EXTENT_KEY);
+		if (unlikely(ret))
+			return ret;
 
 		ret = btrfs_add_new_free_space(block_group, key.objectid,
 					       key.objectid + key.offset,
diff --git a/fs/btrfs/tree-checker.c b/fs/btrfs/tree-checker.c
index 1f15d0793a9c..ec24ffb6641d 100644
--- a/fs/btrfs/tree-checker.c
+++ b/fs/btrfs/tree-checker.c
@@ -2129,6 +2129,10 @@ static int check_free_space_extent(struct extent_buffer *leaf, struct btrfs_key
 			    blocksize, BTRFS_KEY_FMT_VALUE(key));
 		return -EUCLEAN;
 	}
+	if (unlikely(key->offset == 0)) {
+		generic_err(leaf, slot, "free space extent length is 0");
+		return -EUCLEAN;
+	}
 	if (unlikely(btrfs_item_size(leaf, slot) != 0)) {
 		generic_err(leaf, slot,
 			    "invalid item size for free space info, has %u expect 0",
-- 
2.43.0
Re: [PATCH v2] btrfs: free-space-tree: reject mismatched extent and bitmap items
Posted by Cen Zhang 1 month ago
I'm so sorry, I sent the wrong message.

Best regards,
Zhang Cen

Zhang Cen <rollkingzzc@gmail.com> 于2026年5月10日周日 22:42写道:
>
> btrfs_load_free_space_tree() reads FREE_SPACE_INFO once and then chooses
> the bitmap or extent loader for all following free-space records until the
> next FREE_SPACE_INFO item. Those loaders currently enforce the selected
> record type only with ASSERT().
>
> On production builds without CONFIG_BTRFS_ASSERT, a malformed free-space
> tree can therefore be decoded in the wrong mode. An EXTENT item can reach
> btrfs_free_space_test_bit() as bitmap data, while a BITMAP item can be
> added as a full free extent. The latter corrupts the in-memory free-space
> cache and the former can read beyond the item payload.
>
> Validate every post-info key before decoding it. Reject keys whose type
> does not match the mode selected by FREE_SPACE_INFO, and reject keys
> whose range extends past the block group, returning -EUCLEAN instead of
> feeding the wrong record type to the bitmap or extent decoder.
>
> Also reject zero-length FREE_SPACE_EXTENT items in tree-checker, matching
> the existing FREE_SPACE_BITMAP zero-length check. This keeps the loader
> range check simple and prevents a zero-length extent item from being a
> valid on-disk free-space record.
>
> Sanitizer validation reported:
> Oops: general protection fault, probably for non-canonical address 0xdffffc0000000001: 0000 [#1] SMP KASAN NOPTI
> Call trace:
>   assert_eb_folio_uptodate() (fs/btrfs/extent_io.c:4134)
>   extent_buffer_test_bit() (?:?)
>   btrfs_free_space_test_bit() (fs/btrfs/free-space-tree.c:518)
>   srso_alias_return_thunk() (arch/x86/include/asm/nospec-branch.h:375)
>   __entry_text_end() (?:?)
>   __asan_memcpy() (mm/kasan/shadow.c:103)
>   read_extent_buffer() (?:?)
>   load_free_space_bitmaps() (fs/btrfs/free-space-tree.c:1548)
>   btrfs_get_32() (fs/btrfs/free-space-tree.c:?)
>   btrfs_set_16() (fs/btrfs/free-space-tree.c:?)
>   kmem_cache_alloc_noprof() (?:?)
>   btrfs_load_free_space_tree() (fs/btrfs/free-space-tree.c:1685)
>   load_free_space_tree_for_test() (?:?)
>   rcu_disable_urgency_upon_qs() (kernel/rcu/tree.c:721)
>   vprintk_emit() (?:?)
>   __up_write() (kernel/locking/rwsem.c:1401)
>   clone_commit_root_for_test() (?:?)
>   test_extent_as_bitmap_mode_mismatch() (?:?)
>   kmem_cache_free() (?:?)
>   btrfs_free_path() (fs/btrfs/free-space-tree.c:1449)
>   __add_block_group_free_space() (fs/btrfs/free-space-tree.c:20)
>   run_test() (?:?)
>   do_raw_spin_unlock() (?:?)
>   btrfs_test_free_space_tree() (fs/btrfs/tests/free-space-tree-tests.c:547)
>   btrfs_test_qgroups() (fs/btrfs/tests/qgroup-tests.c:462)
>   btrfs_run_sanity_tests() (fs/btrfs/free-space-tree.c:?)
>   init_btrfs_fs() (fs/btrfs/super.c:2690)
>   do_one_initcall() (init/main.c:1382)
>   __kasan_kmalloc() (?:?)
>   rcu_is_watching() (?:?)
>   do_initcalls() (init/main.c:1457)
>   kernel_init_freeable() (init/main.c:1674)
>   kernel_init() (init/main.c:1584)
>   ret_from_fork() (?:?)
>   __switch_to() (?:?)
>   ret_from_fork_asm() (?:?)
>
> Signed-off-by: Zhang Cen <rollkingzzc@gmail.com>
>
> ---
> From ccb16e1f1b9490dc3d76e219cc2d9fe8cdd3afa7 Mon Sep 17 00:00:00 2001
> Message-ID: <ccb16e1f1b9490dc3d76e219cc2d9fe8cdd3afa7.1778403060.git.rollkingzzc@gmail.com>
> In-Reply-To: <20260510074943.2644334-1-rollkingzzc@gmail.com>
> References: <20260510074943.2644334-1-rollkingzzc@gmail.com>
> From: Zhang Cen <rollkingzzc@gmail.com>
> Date: Sun, 10 May 2026 16:51:00 +0800
> Subject: [PATCH v2] btrfs: free-space-tree: reject mismatched extent and
>  bitmap items
> To: Chris Mason <clm@fb.com>,
>     David Sterba <dsterba@suse.com>
> Cc: linux-btrfs@vger.kernel.org,
>     linux-kernel@vger.kernel.org,
>     Qu Wenruo <wqu@suse.com>,
>     zerocling0077@gmail.com,
>     2045gemini@gmail.com,
>     Zhang Cen <rollkingzzc@gmail.com>
>
> btrfs_load_free_space_tree() reads FREE_SPACE_INFO once and then chooses
> the bitmap or extent loader for all following free-space records until the
> next FREE_SPACE_INFO item. Those loaders currently enforce the selected
> record type only with ASSERT().
>
> On production builds without CONFIG_BTRFS_ASSERT, a malformed free-space
> tree can therefore be decoded in the wrong mode. An EXTENT item can reach
> btrfs_free_space_test_bit() as bitmap data, while a BITMAP item can be
> added as a full free extent. The latter corrupts the in-memory free-space
> cache and the former can read beyond the item payload.
>
> Validate every post-info key before decoding it. Reject keys whose type
> does not match the mode selected by FREE_SPACE_INFO, and reject keys
> whose range extends past the block group, returning -EUCLEAN instead of
> feeding the wrong record type to the bitmap or extent decoder.
>
> Also reject zero-length FREE_SPACE_EXTENT items in tree-checker, matching
> the existing FREE_SPACE_BITMAP zero-length check. This keeps the loader
> range check simple and prevents a zero-length extent item from being a
> valid on-disk free-space record.
>
> A malformed extent-as-bitmap record was observed as a KASAN fault in
> extent_buffer_test_bit() (fs/btrfs/extent_io.c:4313), reached through
> btrfs_free_space_test_bit() (fs/btrfs/free-space-tree.c:518) from
> load_free_space_bitmaps() (fs/btrfs/free-space-tree.c:1603).
>
> Signed-off-by: Zhang Cen <rollkingzzc@gmail.com>
> ---
> Changes since v1:
> - Use unlikely() on validate_free_space_key() failure checks.
> - Replace the loader range check with key->objectid + key->offset > end.
> - Reject zero-length free-space extent items in tree-checker.
>
>  fs/btrfs/free-space-tree.c | 37 ++++++++++++++++++++++++++++++++-----
>  fs/btrfs/tree-checker.c    |  4 ++++
>  2 files changed, 36 insertions(+), 5 deletions(-)
>
> diff --git a/fs/btrfs/free-space-tree.c b/fs/btrfs/free-space-tree.c
> index 472b3060e5ac..c1af70761919 100644
> --- a/fs/btrfs/free-space-tree.c
> +++ b/fs/btrfs/free-space-tree.c
> @@ -1545,6 +1545,30 @@ int btrfs_remove_block_group_free_space(struct btrfs_trans_handle *trans,
>         return 0;
>  }
>
> +static int validate_free_space_key(struct btrfs_block_group *block_group,
> +                                  const struct btrfs_key *key,
> +                                  u8 expected_type)
> +{
> +       const u64 end = btrfs_block_group_end(block_group);
> +
> +       if (unlikely(key->type != expected_type)) {
> +               btrfs_err(block_group->fs_info,
> +                         "block group %llu has unexpected free space key type %u, expected %u",
> +                         block_group->start, key->type, expected_type);
> +               return -EUCLEAN;
> +       }
> +
> +       if (unlikely(key->objectid + key->offset > end)) {
> +               btrfs_err(block_group->fs_info,
> +                         "block group %llu has invalid free space key (%llu %u %llu)",
> +                         block_group->start, key->objectid, key->type,
> +                         key->offset);
> +               return -EUCLEAN;
> +       }
> +
> +       return 0;
> +}
> +
>  static int load_free_space_bitmaps(struct btrfs_caching_control *caching_ctl,
>                                    struct btrfs_path *path,
>                                    u32 expected_extent_count)
> @@ -1576,8 +1600,10 @@ static int load_free_space_bitmaps(struct btrfs_caching_control *caching_ctl,
>                 if (key.type == BTRFS_FREE_SPACE_INFO_KEY)
>                         break;
>
> -               ASSERT(key.type == BTRFS_FREE_SPACE_BITMAP_KEY);
> -               ASSERT(key.objectid < end && key.objectid + key.offset <= end);
> +               ret = validate_free_space_key(block_group, &key,
> +                                             BTRFS_FREE_SPACE_BITMAP_KEY);
> +               if (unlikely(ret))
> +                       return ret;
>
>                 offset = key.objectid;
>                 while (offset < key.objectid + key.offset) {
> @@ -1633,7 +1659,6 @@ static int load_free_space_extents(struct btrfs_caching_control *caching_ctl,
>         struct btrfs_fs_info *fs_info = block_group->fs_info;
>         struct btrfs_root *root;
>         struct btrfs_key key;
> -       const u64 end = btrfs_block_group_end(block_group);
>         u64 total_found = 0;
>         u32 extent_count = 0;
>         int ret;
> @@ -1654,8 +1679,10 @@ static int load_free_space_extents(struct btrfs_caching_control *caching_ctl,
>                 if (key.type == BTRFS_FREE_SPACE_INFO_KEY)
>                         break;
>
> -               ASSERT(key.type == BTRFS_FREE_SPACE_EXTENT_KEY);
> -               ASSERT(key.objectid < end && key.objectid + key.offset <= end);
> +               ret = validate_free_space_key(block_group, &key,
> +                                             BTRFS_FREE_SPACE_EXTENT_KEY);
> +               if (unlikely(ret))
> +                       return ret;
>
>                 ret = btrfs_add_new_free_space(block_group, key.objectid,
>                                                key.objectid + key.offset,
> diff --git a/fs/btrfs/tree-checker.c b/fs/btrfs/tree-checker.c
> index 1f15d0793a9c..ec24ffb6641d 100644
> --- a/fs/btrfs/tree-checker.c
> +++ b/fs/btrfs/tree-checker.c
> @@ -2129,6 +2129,10 @@ static int check_free_space_extent(struct extent_buffer *leaf, struct btrfs_key
>                             blocksize, BTRFS_KEY_FMT_VALUE(key));
>                 return -EUCLEAN;
>         }
> +       if (unlikely(key->offset == 0)) {
> +               generic_err(leaf, slot, "free space extent length is 0");
> +               return -EUCLEAN;
> +       }
>         if (unlikely(btrfs_item_size(leaf, slot) != 0)) {
>                 generic_err(leaf, slot,
>                             "invalid item size for free space info, has %u expect 0",
> --
> 2.43.0