[PATCH RFC] btrfs: check system chunk space before removing device extents

"syzbot" <[email protected]>
Newsgroups dev.linux.lists.syzbot
Message-ID <[email protected]>
During a chunk relocation/balance operation, a transaction abort with
-ENOSPC can occur in btrfs_remove_chunk(). This happens because the
filesystem runs out of system space while trying to remove a chunk item
from the chunk tree, and it cannot allocate a new system chunk.

The crash trace is as follows:

WARNING: fs/btrfs/volumes.c:3526 at btrfs_remove_chunk+0xc9b/0x1070
Call Trace:
 <TASK>
 btrfs_relocate_chunk_finish fs/btrfs/volumes.c:3614 [inline]
 btrfs_relocate_chunk+0x3e5/0x810 fs/btrfs/volumes.c:3668
 __btrfs_balance+0x1b5f/0x29d0 fs/btrfs/volumes.c:4586
 btrfs_balance+0xaa6/0x1180 fs/btrfs/volumes.c:4973
 btrfs_ioctl_balance+0x3da/0x640 fs/btrfs/ioctl.c:3481
 vfs_ioctl fs/ioctl.c:51 [inline]
 __do_sys_ioctl fs/ioctl.c:597 [inline]
 __se_sys_ioctl+0xfc/0x170 fs/ioctl.c:583
 do_syscall_x64 arch/x86/entry/syscall_64.c:63 [inline]
 do_syscall_64+0x15f/0x560 arch/x86/entry/syscall_64.c:94
 entry_SYSCALL_64_after_hwframe+0x77/0x7f
 </TASK>

The sequence of events leading to the abort is as follows. First,
btrfs_remove_chunk() removes the device extents from the device tree by
calling btrfs_remove_dev_extents(). It then calls check_system_chunk() to
reserve system space for removing the chunk item from the chunk tree. If
the system space is full, check_system_chunk() attempts to allocate a new
system chunk. To allocate a new chunk, find_free_dev_extent() is called.
However, find_free_dev_extent() searches the commit root of the device tree
to prevent reusing space freed in the current transaction. Because the
device extents were just freed in the current transaction by
btrfs_remove_dev_extents(), find_free_dev_extent() does not see this freed
space. If the device is otherwise full, it returns -ENOSPC.
check_system_chunk() ignores the failure to allocate a system chunk and
does not reserve any space. Finally, remove_chunk_item() is called, which
fails with -ENOSPC because it needs to COW a node in the chunk tree but no
system space was reserved. Since btrfs_remove_dev_extents() has already
modified the device tree, btrfs_remove_chunk() cannot fail gracefully and
is forced to abort the transaction.

To fix this, we must ensure that we have enough system space before
modifying the device tree. We change check_system_chunk() and
reserve_chunk_space() to return an int error code. In btrfs_remove_chunk(),
we call check_system_chunk() early. If it fails, we can fail gracefully and
return -ENOSPC before any irreversible tree modifications occur. If it
succeeds, we immediately release the reserved metadata space using
btrfs_trans_release_chunk_metadata(). This is necessary because
btrfs_remove_dev_extents() can trigger nested chunk allocations which
unconditionally release all chunk metadata reservations. We then proceed to
call check_system_chunk() a second time later in the function to make the
actual reservation, which is now guaranteed to succeed since we ensured the
system space_info has enough free space.

Fixes: 79bd37120b14 ("btrfs: rework chunk allocation to avoid exhaustion of the system chunk array")
Assisted-by: Gemini:gemini-3.5-flash Gemini:gemini-3.1-pro-preview syzbot
Reported-by: [email protected]
Closes: https://syzkaller.appspot.com/bug?extid=e8582cc16881ec70a430
Link: https://syzkaller.appspot.com/ai_job?id=399adcc1-1743-4bd3-bec2-62461394dbb5
To: "Chris Mason" <[email protected]>
To: "David Sterba" <[email protected]>
To: <[email protected]>
To: "Filipe Manana" <[email protected]>
Cc: <[email protected]>

---
diff --git a/fs/btrfs/block-group.c b/fs/btrfs/block-group.c
index 8def7abb7..b99bd7bdf 100644
--- a/fs/btrfs/block-group.c
+++ b/fs/btrfs/block-group.c
@@ -4488,9 +4488,9 @@ static u64 get_profile_num_devs(const struct btrfs_fs_info *fs_info, u64 type)
 	return num_dev;
 }
 
-static void reserve_chunk_space(struct btrfs_trans_handle *trans,
-				u64 bytes,
-				u64 type)
+static int reserve_chunk_space(struct btrfs_trans_handle *trans,
+			       u64 bytes,
+			       u64 type)
 {
 	struct btrfs_fs_info *fs_info = trans->fs_info;
 	struct btrfs_space_info *info;
@@ -4565,13 +4565,15 @@ static void reserve_chunk_space(struct btrfs_trans_handle *trans,
 		if (!ret)
 			trans->chunk_bytes_reserved += bytes;
 	}
+
+	return ret;
 }
 
 /*
  * Reserve space in the system space for allocating or removing a chunk.
  * The caller must be holding fs_info->chunk_mutex.
  */
-void check_system_chunk(struct btrfs_trans_handle *trans, u64 type)
+int check_system_chunk(struct btrfs_trans_handle *trans, u64 type)
 {
 	struct btrfs_fs_info *fs_info = trans->fs_info;
 	const u64 num_devs = get_profile_num_devs(fs_info, type);
@@ -4581,7 +4583,7 @@ void check_system_chunk(struct btrfs_trans_handle *trans, u64 type)
 	bytes = btrfs_calc_metadata_size(fs_info, num_devs) +
 		btrfs_calc_insert_metadata_size(fs_info, 1);
 
-	reserve_chunk_space(trans, bytes, type);
+	return reserve_chunk_space(trans, bytes, type);
 }
 
 /*
diff --git a/fs/btrfs/block-group.h b/fs/btrfs/block-group.h
index 790c2d467..43800523b 100644
--- a/fs/btrfs/block-group.h
+++ b/fs/btrfs/block-group.h
@@ -378,7 +378,7 @@ int btrfs_chunk_alloc(struct btrfs_trans_handle *trans,
 		      struct btrfs_space_info *space_info, u64 flags,
 		      enum btrfs_chunk_alloc_enum force);
 int btrfs_force_chunk_alloc(struct btrfs_trans_handle *trans, u64 type);
-void check_system_chunk(struct btrfs_trans_handle *trans, const u64 type);
+int check_system_chunk(struct btrfs_trans_handle *trans, const u64 type);
 void btrfs_reserve_chunk_metadata(struct btrfs_trans_handle *trans,
 				  bool is_item_insertion);
 u64 btrfs_get_alloc_profile(struct btrfs_fs_info *fs_info, u64 orig_flags);
diff --git a/fs/btrfs/volumes.c b/fs/btrfs/volumes.c
index 6eab4cc73..e77306e09 100644
--- a/fs/btrfs/volumes.c
+++ b/fs/btrfs/volumes.c
@@ -3463,6 +3463,24 @@ int btrfs_remove_chunk(struct btrfs_trans_handle *trans, u64 chunk_offset)
 		return PTR_ERR(map);
 	}
 
+	/*
+	 * Check if we have enough system space to remove the chunk.
+	 * If we don't, and we can't allocate a new system chunk, we fail gracefully
+	 * before modifying the device tree.
+	 */
+	mutex_lock(&fs_info->chunk_mutex);
+	ret = check_system_chunk(trans, map->type);
+	if (ret) {
+		mutex_unlock(&fs_info->chunk_mutex);
+		goto out;
+	}
+	/*
+	 * Release the block reserve. We just wanted to ensure the system
+	 * space_info has enough free space. We will reserve it again later.
+	 */
+	btrfs_trans_release_chunk_metadata(trans);
+	mutex_unlock(&fs_info->chunk_mutex);
+
 	ret = btrfs_remove_dev_extents(trans, map);
 	if (ret)
 		goto out;


base-commit: 075b74841bd0065a3bda3440873c747938e69b68
-- 
This is an AI-generated patch subject to moderation.
Reply with '#syz upstream' to Sign-off the patch as a human author
and send it to the upstream kernel mailing lists.
Reply with '#syz reject' to reject it ('#syz unreject' to undo).

See https://goo.gle/syzbot-ai-patches for information about AI-generated patches.
The person who has signed off on the patch is responsible for
addressing comments.
syzbot engineers can be reached at [email protected].
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.