Re: [PATCH] btrfs: reject root with mismatched level between root_item and node header

From: Qu Wenruo

Date: Thu Mar 12 2026 - 17:29:59 EST




在 2026/3/12 20:52, ZhengYuan Huang 写道:
[...]

[FIX]
Catch the inconsistency in read_tree_root_path(), right after read_tree_block()
returns root->node and the generation and owner checks have passed. At that
point level = btrfs_root_level(&root->root_item) is already known, so
comparing it against btrfs_header_level(root->node) costs nothing. If they
differ, emit a btrfs_crit() message and return -EUCLEAN to prevent the
inconsistent btrfs_root object from being installed in the radix-tree cache
and reaching any caller. read_tree_root_path() is the only place that sees
both root_item.level and the actual root node simultaneously, making it the
correct and minimal location for this cross-block consistency check.
Returning -EUCLEAN is consistent with the existing owner-mismatch check
directly above and with the general btrfs policy of converting detectable
corruption into -EUCLEAN rather than crashing later.

After the fix, btrfs detects the level mismatch at root load time and
fails with -EUCLEAN instead of crashing later in
handle_indirect_tree_backref().

Cc: stable@xxxxxxxxxxxxxxx
Signed-off-by: ZhengYuan Huang <gality369@xxxxxxxxx>
---
fs/btrfs/disk-io.c | 20 ++++++++++++++++++++
1 file changed, 20 insertions(+)

diff --git a/fs/btrfs/disk-io.c b/fs/btrfs/disk-io.c
index 900e462d8ea1..06a8689cbf62 100644
--- a/fs/btrfs/disk-io.c
+++ b/fs/btrfs/disk-io.c
@@ -1067,6 +1067,26 @@ static struct btrfs_root *read_tree_root_path(struct btrfs_root *tree_root,
ret = -EUCLEAN;
goto fail;
}
+ /*
+ * Verify that the root node's on-disk level matches root_item.level.
+ * These can diverge when the root item in the root tree was corrupted
+ * (e.g. a bit flip changing level) while the actual tree block is
+ * already cached in memory at its real level. In that case
+ * read_tree_block() returns the cached buffer without re-running
+ * btrfs_validate_extent_buffer(), silently bypassing the level check.
+ * The mismatch would later cause a null-ptr-deref in backref walking
+ * (handle_indirect_tree_backref) when the commit root's real height is
+ * lower than what root_item.level claims.
+ */
+ if (unlikely(btrfs_header_level(root->node) != level)) {

Nope, we have btrfs_tree_parent_check structure, which has all the needed checks at read time.

The point of using that other than doing it manually here is, if one mirror is bad, but the other mirror is good, then we can still grab the good copy, but checking it here means if we got the bad mirror first, we have no more chance.

And during read of root-node, we have already passed the proper level into it.

So the only possibility is, your fuzzing tool is modifying the memory after the read check.

If so, it's impossible to fix.

+ btrfs_crit(fs_info,
+ "root=%llu block=%llu, root item level mismatch: "
+ "root_item.level=%d block.level=%u",
+ btrfs_root_id(root), root->node->start,
+ level, btrfs_header_level(root->node));
+ ret = -EUCLEAN;
+ goto fail;
+ }
root->commit_root = btrfs_root_node(root);
return root;
fail: