Linux Btrfs filesystem development
 help / color / mirror / Atom feed
* [PATCH] btrfs: tree-checker: reject file extent items for special files
@ 2026-08-18  4:57 Qu Wenruo
  2026-08-31 22:08 ` Boris Burkov
  0 siblings, 1 reply; 2+ messages in thread
From: Qu Wenruo @ 2026-08-18  4:57 UTC (permalink / raw)
  To: linux-btrfs; +Cc: ZhengYuan Huang

File extent items are only utilized by regular files or symlinks, other
files like directory/char/block/fifo/sock files should not have any file
extent item.

Previously we were unable to reject such cases, as the inode item may not
be in the same leaf.
But we already have @prev_key in check_leaf_item(), this means we just
need a new way to pass the mode of the previously hit inode item, then
we can detect such problems.

Introduce a new and tiny helper structure, last_inode_info, to record
the last hit inode number and its mode, and keep it across the whole
leaf.
Then if we hit a file extent item, and the inode item is in the same
leaf, we can refer to that last_inode_info to determine if we need to
reject the file extent item.

Now with the following corrupted fs tree, the kernel can safely reject
the leaf:

	item 0 key (256 INODE_ITEM 0) itemoff 16123 itemsize 160
		generation 3 transid 9 size 12 nbytes 16384
		block group 0 mode 40755 links 1 uid 0 gid 0 rdev 0
		sequence 1 flags 0x0(none)
	item 1 key (256 INODE_REF 256) itemoff 16111 itemsize 12
		index 0 namelen 2 name: ..
	item 2 key (256 DIR_ITEM 496027801) itemoff 16075 itemsize 36
		location key (257 INODE_ITEM 0) type FILE
		transid 9 data_len 0 name_len 6
		name: foobar
	item 3 key (256 DIR_INDEX 2) itemoff 16039 itemsize 36
		location key (257 INODE_ITEM 0) type FILE
		transid 9 data_len 0 name_len 6
		name: foobar
	item 4 key (257 INODE_ITEM 0) itemoff 15879 itemsize 160
		generation 9 transid 9 size 8192 nbytes 8192
		block group 0 mode 60600 links 1 uid 0 gid 0 rdev 0
		                   ^^ This is BLK type, not REG.
		sequence 2 flags 0x0(none)
	item 5 key (257 INODE_REF 256) itemoff 15863 itemsize 16
		index 2 namelen 6 name: foobar
	item 6 key (257 EXTENT_DATA 0) itemoff 15810 itemsize 53
		generation 9 type 1 (regular)
		extent data disk byte 13631488 nr 8192
		extent data offset 0 nr 8192 ram 8192
		extent compression 0 (none)
		extent encryption 0

With the patch, kernel will reject it with the following tree-checker
errors:

 BTRFS critical (device loop0): corrupt leaf: root=5 block=30408704 slot=6 ino=257 file_offset=0, invalid file extent item, should not have any file extent for inode mode 060600
 BTRFS error (device loop0): read time tree block corruption detected on logical 30408704 mirror 1

Reported-by: ZhengYuan Huang <gality369@gmail.com>
Link: https://lore.kernel.org/linux-btrfs/20260817132051.267646-1-gality369@gmail.com/
Assisted-by: LLM (for generating the corrupted image)
Signed-off-by: Qu Wenruo <wqu@suse.com>
---
 fs/btrfs/tree-checker.c | 68 ++++++++++++++++++++++++++++++++++-------
 1 file changed, 57 insertions(+), 11 deletions(-)

diff --git a/fs/btrfs/tree-checker.c b/fs/btrfs/tree-checker.c
index 0ce91396b517..b0858f6c88e4 100644
--- a/fs/btrfs/tree-checker.c
+++ b/fs/btrfs/tree-checker.c
@@ -163,6 +163,12 @@ static void dir_item_err(const struct extent_buffer *eb, int slot,
 	va_end(args);
 }
 
+/* Record info for the last hit inode. */
+struct last_inode_info {
+	u64 ino;
+	u32 mode;
+};
+
 /*
  * This functions checks prev_key->objectid, to ensure current key and prev_key
  * share the same objectid as inode number.
@@ -204,15 +210,47 @@ static bool check_prev_ino(struct extent_buffer *leaf,
 		prev_key->objectid, key->objectid);
 	return false;
 }
+
+static bool check_last_inode_info(struct extent_buffer *leaf,
+				  struct btrfs_key *key, int slot,
+				  u8 fi_type,
+				  const struct last_inode_info *last_inode)
+{
+	/* No inode item in this leaf. */
+	if (last_inode->ino != key->objectid)
+		return true;
+	if (S_ISREG(last_inode->mode))
+		return true;
+	if (S_ISLNK(last_inode->mode)) {
+		/* For symlink, the file extent item should always be inlined. */
+		if (unlikely(fi_type != BTRFS_FILE_EXTENT_INLINE)) {
+			file_extent_err(leaf, slot,
+			"invalid file extent type, have %u expect %u for symlink",
+					fi_type, BTRFS_FILE_EXTENT_INLINE);
+			return false;
+		}
+		return true;
+	}
+	/*
+	 * The remaining are special files, e.g. block/fifo files, which should
+	 * not have any file extent.
+	 */
+	file_extent_err(leaf, slot, "file extent item not allowed for inode mode 0%o",
+			last_inode->mode);
+	return false;
+}
+
 static int check_extent_data_item(struct extent_buffer *leaf,
 				  struct btrfs_key *key, int slot,
-				  struct btrfs_key *prev_key)
+				  struct btrfs_key *prev_key,
+				  const struct last_inode_info *last_inode)
 {
 	struct btrfs_fs_info *fs_info = leaf->fs_info;
 	struct btrfs_file_extent_item *fi;
 	u32 sectorsize = fs_info->sectorsize;
 	u32 item_size = btrfs_item_size(leaf, slot);
 	u64 extent_end;
+	u8 fi_type;
 
 	if (unlikely(!IS_ALIGNED(key->offset, sectorsize))) {
 		file_extent_err(leaf, slot,
@@ -243,15 +281,17 @@ static int check_extent_data_item(struct extent_buffer *leaf,
 				SZ_4K);
 		return -EUCLEAN;
 	}
-	if (unlikely(btrfs_file_extent_type(leaf, fi) >=
-		     BTRFS_NR_FILE_EXTENT_TYPES)) {
+	fi_type = btrfs_file_extent_type(leaf, fi);
+	if (unlikely(fi_type >= BTRFS_NR_FILE_EXTENT_TYPES)) {
 		file_extent_err(leaf, slot,
 		"invalid type for file extent, have %u expect range [0, %u]",
-			btrfs_file_extent_type(leaf, fi),
-			BTRFS_NR_FILE_EXTENT_TYPES - 1);
+			fi_type, BTRFS_NR_FILE_EXTENT_TYPES - 1);
 		return -EUCLEAN;
 	}
 
+	if (unlikely(!check_last_inode_info(leaf, key, slot, fi_type, last_inode)))
+		return -EUCLEAN;
+
 	/*
 	 * Support for new compression/encryption must introduce incompat flag,
 	 * and must be caught in open_ctree().
@@ -270,7 +310,8 @@ static int check_extent_data_item(struct extent_buffer *leaf,
 			btrfs_file_extent_encryption(leaf, fi));
 		return -EUCLEAN;
 	}
-	if (btrfs_file_extent_type(leaf, fi) == BTRFS_FILE_EXTENT_INLINE) {
+
+	if (fi_type == BTRFS_FILE_EXTENT_INLINE) {
 		/* Inline extent must have 0 as key offset */
 		if (unlikely(key->offset)) {
 			file_extent_err(leaf, slot,
@@ -1206,7 +1247,8 @@ static int check_dev_item(struct extent_buffer *leaf,
 }
 
 static int check_inode_item(struct extent_buffer *leaf,
-			    struct btrfs_key *key, int slot)
+			    struct btrfs_key *key, int slot,
+			    struct last_inode_info *last_inode)
 {
 	struct btrfs_fs_info *fs_info = leaf->fs_info;
 	struct btrfs_inode_item *iitem;
@@ -1291,6 +1333,8 @@ static int check_inode_item(struct extent_buffer *leaf,
 			ro_flags);
 		return -EUCLEAN;
 	}
+	last_inode->ino = key->objectid;
+	last_inode->mode = mode;
 	return 0;
 }
 
@@ -2319,14 +2363,15 @@ static int check_free_space_bitmap(struct extent_buffer *leaf,
 static enum btrfs_tree_block_status check_leaf_item(struct extent_buffer *leaf,
 						    struct btrfs_key *key,
 						    int slot,
-						    struct btrfs_key *prev_key)
+						    struct btrfs_key *prev_key,
+						    struct last_inode_info *last_inode)
 {
 	int ret = 0;
 	struct btrfs_chunk *chunk;
 
 	switch (key->type) {
 	case BTRFS_EXTENT_DATA_KEY:
-		ret = check_extent_data_item(leaf, key, slot, prev_key);
+		ret = check_extent_data_item(leaf, key, slot, prev_key, last_inode);
 		break;
 	case BTRFS_EXTENT_CSUM_KEY:
 		ret = check_csum_item(leaf, key, slot, prev_key);
@@ -2356,7 +2401,7 @@ static enum btrfs_tree_block_status check_leaf_item(struct extent_buffer *leaf,
 		ret = check_dev_extent_item(leaf, key, slot, prev_key);
 		break;
 	case BTRFS_INODE_ITEM_KEY:
-		ret = check_inode_item(leaf, key, slot);
+		ret = check_inode_item(leaf, key, slot, last_inode);
 		break;
 	case BTRFS_ROOT_ITEM_KEY:
 		ret = check_root_item(leaf, key, slot);
@@ -2404,6 +2449,7 @@ static enum btrfs_tree_block_status check_leaf_item(struct extent_buffer *leaf,
 enum btrfs_tree_block_status __btrfs_check_leaf(struct extent_buffer *leaf)
 {
 	struct btrfs_fs_info *fs_info = leaf->fs_info;
+	struct last_inode_info last_inode = { 0 };
 	/* No valid key type is 0, so all key should be larger than this key */
 	struct btrfs_key prev_key = {0, 0, 0};
 	struct btrfs_key key;
@@ -2539,7 +2585,7 @@ enum btrfs_tree_block_status __btrfs_check_leaf(struct extent_buffer *leaf)
 		}
 
 		/* Check if the item size and content meet other criteria. */
-		ret = check_leaf_item(leaf, &key, slot, &prev_key);
+		ret = check_leaf_item(leaf, &key, slot, &prev_key, &last_inode);
 		if (unlikely(ret != BTRFS_TREE_BLOCK_CLEAN))
 			return ret;
 
-- 
2.54.0


^ permalink raw reply related	[flat|nested] 2+ messages in thread

* Re: [PATCH] btrfs: tree-checker: reject file extent items for special files
  2026-08-18  4:57 [PATCH] btrfs: tree-checker: reject file extent items for special files Qu Wenruo
@ 2026-08-31 22:08 ` Boris Burkov
  0 siblings, 0 replies; 2+ messages in thread
From: Boris Burkov @ 2026-08-31 22:08 UTC (permalink / raw)
  To: Qu Wenruo; +Cc: linux-btrfs, ZhengYuan Huang

On Tue, Aug 18, 2026 at 02:27:11PM +0930, Qu Wenruo wrote:
> File extent items are only utilized by regular files or symlinks, other
> files like directory/char/block/fifo/sock files should not have any file
> extent item.
> 
> Previously we were unable to reject such cases, as the inode item may not
> be in the same leaf.
> But we already have @prev_key in check_leaf_item(), this means we just
> need a new way to pass the mode of the previously hit inode item, then
> we can detect such problems.
> 
> Introduce a new and tiny helper structure, last_inode_info, to record
> the last hit inode number and its mode, and keep it across the whole
> leaf.
> Then if we hit a file extent item, and the inode item is in the same
> leaf, we can refer to that last_inode_info to determine if we need to
> reject the file extent item.
> 
> Now with the following corrupted fs tree, the kernel can safely reject
> the leaf:
> 
> 	item 0 key (256 INODE_ITEM 0) itemoff 16123 itemsize 160
> 		generation 3 transid 9 size 12 nbytes 16384
> 		block group 0 mode 40755 links 1 uid 0 gid 0 rdev 0
> 		sequence 1 flags 0x0(none)
> 	item 1 key (256 INODE_REF 256) itemoff 16111 itemsize 12
> 		index 0 namelen 2 name: ..
> 	item 2 key (256 DIR_ITEM 496027801) itemoff 16075 itemsize 36
> 		location key (257 INODE_ITEM 0) type FILE
> 		transid 9 data_len 0 name_len 6
> 		name: foobar
> 	item 3 key (256 DIR_INDEX 2) itemoff 16039 itemsize 36
> 		location key (257 INODE_ITEM 0) type FILE
> 		transid 9 data_len 0 name_len 6
> 		name: foobar
> 	item 4 key (257 INODE_ITEM 0) itemoff 15879 itemsize 160
> 		generation 9 transid 9 size 8192 nbytes 8192
> 		block group 0 mode 60600 links 1 uid 0 gid 0 rdev 0
> 		                   ^^ This is BLK type, not REG.
> 		sequence 2 flags 0x0(none)
> 	item 5 key (257 INODE_REF 256) itemoff 15863 itemsize 16
> 		index 2 namelen 6 name: foobar
> 	item 6 key (257 EXTENT_DATA 0) itemoff 15810 itemsize 53
> 		generation 9 type 1 (regular)
> 		extent data disk byte 13631488 nr 8192
> 		extent data offset 0 nr 8192 ram 8192
> 		extent compression 0 (none)
> 		extent encryption 0
> 
> With the patch, kernel will reject it with the following tree-checker
> errors:
> 
>  BTRFS critical (device loop0): corrupt leaf: root=5 block=30408704 slot=6 ino=257 file_offset=0, invalid file extent item, should not have any file extent for inode mode 060600
>  BTRFS error (device loop0): read time tree block corruption detected on logical 30408704 mirror 1
> 
> Reported-by: ZhengYuan Huang <gality369@gmail.com>
> Link: https://lore.kernel.org/linux-btrfs/20260817132051.267646-1-gality369@gmail.com/
> Assisted-by: LLM (for generating the corrupted image)
> Signed-off-by: Qu Wenruo <wqu@suse.com>
> ---
>  fs/btrfs/tree-checker.c | 68 ++++++++++++++++++++++++++++++++++-------
>  1 file changed, 57 insertions(+), 11 deletions(-)
> 
> diff --git a/fs/btrfs/tree-checker.c b/fs/btrfs/tree-checker.c
> index 0ce91396b517..b0858f6c88e4 100644
> --- a/fs/btrfs/tree-checker.c
> +++ b/fs/btrfs/tree-checker.c
> @@ -163,6 +163,12 @@ static void dir_item_err(const struct extent_buffer *eb, int slot,
>  	va_end(args);
>  }
>  
> +/* Record info for the last hit inode. */
> +struct last_inode_info {
> +	u64 ino;
> +	u32 mode;
> +};
> +
>  /*
>   * This functions checks prev_key->objectid, to ensure current key and prev_key
>   * share the same objectid as inode number.
> @@ -204,15 +210,47 @@ static bool check_prev_ino(struct extent_buffer *leaf,
>  		prev_key->objectid, key->objectid);
>  	return false;
>  }
> +
> +static bool check_last_inode_info(struct extent_buffer *leaf,
> +				  struct btrfs_key *key, int slot,
> +				  u8 fi_type,
> +				  const struct last_inode_info *last_inode)
> +{

This function name confused me as it doesn't refer to the fact that it
is in the context of checking an extent data item.

maybe something like "should_have_extent_data()" and do the error
logging in the caller when the result is unexpected?

> +	/* No inode item in this leaf. */
> +	if (last_inode->ino != key->objectid)
> +		return true;
> +	if (S_ISREG(last_inode->mode))
> +		return true;
> +	if (S_ISLNK(last_inode->mode)) {
> +		/* For symlink, the file extent item should always be inlined. */
> +		if (unlikely(fi_type != BTRFS_FILE_EXTENT_INLINE)) {
> +			file_extent_err(leaf, slot,
> +			"invalid file extent type, have %u expect %u for symlink",
> +					fi_type, BTRFS_FILE_EXTENT_INLINE);
> +			return false;
> +		}
> +		return true;
> +	}
> +	/*
> +	 * The remaining are special files, e.g. block/fifo files, which should
> +	 * not have any file extent.
> +	 */
> +	file_extent_err(leaf, slot, "file extent item not allowed for inode mode 0%o",
> +			last_inode->mode);
> +	return false;
> +}
> +
>  static int check_extent_data_item(struct extent_buffer *leaf,
>  				  struct btrfs_key *key, int slot,
> -				  struct btrfs_key *prev_key)
> +				  struct btrfs_key *prev_key,
> +				  const struct last_inode_info *last_inode)
>  {
>  	struct btrfs_fs_info *fs_info = leaf->fs_info;
>  	struct btrfs_file_extent_item *fi;
>  	u32 sectorsize = fs_info->sectorsize;
>  	u32 item_size = btrfs_item_size(leaf, slot);
>  	u64 extent_end;
> +	u8 fi_type;
>  
>  	if (unlikely(!IS_ALIGNED(key->offset, sectorsize))) {
>  		file_extent_err(leaf, slot,
> @@ -243,15 +281,17 @@ static int check_extent_data_item(struct extent_buffer *leaf,
>  				SZ_4K);
>  		return -EUCLEAN;
>  	}
> -	if (unlikely(btrfs_file_extent_type(leaf, fi) >=
> -		     BTRFS_NR_FILE_EXTENT_TYPES)) {
> +	fi_type = btrfs_file_extent_type(leaf, fi);
> +	if (unlikely(fi_type >= BTRFS_NR_FILE_EXTENT_TYPES)) {
>  		file_extent_err(leaf, slot,
>  		"invalid type for file extent, have %u expect range [0, %u]",
> -			btrfs_file_extent_type(leaf, fi),
> -			BTRFS_NR_FILE_EXTENT_TYPES - 1);
> +			fi_type, BTRFS_NR_FILE_EXTENT_TYPES - 1);
>  		return -EUCLEAN;
>  	}
>  
> +	if (unlikely(!check_last_inode_info(leaf, key, slot, fi_type, last_inode)))
> +		return -EUCLEAN;
> +
>  	/*
>  	 * Support for new compression/encryption must introduce incompat flag,
>  	 * and must be caught in open_ctree().
> @@ -270,7 +310,8 @@ static int check_extent_data_item(struct extent_buffer *leaf,
>  			btrfs_file_extent_encryption(leaf, fi));
>  		return -EUCLEAN;
>  	}
> -	if (btrfs_file_extent_type(leaf, fi) == BTRFS_FILE_EXTENT_INLINE) {
> +
> +	if (fi_type == BTRFS_FILE_EXTENT_INLINE) {
>  		/* Inline extent must have 0 as key offset */
>  		if (unlikely(key->offset)) {
>  			file_extent_err(leaf, slot,
> @@ -1206,7 +1247,8 @@ static int check_dev_item(struct extent_buffer *leaf,
>  }
>  
>  static int check_inode_item(struct extent_buffer *leaf,
> -			    struct btrfs_key *key, int slot)
> +			    struct btrfs_key *key, int slot,
> +			    struct last_inode_info *last_inode)

Isn't it more like cur_inode?

>  {
>  	struct btrfs_fs_info *fs_info = leaf->fs_info;
>  	struct btrfs_inode_item *iitem;
> @@ -1291,6 +1333,8 @@ static int check_inode_item(struct extent_buffer *leaf,
>  			ro_flags);
>  		return -EUCLEAN;
>  	}
> +	last_inode->ino = key->objectid;
> +	last_inode->mode = mode;
>  	return 0;
>  }
>  
> @@ -2319,14 +2363,15 @@ static int check_free_space_bitmap(struct extent_buffer *leaf,
>  static enum btrfs_tree_block_status check_leaf_item(struct extent_buffer *leaf,
>  						    struct btrfs_key *key,
>  						    int slot,
> -						    struct btrfs_key *prev_key)
> +						    struct btrfs_key *prev_key,
> +						    struct last_inode_info *last_inode)
>  {
>  	int ret = 0;
>  	struct btrfs_chunk *chunk;
>  
>  	switch (key->type) {
>  	case BTRFS_EXTENT_DATA_KEY:
> -		ret = check_extent_data_item(leaf, key, slot, prev_key);
> +		ret = check_extent_data_item(leaf, key, slot, prev_key, last_inode);
>  		break;
>  	case BTRFS_EXTENT_CSUM_KEY:
>  		ret = check_csum_item(leaf, key, slot, prev_key);
> @@ -2356,7 +2401,7 @@ static enum btrfs_tree_block_status check_leaf_item(struct extent_buffer *leaf,
>  		ret = check_dev_extent_item(leaf, key, slot, prev_key);
>  		break;
>  	case BTRFS_INODE_ITEM_KEY:
> -		ret = check_inode_item(leaf, key, slot);
> +		ret = check_inode_item(leaf, key, slot, last_inode);
>  		break;
>  	case BTRFS_ROOT_ITEM_KEY:
>  		ret = check_root_item(leaf, key, slot);
> @@ -2404,6 +2449,7 @@ static enum btrfs_tree_block_status check_leaf_item(struct extent_buffer *leaf,
>  enum btrfs_tree_block_status __btrfs_check_leaf(struct extent_buffer *leaf)
>  {
>  	struct btrfs_fs_info *fs_info = leaf->fs_info;
> +	struct last_inode_info last_inode = { 0 };
>  	/* No valid key type is 0, so all key should be larger than this key */
>  	struct btrfs_key prev_key = {0, 0, 0};
>  	struct btrfs_key key;
> @@ -2539,7 +2585,7 @@ enum btrfs_tree_block_status __btrfs_check_leaf(struct extent_buffer *leaf)
>  		}
>  
>  		/* Check if the item size and content meet other criteria. */
> -		ret = check_leaf_item(leaf, &key, slot, &prev_key);
> +		ret = check_leaf_item(leaf, &key, slot, &prev_key, &last_inode);
>  		if (unlikely(ret != BTRFS_TREE_BLOCK_CLEAN))
>  			return ret;
>  
> -- 
> 2.54.0
> 

^ permalink raw reply	[flat|nested] 2+ messages in thread

end of thread, other threads:[~2026-08-31 22:09 UTC | newest]

Thread overview: 2+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-08-18  4:57 [PATCH] btrfs: tree-checker: reject file extent items for special files Qu Wenruo
2026-08-31 22:08 ` Boris Burkov

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox